教程:插件开发

开始之前

我们假定你已经

  • 掌握了基本的WPF开发知识与技巧
  • 对 依赖注入/控制反转 有一定的了解

开发适用于 Snap Genshin 的插件程序集

Snap Genshin 的插件系统设计 使得开发者能够开发权限较高的插件
可以调整 Snap Genshin 的默认行为,修改已经存在的服务与视图
可以进行任何类型的 服务/工厂/视图模型/视图 注册

开发插件前,你需要 Clone 整个 Snap Genshin 仓库到本地
完整克隆的方法请参阅 开发人员文档
Clone 完成后,使用 Visual Studio 2022 打开 Snap.Genshin.sln 文件

新建 .NET 6 类库

我们推荐你在 Plugins 文件夹下新建项目,这样可以与我们的教程高度匹配
否则,可能需要按要求修改一些相对路径
新建项目完成后,修改项目的项目文件 *.csproj
下面给出示例xml

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <!--需要基于 `net6.0-windows10.0.18362` 才能使插件正常通过编译-->
  4. <TargetFramework>net6.0-windows10.0.18362</TargetFramework>
  5. <ImplicitUsings>disable</ImplicitUsings>
  6. <Nullable>enable</Nullable>
  7. <!--必须启用动态加载-->
  8. <EnableDynamicLoading>true</EnableDynamicLoading>
  9. <!--必须指定生成目标为x64-->
  10. <PlatformTarget>x64</PlatformTarget>
  11. <!--将PDB嵌入到生成的程序集内-->
  12. <DebugType>embedded</DebugType>
  13. <UseWPF>true</UseWPF>
  14. <!--不能生成为引用程序集-->
  15. <ProduceReferenceAssembly>False</ProduceReferenceAssembly>
  16. <Platforms>AnyCPU;x64</Platforms>
  17. </PropertyGroup>
  18. <ItemGroup>
  19. <Compile Remove="bin\**" />
  20. <EmbeddedResource Remove="bin\**" />
  21. <None Remove="bin\**" />
  22. </ItemGroup>
  23. <ItemGroup>
  24. <ProjectReference Include="..\..\DGP.Genshin\DGP.Genshin.csproj">
  25. <Private>false</Private>
  26. <ExcludeAssets>runtime</ExcludeAssets>
  27. </ProjectReference>
  28. </ItemGroup>
  29. <Target Name="PostBuild" AfterTargets="PostBuildEvent">
  30. <!--将生成的主程序集复制到Plugins文件夹内-->
  31. <Exec Command="xcopy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)Build\Debug\net6.0-windows10.0.18362.0\Plugins&quot; /y" />
  32. </Target>
  33. </Project>

添加 SnapGenshinPlugin 特性

Snap Genshin 凭借

  1. DGP.Genshin.Core.Plugins.SnapGenshinPluginAttribute

进行插件程序集与普通程序集的区分
只有包含了该特性的程序集才会被Snap Genshin 认为是插件而加载

  1. [assembly:SnapGenshinPlugin]

可以将该行代码放在项目的任何 C# 文件中,注意:assembly 特性不能放置在 名称空间中
较为合理的位置是开发者熟悉的 AssemblyInfo.cs 文件

实现 IPlugin 主接口

在上述工作完成后,你需要主类以实现

  1. DGP.Genshin.Core.Plugins.IPlugin

接口

  1. using DGP.Genshin.Core.Plugins;
  2. using System;
  3. namespace DGP.Genshin.Sample.Plugin
  4. {
  5. /// <summary>
  6. /// 插件实例实现
  7. /// </summary>
  8. public class SamplePlugin : IPlugin
  9. {
  10. public string Name => "插件名称";
  11. public string Description => "插件描述";
  12. public string Author => "DGP Studio";
  13. public Version Version => new("0.0.0.1");
  14. [Obsolete] public bool IsEnabled { get; set; }
  15. }
  16. }

此时若生成项目,则 Snap Genshin 已经能在插件管理页面中发现新的插件

ImportPage 添加导航页面

此操作是可选的

如果需要添加可导航的新页面则需要准备好一个新的Page
对应的 xaml 文件中的代码在此省略

  1. using Snap.Core.DependencyInjection;
  2. using System.Windows.Controls;
  3. namespace DGP.Genshin.Sample.Plugin
  4. {
  5. [View]
  6. public partial class SamplePage : Page
  7. {
  8. public SamplePage(SmapleViewModel vm)
  9. {
  10. DataContext = vm;
  11. InitializeComponent();
  12. }
  13. }
  14. }

