現代應用程式開發越來越重視「可擴展性」。使用者希望能在不修改核心程式碼的情況下,動態新增功能。Microsoft .NET Framework 3.5 引入的 System.AddIn(代號「增益集框架」)正是為了解決這個問題而生。它提供了一套完整的管線機制,支援版本控制、隔離層級(進程內、應用程式域、進程外)以及安全權限管理,讓宿主應用程式能安全地載入並執行第三方擴充元件。
本文以 ShapeApp 為例,示範如何將現有的 Windows Forms 應用程式改造為支援加載項的宿主。ShapeApp 是一個簡單的繪圖工具,支援基本圖形(正方形、圓形等)的新增、移動、顏色與大小變更,並可管理多個繪圖分頁。原始版本由 VSTA 團隊開發,作為嵌入 Visual Studio IDE 的範例;我們將從其非擴充版本著手,逐步整合 System.AddIn。
加載項框架的隔離機制要求:宿主視圖組件不得參考除 .NET Framework 以外的任何組件,且禁止使用 MarshalByRefObject。因此,我們必須先梳理出加載項真正需要使用的類型,定義乾淨的介面契約。
| 類型 | 說明 |
|---|---|
| ShapeApplication | 主應用程式物件,提供應用程式層級操作 |
| Drawing | 代表單一文件/繪圖 |
| Shape | 代表繪圖中的單一圖形 |
| DrawingCollection / ShapeCollection | 集合類型 |
| EventArgs 衍生類型 | 自訂事件參數 |
宿主需要一種機制來發現、載入與管理加載項。範例中實作了一個簡單的 AddInManager 靜態類別與管理視窗(透過選單項目叫用),核心邏輯僅需三行程式碼:
// 更新加載項存儲(掃描應用程式目錄)
string[] warnings = AddInStore.Update(PipelineStoreLocation.ApplicationBase);
// 取得宿主視圖介面類型
Type hostViewOfAddIn = typeof(ShapeAppHostViews.IShapeAddIn);
// 尋找所有實作該視圖的加載項
ICollection<AddInToken> addIns = AddInStore.FindAddIns(hostViewOfAddIn, PipelineStoreLocation.ApplicationBase);
啟用加載項同樣簡潔:
IShapeAddIn addIn = token.Activate<IShapeAddIn>(AddInSecurityLevel.FullTrust);
框架支援多種隔離層級(進程內、新應用程式域、新進程)與信任等級,開發者可依安全需求彈性選擇。
在 HostViews 組件中,以 介面 定義宿主視圖(而非抽象類別),主要原因有三:
namespace ShapeAppHostViews {
public interface IShapeApplication {
IDrawing ActiveDrawing { get; }
IShapeCollection AvailableShapes { get; }
IDrawingCollection Drawings { get; }
bool Visible { get; set; }
IDrawing NewDrawing();
void Quit();
event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
}
宿主類別 ShapeApplication 需實作此介面。對於簽名相同的成員(如 Visible),直接滿足介面契約即可;但對於回傳內部具象類型的成員(如 ActiveDrawing 回傳 Drawing),必須使用 EIMI 進行轉接:
// 原始屬性,回傳內部具象類型
public Drawing ActiveDrawing { get { ... } }
// EIMI 實作介面成員,回傳視圖介面
ShapeAppHostViews.IDrawing ShapeAppHostViews.IShapeApplication.ActiveDrawing {
get { return this.ActiveDrawing; } // 隱式轉換
}
加載項可能從任何執行緒呼叫公開成員,若直接操作 Windows Forms 控制項會導致不可預期行為。必須在每個公開成員中檢查 Control.InvokeRequired 並透過 Control.Invoke 執行:
private delegate Drawing NewDrawingDelegate();
public Drawing NewDrawing() {
if (this.ApplicationForm.InvokeRequired) {
var del = new NewDrawingDelegate(NewDrawing);
return (Drawing)ApplicationForm.Invoke(del);
}
// 原始邏輯...
}
管線組件包括:宿主視圖 → 宿主端適配器 → 約定 → 加載項端適配器 → 加載項視圖。可使用 Pipeline Builder 工具自動產生,或手工編寫以支援跨版本相容。
| 物件類型 | 所在位置 | 所需適配器 |
|---|---|---|
| 宿主端物件(如 ShapeApplication) | 宿主 | 「視圖→約定」宿主適配器 + 「約定→視圖」加載項適配器 |
| 加載項端物件(如 ShapeAddIn) | 加載項 | 方向相反,適配器位置互換 |
| 兩端物件 | 雙方皆可建立 | 每端各兩個適配器(共四個),若視圖共用組件可減為兩個 |
約定不支援原生事件,需以 Add/Remove 模式模擬:
// 約定介面
public interface IShapeApplicationContract : IContract {
void CreatedDrawingAdd(ICreatedDrawingEventHandlerContract handler);
void CreatedDrawingRemove(ICreatedDrawingEventHandlerContract handler);
}
public interface ICreatedDrawingEventHandlerContract : IContract {
void Handler(ICreatedDrawingEventArgsContract args);
}
適配器負責在兩側轉譯事件訂閱與觸發。注意:加載項必須在同一適配器實例上註冊與解除註冊事件,否則解除無效。
同一宿主物件多次傳回加載項時,會產生多個適配器實例,導致 ReferenceEquals 失敗、集合 Contains 判斷錯誤。解決方案:
Equals 與 GetHashCode,轉呼叫真實宿主物件;加載項開發極為簡單:建立實作加載項視圖的類別,標記 [AddIn] 屬性即可。範例中包含一個 協作加載項,展示強大功能:
協作加載項訂閱宿主所有事件,將使用者操作封裝為訊息透過 WCF 轉發;接收端在重播操作時暫時取消事件訂閱,避免無限迴圈。
WindowsFormsHost)。System.AddIn 提供了企業級的擴充性基礎設施,雖然管線建置初期投入較大,但換來的是嚴格的版本隔離、安全邊界與長期維護性。ShapeApp 案例證明:只需公開核心物件模型,即可衍生出「自動化加載項」與「協作編輯」等高階應用。
作者:Mueez Siddiqui,Microsoft CLR 安全性與擴充性團隊軟體開發工程師。