mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-20 13:51:28 +08:00
feat.文档完善
This commit is contained in:
683
docs/01-插件开发/02-核心概念/01-插件生命周期.md
Normal file
683
docs/01-插件开发/02-核心概念/01-插件生命周期.md
Normal file
@@ -0,0 +1,683 @@
|
||||
# 插件生命周期
|
||||
|
||||
本文档详细介绍阑山桌面插件的生命周期、加载流程和各个阶段的职责。
|
||||
|
||||
## 生命周期概览
|
||||
|
||||
插件从加载到卸载经历以下阶段:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 插件生命周期 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
1. 发现 (Discovery)
|
||||
├─ 扫描插件目录
|
||||
├─ 读取 plugin.json
|
||||
└─ 验证基本信息
|
||||
↓
|
||||
2. 加载 (Load)
|
||||
├─ 创建 PluginLoadContext
|
||||
├─ 加载程序集
|
||||
├─ 解析依赖关系
|
||||
└─ 验证兼容性
|
||||
↓
|
||||
3. 实例化 (Instantiate)
|
||||
├─ 反射查找 IPlugin 实现
|
||||
├─ 创建插件实例
|
||||
└─ 注入依赖
|
||||
↓
|
||||
4. 初始化 (Initialize)
|
||||
├─ 调用 InitializeAsync()
|
||||
├─ 注册组件
|
||||
├─ 注册设置页
|
||||
├─ 注册服务
|
||||
└─ 订阅事件
|
||||
↓
|
||||
5. 运行中 (Running)
|
||||
├─ 组件渲染和更新
|
||||
├─ 处理用户交互
|
||||
├─ 响应事件
|
||||
└─ 执行后台任务
|
||||
↓
|
||||
6. 关闭 (Shutdown)
|
||||
├─ 调用 ShutdownAsync()
|
||||
├─ 保存状态
|
||||
├─ 取消订阅
|
||||
├─ 清理资源
|
||||
└─ 卸载程序集
|
||||
```
|
||||
|
||||
## 详细阶段说明
|
||||
|
||||
### 1. 发现阶段 (Discovery)
|
||||
|
||||
**时机**: 宿主启动时
|
||||
|
||||
**职责**: 扫描和识别插件
|
||||
|
||||
**流程**:
|
||||
|
||||
```csharp
|
||||
// PluginDiscoveryService.cs (宿主代码)
|
||||
public List<PluginDescriptor> DiscoverPlugins()
|
||||
{
|
||||
var pluginsDir = Path.Combine(
|
||||
AppDataPath,
|
||||
"plugins"
|
||||
);
|
||||
|
||||
var descriptors = new List<PluginDescriptor>();
|
||||
|
||||
// 1. 扫描插件目录
|
||||
foreach (var dir in Directory.GetDirectories(pluginsDir))
|
||||
{
|
||||
var manifestPath = Path.Combine(dir, "plugin.json");
|
||||
|
||||
// 2. 读取 plugin.json
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
_logger.LogWarning($"Plugin manifest not found: {dir}");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<PluginManifest>(json);
|
||||
|
||||
// 3. 验证基本信息
|
||||
if (string.IsNullOrEmpty(manifest?.Id))
|
||||
{
|
||||
_logger.LogWarning($"Invalid plugin manifest: {dir}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new PluginDescriptor
|
||||
{
|
||||
Id = manifest.Id,
|
||||
Name = manifest.Name,
|
||||
Version = manifest.Version,
|
||||
Directory = dir,
|
||||
Manifest = manifest
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Failed to read plugin manifest: {dir}");
|
||||
}
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ 确保 `plugin.json` 存在且格式正确
|
||||
- ✅ 确保插件 ID 唯一
|
||||
- ✅ 确保版本号符合语义化版本规范
|
||||
|
||||
### 2. 加载阶段 (Load)
|
||||
|
||||
**时机**: 发现插件后
|
||||
|
||||
**职责**: 加载插件程序集
|
||||
|
||||
**流程**:
|
||||
|
||||
```csharp
|
||||
// PluginLoader.cs (宿主代码)
|
||||
public PluginLoadResult LoadPlugin(PluginDescriptor descriptor)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 创建隔离的加载上下文
|
||||
var loadContext = new PluginLoadContext(descriptor.Directory);
|
||||
|
||||
// 2. 查找主程序集
|
||||
var assemblyPath = Path.Combine(
|
||||
descriptor.Directory,
|
||||
$"{descriptor.Id}.dll"
|
||||
);
|
||||
|
||||
if (!File.Exists(assemblyPath))
|
||||
{
|
||||
return PluginLoadResult.Failed(
|
||||
$"Plugin assembly not found: {assemblyPath}"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 加载程序集
|
||||
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
|
||||
|
||||
// 4. 验证依赖
|
||||
if (!ValidateDependencies(descriptor.Manifest.Dependencies))
|
||||
{
|
||||
return PluginLoadResult.Failed("Dependency validation failed");
|
||||
}
|
||||
|
||||
// 5. 验证宿主版本兼容性
|
||||
if (!IsHostVersionCompatible(descriptor.Manifest.MinHostVersion))
|
||||
{
|
||||
return PluginLoadResult.Failed(
|
||||
$"Incompatible host version. Required: {descriptor.Manifest.MinHostVersion}"
|
||||
);
|
||||
}
|
||||
|
||||
return PluginLoadResult.Success(assembly, loadContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Failed to load plugin: {descriptor.Id}");
|
||||
return PluginLoadResult.Failed(ex.Message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**依赖解析**:
|
||||
|
||||
```json
|
||||
// plugin.json
|
||||
{
|
||||
"Dependencies": [
|
||||
{
|
||||
"PluginId": "com.example.anotherplugin",
|
||||
"MinVersion": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ 主程序集名称应与插件 ID 匹配(或在清单中指定)
|
||||
- ✅ 所有依赖的 DLL 应在插件目录中
|
||||
- ✅ 声明对其他插件的依赖关系
|
||||
|
||||
### 3. 实例化阶段 (Instantiate)
|
||||
|
||||
**时机**: 程序集加载后
|
||||
|
||||
**职责**: 创建插件实例
|
||||
|
||||
**流程**:
|
||||
|
||||
```csharp
|
||||
// PluginActivator.cs (宿主代码)
|
||||
public IPlugin? CreatePluginInstance(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 查找 IPlugin 实现类
|
||||
var pluginType = assembly.GetTypes()
|
||||
.FirstOrDefault(t =>
|
||||
typeof(IPlugin).IsAssignableFrom(t) &&
|
||||
!t.IsAbstract &&
|
||||
!t.IsInterface
|
||||
);
|
||||
|
||||
if (pluginType == null)
|
||||
{
|
||||
_logger.LogError("No IPlugin implementation found");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 创建实例
|
||||
var plugin = Activator.CreateInstance(pluginType) as IPlugin;
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
_logger.LogError("Failed to create plugin instance");
|
||||
return null;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to instantiate plugin");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ 插件类必须有公共无参构造函数
|
||||
- ✅ 一个插件程序集只能有一个 IPlugin 实现
|
||||
- ✅ 不要在构造函数中执行耗时操作
|
||||
|
||||
### 4. 初始化阶段 (Initialize)
|
||||
|
||||
**时机**: 插件实例创建后
|
||||
|
||||
**职责**: 注册组件、服务和事件
|
||||
|
||||
**插件代码示例**:
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
// 1. 记录日志
|
||||
context.Logger.LogInformation($"{Name} is initializing...");
|
||||
|
||||
// 2. 注册组件
|
||||
var componentRegistry = context.Services
|
||||
.GetService<IComponentRegistry>();
|
||||
|
||||
if (componentRegistry != null)
|
||||
{
|
||||
// 注册多个组件
|
||||
componentRegistry.RegisterComponent<WeatherComponent>();
|
||||
componentRegistry.RegisterComponent<ClockComponent>();
|
||||
|
||||
context.Logger.LogInformation("Components registered");
|
||||
}
|
||||
|
||||
// 3. 注册设置页
|
||||
var settingsRegistry = context.Services
|
||||
.GetService<ISettingsPageRegistry>();
|
||||
|
||||
if (settingsRegistry != null)
|
||||
{
|
||||
settingsRegistry.RegisterPage<MySettingsPage>(
|
||||
title: "我的插件",
|
||||
category: "插件"
|
||||
);
|
||||
|
||||
context.Logger.LogInformation("Settings page registered");
|
||||
}
|
||||
|
||||
// 4. 注册公共 IPC 服务(如果需要)
|
||||
var ipcBuilder = context.Services
|
||||
.GetService<IPluginPublicIpcBuilder>();
|
||||
|
||||
if (ipcBuilder != null)
|
||||
{
|
||||
ipcBuilder.AddService<IMyPublicService>(
|
||||
objectId: "default",
|
||||
notifyIds: new[] { "myplugin.event.changed" }
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 订阅宿主事件
|
||||
var eventBus = context.Services
|
||||
.GetService<IEventBus>();
|
||||
|
||||
if (eventBus != null)
|
||||
{
|
||||
eventBus.Subscribe<ThemeChangedEvent>(OnThemeChanged);
|
||||
}
|
||||
|
||||
// 6. 初始化后台服务(如果有)
|
||||
await InitializeBackgroundServicesAsync(context);
|
||||
|
||||
context.Logger.LogInformation($"{Name} initialized successfully");
|
||||
}
|
||||
|
||||
private void OnThemeChanged(ThemeChangedEvent evt)
|
||||
{
|
||||
// 响应主题变更
|
||||
}
|
||||
|
||||
private async Task InitializeBackgroundServicesAsync(IPluginContext context)
|
||||
{
|
||||
// 启动定时任务等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**初始化最佳实践**:
|
||||
|
||||
```csharp
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ✅ 使用 try-catch 捕获异常
|
||||
// ✅ 记录详细的日志
|
||||
// ✅ 验证服务是否可用
|
||||
// ✅ 使用 async/await 处理异步操作
|
||||
// ❌ 不要阻塞 UI 线程
|
||||
// ❌ 不要执行超过 5 秒的操作
|
||||
|
||||
_context = context;
|
||||
|
||||
// 快速初始化
|
||||
RegisterComponents(context);
|
||||
RegisterSettings(context);
|
||||
|
||||
// 耗时操作使用后台任务
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Plugin initialization failed");
|
||||
throw; // 让宿主知道初始化失败
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ InitializeAsync 应尽快完成(< 5 秒)
|
||||
- ✅ 耗时操作放在后台线程
|
||||
- ✅ 妥善处理异常
|
||||
- ✅ 保存 IPluginContext 引用供后续使用
|
||||
- ❌ 不要在此阶段访问其他插件的服务(可能还未加载)
|
||||
|
||||
### 5. 运行中阶段 (Running)
|
||||
|
||||
**时机**: 初始化完成后
|
||||
|
||||
**职责**: 响应用户交互和系统事件
|
||||
|
||||
**组件更新循环**:
|
||||
|
||||
```csharp
|
||||
// 宿主会定时调用组件的 UpdateAsync()
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
private HttpClient _httpClient;
|
||||
private DateTime _lastUpdate;
|
||||
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
// 定时更新数据(默认 1 秒调用一次)
|
||||
if (DateTime.Now - _lastUpdate > TimeSpan.FromMinutes(5))
|
||||
{
|
||||
await FetchDataAsync();
|
||||
_lastUpdate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _httpClient.GetStringAsync("https://api.example.com/data");
|
||||
// 更新组件属性
|
||||
Data = ParseData(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to fetch data");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**事件响应**:
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
// 订阅系统事件
|
||||
var eventBus = context.Services.GetService<IEventBus>();
|
||||
|
||||
eventBus?.Subscribe<ThemeChangedEvent>(OnThemeChanged);
|
||||
eventBus?.Subscribe<LanguageChangedEvent>(OnLanguageChanged);
|
||||
eventBus?.Subscribe<SettingChangedEvent>(OnSettingChanged);
|
||||
}
|
||||
|
||||
private void OnThemeChanged(ThemeChangedEvent evt)
|
||||
{
|
||||
_logger.LogInformation($"Theme changed to: {evt.NewTheme}");
|
||||
// 更新组件外观
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(LanguageChangedEvent evt)
|
||||
{
|
||||
_logger.LogInformation($"Language changed to: {evt.NewLanguage}");
|
||||
// 重新加载本地化资源
|
||||
}
|
||||
|
||||
private void OnSettingChanged(SettingChangedEvent evt)
|
||||
{
|
||||
if (evt.Key.StartsWith("MyPlugin."))
|
||||
{
|
||||
// 响应插件设置变更
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ 组件更新应快速完成
|
||||
- ✅ 使用缓存避免重复计算
|
||||
- ✅ 异步操作使用 async/await
|
||||
- ✅ 妥善处理网络错误
|
||||
- ❌ 不要在 UpdateAsync 中执行超过 1 秒的操作
|
||||
|
||||
### 6. 关闭阶段 (Shutdown)
|
||||
|
||||
**时机**:
|
||||
- 宿主应用退出
|
||||
- 插件被禁用
|
||||
- 插件热重载
|
||||
|
||||
**职责**: 清理资源和保存状态
|
||||
|
||||
**插件代码示例**:
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
private IDisposable? _eventSubscription;
|
||||
private HttpClient? _httpClient;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"{Name} is shutting down...");
|
||||
|
||||
// 1. 取消正在进行的操作
|
||||
_cts?.Cancel();
|
||||
|
||||
// 2. 取消事件订阅
|
||||
_eventSubscription?.Dispose();
|
||||
|
||||
// 3. 保存状态
|
||||
await SaveStateAsync();
|
||||
|
||||
// 4. 释放资源
|
||||
_httpClient?.Dispose();
|
||||
|
||||
// 5. 停止后台任务
|
||||
await StopBackgroundServicesAsync();
|
||||
|
||||
_logger.LogInformation($"{Name} shutdown completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during plugin shutdown");
|
||||
// 不要抛出异常,避免影响其他插件
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveStateAsync()
|
||||
{
|
||||
// 保存插件状态到设置
|
||||
_context?.Settings.SetValue("LastUpdateTime", DateTime.Now);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StopBackgroundServicesAsync()
|
||||
{
|
||||
// 停止定时任务等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关闭最佳实践**:
|
||||
|
||||
```csharp
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// ✅ 尽快完成(< 3 秒)
|
||||
// ✅ 使用 try-catch 避免异常
|
||||
// ✅ 按相反顺序清理资源
|
||||
// ✅ 保存关键状态
|
||||
// ❌ 不要抛出异常
|
||||
// ❌ 不要执行耗时操作
|
||||
|
||||
// 取消异步操作
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
// 取消订阅(防止内存泄漏)
|
||||
UnsubscribeEvents();
|
||||
|
||||
// 释放托管资源
|
||||
DisposeResources();
|
||||
|
||||
// 保存状态(快速)
|
||||
SaveCriticalState();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录但不抛出
|
||||
_logger?.LogError(ex, "Shutdown error");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**开发者注意事项**:
|
||||
- ✅ ShutdownAsync 必须快速完成(< 3 秒)
|
||||
- ✅ 取消所有异步操作
|
||||
- ✅ 取消事件订阅(防止内存泄漏)
|
||||
- ✅ 释放所有 IDisposable 资源
|
||||
- ✅ 保存关键状态
|
||||
- ❌ 不要抛出异常
|
||||
|
||||
## 生命周期事件
|
||||
|
||||
插件可以监听宿主的生命周期事件:
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var hostLifecycle = context.Services
|
||||
.GetService<IHostLifecycleService>();
|
||||
|
||||
if (hostLifecycle != null)
|
||||
{
|
||||
hostLifecycle.Starting += OnHostStarting;
|
||||
hostLifecycle.Started += OnHostStarted;
|
||||
hostLifecycle.Stopping += OnHostStopping;
|
||||
hostLifecycle.Stopped += OnHostStopped;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHostStarting(object? sender, EventArgs e)
|
||||
{
|
||||
// 宿主正在启动
|
||||
}
|
||||
|
||||
private void OnHostStarted(object? sender, EventArgs e)
|
||||
{
|
||||
// 宿主已启动完成
|
||||
}
|
||||
|
||||
private void OnHostStopping(object? sender, EventArgs e)
|
||||
{
|
||||
// 宿主即将关闭
|
||||
}
|
||||
|
||||
private void OnHostStopped(object? sender, EventArgs e)
|
||||
{
|
||||
// 宿主已关闭
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
// 取消订阅
|
||||
var hostLifecycle = _context?.Services
|
||||
.GetService<IHostLifecycleService>();
|
||||
|
||||
if (hostLifecycle != null)
|
||||
{
|
||||
hostLifecycle.Starting -= OnHostStarting;
|
||||
hostLifecycle.Started -= OnHostStarted;
|
||||
hostLifecycle.Stopping -= OnHostStopping;
|
||||
hostLifecycle.Stopped -= OnHostStopped;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 初始化失败
|
||||
|
||||
如果插件初始化失败,宿主会:
|
||||
|
||||
1. 记录错误日志
|
||||
2. 标记插件为"加载失败"
|
||||
3. 继续加载其他插件
|
||||
4. 在 UI 中显示失败状态
|
||||
|
||||
### 运行时异常
|
||||
|
||||
组件代码中的未捕获异常:
|
||||
|
||||
1. 被宿主捕获并记录
|
||||
2. 组件标记为"错误"状态
|
||||
3. 组件停止更新
|
||||
4. 不影响其他组件
|
||||
|
||||
### 关闭超时
|
||||
|
||||
如果 ShutdownAsync 超过 5 秒:
|
||||
|
||||
1. 宿主强制终止
|
||||
2. 记录超时警告
|
||||
3. 继续关闭其他插件
|
||||
|
||||
## 插件热重载
|
||||
|
||||
宿主支持插件热重载(开发中功能):
|
||||
|
||||
```
|
||||
1. 用户触发重载
|
||||
↓
|
||||
2. 调用 ShutdownAsync()
|
||||
↓
|
||||
3. 卸载程序集
|
||||
↓
|
||||
4. 重新加载程序集
|
||||
↓
|
||||
5. 创建新实例
|
||||
↓
|
||||
6. 调用 InitializeAsync()
|
||||
↓
|
||||
7. 恢复组件状态
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
插件生命周期的关键点:
|
||||
|
||||
- ✅ **发现**: 确保 plugin.json 正确
|
||||
- ✅ **加载**: 管理好依赖关系
|
||||
- ✅ **初始化**: 快速注册,耗时操作后台执行
|
||||
- ✅ **运行**: 高效更新,异步处理
|
||||
- ✅ **关闭**: 及时清理,避免异常
|
||||
|
||||
## 下一步
|
||||
|
||||
- [组件系统详解](02-组件系统.md) - 学习组件开发
|
||||
- [设置系统](03-设置系统.md) - 管理插件配置
|
||||
- [插件通信](05-插件通信.md) - 插件间协作
|
||||
- [IPlugin 接口](../03-API参考/01-IPlugin接口.md) - API 详细文档
|
||||
789
docs/01-插件开发/02-核心概念/02-组件系统.md
Normal file
789
docs/01-插件开发/02-核心概念/02-组件系统.md
Normal file
@@ -0,0 +1,789 @@
|
||||
# 组件系统详解
|
||||
|
||||
本文档详细介绍阑山桌面的桌面组件(Widget)系统,包括组件架构、生命周期、渲染机制和最佳实践。
|
||||
|
||||
## 什么是桌面组件?
|
||||
|
||||
**桌面组件(Widget)** 是显示在桌面上的可视化模块,提供信息展示和快捷操作功能。
|
||||
|
||||
### 组件特点
|
||||
|
||||
- 🎨 **固定在桌面** - 显示在桌面图层,不会被普通窗口遮挡
|
||||
- 🔄 **实时更新** - 定时刷新数据,保持信息最新
|
||||
- ⚙️ **可配置** - 用户可以自定义组件行为和外观
|
||||
- 🖱️ **可交互** - 支持点击、拖拽等用户操作
|
||||
- 📐 **可布局** - 用户可以自由调整位置和大小
|
||||
|
||||
### 典型组件示例
|
||||
|
||||
| 组件类型 | 功能 | 更新频率 |
|
||||
|---------|------|---------|
|
||||
| **时钟组件** | 显示当前时间和日期 | 1秒 |
|
||||
| **天气组件** | 显示天气信息 | 5-15分钟 |
|
||||
| **日历组件** | 显示日程和待办 | 1小时 |
|
||||
| **系统监控** | CPU、内存使用率 | 2秒 |
|
||||
| **倒计时** | 重要日期倒计时 | 1秒 |
|
||||
|
||||
## 组件架构
|
||||
|
||||
### 组件三层结构
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Component (组件模型) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ 业务逻辑 │ │
|
||||
│ │ - 数据获取 │ │
|
||||
│ │ - 状态管理 │ │
|
||||
│ │ - 设置持久化 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└────────────────┬───────────────────────┘
|
||||
│ 数据绑定
|
||||
┌────────────────▼───────────────────────┐
|
||||
│ ViewModel (视图模型) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ 展示逻辑 │ │
|
||||
│ │ - 属性通知 │ │
|
||||
│ │ - 命令处理 │ │
|
||||
│ │ - 数据格式化 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└────────────────┬───────────────────────┘
|
||||
│ UI 绑定
|
||||
┌────────────────▼───────────────────────┐
|
||||
│ View (视图) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ UI 渲染 │ │
|
||||
│ │ - Avalonia AXAML │ │
|
||||
│ │ - 样式和主题 │ │
|
||||
│ │ - 用户交互 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 组件基类层次
|
||||
|
||||
```
|
||||
object
|
||||
↓
|
||||
ObservableObject (MVVM Toolkit)
|
||||
↓
|
||||
ComponentBase (Plugin SDK)
|
||||
↓
|
||||
YourComponent (你的组件)
|
||||
```
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 步骤 1: 定义组件类
|
||||
|
||||
```csharp
|
||||
using LanMountainDesktop.PluginSdk.Components;
|
||||
using LanMountainDesktop.Shared.Contracts.Components;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace MyPlugin.Components;
|
||||
|
||||
/// <summary>
|
||||
/// 天气组件 - 显示当前天气信息
|
||||
/// </summary>
|
||||
[Component(
|
||||
Id = "com.example.myplugin.weather",
|
||||
Name = "天气",
|
||||
Description = "显示当前天气和温度",
|
||||
Category = "信息",
|
||||
Icon = "avares://MyPlugin/Assets/weather-icon.png",
|
||||
DefaultWidth = 200,
|
||||
DefaultHeight = 150
|
||||
)]
|
||||
public class WeatherComponent : ComponentBase
|
||||
{
|
||||
// 组件唯一标识
|
||||
public override string Id => "com.example.myplugin.weather";
|
||||
|
||||
// 组件显示名称
|
||||
public override string Name => "天气";
|
||||
|
||||
// === 数据属性 ===
|
||||
|
||||
private string _location = "北京";
|
||||
private double _temperature = 0;
|
||||
private string _condition = "晴";
|
||||
private string _icon = "☀️";
|
||||
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProperty(ref _location, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 温度(摄氏度)
|
||||
/// </summary>
|
||||
public double Temperature
|
||||
{
|
||||
get => _temperature;
|
||||
set => SetProperty(ref _temperature, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 天气状况
|
||||
/// </summary>
|
||||
public string Condition
|
||||
{
|
||||
get => _condition;
|
||||
set => SetProperty(ref _condition, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 天气图标
|
||||
/// </summary>
|
||||
public string Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetProperty(ref _icon, value);
|
||||
}
|
||||
|
||||
// === 配置属性 ===
|
||||
|
||||
private bool _useFahrenheit = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用华氏度
|
||||
/// </summary>
|
||||
public bool UseFahrenheit
|
||||
{
|
||||
get => _useFahrenheit;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useFahrenheit, value))
|
||||
{
|
||||
// 保存到设置
|
||||
Settings.SetValue("UseFahrenheit", value);
|
||||
// 触发更新
|
||||
OnPropertyChanged(nameof(DisplayTemperature));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示温度(根据单位)
|
||||
/// </summary>
|
||||
public string DisplayTemperature
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UseFahrenheit)
|
||||
{
|
||||
var fahrenheit = Temperature * 9 / 5 + 32;
|
||||
return $"{fahrenheit:F1}°F";
|
||||
}
|
||||
return $"{Temperature:F1}°C";
|
||||
}
|
||||
}
|
||||
|
||||
// === 生命周期方法 ===
|
||||
|
||||
/// <summary>
|
||||
/// 组件初始化
|
||||
/// </summary>
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
// 从设置加载配置
|
||||
Location = Settings.GetValue("Location", "北京");
|
||||
UseFahrenheit = Settings.GetValue("UseFahrenheit", false);
|
||||
|
||||
// 首次加载数据
|
||||
await FetchWeatherDataAsync();
|
||||
|
||||
Logger.LogInformation($"WeatherComponent initialized for {Location}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组件定时更新
|
||||
/// </summary>
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
// 每 10 分钟更新一次天气数据
|
||||
var lastUpdate = Settings.GetValue<DateTime>("LastUpdate", DateTime.MinValue);
|
||||
if (DateTime.Now - lastUpdate > TimeSpan.FromMinutes(10))
|
||||
{
|
||||
await FetchWeatherDataAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组件销毁
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
// 清理资源
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
// === 业务逻辑 ===
|
||||
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
private async Task FetchWeatherDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_httpClient ??= new HttpClient();
|
||||
|
||||
// 调用天气 API
|
||||
var url = $"https://api.weather.com/data?city={Location}";
|
||||
var response = await _httpClient.GetStringAsync(url);
|
||||
|
||||
// 解析数据
|
||||
var weatherData = ParseWeatherData(response);
|
||||
|
||||
// 更新属性
|
||||
Temperature = weatherData.Temperature;
|
||||
Condition = weatherData.Condition;
|
||||
Icon = GetWeatherIcon(weatherData.Condition);
|
||||
|
||||
// 记录更新时间
|
||||
Settings.SetValue("LastUpdate", DateTime.Now);
|
||||
|
||||
Logger.LogInformation($"Weather data updated for {Location}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to fetch weather data");
|
||||
Condition = "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
private WeatherData ParseWeatherData(string json)
|
||||
{
|
||||
// 解析 JSON 数据
|
||||
// 实际项目中使用 System.Text.Json 或 Newtonsoft.Json
|
||||
return new WeatherData
|
||||
{
|
||||
Temperature = 25.5,
|
||||
Condition = "晴"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetWeatherIcon(string condition)
|
||||
{
|
||||
return condition switch
|
||||
{
|
||||
"晴" => "☀️",
|
||||
"多云" => "⛅",
|
||||
"阴" => "☁️",
|
||||
"雨" => "🌧️",
|
||||
"雪" => "❄️",
|
||||
_ => "🌤️"
|
||||
};
|
||||
}
|
||||
|
||||
private class WeatherData
|
||||
{
|
||||
public double Temperature { get; set; }
|
||||
public string Condition { get; set; } = "";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2: 创建视图
|
||||
|
||||
创建 `Views/WeatherComponentView.axaml`:
|
||||
|
||||
```xml
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:MyPlugin.ViewModels"
|
||||
x:Class="MyPlugin.Views.WeatherComponentView"
|
||||
x:DataType="vm:WeatherComponentViewModel">
|
||||
|
||||
<!-- 组件容器 -->
|
||||
<Border Background="{DynamicResource CardBackgroundBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="16"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1"
|
||||
BoxShadow="0 2 8 0 #20000000">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<StackPanel Grid.Row="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
Margin="0,0,0,12">
|
||||
<TextBlock Text="📍" FontSize="16" />
|
||||
<TextBlock Text="{Binding Component.Location}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<StackPanel Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
|
||||
<!-- 天气图标 -->
|
||||
<TextBlock Text="{Binding Component.Icon}"
|
||||
FontSize="48"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<!-- 温度 -->
|
||||
<TextBlock Text="{Binding Component.DisplayTemperature}"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
|
||||
<!-- 天气状况 -->
|
||||
<TextBlock Text="{Binding Component.Condition}"
|
||||
FontSize="16"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,12,0,0">
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<Button Command="{Binding RefreshCommand}"
|
||||
Padding="8,4"
|
||||
ToolTip.Tip="刷新">
|
||||
<TextBlock Text="🔄" FontSize="14" />
|
||||
</Button>
|
||||
|
||||
<!-- 设置按钮 -->
|
||||
<Button Command="{Binding SettingsCommand}"
|
||||
Padding="8,4"
|
||||
ToolTip.Tip="设置">
|
||||
<TextBlock Text="⚙️" FontSize="14" />
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
代码后台 `WeatherComponentView.axaml.cs`:
|
||||
|
||||
```csharp
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace MyPlugin.Views;
|
||||
|
||||
public partial class WeatherComponentView : UserControl
|
||||
{
|
||||
public WeatherComponentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3: 创建视图模型
|
||||
|
||||
创建 `ViewModels/WeatherComponentViewModel.cs`:
|
||||
|
||||
```csharp
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MyPlugin.Components;
|
||||
|
||||
namespace MyPlugin.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 天气组件视图模型
|
||||
/// </summary>
|
||||
public partial class WeatherComponentViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private WeatherComponent _component;
|
||||
|
||||
public WeatherComponentViewModel(WeatherComponent component)
|
||||
{
|
||||
_component = component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
// 强制刷新天气数据
|
||||
await Component.UpdateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void Settings()
|
||||
{
|
||||
// 打开组件设置对话框
|
||||
// 实际实现需要调用宿主的对话框服务
|
||||
Component.Logger.LogInformation("Settings clicked");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 4: 注册组件
|
||||
|
||||
在插件入口注册组件:
|
||||
|
||||
```csharp
|
||||
public class Plugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var componentRegistry = context.Services
|
||||
.GetService<IComponentRegistry>();
|
||||
|
||||
if (componentRegistry != null)
|
||||
{
|
||||
// 注册天气组件
|
||||
componentRegistry.RegisterComponent<WeatherComponent>(
|
||||
componentFactory: () => new WeatherComponent(),
|
||||
viewFactory: (component) => new WeatherComponentView
|
||||
{
|
||||
DataContext = new WeatherComponentViewModel(
|
||||
(WeatherComponent)component
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
context.Logger.LogInformation("WeatherComponent registered");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ComponentBase API
|
||||
|
||||
### 核心属性
|
||||
|
||||
```csharp
|
||||
public abstract class ComponentBase : ObservableObject, IComponent
|
||||
{
|
||||
// === 标识属性 ===
|
||||
|
||||
/// <summary>
|
||||
/// 组件唯一标识符
|
||||
/// </summary>
|
||||
public abstract string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 组件显示名称
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
// === 服务访问 ===
|
||||
|
||||
/// <summary>
|
||||
/// 日志记录器
|
||||
/// </summary>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置服务
|
||||
/// </summary>
|
||||
protected IComponentSettings Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供者
|
||||
/// </summary>
|
||||
protected IServiceProvider Services { get; }
|
||||
|
||||
// === 生命周期方法 ===
|
||||
|
||||
/// <summary>
|
||||
/// 组件初始化(创建时调用一次)
|
||||
/// </summary>
|
||||
public virtual Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// 组件更新(定时调用,默认1秒)
|
||||
/// </summary>
|
||||
public virtual Task UpdateAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// 组件销毁(清理资源)
|
||||
/// </summary>
|
||||
public virtual void Dispose() { }
|
||||
}
|
||||
```
|
||||
|
||||
### 辅助方法
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 设置属性值并触发通知
|
||||
/// </summary>
|
||||
protected bool SetProperty<T>(
|
||||
ref T field,
|
||||
T value,
|
||||
[CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发属性变更通知
|
||||
/// </summary>
|
||||
protected void OnPropertyChanged(
|
||||
[CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(
|
||||
this,
|
||||
new PropertyChangedEventArgs(propertyName)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
### 完整生命周期
|
||||
|
||||
```
|
||||
1. 用户添加组件
|
||||
↓
|
||||
2. ComponentRegistry.CreateInstance()
|
||||
├─ 调用 componentFactory()
|
||||
├─ 创建组件实例
|
||||
└─ 注入依赖(Logger, Settings, Services)
|
||||
↓
|
||||
3. 调用 InitializeAsync()
|
||||
├─ 加载设置
|
||||
├─ 初始化数据
|
||||
└─ 订阅事件
|
||||
↓
|
||||
4. ComponentRegistry.CreateView()
|
||||
├─ 调用 viewFactory()
|
||||
├─ 创建视图
|
||||
└─ 设置 DataContext
|
||||
↓
|
||||
5. 添加到桌面
|
||||
├─ 包装到 DesktopWidgetWindow
|
||||
├─ 设置位置和大小
|
||||
└─ 显示窗口
|
||||
↓
|
||||
6. 定时更新循环
|
||||
├─ 每 1 秒(可配置)
|
||||
├─ 调用 UpdateAsync()
|
||||
└─ UI 自动刷新(数据绑定)
|
||||
↓
|
||||
7. 用户移除组件 / 应用关闭
|
||||
↓
|
||||
8. 调用 Dispose()
|
||||
├─ 取消订阅
|
||||
├─ 保存状态
|
||||
└─ 释放资源
|
||||
↓
|
||||
9. 从桌面移除
|
||||
└─ 关闭窗口
|
||||
```
|
||||
|
||||
### 更新频率控制
|
||||
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
private DateTime _lastUpdate;
|
||||
private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
// 控制更新频率
|
||||
if (DateTime.Now - _lastUpdate < _updateInterval)
|
||||
return;
|
||||
|
||||
await FetchDataAsync();
|
||||
_lastUpdate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件设置
|
||||
|
||||
### 使用设置服务
|
||||
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
// 读取设置(带默认值)
|
||||
var city = Settings.GetValue("City", "北京");
|
||||
var refreshRate = Settings.GetValue("RefreshRate", 10);
|
||||
var enabled = Settings.GetValue("Enabled", true);
|
||||
|
||||
// 读取复杂对象
|
||||
var config = Settings.GetValue<MyConfig>("Config", new MyConfig());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void SaveCity(string city)
|
||||
{
|
||||
// 保存设置
|
||||
Settings.SetValue("City", city);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听设置变更
|
||||
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
// 监听设置变更
|
||||
Settings.SettingChanged += OnSettingChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
|
||||
{
|
||||
if (e.Key == "City")
|
||||
{
|
||||
var newCity = e.NewValue as string;
|
||||
// 响应城市变更
|
||||
_ = FetchWeatherForCity(newCity);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
// 取消订阅
|
||||
Settings.SettingChanged -= OnSettingChanged;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ 性能优化
|
||||
|
||||
```csharp
|
||||
// ✅ 好:使用缓存
|
||||
private string? _cachedData;
|
||||
private DateTime _cacheTime;
|
||||
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
if (_cachedData != null &&
|
||||
DateTime.Now - _cacheTime < TimeSpan.FromMinutes(5))
|
||||
{
|
||||
return _cachedData;
|
||||
}
|
||||
|
||||
_cachedData = await FetchDataAsync();
|
||||
_cacheTime = DateTime.Now;
|
||||
return _cachedData;
|
||||
}
|
||||
|
||||
// ❌ 差:每次都重新获取
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
return await FetchDataAsync(); // 浪费资源
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 异步编程
|
||||
|
||||
```csharp
|
||||
// ✅ 好:使用 async/await
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
await FetchDataAsync();
|
||||
}
|
||||
|
||||
// ❌ 差:阻塞线程
|
||||
public override Task UpdateAsync()
|
||||
{
|
||||
FetchDataAsync().Wait(); // 阻塞!
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 错误处理
|
||||
|
||||
```csharp
|
||||
// ✅ 好:捕获并记录异常
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await FetchDataAsync();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Network error");
|
||||
DisplayError("网络错误");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Unexpected error");
|
||||
DisplayError("未知错误");
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差:忽略异常
|
||||
public override async Task UpdateAsync()
|
||||
{
|
||||
await FetchDataAsync(); // 异常会传播到宿主
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 资源管理
|
||||
|
||||
```csharp
|
||||
// ✅ 好:正确释放资源
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
private HttpClient? _httpClient;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_httpClient?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差:不释放资源
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
private HttpClient _httpClient = new(); // 内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [设置系统](03-设置系统.md) - 管理组件配置
|
||||
- [主题与外观](04-主题外观.md) - 适配主题
|
||||
- [ComponentBase API](../03-API参考/03-组件API.md) - API 详细文档
|
||||
- [天气组件案例](../04-实战案例/01-天气组件.md) - 完整实战
|
||||
858
docs/01-插件开发/02-核心概念/03-设置系统.md
Normal file
858
docs/01-插件开发/02-核心概念/03-设置系统.md
Normal file
@@ -0,0 +1,858 @@
|
||||
# 设置系统
|
||||
|
||||
本文档介绍阑山桌面的设置系统,包括配置管理、持久化、设置页面和最佳实践。
|
||||
|
||||
## 设置系统概览
|
||||
|
||||
阑山桌面提供了统一的设置系统,用于管理应用、插件和组件的配置数据。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 💾 **自动持久化** - 设置自动保存到本地
|
||||
- 🔔 **变更通知** - 监听设置变更事件
|
||||
- 📁 **分域管理** - 按命名空间组织设置
|
||||
- 🔒 **类型安全** - 泛型 API 保证类型安全
|
||||
- 🎨 **UI 集成** - 轻松创建设置页面
|
||||
|
||||
### 设置存储位置
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\LanMountainDesktop\
|
||||
└── settings\
|
||||
├── app.json # 应用设置
|
||||
├── appearance.json # 外观设置
|
||||
├── plugins\
|
||||
│ ├── com.example.plugin1.json
|
||||
│ └── com.example.plugin2.json
|
||||
└── components\
|
||||
└── com.example.plugin1.component1.json
|
||||
```
|
||||
|
||||
## 使用设置服务
|
||||
|
||||
### 在插件中使用
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
private IPluginContext? _context;
|
||||
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
// 通过 context 访问设置
|
||||
var settings = context.Settings;
|
||||
|
||||
// 读取设置
|
||||
var apiKey = settings.GetValue("ApiKey", "");
|
||||
var refreshRate = settings.GetValue("RefreshRate", 60);
|
||||
var enableNotifications = settings.GetValue("EnableNotifications", true);
|
||||
|
||||
// 保存设置
|
||||
settings.SetValue("LastStartTime", DateTime.Now);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在组件中使用
|
||||
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
// 组件有自己的设置域
|
||||
// 自动命名空间:{PluginId}.{ComponentId}
|
||||
|
||||
// 读取设置
|
||||
var location = Settings.GetValue("Location", "北京");
|
||||
var useFahrenheit = Settings.GetValue("UseFahrenheit", false);
|
||||
|
||||
// 读取复杂对象
|
||||
var config = Settings.GetValue<ComponentConfig>("Config", new ComponentConfig());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void UpdateLocation(string location)
|
||||
{
|
||||
Location = location;
|
||||
|
||||
// 保存设置
|
||||
Settings.SetValue("Location", location);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 设置 API
|
||||
|
||||
### ISettingsService 接口
|
||||
|
||||
```csharp
|
||||
public interface ISettingsService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取设置值
|
||||
/// </summary>
|
||||
T GetValue<T>(string key, T defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// 设置值
|
||||
/// </summary>
|
||||
void SetValue<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 删除设置
|
||||
/// </summary>
|
||||
void Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 检查设置是否存在
|
||||
/// </summary>
|
||||
bool Contains(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有键
|
||||
/// </summary>
|
||||
IEnumerable<string> GetAllKeys();
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有设置
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 设置变更事件
|
||||
/// </summary>
|
||||
event EventHandler<SettingChangedEventArgs>? SettingChanged;
|
||||
}
|
||||
```
|
||||
|
||||
### 基本用法
|
||||
|
||||
```csharp
|
||||
// 读取设置
|
||||
var value = settings.GetValue<string>("Key", "DefaultValue");
|
||||
|
||||
// 保存设置
|
||||
settings.SetValue("Key", "NewValue");
|
||||
|
||||
// 删除设置
|
||||
settings.Remove("Key");
|
||||
|
||||
// 检查是否存在
|
||||
if (settings.Contains("Key"))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取所有键
|
||||
var keys = settings.GetAllKeys();
|
||||
|
||||
// 清空所有设置
|
||||
settings.Clear();
|
||||
```
|
||||
|
||||
## 支持的数据类型
|
||||
|
||||
### 基本类型
|
||||
|
||||
```csharp
|
||||
// 字符串
|
||||
settings.SetValue("Name", "张三");
|
||||
var name = settings.GetValue("Name", "");
|
||||
|
||||
// 数字
|
||||
settings.SetValue("Age", 25);
|
||||
var age = settings.GetValue("Age", 0);
|
||||
|
||||
settings.SetValue("Price", 99.99);
|
||||
var price = settings.GetValue("Price", 0.0);
|
||||
|
||||
// 布尔值
|
||||
settings.SetValue("Enabled", true);
|
||||
var enabled = settings.GetValue("Enabled", false);
|
||||
|
||||
// 日期时间
|
||||
settings.SetValue("LastUpdate", DateTime.Now);
|
||||
var lastUpdate = settings.GetValue("LastUpdate", DateTime.MinValue);
|
||||
|
||||
// 枚举
|
||||
settings.SetValue("Theme", AppTheme.Dark);
|
||||
var theme = settings.GetValue("Theme", AppTheme.Light);
|
||||
```
|
||||
|
||||
### 复杂对象
|
||||
|
||||
```csharp
|
||||
// 定义配置类
|
||||
public class WeatherConfig
|
||||
{
|
||||
public string City { get; set; } = "北京";
|
||||
public string Unit { get; set; } = "Celsius";
|
||||
public int RefreshInterval { get; set; } = 10;
|
||||
public List<string> FavoriteCities { get; set; } = new();
|
||||
}
|
||||
|
||||
// 保存对象
|
||||
var config = new WeatherConfig
|
||||
{
|
||||
City = "上海",
|
||||
Unit = "Celsius",
|
||||
RefreshInterval = 15,
|
||||
FavoriteCities = new List<string> { "北京", "上海", "广州" }
|
||||
};
|
||||
settings.SetValue("WeatherConfig", config);
|
||||
|
||||
// 读取对象
|
||||
var savedConfig = settings.GetValue<WeatherConfig>(
|
||||
"WeatherConfig",
|
||||
new WeatherConfig()
|
||||
);
|
||||
```
|
||||
|
||||
### 集合类型
|
||||
|
||||
```csharp
|
||||
// 列表
|
||||
var favoriteColors = new List<string> { "红色", "蓝色", "绿色" };
|
||||
settings.SetValue("FavoriteColors", favoriteColors);
|
||||
var colors = settings.GetValue<List<string>>("FavoriteColors", new List<string>());
|
||||
|
||||
// 字典
|
||||
var preferences = new Dictionary<string, string>
|
||||
{
|
||||
["Language"] = "zh-CN",
|
||||
["Timezone"] = "Asia/Shanghai"
|
||||
};
|
||||
settings.SetValue("Preferences", preferences);
|
||||
var prefs = settings.GetValue<Dictionary<string, string>>(
|
||||
"Preferences",
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
```
|
||||
|
||||
## 监听设置变更
|
||||
|
||||
### 订阅变更事件
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
private ISettingsService? _settings;
|
||||
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
_settings = context.Settings;
|
||||
|
||||
// 订阅设置变更事件
|
||||
_settings.SettingChanged += OnSettingChanged;
|
||||
}
|
||||
|
||||
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
|
||||
{
|
||||
// e.Key - 变更的设置键
|
||||
// e.OldValue - 旧值
|
||||
// e.NewValue - 新值
|
||||
|
||||
if (e.Key == "ApiKey")
|
||||
{
|
||||
var newApiKey = e.NewValue as string;
|
||||
_logger.LogInformation($"API Key changed to: {newApiKey}");
|
||||
|
||||
// 重新初始化服务
|
||||
ReinitializeService(newApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
// 取消订阅(防止内存泄漏)
|
||||
if (_settings != null)
|
||||
{
|
||||
_settings.SettingChanged -= OnSettingChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在组件中监听
|
||||
|
||||
```csharp
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
// 监听设置变更
|
||||
Settings.SettingChanged += OnSettingChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnSettingChanged(object? sender, SettingChangedEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case "Location":
|
||||
Location = e.NewValue as string ?? "北京";
|
||||
_ = RefreshWeatherAsync();
|
||||
break;
|
||||
|
||||
case "UseFahrenheit":
|
||||
UseFahrenheit = (bool)(e.NewValue ?? false);
|
||||
OnPropertyChanged(nameof(DisplayTemperature));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
// 取消订阅
|
||||
Settings.SettingChanged -= OnSettingChanged;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 创建设置页面
|
||||
|
||||
### 步骤 1: 创建设置页视图
|
||||
|
||||
创建 `Settings/MyPluginSettingsPage.axaml`:
|
||||
|
||||
```xml
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:MyPlugin.ViewModels"
|
||||
x:Class="MyPlugin.Settings.MyPluginSettingsPage"
|
||||
x:DataType="vm:MyPluginSettingsViewModel">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="16" Margin="24">
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<TextBlock Text="天气插件设置"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- 基本设置 -->
|
||||
<Border Background="{DynamicResource CardBackgroundBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="16"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<!-- 分组标题 -->
|
||||
<TextBlock Text="基本设置"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold" />
|
||||
|
||||
<!-- 城市设置 -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="城市:" />
|
||||
<TextBox Text="{Binding Location, Mode=TwoWay}"
|
||||
Watermark="输入城市名称"
|
||||
Width="300"
|
||||
HorizontalAlignment="Left" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- API Key -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="API Key:" />
|
||||
<TextBox Text="{Binding ApiKey, Mode=TwoWay}"
|
||||
Watermark="输入 API Key"
|
||||
PasswordChar="●"
|
||||
Width="300"
|
||||
HorizontalAlignment="Left" />
|
||||
<TextBlock Text="从 https://api.weather.com 获取"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<Border Background="{DynamicResource CardBackgroundBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="16"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<TextBlock Text="显示设置"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold" />
|
||||
|
||||
<!-- 温度单位 -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="温度单位:" />
|
||||
<ComboBox SelectedIndex="{Binding TemperatureUnitIndex, Mode=TwoWay}"
|
||||
Width="200"
|
||||
HorizontalAlignment="Left">
|
||||
<ComboBoxItem Content="摄氏度 (°C)" />
|
||||
<ComboBoxItem Content="华氏度 (°F)" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 刷新间隔 -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="刷新间隔 (分钟):" />
|
||||
<NumericUpDown Value="{Binding RefreshInterval, Mode=TwoWay}"
|
||||
Minimum="5"
|
||||
Maximum="60"
|
||||
Increment="5"
|
||||
Width="200"
|
||||
HorizontalAlignment="Left" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 开关选项 -->
|
||||
<CheckBox IsChecked="{Binding ShowIcon, Mode=TwoWay}"
|
||||
Content="显示天气图标" />
|
||||
|
||||
<CheckBox IsChecked="{Binding EnableNotifications, Mode=TwoWay}"
|
||||
Content="启用天气预警通知" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 高级设置 -->
|
||||
<Border Background="{DynamicResource CardBackgroundBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="16"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<TextBlock Text="高级设置"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold" />
|
||||
|
||||
<!-- 收藏城市 -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="收藏城市:" />
|
||||
<ListBox ItemsSource="{Binding FavoriteCities}"
|
||||
Height="150"
|
||||
Width="300"
|
||||
HorizontalAlignment="Left" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBox x:Name="NewCityTextBox"
|
||||
Watermark="添加城市"
|
||||
Width="200" />
|
||||
<Button Content="添加"
|
||||
Command="{Binding AddCityCommand}"
|
||||
CommandParameter="{Binding #NewCityTextBox.Text}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button Content="保存"
|
||||
Command="{Binding SaveCommand}"
|
||||
IsDefault="True" />
|
||||
<Button Content="重置"
|
||||
Command="{Binding ResetCommand}" />
|
||||
<Button Content="测试连接"
|
||||
Command="{Binding TestConnectionCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
Foreground="{Binding StatusColor}"
|
||||
IsVisible="{Binding !!StatusMessage}" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
```
|
||||
|
||||
### 步骤 2: 创建设置页视图模型
|
||||
|
||||
创建 `ViewModels/MyPluginSettingsViewModel.cs`:
|
||||
|
||||
```csharp
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace MyPlugin.ViewModels;
|
||||
|
||||
public partial class MyPluginSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MyPluginSettingsViewModel(
|
||||
ISettingsService settings,
|
||||
ILogger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
|
||||
// 加载设置
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
// === 属性 ===
|
||||
|
||||
[ObservableProperty]
|
||||
private string _location = "北京";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _apiKey = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _temperatureUnitIndex = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _refreshInterval = 10;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showIcon = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _enableNotifications = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<string> _favoriteCities = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _statusMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusColor = "Green";
|
||||
|
||||
// === 命令 ===
|
||||
|
||||
/// <summary>
|
||||
/// 保存命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 保存所有设置
|
||||
_settings.SetValue("Location", Location);
|
||||
_settings.SetValue("ApiKey", ApiKey);
|
||||
_settings.SetValue("UseFahrenheit", TemperatureUnitIndex == 1);
|
||||
_settings.SetValue("RefreshInterval", RefreshInterval);
|
||||
_settings.SetValue("ShowIcon", ShowIcon);
|
||||
_settings.SetValue("EnableNotifications", EnableNotifications);
|
||||
_settings.SetValue("FavoriteCities", FavoriteCities.ToList());
|
||||
|
||||
ShowStatus("设置已保存", "Green");
|
||||
_logger.LogInformation("Settings saved successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowStatus($"保存失败: {ex.Message}", "Red");
|
||||
_logger.LogError(ex, "Failed to save settings");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void Reset()
|
||||
{
|
||||
// 重新加载设置
|
||||
LoadSettings();
|
||||
ShowStatus("已重置到上次保存的值", "Orange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加城市命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void AddCity(string? city)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(city))
|
||||
return;
|
||||
|
||||
if (!FavoriteCities.Contains(city))
|
||||
{
|
||||
FavoriteCities.Add(city);
|
||||
ShowStatus($"已添加城市: {city}", "Green");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowStatus("城市已存在", "Orange");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试连接命令
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
ShowStatus("正在测试连接...", "Blue");
|
||||
|
||||
try
|
||||
{
|
||||
// 测试 API 连接
|
||||
var result = await TestWeatherApiAsync(ApiKey, Location);
|
||||
|
||||
if (result)
|
||||
{
|
||||
ShowStatus("连接成功!", "Green");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowStatus("连接失败,请检查 API Key 和城市名称", "Red");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowStatus($"测试失败: {ex.Message}", "Red");
|
||||
_logger.LogError(ex, "Connection test failed");
|
||||
}
|
||||
}
|
||||
|
||||
// === 辅助方法 ===
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
Location = _settings.GetValue("Location", "北京");
|
||||
ApiKey = _settings.GetValue("ApiKey", "");
|
||||
|
||||
var useFahrenheit = _settings.GetValue("UseFahrenheit", false);
|
||||
TemperatureUnitIndex = useFahrenheit ? 1 : 0;
|
||||
|
||||
RefreshInterval = _settings.GetValue("RefreshInterval", 10);
|
||||
ShowIcon = _settings.GetValue("ShowIcon", true);
|
||||
EnableNotifications = _settings.GetValue("EnableNotifications", true);
|
||||
|
||||
var cities = _settings.GetValue<List<string>>("FavoriteCities", new List<string>());
|
||||
FavoriteCities = new ObservableCollection<string>(cities);
|
||||
}
|
||||
|
||||
private void ShowStatus(string message, string color)
|
||||
{
|
||||
StatusMessage = message;
|
||||
StatusColor = color;
|
||||
|
||||
// 3 秒后清除状态
|
||||
Task.Delay(3000).ContinueWith(_ =>
|
||||
{
|
||||
StatusMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<bool> TestWeatherApiAsync(string apiKey, string location)
|
||||
{
|
||||
// 实际实现中测试 API 连接
|
||||
await Task.Delay(1000);
|
||||
return !string.IsNullOrEmpty(apiKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3: 注册设置页
|
||||
|
||||
在插件入口注册:
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var settingsRegistry = context.Services
|
||||
.GetService<ISettingsPageRegistry>();
|
||||
|
||||
if (settingsRegistry != null)
|
||||
{
|
||||
// 注册设置页
|
||||
settingsRegistry.RegisterPage(
|
||||
title: "天气插件",
|
||||
category: "插件",
|
||||
icon: "avares://MyPlugin/Assets/settings-icon.png",
|
||||
pageFactory: () =>
|
||||
{
|
||||
var viewModel = new MyPluginSettingsViewModel(
|
||||
context.Settings,
|
||||
context.Logger
|
||||
);
|
||||
|
||||
return new MyPluginSettingsPage
|
||||
{
|
||||
DataContext = viewModel
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
context.Logger.LogInformation("Settings page registered");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 设置最佳实践
|
||||
|
||||
### ✅ 提供默认值
|
||||
|
||||
```csharp
|
||||
// ✅ 好:提供合理的默认值
|
||||
var timeout = settings.GetValue("Timeout", 30);
|
||||
var apiUrl = settings.GetValue("ApiUrl", "https://api.example.com");
|
||||
|
||||
// ❌ 差:不提供默认值
|
||||
var timeout = settings.GetValue<int>("Timeout", 0); // 0 可能不合理
|
||||
```
|
||||
|
||||
### ✅ 验证设置值
|
||||
|
||||
```csharp
|
||||
// ✅ 好:验证设置值
|
||||
public void SetRefreshInterval(int minutes)
|
||||
{
|
||||
if (minutes < 1 || minutes > 60)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(minutes),
|
||||
"刷新间隔必须在 1-60 分钟之间"
|
||||
);
|
||||
}
|
||||
|
||||
RefreshInterval = minutes;
|
||||
Settings.SetValue("RefreshInterval", minutes);
|
||||
}
|
||||
|
||||
// ❌ 差:不验证
|
||||
public void SetRefreshInterval(int minutes)
|
||||
{
|
||||
Settings.SetValue("RefreshInterval", minutes); // 可能是非法值
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 使用类型化配置
|
||||
|
||||
```csharp
|
||||
// ✅ 好:使用强类型配置类
|
||||
public class PluginConfig
|
||||
{
|
||||
public string ApiKey { get; set; } = "";
|
||||
public string Location { get; set; } = "北京";
|
||||
public int RefreshInterval { get; set; } = 10;
|
||||
public bool EnableNotifications { get; set; } = true;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ApiKey))
|
||||
throw new InvalidOperationException("API Key is required");
|
||||
|
||||
if (RefreshInterval < 1 || RefreshInterval > 60)
|
||||
throw new ArgumentOutOfRangeException(nameof(RefreshInterval));
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
var config = settings.GetValue<PluginConfig>("Config", new PluginConfig());
|
||||
config.Validate();
|
||||
|
||||
// ❌ 差:分散的设置键
|
||||
var apiKey = settings.GetValue<string>("ApiKey", "");
|
||||
var location = settings.GetValue<string>("Location", "");
|
||||
var interval = settings.GetValue<int>("RefreshInterval", 10);
|
||||
```
|
||||
|
||||
### ✅ 取消事件订阅
|
||||
|
||||
```csharp
|
||||
// ✅ 好:在 Dispose 中取消订阅
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
Settings.SettingChanged += OnSettingChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Settings.SettingChanged -= OnSettingChanged;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差:忘记取消订阅(内存泄漏)
|
||||
public class MyComponent : ComponentBase
|
||||
{
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
Settings.SettingChanged += OnSettingChanged;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
// 没有 Dispose,导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 设置迁移
|
||||
|
||||
### 版本升级时的设置迁移
|
||||
|
||||
```csharp
|
||||
public class MyPlugin : IPlugin
|
||||
{
|
||||
public async Task InitializeAsync(IPluginContext context)
|
||||
{
|
||||
var settings = context.Settings;
|
||||
|
||||
// 检查设置版本
|
||||
var settingsVersion = settings.GetValue("SettingsVersion", 1);
|
||||
|
||||
if (settingsVersion < 2)
|
||||
{
|
||||
// 迁移到版本 2
|
||||
MigrateToV2(settings);
|
||||
settings.SetValue("SettingsVersion", 2);
|
||||
}
|
||||
|
||||
if (settingsVersion < 3)
|
||||
{
|
||||
// 迁移到版本 3
|
||||
MigrateToV3(settings);
|
||||
settings.SetValue("SettingsVersion", 3);
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateToV2(ISettingsService settings)
|
||||
{
|
||||
// 例如:重命名设置键
|
||||
if (settings.Contains("OldKey"))
|
||||
{
|
||||
var value = settings.GetValue<string>("OldKey", "");
|
||||
settings.SetValue("NewKey", value);
|
||||
settings.Remove("OldKey");
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateToV3(ISettingsService settings)
|
||||
{
|
||||
// 例如:更改数据格式
|
||||
var oldFormat = settings.GetValue<string>("Location", "");
|
||||
var newFormat = new LocationConfig
|
||||
{
|
||||
City = oldFormat,
|
||||
Country = "中国"
|
||||
};
|
||||
settings.SetValue("LocationConfig", newFormat);
|
||||
settings.Remove("Location");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [主题与外观](04-主题外观.md) - 适配主题系统
|
||||
- [插件通信](05-插件通信.md) - 插件间协作
|
||||
- [设置 API 详解](../03-API参考/04-设置API.md) - API 参考文档
|
||||
- [创建设置页](../04-实战案例/04-开发设置页.md) - 实战案例
|
||||
Reference in New Issue
Block a user