mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* ava12升级 * Enable centralized package versioning Add <Project> and <PropertyGroup> with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> to Directory.Packages.props to enable centralized package version management across the repository. This allows package versions to be controlled from this single file instead of individual project files. * Migrate codebase to Avalonia 12 APIs Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing. * Migrate to Avalonia 12 and Plugin SDK v5 Upgrade project to the Avalonia 12 baseline and Plugin SDK v5: centralize Avalonia packages, remove legacy WebView.Avalonia usage (use NativeWebView/WebView2 EnvironmentRequested), and update Fluent/Material icon/package usages. Bump multiple package/project versions to 5.0.0 and Avalonia 12.0.1, update plugin template and README/docs to SDK v5, and add PLUGIN_SDK_V5_MIGRATION.md. Also fix runtime/behavior bugs: make DataLocationResolver use a fixed bootstrap launcher data path and avoid recursive ResolveDataRoot; add legacy-state handling and extraction in OobeStateService; and update component settings tests to reflect migrated storage (DB/backup) and reset cache for test reloads. Various csproj, tests, and docs updated to reflect the migration and ensure build/test compatibility. * Update icon glyphs and symbol mappings Replace and refine icon sources across settings pages and controls: many FAFontIconSource glyphs were updated to specific Seagull Fluent Icons codepoints, some FASymbolIconSource usages were replaced with FAFontIconSource, and a number of symbol-to-Symbol enum mappings were adjusted (e.g. "Bell" -> AlertOn, "Shield" -> ShieldLock). Also clarified a comment in SettingsWindow and fixed a trailing newline in StudySettingsPage. Changes standardize icon visuals and bridge FluentIcons glyphs into FluentAvalonia icon sources. * fix.修复合并产生的问题。
13 KiB
13 KiB
01-插件生命周期
理解插件的生命周期,是开发稳定可靠插件的基础。本文详细讲解插件从加载到卸载的完整过程。
🔄 生命周期概览
┌─────────────────────────────────────────────────────────────┐
│ 阑山桌面启动 │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 发现插件 │
│ - 扫描插件目录 │
│ - 解析 plugin.json │
│ - 验证 API 版本兼容性 │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 加载插件 │
│ - 创建 AssemblyLoadContext │
│ - 加载插件 DLL │
│ - 查找入口类(带 [PluginEntrance] 特性) │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 初始化(Initialize) │
│ - 调用 Plugin.Initialize() │
│ - 注册组件、设置页面、服务 │
│ - ⚠️ 此时 UI 尚未完全就绪 │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. 运行中 │
│ - 组件被添加到桌面 │
│ - 用户与组件交互 │
│ - 设置页面被打开 │
└───────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. 停用/卸载 │
│ - 用户禁用插件 │
│ - 或关闭阑山桌面 │
│ - 释放资源(当前版本无显式卸载回调) │
└─────────────────────────────────────────────────────────────┘
📋 各阶段详解
阶段 1:发现插件
时机: 阑山桌面启动时
过程:
- 扫描
%LOCALAPPDATA%\LanMountainDesktop\plugins\目录 - 读取每个
.laapp包中的plugin.json - 验证
apiVersion是否与宿主兼容 - 检查
id是否唯一
可能失败的原因:
plugin.json格式错误apiVersion不兼容id与其他插件冲突
阶段 2:加载插件
时机: 发现成功后
过程:
- 创建独立的
AssemblyLoadContext - 加载插件 DLL 及其依赖项
- 查找带有
[PluginEntrance]特性的类 - 实例化插件入口类
代码示例:
[PluginEntrance] // ← 这个特性标记入口类
public sealed class Plugin : PluginBase
{
// 插件实例在此阶段被创建
}
⚠️ 重要: 此阶段不要执行耗时操作,只应进行简单的字段初始化。
阶段 3:初始化(Initialize)
时机: 插件加载完成后
这是插件开发中最重要的阶段!
方法签名
public override void Initialize(
HostBuilderContext context, // 宿主构建上下文
IServiceCollection services) // 服务注册集合
可执行的操作
✅ 可以做的:
- 注册桌面组件
- 注册设置页面
- 注册服务到依赖注入容器
- 读取配置
- 初始化资源
❌ 不应该做的:
- 访问 UI 元素(UI 尚未就绪)
- 执行耗时阻塞操作
- 创建窗口或对话框
典型初始化代码
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 1. 注册服务
services.AddSingleton<IWeatherService, WeatherService>();
// 2. 注册桌面组件
services.AddPluginDesktopComponent<WeatherWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "MyPlugin.Weather",
DisplayName = "天气",
IconKey = "Weather",
Category = "工具",
MinWidthCells = 4,
MinHeightCells = 3
});
// 3. 注册设置页面
services.AddPluginSettingsSection(
"myplugin-settings",
"天气设置",
section => section
.AddToggle("auto_refresh", "自动刷新", defaultValue: true)
.AddNumber("interval", "刷新间隔(分钟)", defaultValue: 30),
iconKey: "Settings");
}
阶段 4:运行中
时机: 初始化完成后,直到插件被禁用或宿主关闭
特点:
- 组件可以被添加到桌面
- 用户可以与组件交互
- 设置页面可以被打开
- 定时器可以运行
组件生命周期
用户添加组件
│
▼
┌─────────────────┐
│ 创建组件实例 │ ← 调用构造函数
│ (Dependency │ 注入 IServiceProvider
│ Injection) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 组件初始化 │ ← 可在此时加载数据
│ (Loaded事件) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 渲染显示 │ ← 用户看到组件
└────────┬────────┘
│
┌────┴────┐
▼ ▼
用户交互 定时更新
│ │
└────┬────┘
│
▼
┌─────────────────┐
│ 组件移除 │ ← 用户删除组件
│ (Unloaded事件) │ 或关闭宿主
└─────────────────┘
阶段 5:停用/卸载
时机:
- 用户在设置中禁用插件
- 卸载插件
- 关闭阑山桌面
当前限制:
- SDK v5 暂无显式的卸载回调方法
- 资源释放依赖 .NET 垃圾回收
- 建议:
- 使用
IDisposable模式管理资源 - 在组件卸载事件中清理资源
- 使用
⏱️ 启动时序图
阑山桌面 插件系统 你的插件
│ │ │
│── 启动 ───────►│ │
│ │ │
│ │── 发现插件 ───►│
│ │ │ (读取 plugin.json)
│ │◄───────────────│
│ │ │
│ │── 加载 DLL ───►│
│ │ │ (AssemblyLoadContext)
│ │◄───────────────│
│ │ │
│ │── 创建实例 ───►│
│ │ │ (调用构造函数)
│ │◄───────────────│
│ │ │
│ │── Initialize ─►│
│ │ │ (注册组件/服务)
│ │◄───────────────│
│ │ │
│◄───────────────│ │
│ │ │
│── UI就绪 ─────►│ │
│ │ │
│ │── 用户添加组件 ─►│
│ │ │ (创建组件实例)
💡 最佳实践
1. Initialize 方法保持轻量
// ✅ 好的做法:快速注册
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<IDataService, DataService>();
services.AddPluginDesktopComponent<MyWidget>(options);
}
// ❌ 避免:耗时操作
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 不要这样做!
var data = FetchDataFromInternet().Result; // 阻塞!
services.AddSingleton(data);
}
2. 延迟加载数据
public class MyWidget : Border
{
private readonly IDataService _dataService;
public MyWidget(PluginDesktopComponentContext context)
{
_dataService = context.ServiceProvider.GetRequiredService<IDataService>();
// 在 Loaded 事件中加载数据,而不是构造函数
Loaded += async (_, _) =>
{
await LoadDataAsync();
};
}
private async Task LoadDataAsync()
{
var data = await _dataService.GetDataAsync();
// 更新 UI
}
}
3. 正确处理资源释放
public class MyWidget : Border, IDisposable
{
private readonly Timer _timer;
private bool _disposed;
public MyWidget()
{
_timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
// 在卸载时释放资源
Unloaded += (_, _) => Dispose();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_timer?.Dispose();
}
}
4. 避免循环依赖
// ❌ 避免:服务之间相互依赖
public class ServiceA
{
public ServiceA(ServiceB b) { } // 循环依赖风险
}
public class ServiceB
{
public ServiceB(ServiceA a) { }
}
// ✅ 好的做法:使用接口解耦
public class ServiceA
{
public ServiceA(IServiceB b) { }
}
🐛 常见问题
问题 1:Initialize 中访问 UI 报错
现象: InvalidOperationException 或空引用
原因: Initialize 在 UI 就绪前调用
解决: 延迟到组件创建后再访问 UI
问题 2:服务注册顺序问题
现象: 依赖注入找不到服务
原因: 服务注册顺序不正确
解决: 先注册服务,再注册依赖这些服务的组件
问题 3:插件加载慢
现象: 宿主启动变慢
原因: Initialize 中执行耗时操作
解决: 将耗时操作移到后台线程或延迟执行
📚 参考资源
🎯 下一步
理解生命周期后,学习如何创建桌面组件:
👉 02-桌面组件系统 - 创建可视化组件
最后更新:2026年4月