使用服务、扩展和包扩展你的应用

Windows提供了多种技术,使应用能够向其他应用提供功能或使用第三方外接程序。本文比较Windows 应用 SDK桌面应用的可用扩展性选项。

扩展性选项概述

科技 Description 需要提供包标识 最低操作系统
应用服务 通过 AppServiceConnection 进行应用之间的请求/响应通信 是的 Windows 10 1607
应用程序扩展 插件模型 - 主机应用从扩展包发现内容 是的 Windows 10 1607
包扩展 具有更广泛的包级扩展性 uap17:PackageExtension 是的 Windows 11
可选软件包 补充主应用的其他内容包 是的 Windows 10 1709
资源包 按市场分隔的语言、规模和辅助功能资产 是的 Windows 10操作系统

选择正确的技术

在以下情况下使用应用服务

  • 需要在单独的应用之间进行双向通信。
  • 使用者应用发送请求并等待响应。
  • 你想要向其他应用公开类似 API 的接口。

示例:其他应用可调用的文本翻译服务。

在以下情况下使用应用扩展

  • 你的应用需要一个插件模型,其中第三方提供内容、主题或外接程序。
  • 在运行时,会从已安装的软件包中发现扩展。
  • 扩展提供数据或配置,而不是可执行代码(代码执行应使用应用服务)。

示例:可从已安装的扩展包中识别滤镜包的图像编辑器。

在以下情况下使用包扩展

  • 需要在Windows 11上实现更广泛的包级扩展性。
  • 扩展需要访问比 PublicFolder 模型允许的更多包内容。

在以下情况下使用可选包

  • 你有以单独的软件包形式分发的附加内容(DLC、高级功能)。
  • 内容由同一发布者创作。

体系结构模式

支持扩展发现的应用服务

将应用扩展与应用服务相结合,实现完整的插件体系结构:

  1. 您的主机应用使用 AppExtensionCatalog 来发现已安装的扩展。
  2. 每个扩展声明描述其功能的属性。
  3. 当用户激活扩展时,主机应用会连接到扩展的应用服务进行双向通信。
┌─────────────────┐      ┌──────────────────┐
│   Host app       │      │  Extension app    │
│                  │      │                   │
│ AppExtension     │◄────►│ AppExtension      │
│   Catalog        │      │   declaration     │
│                  │      │                   │
│ AppService       │◄────►│ AppService        │
│   Connection     │      │   provider        │
└─────────────────┘      └──────────────────┘

仅限内容扩展

对于扩展提供静态内容(主题、模板、数据文件)的更简单方案:

  1. 主机应用通过 AppExtensionCatalog 发现扩展。
  2. 它从扩展程序的 PublicFolder 读取文件。
  3. 不需要应用服务。

与 UWP 可扩展性的区别

此处所述的扩展性技术的工作方式与在 UWP 中Windows 应用 SDK桌面应用的工作方式相同,但有一项要求:MSIX 包标识。 所有扩展功能都依赖包清单进行声明,并依赖包目录进行发现。

如果桌面应用已解压缩,则无法使用这些扩展性技术。 请考虑其他方法,例如:

  • 基于 COM 的插件接口
  • 基于文件系统的扩展发现
  • 命名管道或其他进程间通信机制

适用于未打包应用的基于文件的插件发现机制

对于未打包的 WinUI 3 应用,可以使用 .NET 的 AssemblyLoadContext 从已知文件夹加载扩展,以实现插件系统:

public class PluginLoader
{
    private readonly string _pluginDirectory;

    public PluginLoader(string pluginDirectory)
    {
        _pluginDirectory = pluginDirectory;
    }

    public IEnumerable<T> LoadPlugins<T>() where T : class
    {
        if (!Directory.Exists(_pluginDirectory))
            yield break;

        foreach (var dll in Directory.GetFiles(_pluginDirectory, "*.dll"))
        {
            var context = new PluginLoadContext(dll);
            var assembly = context.LoadFromAssemblyPath(Path.GetFullPath(dll));

            foreach (var type in assembly.GetTypes()
                .Where(t => typeof(T).IsAssignableFrom(t) && !t.IsAbstract))
            {
                if (Activator.CreateInstance(type) is T plugin)
                    yield return plugin;
            }
        }
    }
}

// Custom AssemblyLoadContext to isolate plugin dependencies
public class PluginLoadContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver _resolver;

    public PluginLoadContext(string pluginPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        var path = _resolver.ResolveAssemblyToPath(assemblyName);
        return path != null ? LoadFromAssemblyPath(path) : null;
    }
}

Warning

在没有验证的情况下从磁盘加载程序集是一种安全风险。 在生产环境中,在加载之前验证程序集签名(如 Authenticode),限制插件目录的 ACL 权限,并考虑在单独的进程中运行插件并降低特权。

在宿主和插件共同引用的单独程序集中定义共享接口契约:

// Contoso.App.Contracts (shared assembly)
public interface IPluginExtension
{
    string Name { get; }
    string Description { get; }
    void Execute(IServiceProvider services);
}

注释

AssemblyLoadContext中使用isCollectible: true可让您在运行时卸载插件。 这种方法可避免 MEF(托管可扩展性框架)在桌面应用程序中可能引入的版本问题。