Skip to content

实战案例集

以下片段基于 SDK 0.1.0-alpha.4。示例中的服务应同步声明到 plugin.json

案例 1:注册一个命令并显示通知

csharp
using PCL.N.Plugin;

IPluginCommandService commands = context.Services.Require<IPluginCommandService>();
IPluginNotificationService notifications = context.Services.Require<IPluginNotificationService>();

context.Lifetime.Track(commands.Register(new PluginCommandDescriptor(
    id: "dev.example.tools.say-hello",
    title: "打个招呼",
    executeAsync: cancellationToken =>
    {
        cancellationToken.ThrowIfCancellationRequested();
        notifications.ShowInformation("你好,PCL N!");
        return Task.CompletedTask;
    },
    description: "显示一条测试通知。",
    icon: "lucide/message-circle")));

Manifest:

json
"services": {
  "required": {
    "pcl.commands": ">=0.1 <1.0",
    "pcl.notifications": ">=0.1 <1.0"
  }
},
"permissions": [
  { "id": "pcl.commands", "reason": "注册插件命令。" },
  { "id": "pcl.notifications", "reason": "显示命令执行结果。" }
]

案例 2:保存和读取插件设置

csharp
private static readonly PluginSettingKey<bool> EnabledKey = new("enabled");

IPluginSettingsStore settings = context.Services.Require<IPluginSettingsStore>();
bool enabled = await settings.GetAsync(EnabledKey, defaultValue: true, cancellationToken);

if (!enabled)
    context.Logger.Info("该功能已由用户关闭。");

await settings.SetAsync(EnabledKey, false, cancellationToken);

设置存储按插件隔离。Key 只能是简单名称,不能含 /\\..

案例 3:运行可追踪的后台任务

不要使用裸 Task.RunThreadTimerFileSystemWatcher。使用宿主任务服务:

csharp
IPluginTaskService tasks = context.Services.Require<IPluginTaskService>();

IPluginTaskRegistration task = tasks.SchedulePeriodic(
    "dev.example.tools.refresh",
    TimeSpan.FromMinutes(5),
    async stoppingToken =>
    {
        await RefreshCacheAsync(stoppingToken);
        context.Logger.Debug("缓存刷新完成。");
    });

context.Lifetime.Track(task);

插件停用时宿主会取消 Token、等待任务结束并释放注册项。

案例 4:读取 Minecraft 实例列表

csharp
if (context.Services.TryGet<IPluginInstanceReadService>(out var instances))
{
    foreach (PluginInstanceInfo instance in instances.ListInstances())
        context.Logger.Info($"{instance.Name}: {instance.InstanceDirectory}");
}
else
{
    context.Logger.Warn("当前宿主没有提供实例读取服务。");
}

Manifest 把它声明为可选服务,并声明读取权限:

json
"services": {
  "optional": { "pcl.instances.read": ">=0.1 <1.0" }
},
"permissions": [
  { "id": "pcl.instances.read", "reason": "列出实例名称以生成报告。" }
]

案例 5:注册一个设置页

csharp
using PCL.N.Plugin.Sdk;

if (context.Capabilities.TryGet<IPluginSettingsPageCapability>(out var pages))
{
    context.Lifetime.Track(pages.Register(new PluginSettingsPageDescriptor(
        "dev.example.tools.settings",
        "示例工具",
        "lucide/wrench",
        "示例工具设置",
        "管理示例工具的行为。",
        [
            new PluginSettingsHintDescriptor(
                "这是由第三方插件注册的页面。",
                PluginSettingsHintKind.Information)
        ])
    {
        Order = 500
    }));
}

Capability 可能因宿主模式而不存在,适合用 TryGet 做降级。注册结果仍必须交给 Lifetime。

案例 6:向启动页注入声明式 AXAML

plugin.json

