教程:插件开发

开始之前

我们假定你已经

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

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

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

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

新建 .NET 6 类库

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

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!--需要基于 `net6.0-windows10.0.18362` 才能使插件正常通过编译-->
    <TargetFramework>net6.0-windows10.0.18362</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <!--必须启用动态加载-->
    <EnableDynamicLoading>true</EnableDynamicLoading>
    <!--必须指定生成目标为x64-->
    <PlatformTarget>x64</PlatformTarget>
    <!--将PDB嵌入到生成的程序集内-->
    <DebugType>embedded</DebugType>
    <UseWPF>true</UseWPF>
    <!--不能生成为引用程序集-->
    <ProduceReferenceAssembly>False</ProduceReferenceAssembly>
    <Platforms>AnyCPU;x64</Platforms>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="bin\**" />
    <EmbeddedResource Remove="bin\**" />
    <None Remove="bin\**" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\DGP.Genshin\DGP.Genshin.csproj">
      <Private>false</Private>
      <ExcludeAssets>runtime</ExcludeAssets>
    </ProjectReference>
  </ItemGroup>

  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <!--将生成的主程序集复制到Plugins文件夹内-->
    <Exec Command="xcopy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)Build\Debug\net6.0-windows10.0.18362.0\Plugins&quot; /y" />
  </Target>

</Project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

添加 SnapGenshinPlugin 特性

Snap Genshin 凭借

DGP.Genshin.Core.Plugins.SnapGenshinPluginAttribute
1

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

[assembly:SnapGenshinPlugin]
1

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

实现 IPlugin 主接口

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

DGP.Genshin.Core.Plugins.IPlugin
1

接口

using DGP.Genshin.Core.Plugins;
using System;

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

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

ImportPage 添加导航页面

此操作是可选的

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

using Snap.Core.DependencyInjection;
using System.Windows.Controls;

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

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

using DGP.Genshin.Core.Plugins;
using System;

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

Snap Genshin 基于

DGP.Genshin.Core.Plugins.ImportPageAttribute
1

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

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

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

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

依赖关系注入

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

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

通过

using Snap.Core.DependencyInjection;
1

以使用这些特性

using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Core.DependencyInjection;
using System.Collections.Generic;
using System.Windows.Media;

namespace DGP.Genshin.Sample.Plugin
{
    [ViewModel(InjectAs.Transient)]
    internal class SampleViewModel : ObservableObject
    {
        private IEnumerable<object> icons;

        public IEnumerable<object> Icons { get => icons; set => SetProperty(ref icons, value); }

        public SampleViewModel()
        {
            icons = new();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

进阶

Snap Genshin 生命周期感知

IAppStartUp 应用程序启动事件感知

在你的插件的主类上实现

DGP.Genshin.Core.LifeCycle.IAppStartUp
1

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

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

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

AppExitingMessage 应用程序退出事件感知

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

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

    接口

  • 并在构造器中注入

    CommunityToolkit.Mvvm.Messaging.IMessaenger
    
    1

    实例

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

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

异步命令

CommunityToolkit.Mvvm.Input.AsyncRelayCommand
1

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

异步 ICommand 异常捕获

我们提供了

DGP.Genshin.Factory.Abstraction.IAsyncRelayCommandFactory
1

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

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

保存设置

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

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

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

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

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

项目示例

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