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、高级功能)。
- 内容由同一发布者创作。
体系结构模式
支持扩展发现的应用服务
将应用扩展与应用服务相结合,实现完整的插件体系结构:
- 您的主机应用使用
AppExtensionCatalog来发现已安装的扩展。 - 每个扩展声明描述其功能的属性。
- 当用户激活扩展时,主机应用会连接到扩展的应用服务进行双向通信。
┌─────────────────┐ ┌──────────────────┐
│ Host app │ │ Extension app │
│ │ │ │
│ AppExtension │◄────►│ AppExtension │
│ Catalog │ │ declaration │
│ │ │ │
│ AppService │◄────►│ AppService │
│ Connection │ │ provider │
└─────────────────┘ └──────────────────┘
仅限内容扩展
对于扩展提供静态内容(主题、模板、数据文件)的更简单方案:
- 主机应用通过
AppExtensionCatalog发现扩展。 - 它从扩展程序的
PublicFolder读取文件。 - 不需要应用服务。
与 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(托管可扩展性框架)在桌面应用程序中可能引入的版本问题。