目錄
調整宿主
公開對象模型適配器、約定和加載項視圖編寫加載項協作加載項總結
您知道現在可以使用新的 Microsoft® .NET 加載項框架 (System.AddIn) 創建可擴展的 Windows® 窗體應用程序嗎?在本期專欄中 ,我將修改 ShapeApp 繪圖應用程序,以通過加載項框架公開其對象模型。這將 ,我就能夠創建出在宿主應用程序上自動執行任務的加載項(自動化加載項)。 另外,我還將討論此方案以及現有解決方案中的常見問題。
如果您在開始前需要了解有關加載項框架的背景信息,請參閱 CLR 加載項團 隊博客上的資源頁,網址為 go.microsoft.com/fwlink/?LinkId=117519。您還 可以考慮閱讀以前的“CLR 全面透徹解析”專欄中有關加載項框架的 文章。
ShapeApp 是一個可供您使用基本形狀進行繪圖的繪圖應用程序。您可以插入 新形狀、隨意移動它們、更改其顏色和大小等。它允許您將繪圖保存到文件,並 在選項卡中打開多個繪圖。在 ShapeApp 中創建的簡單繪圖,如圖 1 所示。
圖 1 ShapeApp 的屏幕快照
ShapeApp 由 Visual Studio® Tools for Applications (VSTA) 團隊開 發,作為示例可用於將 Visual Studio IDE 嵌入到您的應用程序中。該 IDE 可 隨後用於為應用程序編寫擴展。他們已提供了包含和不包含其可擴展代碼的應用 程序版本。在本示例中,我先從基礎版本開始,並調整它使其能夠與加載項框架 配合使用。
調整宿主
在本部分中,我將調整應用程序以公開其對象模型,該模型與 HTML 中的文 件對象模型 (DOM) 很相似。這樣,我就能夠創建在應用程序中自動執行任務的 加載項。
調整宿主的第一步是定義宿主將要公開的對象模型(請注意,我還不需要編 寫任何代碼)。加載項框架的隔離和版本控制功能將對對象模型加以限制。通常 ,對象模型應定義加載項中將使用的但未內置在 .NET Framework 中的所有類型 ;換言之,視圖程序集不應該引用除 .NET Framework 程序集以外的任何程序集 。明確禁止使用 MarshalByRefObject 類型。有關允許類型分類的詳細信息,請 參閱博客上有關約定的帖子,網址為 go.microsoft.com/fwlink/? LinkId=117520。
圖 2 列出了 ShapeApp 對象模型中的類型。此處列出的所有類都存在於宿主 應用程序的內部。稍後我將通過管道公開它們。
圖 2 ShapeApp 類型類型 說明 ShapeApplication 主應用程序對象。 Drawing 表示應用程序中繪圖的對象。 Shape 表示繪圖中形狀的對象。 DrawingCollection 繪圖對象集合。 ShapeCollection 形狀對象集合。 EventArgs 相關類型 從 System.EventArgs 中派生的幾種類型,用於自定義事件參 數。
下一步是添加一種供宿主發現和激活加載項的方法。為此,我在宿主上使用 了簡單的加載項管理器。其中包括一個可用於加載和跟蹤加載項的靜態類 AddInManager,以及一個供用戶管理加載項的窗體。該窗體通過宿主上的菜單項 激活,它需要盡可能少地對宿主進行修改。該窗體如圖 3 所示。
圖 3 加載項管理器窗體
加載項管理器的代碼非常簡單。以下是查找可用加載項所需的三行代碼:
// update the add-in store to include add-ins located in the
// current program folder
string[] warnings = AddInStore.Update (PipelineStoreLocation.ApplicationBase);
// get the view type defined in the host views assembly
System.Type hostViewOfAddIn = typeof (ShapeAppHostViews.IShapeAddIn);
// find all add-ins that implement the view
ICollection<AddInToken> addIns = AddInStore.FindAddIns (hostViewOfAddIn,
PipelineStoreLocation.ApplicationBase);
第一行代碼 AddInStore.Update 用於搜索可用的加載項和管道組件。 PipelineStoreLocation.ApplicationBase 告訴它在應用程序文件夾中查找。有 關文件夾結構的詳細說明,請參閱 go.microsoft.com/fwlink/?LinkId=117521 。接下來,我得到了自己想要激活的加載項類型。在本例中,該類型為宿主視圖 程序集中定義的 IShapeAddIn 類型。最後,我調用了 AddInStore.FindAddIns,它將返回一個可供檢查和激活的可用加載項令牌 集合。從該集合中選定令牌後,使用一行代碼將其激活:
// activate the add-in in full trust mode
ShapeAppHostViews.IShapeAddIn addIn =
token.Activate<ShapeAppHostViews.IShapeAddIn>(
AddInSecurityLevel.FullTrust);
我還可以選擇在進程外部或以其他的信任級別激活加載項。有關隔離級別列 表的信息,請轉到 go.microsoft.com/fwlink/?LinkId=117522。有關隔離級別 性能比較的信息,請參閱博客上有關性能的帖子,網址為 go.microsoft.com/fwlink/?LinkId=117523。
公開對象模型
在空管道和加載項管理項就緒後,就可以開始公開從宿主到加載項的功能。 要公開對象模型中包含的類型,首先需要在 HostViews 程序集中為要公開的各 類型創建宿主視圖。我可以在該視圖中公開屬性、方法和事件。宿主視圖可以是 抽象基類或接口。
在 ShapeApp 中,我基於三個理由選擇使用接口。第一,在 Visual Basic® 中使用時,事件需要使用接口才能正常工作。第二,C# 不支持多繼 承,因此一個類不能從兩個基類中繼承。所以,該視圖應該是允許所有宿主類實 現的接口(即使它們已有基類)。第三,接口支持 EIMI(顯式接口方法實現) 。我稍後將介紹這一點的重要性。
圖 4 顯示了 ShapeApplication 對象在宿主視圖中的顯示。成員使用的類型 可以是內置的框架類型(如 EventHandler<T>)或其他視圖(如 IDrawing)。您可以通過我們的 CodePlex 站點下載 FxCop 規則,以檢驗視圖 是否能輕松地適用於完整的管道,網址為 go.microsoft.com/fwlink/? LinkId=117524。
圖 4 ShapeApplication 對象宿主視圖namespace ShapeAppHostViews
{
public interface IShapeApplication
{
// the drawing present in the selected tab
IDrawing ActiveDrawing { get; }
// a collection of available shapes (square, circle, etc)
IShapeCollection AvailableShapes { get; }
// a collection of drawings currently open in the application
IDrawingCollection Drawings { get; }
// main window visibility
bool Visible { get; set; }
// create a new drawing in the application
IDrawing NewDrawing();
// completely exit the application
void Quit();
// event fired when a drawing is created
event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
}
創建視圖程序集後,我需要修改宿主類以從相應的視圖中繼承。下面以粗體 顯示對 ShapeApplication 類的更改:
public class ShapeApplication : System.Windows.Forms.IWin32Window
{
...
}
public class ShapeApplication : ShapeAppHostViews.IShapeApplication,
System.Windows.Forms.IWin32Window
{
...
}
接下來,我需要實現宿主視圖中的每個成員。對於使用內置類型作為參數和 返回值的成員而言,不需要任何特殊實現(除確保成員是宿主類中的公共成員以 外)。例如,ShapeApplication 類中 Visible 屬性與 Visible 接口成員的簽 名相同:
public bool Visible
{
get {...}
set {...}
}
這意味著宿主 ShapeApplication 類中現有的 Visible 實現將可用作視圖實 現。
但使用宿主視圖中定義的其他類型的成員需要顯式地實現。例如, ActiveDrawing 屬性在 ShapeApplication 類中與在 IShapeApplication 宿主 視圖中的簽名並不相同。宿主類中的原始版本返回 Drawing 對象:
public Drawing ActiveDrawing
{
get {...}
}
而宿主視圖 ShapeApplication 中的版本只能返回相應的視圖 IDrawing:
IDrawing ActiveDrawing
{
get;
}
要實現宿主類中的第二版本,需要使用 EIMI。我的做法是在具有相同名稱的 宿主類中添加新屬性 ActiveDrawing。現在,宿主的 ShapeApplication 類具有 兩個名為 ActiveDrawing 的屬性(請參閱圖 5)。
圖 5 ActiveDrawing 屬性// original property
public Drawing ActiveDrawing
{
get {...}
}
// implementation of the host view's version of ActiveDrawing
ShapeAppHostViews.IDrawing
ShapeAppHostViews.IShapeApplication.ActiveDrawing
{
get
{
// a call to the original property
return this.ActiveDrawing;
}
}
新實現將 Drawing 對象隱式地轉換為 IDrawing 類型。如果您正在公開 Windows 窗體應用程序的對象模型,很有可能某些已公開的成員將訪問和修改該 Windows 窗體控件。如果不是從創建該控件的線程中調用此類成員,將會導致 GUI 中出現不可預測的行為。
由於可以從任何線程中調用向加載項公開的成員,因此我必須通過使用 Control.InvokeRequired 和 Control.Invoke 來確保所有已公開的成員都能安 全地訪問或修改 GUI 控件。例如,ShapeApplication.NewDrawing 函數訪問 ApplicationForm 對象,因而需要安全實現 Invoke 的使用。
以下內容為原有的實現:
public Drawing NewDrawing()
{
Drawing newDrawing = new Drawing(this);
...
this.ApplicationForm.drawingsTabControl.TabPages.Add(
newDrawing.DrawingSurface);
...
return newDrawing;
}
新的實現如圖 6 所示(更改以粗體表示)。
圖 6 使用 Invoke 的 NewDrawing 方法// delegate added to allow invoke
private delegate Drawing NewDrawingDelegate();
public Drawing NewDrawing()
{
// check if we need an invoke
if (this.ApplicationForm.InvokeRequired)
{
// invoke this method using the ApplicationForm object
NewDrawingDelegate del = new NewDrawingDelegate (NewDrawing);
return (Drawing) ApplicationForm.Invoke(del);
}
else
{
Drawing newDrawing = new Drawing(this);
...
this.ApplicationForm.drawingsTabControl.TabPages.Add(
newDrawing.DrawingSurface);
...
return newDrawing;
}
}
適配器、約定和加載項視圖
為了使加載項能夠訪問宿主公開的功能,需要創建適配器、約定和加載項視 圖。管道組件可以由宿主視圖自動生成。Pipeline Builder 正好可以完成這項 工作,該工具可以從 go.microsoft.com/fwlink/?LinkId=117525 獲得。該應用 程序的第一個版本運行良好。
還可以手工為管道編碼。在編寫跨版本的適配器時需要手動編碼。在 Visual Studio 中,可以通過將每個所需程序集(視圖、適配器、約定等)的項目添加 到 ShapeApp 解決方案中來設置管道。在 Visual Studio 中創建管道的分步指 南可以從 go.microsoft.com/fwlink/?LinkId=117526 中獲得。
在本示例中,我采用手動方式對管道編碼。由於這是 ShapeApp 對象模型的 第一個版本,所以宿主和加載項視圖相同。因此,我可以對這兩種視圖使用同一 個程序集。當手動編碼時,最初設置空管道非常有用。此類管道的視圖如下所示 :
public interface IShapeAddIn
{
void Initialize(IShapeApplication application);
}
public interface IShapeApplication
{
// TODO: Implement this.
}
Initialize 方法用於在加載時將主應用程序對象傳遞給加載項。這使得加載 項能夠隨時控制宿主,而無需宿主顯式地請求任何服務(這正是它被稱之為自動 化加載項的原因)。
創建空管道之後,我可以逐次公開宿主的其余功能、編譯並進行測試。此迭 代方法有助於避免表面上無休止的編譯錯誤字符串。當然,在確定接口並發布應 用程序之後,不能再向現有接口添加任何方法。所需的適配器類型取決於對象的 位置和訪問對象的位置。通常,對象可分屬於以下三種類型中的一種:
宿主端對象這些對象存在於宿主端並可從加載項訪問(如 ShapeApplication 類)。為了將它們公開給加載項,需要使用圖 7 所示的管道組件。由於對象存 在於宿主端,因此我將從頂部的宿主視圖開始。我使用“視圖到約定 ”宿主適配器將其轉換為約定,然後使用“約定到視圖”加載 項適配器將其轉換為加載項視圖。這兩個適配器使加載項能夠訪問宿主端對象。
圖 7 所需的管道組件宿主端對象 加載項端對象 宿主(對象所在位置) 加載項(對象所在位置) 宿主視圖 加載項視圖 “視圖到約定”宿主適配器 “視圖到約定”加載項適配器 約定 約定 “約定到視圖”加載項適配器 “約定到視圖”宿主適配器 加載項視圖 宿主視圖 加載項(在此處訪問對象) 宿主(在此處訪問對象)
加載項端對象這些對象存在於加載項端,並可從宿主進行訪問(如 ShapeAddIn 類)。此處管道組件很相似但適配器的方向相反,從而使宿主能夠 訪問加載項端對象。“視圖到約定”適配器現在位於加載項端而不是 宿主端,而“約定到視圖”適配器現在位於宿主端。請注意,管道的 其余部分(視圖和約定)相同。
兩端對象這些對象存在於兩端中的任意一端,並可以從任意一端進行訪問。 這些對象需要四個適配器,每端兩個,因為我需要對它們進行雙向修改。請注意 ,如果宿主和加載項視圖使用相同的程序集,那您還可以重用適配器。這樣您只 需要兩個適配器即可。
約定不支持本機事件。但是,可以通過使用下列模式在約定中模擬事件(示 例來自 IShapeApplication.CreatedDrawing 事件):
宿主視圖:
public interface IShapeApplication
{
...
// event fired when a drawing is created
event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
相應的約定:
public interface IShapeApplicationContract : IContract
{
...
void CreatedDrawingAdd(ICreatedDrawingEventHandlerContract handler);
void CreatedDrawingRemove(ICreatedDrawingEventHandlerContract
handler);
}
public interface ICreatedDrawingEventHandlerContract : IContract
{
void Handler(ICreatedDrawingEventArgsContract args);
}
您可以看到,我已經為事件處理程序創建了新約定 ICreatedDrawingEventHandlerContract,因為無法在合約中使用類似 System.EventHandler<CreatedDrawingEventArgs> 這樣的委托。
加載項端適配器通過 Add 和 Remove 方法在宿主端注冊處理程序,並且它還 維護加載項可訂閱的本地事件。當觸發事件時,宿主端適配器將調用 Handler 函數。
這種增加的復雜性對加載項開發人員來說大部分是透明的,只有以下一點需 要特別注意:加載項必須在對象的同一適配器實例上注冊和取消注冊自身。否則 ,取消注冊將無效。
接下來,我將介紹宿主對象如何能夠使兩組(或更多)適配器對象引用它。 當宿主端的某個對象返回加載項(通過屬性訪問或函數調用)時,將創建兩個能 夠訪問該宿主對象的適配器對象(宿主端適配器和加載項端適配器)。當相同的 對象再次返回到加載項時,又將創建兩個新的適配器。
例如,訪問 ShapeApplication.ActiveDrawing 兩次將返回兩個不同的加載 項對象引用,並且 ShapeApplication.ActiveDrawing.ReferenceEquals (ShapeApplication.ActiveDrawing) 將返回 false。此外,當注冊/取消注冊事 件並在集合中存儲宿主對象時,相同宿主對象存在兩個(或多個)適配器會出現 問題。
為有助於解決這些問題,可在適配器中替換 .Equals 和 .GetHashCode 函數 以調用實際宿主對象中的相應函數。這使我們能夠將宿主對象加入加載項中的集 合,且 .Contains 之類的方法可正常工作。當然,加載項開發人員仍然必須確 保在相同的對象上注冊和取消注冊事件。並且您應該知道 .Equals 函數可以幫 助完成這項工作。
正如您猜測的那樣,另一個選項是緩存適配器。但它實際上難以實現,因為 沒有什麼簡單的方法可以存儲對象到適配器的弱映射(此處的弱是指弱對象引用 ,垃圾回收器會忽略此類引用)。使用正則字典可以防止垃圾回收適配器和對象 。
編寫加載項
管道就緒後,我可以按照加載項視圖編寫加載項。盡可能簡單地編寫加載項 。加載項開發人員只需創建繼承自加載項視圖的類,然後使用 AddIn 屬性標記 加載項實現即可。剩下的代碼可以按照宿主和加載項之間不存在管道的方式編寫 。但要特別注意兩點。一是我前面討論過的對象標識問題,二是性能取決於所使 用的隔離邊界。有關性能基准的信息,請參閱 go.microsoft.com/fwlink/? LinkId=117527。
目前,加載項不能在宿主應用程序的窗體上直接顯示 Windows 窗體控件。但 它們可以使用以下三種方法中的任意一種:它們可以顯示其自身的窗體(請注意 ,如果在某些部分信任的方案中(如 Internet 信任級別)運行加載項,則用戶 會在加載時看到加載項窗體中出現安全警報);它們可以直接在宿主應用程序窗 體上顯示 Windows Presentation Foundation (WPF) 控件(請參閱 go.microsoft.com/fwlink/?LinkId=117528);它們還可以使用包裝在 WPF 容 器中的 Windows 窗體控件(請參閱 go.microsoft.com/fwlink/?LinkId=117529 )。
Windows 窗體必須滿足一些線程要求。例如,加載項擴展命令行應用程序時 必須創建新的線程以構建窗體和處理其事件。這是因為命令行應用程序默認使用 多線程單元 (MTA) 模塊,而對於 UI 線程 Windows 窗體需要使用單線程單元 (STA) 模塊。解決方案很簡單:擴展 Windows 窗體應用程序的加載項只需包含 兩行代碼以顯示窗體(假定該加載項已激活且從宿主的 UI 線程中調用),如下 所示:
AddInForm form = new AddInForm();
form.Show();
協作加載項
使用公開接口可以創建功能非常強大的加載項。其中一個例子是此示例中包 含的協作加載項。它允許兩位 ShapeApp 用戶使用兩台不同的計算機實時共同編 輯繪圖。協作加載項通過 Windows Communication Foundation (WCF) 相互連接 ,從而使交互能夠通過 Internet 以全局方式工作。連接屏幕的屏幕快照如圖 8 所示。
圖 8 協作加載項 UI
在兩個加載項相互連接後,在其中一端創建或打開的任何新文檔都可以共享 ;即,在一台計算機上的更改會實時發送到另一台計算機。這是通過使用事件來 實現的。當加載完協作加載項後,它將訂閱應用程序的所有事件。當創建新的繪 圖時,將觸發 CreatedDrawing 事件。加載項接收此事件並訂閱新繪圖的所有事 件。類似地,當創建形狀時,它也會訂閱與形狀相關的所有事件。這使得加載項 能夠跟蹤所有的用戶操作並將其傳播到其對等端。
圖 9 顯示了事件通過協作加載項時所采用的路徑。在計算機 1 上,用戶執 行某個操作(例如更改某個形狀的位置)。這將使宿主觸發事件,而協作加載項 將接收到該事件。協作加載項將創建消息,並通過 WCF 將消息發送至計算機 2 上的加載項。然後,此加載項將在該宿主上執行相同的操作。請注意,當在宿主 上執行操作時會暫時取消掛接事件處理程序。這是為了防止事件返回加載項而導 致無限循環。
圖 9 事件通過協作加載項時所采用的路徑
通過使用 WCF 可以提供更有趣的方案。由於協作加載項可托管服務,所以可 以有任何數量的客戶端與其建立連接。這使多人能夠無縫地共同繪圖。圖 10 顯 示了三台計算機之間的連接。每台計算機上的協作加載項都可與所有其他計算機 連接。
圖 10 三個 ShapeApp 實例相互連接
現在您已經看到 ShapeApp 如何使用 .NET 加載項框架適應宿主加載項。相 信您應該已經充分了解到加載項框架的功能,以及如何使用它來創建能夠將 ShapeApp 無縫轉換為實時協作編輯器的方法。歡迎您隨時訪問加載項團隊博客 提出反饋意見或問題,網址為 go.microsoft.com/fwlink/?LinkId=117530。
Mueez Siddiqui 是 Microsoft 的 CLR 安全性和可擴展性小組的軟件開發工 程師。
本文配套源碼