Skip to content

CSharp Plugins

Ling edited this page Jul 12, 2026 · 4 revisions

C# DLL 插件

C# DLL 插件适合长期维护、需要 WPF UI、需要调用 .NET 库或实现复杂逻辑的插件。当前公开 SDK 契约位于 PCL.Plugin.Abstractions,NuGet 包 ID 为 PlainCraftLauncher.Plugin.Abstractions

SDK 包

插件项目引用 SDK 包:

<PackageReference Include="PlainCraftLauncher.Plugin.Abstractions" Version="1.2.1" ExcludeAssets="runtime" />

包地址:https://www.nuget.org/packages/PlainCraftLauncher.Plugin.Abstractions/

注意:包 ID 是 PlainCraftLauncher.Plugin.Abstractions,代码中使用的命名空间仍是:

using PCL.Plugin.Abstractions;

如果 SDK 包尚未发布到 NuGet,可在本地开发时引用源码仓库中的 PCL.Plugin.Abstractions.csproj

项目文件

推荐项目文件:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
    <OutputType>Library</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PlainCraftLauncher.Plugin.Abstractions" Version="1.2.1" ExcludeAssets="runtime" />
  </ItemGroup>
</Project>

ExcludeAssets="runtime" 的目的,是避免插件输出目录携带 SDK 程序集。宿主会提供同一份 PCL.Plugin.Abstractions.dll,插件应复用宿主加载的契约程序集。

入口类

插件入口必须实现 IPclPlugin

public interface IPclPlugin
{
    Task LoadAsync(IPluginContext context, CancellationToken cancellationToken = default);
    Task UnloadAsync(CancellationToken cancellationToken = default);
}

一个插件程序集应有且仅有一个入口类型使用 [Plugin] 标注。入口类型必须是 public,并提供公共无参构造函数。

推荐继承 PclPluginBase,它会保存 Context,并创建以插件名为 category 的 Log

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using PCL.Plugin.Abstractions;

namespace Example.Tools;

[Plugin(
    id: "com.example.tools",
    name: "Example Tools",
    version: "1.0.0.0",
    Author = "Example",
    Description = "Adds a tool page.",
    MinApiVersion = "1.2.1.0",
    Capabilities = PluginCapabilities.ContributeTools,
    LoadTiming = PluginLoadTiming.WindowCreated)]
public sealed class ExampleToolsPlugin : PclPluginBase
{
    private IDisposable? _toolsPanel;

    public override Task LoadAsync(IPluginContext context, CancellationToken cancellationToken = default)
    {
        base.LoadAsync(context, cancellationToken);

        var ui = context.Host.Ui ?? throw new InvalidOperationException("UI API is unavailable.");
        _toolsPanel = ui.ContributeToolsPanel(new ToolsPanelDescriptor
        {
            Id = "example-tools",
            Title = "Example Tools",
            Group = "Plugins",
            Icon = "lucide/wrench",
            Order = 100,
            Factory = () => new TextBlock { Text = "Hello from C# plugin." }
        });

        return Task.CompletedTask;
    }

    public override Task UnloadAsync(CancellationToken cancellationToken = default)
    {
        _toolsPanel?.Dispose();
        _toolsPanel = null;
        return Task.CompletedTask;
    }
}

plugin.json[Plugin]

C# 插件包仍需要 plugin.json

{
  "id": "com.example.tools",
  "name": "Example Tools",
  "version": "1.0.0.0",
  "author": "Example",
  "runtime": "dotnet",
  "entryAssembly": "Example.Tools.dll",
  "minApiVersion": "1.2.1.0",
  "capabilities": ["ContributeTools"]
}

plugin.json 用于安装校验、启用列表展示和定位入口 DLL。真正加载 C# 入口时,加载器会扫描 DLL 中带 [Plugin]IPclPlugin 类型,并使用该 attribute 生成运行时清单。因此两处的 idnameversioncapabilities 应保持一致。

生命周期

LoadAsync

  • 读取配置。
  • 注册 UI、URI、事件订阅或扩展点贡献。
  • 启动轻量后台任务。
  • 不要长时间同步阻塞;加载器最多等待 30 秒。

UnloadAsync

  • 释放所有 IDisposable 注册项。
  • 取消事件订阅。
  • 停止后台任务。
  • 关闭文件句柄、本地进程或网络连接。

卸载逻辑应允许字段为空、允许重复调用,也要能处理 LoadAsync 只执行了一半就失败的状态。

获取上下文

IPluginContext 提供:

属性 说明
Manifest 当前插件运行时清单
DataDirectory 插件专属数据目录,已创建,可直接读写
Host 宿主 API 门面
HostStopping 宿主关闭或插件卸载取消令牌

示例:

var logger = context.Host.Core.GetLogger("main");
logger.Info($"Data directory: {context.DataDirectory}");

var enabled = context.Host.Config.GetBool("enabled", true);
context.Host.Config.Set("enabled", enabled);

常见注册项管理

多个注册项可以集中保存:

private readonly List<IDisposable> _registrations = [];

public override Task LoadAsync(IPluginContext context, CancellationToken cancellationToken = default)
{
    var events = context.Host.Events.Subscribe<MyEvent>("pcl:plugin:example", OnEventAsync);
    _registrations.Add(events);
    return Task.CompletedTask;
}

public override Task UnloadAsync(CancellationToken cancellationToken = default)
{
    for (var i = _registrations.Count - 1; i >= 0; i--)
        _registrations[i].Dispose();

    _registrations.Clear();
    return Task.CompletedTask;
}

不建议做的事

  • 不要直接引用启动器内部项目。
  • 不要把 PCL.Plugin.Abstractions.dll 放进插件包。
  • 不要在 LoadAsync 中执行长时间同步阻塞。
  • 不要在非 UI 线程直接操作 WPF 控件;使用 context.Host.Ui.InvokeOnUi(...)
  • 不要把插件运行数据写到启动器程序目录或插件安装目录;使用 context.DataDirectory

Clone this wiki locally