json
"services": {
  "required": {
    "pcl.commands": ">=0.1 <1.0",
    "pcl.ui": ">=0.1 <1.0"
  }
},
"permissions": [
  { "id": "pcl.commands", "reason": "处理面板按钮。" },
  { "id": "ui.inject", "reason": "在启动页显示工具面板。" }
],
"ui": {
  "api": { "minimum": "0.1", "maximumExclusive": "1.0" },
  "avalonia": { "minimum": "12.0", "maximumExclusive": "13.0" },
  "targets": [
    {
      "target": "pcl.page.launch",
      "surface": ">=3.0 <4.0",
      "access": ["inject"],
      "operations": [
        {
          "id": "tools-panel",
          "kind": "inject",
          "slot": "primary-actions.after",
          "axaml": "ui/ToolsPanel.axaml",
          "command": "dev.example.tools.open",
          "required": false,
          "fallback": "disable-feature"
        }
      ]
    }
  ]
}

ui/ToolsPanel.axaml

xml
<Border xmlns="https://github.com/avaloniaui"
        Padding="12"
        CornerRadius="8">
  <StackPanel Spacing="8">
    <TextBlock FontWeight="SemiBold" Text="示例工具" />
    <Button Content="打开"
            Command="{Binding Commands[dev.example.tools.open]}" />
  </StackPanel>
</Border>

AXAML 不得包含 x:Class 或代码隐藏。行为由公开命令 ID 连接。

案例 7:在不同宿主版本中优雅降级

csharp
PluginApiVersionRange range = PluginApiVersionRange.Parse(">=0.1 <1.0");

if (context.Services.Supports(PluginServiceIds.Notifications, range) &&
    context.Services.TryGet<IPluginNotificationService>(out var notifications))
{
    notifications.ShowInformation("通知服务可用。");
}
else
{
    context.Logger.Info("通知服务不可用,改为只写日志。");
}

真正不可缺少的能力放进 services.required;增强体验的能力放进 services.optional

案例 8:声明前置与不兼容插件

json
"dependencies": [
  {
    "id": "dev.example.core",
    "version": ">=1.2.0 <2.0.0",
    "kind": "required"
  },
  {
    "id": "dev.example.theme",
    "version": ">=0.5.0",
    "kind": "optional"
  }
],
"incompatibilities": [
  {
    "id": "dev.example.legacy",
    "version": "<2.0.0",
    "reason": "两者会修改同一个启动页区域。"
  }
]

运行时会先加载 required 前置,并在检测到已启用的不兼容版本时默认禁用冲突插件。

案例 9:用内存宿主测试命令

csharp
using PCL.N.Plugin;
using PCL.N.Plugin.Testing;

[TestMethod]
public async Task Initialize_registers_and_executes_command()
{
    PluginDescriptor descriptor = new(
        new PluginId("dev.example.tools"),
        "Example Tools",
        PluginVersion.Parse("0.1.0"));

    await using TestPluginContext context = new(descriptor, new PluginApiVersion(0, 2));
    TestPluginCommandService commands = new();
    TestPluginNotificationService notifications = new();
    context.TestServices.Add<IPluginCommandService>(commands);
    context.TestServices.Add<IPluginNotificationService>(notifications);

    ExamplePlugin plugin = new();
    await plugin.InitializeAsync(context, CancellationToken.None);
    await commands.InvokeAsync("dev.example.tools.say-hello");

    CollectionAssert.Contains(notifications.Messages.ToArray(), "info:你好,PCL N!");
}

更多测试模式见 测试插件

案例 10:携带多平台原生库

xml
<ItemGroup>
  <PclNPluginNative Include="native/win-x64/example.dll"
                    RuntimeIdentifier="win-x64" />
  <PclNPluginNative Include="native/linux-x64/libexample.so"
                    RuntimeIdentifier="linux-x64" />
  <PclNPluginNative Include="native/osx-arm64/libexample.dylib"
                    RuntimeIdentifier="osx-arm64" />
</ItemGroup>

构建器会放入 runtimes/<rid>/native/。同时在 Manifest 的 platforms 中声明实际支持的系统、架构和 RID。纯托管插件应保持 AnyCPU