Windows 提供多種技術,讓你的應用程式能為其他應用程式提供功能,或使用第三方外掛。本文比較了 Windows 應用程式 SDK 桌面應用程式可用的擴充性選項。
擴充性選項概述
| 科技 | Description | 需要封裝識別碼 | 最低作業系統 |
|---|---|---|---|
| 應用程式服務 | 應用程式間透過 AppServiceConnection 進行的請求/回應通訊 |
Yes | Windows 10 1607 |
| 應用程式擴充功能 | 外掛模式 — 主機應用程式從擴充套件中發現內容 | Yes | Windows 10 1607 |
| 套件擴充功能 | 更廣泛的套件層級擴充能力 uap17:PackageExtension |
Yes | Windows 11 |
| 選用套件 | 補充主應用程式的額外內容包 | Yes | Windows 10 1709 |
| 資源套件 | 語言、規模與無障礙資產依市場區分 | Yes | Windows 10 |
選擇合適的技術
何時使用應用程式服務
- 你需要不同應用程式之間的雙向通訊。
- 消費者應用程式會發送請求並等待回應。
- 你想讓類似 API 的介面暴露給其他應用程式。
舉例:一個翻譯服務,其他應用程式可以呼叫它來翻譯文字。
在下列情況下使用 App 擴充功能
- 你的應用程式需要一個外掛模式,讓第三方提供內容、主題或附加元件。
- 擴充功能是在執行時從已安裝的套件中發現的。
- 擴充功能提供資料或設定,而非可執行程式碼(程式碼執行應該使用應用服務)。
範例:一個影像編輯器,能從已安裝的擴充套件中發現過濾包。
在下列情況下使用套件延伸模組
- 你需要在 Windows 11 上具備更廣泛的套件層級擴充性。
- 擴充套件需要存取比
PublicFolder模型允許更多的套件內容。
在…時使用選用套件
- 你會有額外內容(DLC、高級功能)以獨立套裝形式分發。
- 內容由同一家出版社撰寫。
架構模式
支援擴充功能探索的應用程式服務
將應用程式擴充功能與應用程式服務結合,打造完整的外掛架構:
- 你的主機應用程式會用
AppExtensionCatalog來發現已安裝的擴充功能。 - 每個擴充化都會宣告描述其能力的屬性。
- 當使用者啟用擴充功能時,主機應用程式會連接該擴充功能的應用服務進行雙向通訊。
┌─────────────────┐ ┌──────────────────┐
│ Host app │ │ Extension app │
│ │ │ │
│ AppExtension │◄────►│ AppExtension │
│ Catalog │ │ declaration │
│ │ │ │
│ AppService │◄────►│ AppService │
│ Connection │ │ provider │
└─────────────────┘ └──────────────────┘
僅限內容延伸
對於擴充功能提供靜態內容(主題、範本、資料檔案)的簡單情境:
- 主機應用程式透過
AppExtensionCatalog探索擴充功能。 - 它會從擴充功能的
PublicFolder讀取檔案。 - 不需要任何應用程式服務。
與 UWP 擴充性的差異
此處描述的擴充性技術在 Windows 應用程式 SDK 桌面應用程式中與 UWP 中運作方式相同,但有一個要求:MSIX 套件識別碼。 所有擴充功能都依賴套件清單來進行宣告,並依賴套件目錄作為發現。
如果你的桌面應用程式是未封裝的,就無法使用這些擴充性技術。 考慮其他方法,例如:
- 基於 COM 的外掛介面
- 以檔案系統為基礎的擴充功能探索
- 命名管道或其他IPC機制
未封裝應用程式的檔案式外掛發現
對於未封裝的 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);
}
Note
在 AssemblyLoadContext 中使用 isCollectible: true 可讓你在執行階段卸載外掛程式。 這種方法可避免 MEF(Managed Extensibility Framework)在桌面應用程式中可能引入的版本控制問題。