程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> CLR全面透徹解析 - 使用System.AddIn擴展Windows窗體應用程序

CLR全面透徹解析 - 使用System.AddIn擴展Windows窗體應用程序

編輯:關於.NET

目錄

調整宿主

公開對象模型適配器、約定和加載項視圖編寫加載項協作加載項總結

您知道現在可以使用新的 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 類型。最後,我調用了 Add­InStore.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;
  }
}

創建視圖程序集後,我需要修改宿主類以從相應的視圖中繼承。下面以粗體 顯示對 Shape­Application 類的更改:

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 安全性和可擴展性小組的軟件開發工 程師。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved