最近聽到很多關於托管擴展框架的描述,雖然很多人明白它的意思,但是卻不理解該工具的工作原理。這一工具能幫助客戶向WPF程序添加自己的表單嗎?我們的菜單中需要這些新表單不過不希望VB源代碼被客戶重新編譯。本文會教大家如何使用托管擴展框架。
聽起來這似乎是對MEF的有效應用。你也可以使用相同的方法來應用到WinForms或ASP.NET中,而且c#代碼與下文要展示的代碼相似。
MEF是一種能夠通過簡單模型使元件進行互動的擴展模型。MEF會調用部分元件。MEF對於不同模式類型都具有可擴展性,但同時會配備一個屬性模型。這樣你就可以通過.NET屬性定義部件的互動。如果某部件需要什麼,它會使用Import屬性,如果部件要供給什麼,它會使用Export屬性。任何部件都可用作主機或客戶端,供應商或客戶。MEF的這些互動以協議會基礎並提供靈活的發掘模型。你的應用程序會向MEF發送請求以提供執行。這樣應用程序就可以脫離依賴並讓你進行替換執行。
MEF目前還只是在CodePlex上作為一個預覽版推出。它位於System.ComponentModel命名空間中,這表明它將會以.NET Framework 4.0中的正式一員出現。某些微軟項目都將與之相關,包括VS 2010代碼編輯器。在它還未出現在.NET框架中以前,我們都需要下載MEF並將 System.ComponentModel.Composition.dll放到可訪問的文章。你還要意識到應用程序接口可能會發生變化。
應用程序將是MEF的主機而客戶會寫出MEF客戶端或擴展。擴展要易於寫客戶端因為主機必須管理CompositionContainer。我們會按照創建接口,創建擴展,創建WPF主機的步驟來講解整個過程。WPF主機會向CompositionContainer穿送一個目錄,它會指定用於搜索菜單擴展的目錄。
擴展支持所訂立的協議。最普遍的協議是接口,而這些接口都很簡單。
Public Interface IExtension
Sub ShowWindow()
End Interface
客戶可以通過執行這一接口來添加擴展並從System.ComponentModel.Composition命名空間中指定Export屬性:
Export(GetType(IExtension))> _
_
Partial Public Class First
Implements IExtension
Public Sub ShowWindow() _
Implements Common.IExtension.ShowWindow
End Sub
End Class
你也可以使用任意字符串來確定輸出,但是最普遍的擴展是通過一個接口協議來檢索。ExportMetadata屬性會包含主機能夠檢索的額外擴展信息,而不需要真正地展示基本對象,這樣可以讓我們控制示例並保護性能。
設計方案時,將接口設置為由主機和所有擴展組件單獨引用。不要在主機和擴展之間建立直接的引用。所有的組件都需要引用 System.ComponentModel.Composition.dll。編譯的擴展要放在方便的位置。你可以在屬性屬性對話框中指定建立的位置。
主機要管理CompositionContainer。CompositionContainer接通幕後的輸入與輸出,而且它必須了解所能獲取的輸出。創建一個容器項目級別的變量以清理OnExit。對容器Dispose方法調用會處理所有執行IDisposable的部件:
Protected Overrides Sub OnExit( _
ByVal e As System.Windows.ExitEventArgs)
MyBase.OnExit(e)
If mContainer IsNot Nothing Then
mContainer.Dispose()
End If
End Sub
覆蓋OnStartup方法會在應用程序啟動時讓你准備容器:
Protected Overrides Sub OnStartup( _
ByVal e As StartupEventArgs)
MyBase.OnStartup(e)
If Compose() Then
MainWindow.Show()
Else
Shutdown()
End If
End Sub
構成方法才是真正做准備工作的方法:
Private Function Compose() As Boolean
Dim cat As New AggregateCatalog
總目錄允許你對若干目錄進行統籌管理。如果一套目錄在總目錄中同時出現,所有符合這一套目錄的條目都會被找到。目錄中的不同類型也可以獲取,包括 DirectoryCatalog,這個類型在所有指定目錄中加載了所有組件。筆者建議將擴展的位置作為應用設定的一部分加入其中以實現更大的配置。你還可以添加當前組件:
Dim extLocation = My.Settings.ExtensionLocation
cat.Catalogs.Add(New DirectoryCatalog(extLocation))
cat.Catalogs.Add(New AssemblyCatalog( _
Me.GetType.Assembly))
mContainer = New CompositionContainer(cat)
從這一點講容器已經處於待命狀態只是還沒有請求發出。當你調用Compose方法的時候,一個構建批處理會讓你指定條目以評估Import請求。任何稍後被Import 請求實例化的對象將會被額外的Import請求評估。許多情況下,首先對Compose的調用是唯一需要演示的調用。因為Compose演示了組成部分,如果組成規則沒有得到貫徹這一演示會失敗,因此Try/Catch鎖定提供了報告:
Dim batch = New Hosting.CompositionBatch()
batch.AddPart(Me)
Try
mContainer.Compose(batch)
Catch ex As CompositionException
MessageBox.Show(ex.ToString())
Return False
End Try
Return True
End Function
應用程序的主窗口在域中使用Import屬性。滿足這一請求並創建域值,主窗口自身需通過MEF來提供——MEF會對任何明確添加到批處理以及任何通過另一Import實例化的Import屬性進行評估。你可以用Overloads來替換MainWindows方法:
_
Public Overloads Property MainWindow() As Window
Get
Return MyBase.MainWindow
End Get
Set(ByVal value As Window)
MyBase.MainWindow = value
End Set
End Property
在VB中則更為詳細。應用程序框架會繞開主窗口的屬性,自動對指定窗口進行實例化操作。禁用應用程序框架並往Application.xaml.vb文件中添加一個Sub Main:
'''
'''Application Entry Point.
'''
_
Public Shared Sub Main()
Dim app As Application = New Application
app.Run()
End Sub
這會導致應用程序通過所有物檢索主窗口以至於在主窗口中評估MEF請求。主窗口將自己輸出到MEF以匹配MainWindow所有物的輸入:
_
Partial Public Class Main
主窗口使用MEF來發掘可獲取的執行IExtension接口的擴展:
_
Private exportExtensions As ExportCollection( _
Of IExtension)
任何出現在目錄或人為添加到容器以及執行了IExtension接口的數據都會出現在集合中。注意這會創建Export對象的集合,該集合 包含足夠的信息使擴展實例化,但是它們還沒有將其實例化。對於保存性能來說這顯得尤為重要。主窗口會用標准WPF代碼填充菜單:
Private Sub Main_Loaded() Handles Me.Loaded
For Each export In exportExtensions
Dim newItem = New MenuItem()
newItem.Header = export.Metadata("MenuCaption")
Me.ExtensionMenu.Items.Add(newItem)
Next
End Sub
注意菜單標題取自MEF元數據。雖然這樣有效,但是你的目標是要讓編寫擴展變得更簡單。該元數據方法要求程序員懂得使用確切字符串“MenuCaption”的方法。使用強類型MEF元數據可以解決這一問題。首先,創建一個接口:
Public Interface IExtensionMetadata
ReadOnly Property MenuCaption() As String
End Interface
現在創建使接口並行的屬性。該屬性需要匹配元數據接口。雖然不需要執行這一接口,但是這是使其同步的最簡單的方法:
AttributeTargets.Class)> _
Public Class ExtensionMetadataAttribute
Inherits Attribute
Implements IExtensionMetadata
Private mMenuCaption As String
Public Sub New(ByVal menuCaption As String)
mMenuCaption = menuCaption
End Sub
Public ReadOnly Property MenuCaption() _
As String Implements IExtensionMetadata.MenuCaption
Get
Return mMenuCaption
End Get
End Property
End Class
你的客戶可以用屬性來裝配自己的擴展:
_
_
Partial Public Class First
你可以用替換主窗口中Import請求的方法來利用該元數據以便包含第二個類型參數:
_
Private exportExtensions As ExportCollection( _
Of IExtension, IExtensionMetadata)
這樣就提供了強類型的MetadataView以簡化對輸出元數據的訪問:
newItem.Header = export.MetadataView.MenuCaption
雖然窗口可以顯示了但是還不能與其他窗口或應用程序互動。下一步很容易,因為MEF使得擴展與主機間沒有區別而同時也解決了Import和 Export屬性的問題。下載的中的示例使用傳統接口為主機擴展提供字符串。你的應用程序或許可以提供更為先進的功能,如上層窗口。
重新編譯了非引用組件和一個可組合應用程序的編譯器快捷方式不會保存引用。在測試應用程序前用Rebuild Solution避免過期組件的問題。
MEF類似於System.Addln,即所謂的MAF。MAF的使用更為復雜,但是可以解決孤立隔離和版本化等額外的問題。MEF擴展在主機的AppDomain中運行。這意味著使用MEF的時候,你必須相信擴展不會運行惡意代碼或通過代碼訪問安全為其提供保護。MAF通過在單獨 AppDomain中創建擴展解決問題。
你正在維護發布到客戶端的接口。如果改變接口,就會破壞他們的代碼。相反,你可以創建新的接口,使之前的接口完好無損。MEF本身是可擴展的。如果你遇到版本化問題或AppDomain隔離問題貓膩可以創建一個額外的編程模式來結合MEF和MAF。
MEF也擁有直接存取的應用程序接口,但是大多數情況下VB或c#都需要Import/Export屬性模式。MEF完整的可擴展性使得它可以在其他情境中使用,包括動態語言,外部定義和候補發現機制。
到此,我們已經跳過了基數和生命周期的重要問題。當你將Import屬性放在單獨項目中時,要表述你需要單獨項目。如果不匹配或找到多個匹配,那麼標准的MEF容器配置會拋出一個異常情況。
_
Private textToDisplay As ITextToDisplay
如果你預先知道會出現多個匹配的情況,就能夠輸入一個IEnumerable或一個ExportCollection。如果不想有異常拋出或找不到匹配結果時,可以設置Import屬性中的AllowDefault參數值為True。如果不知道匹配結果的數量,要將Import放到集合中並將正確實例加入代碼中。
生命周期是指輸出的單獨實例是否用於所有請求,或新創建的實例是否用於每個請求。你要申明生命周期使用的是Export屬性的 CreationPolicy參數或Import屬性中的RequiredCreationPolicy。默認的創建政策是Any,而如果Export和 Import屬性都具備可用於Any的CreationPolicy,其結果是共享實例。如果CreationPolicies沖突,MEF會拋出異常。這可以讓你細致而靈活地控制實例創建過程。
MEF可以賦予你創建高度解耦型應用程序的能力,這樣的程序支持客戶擴展,粒度開發和優秀的編程設計,是程序員的好幫手。