并在实现了插件的主类上标注对应的 [ImportPage] 特性

  1. using DGP.Genshin.Core.Plugins;
  2. using System;
  3. namespace DGP.Genshin.Sample.Plugin
  4. {
  5. /// <summary>
  6. /// 插件实例实现
  7. /// </summary>
  8. [ImportPage(typeof(SamplePage), "插件页面名称", "\uE734")]
  9. public class SamplePlugin : IPlugin
  10. {
  11. ···
  12. }
  13. }

Snap Genshin 基于

  1. DGP.Genshin.Core.Plugins.ImportPageAttribute

特性发现插件注册的导航页面

  1. [ImportPage(typeof(SamplePage), "插件页面名称", "\uE734")]

一个插件可以通过此方法注册多个导航页面

  • 第三个参数是图标的字符串形式,详见 segoe-fluent-icons-font
  • 也可以使用另一个 ImportPage 的构造函数,采用了 IconFactory 类作为第三个参数

此时生成程序集可以发现 Snap Genshin 的左侧导航栏已经包含了新的导航页面入口

依赖关系注入

由于 Snap Genshin 实现了依赖注入,你也完全可以依赖于这一套系统来注入服务或视图模型
例如:

  • 可以在服务类上添加 [Service] 特性
  • 在视图模型上添加 [ViewModel] 特性
  • 在页面上添加 [View] 特性
  • 在工厂类上添加 [Factory]特性

通过

  1. using Snap.Core.DependencyInjection;

以使用这些特性

  1. using CommunityToolkit.Mvvm.ComponentModel;
  2. using Snap.Core.DependencyInjection;
  3. using System.Collections.Generic;
  4. using System.Windows.Media;
  5. namespace DGP.Genshin.Sample.Plugin
  6. {
  7. [ViewModel(InjectAs.Transient)]
  8. internal class SampleViewModel : ObservableObject
  9. {
  10. private IEnumerable<object> icons;
  11. public IEnumerable<object> Icons { get => icons; set => SetProperty(ref icons, value); }
  12. public SampleViewModel()
  13. {
  14. icons = new();
  15. }
  16. }
  17. }

进阶

Snap Genshin 生命周期感知

IAppStartUp 应用程序启动事件感知

在你的插件的主类上实现

  1. DGP.Genshin.Core.LifeCycle.IAppStartUp

接口,该接口提供了 Happen(IContainer) 方法
以便在程序启动时对你的插件注入的类进行操作

IContainer 提供 Find() 方法 以便插件发现注入的类

该容器是已经定型的容器,仅能从中发现你的插件注入的服务

AppExitingMessage 应用程序退出事件感知

  • 在任何你注入的类中 实现

    1. CommunityToolkit.Mvvm.Messaging.IRecipient<DGP.Genshin.Message.AppExitingMessage>

    接口

  • 并在构造器中注入

    1. CommunityToolkit.Mvvm.Messaging.IMessaenger

    实例

  • 在构造器中 调用 IMessenger 的相关注册消息方法

  • IRecipient<AppExitingMessage>Receive 方法中就可以处理应用程序退出时的逻辑了

异步命令

  1. CommunityToolkit.Mvvm.Input.AsyncRelayCommand

默认不会处理或打印异常的详细信息,使得调试异步命令的错误较为困难

异步 ICommand 异常捕获

我们提供了

  1. DGP.Genshin.Factory.Abstraction.IAsyncRelayCommandFactory

接口,以便创建能够处理异步操作异常的命令
当由此接口创建的命令发生异常时
会在控制台打印异常的详细调用堆栈信息
在发行版中更会将异常信息上传

可以通过依赖注入的方式获得此接口的默认实现

保存设置

与应用程序设置储存到一起

在你访问设置的类中实例化一个

  1. DGP.Genshin.Service.Abstraction.Setting.SettingDefinition<T>

的静态只读变量
该类提供了方便的方法供你储存与读取设置
设置项会在程序启动时读取完成,会在程序退出的最后保存

在注册新的设置项前需要前往
DGP.Genshin.Service.Abstraction.Setting.Setting2
静态类中查看已有的设置项,避免与已有的注册项冲突

项目示例

关于详细的项目示例,请参考 官方插件示例