[C#]MEF開發系列 - Managed Extensibility Framework(MEF)的概念與簡介

  • 25217
  • 0
  • C#
  • 2012-12-13

[C#]MEF開發系列 - Managed Extensibility Framework(MEF)的概念與簡介

 image

Managed Extensibility Framework(MEF)是.NET 4.0與Silverlight 4.5開始內建在BCL中的輕量型可擴充式框架(若是使用.NET 3.5,也可以另行加入組件使用),可以輔助開發人員建立具有擴充彈性且易於維護的應用程式。

 

在以往若是我們要為程式加上可擴充性的功能,可能必須要使用反射來做,可參閱筆者使用反射(Reflection)實現應用程式擴充元件機制這篇。為了要達到類似的效果,我們必需去找出目錄所內含的組件檔,循序的去將找到的組件載入,載入後遍循所有可被實體化的類別,並從中找出有實作指定介面的類別,再將其實體化後塞入正確的位置。在做這樣的動作時,目錄內可能會存放著非.net的組件,不小心載入系統就會丟出例外而讓整個載入流程中斷。而將類別實體化為物件時,又需考慮類別的建構子,建構子不對也無法正確地將類別實體化為物件。而且做這樣的動作多半必需依賴著特定型別或介面去做。當系統中的擴充點一多,這樣的動作就得成倍數成長,十分的不好處理。

 

MEF的出現可以讓開發人員有效的簡化這樣的動作,開發時開發人員只要知道哪邊是會有變動或是需要擴充的點、要塞入擴充點的動作為何、以及甚麼時候要將擴充的動作塞入擴充點。擴充功能的探索以及擴充功能與擴充點的組合,MEF都做了相當的封裝,使用起來非常的方便,而前面提到載入時可能發生的問題MEF也都處理掉了,開發人員可以更專注於擴充點與擴充功能的建立。

 

為了易於了解,這邊我們可以把擴充點想成是插座(母頭),擴充的功能是電器的插頭(公頭)。每個插座雖然在外觀上會有些差異(接口不同),但都是提供電器運轉所需要的電,而每個電器也都有著不同的功能,只要能插到正確的插座就能夠發揮電器該有的效果。同一個插座可能依插上的電器不同一會具有照明的功能,一會具有發熱的功能,或是透過擴充插座同時照明與發熱,這就是我們程式開發所需要的擴充彈性。而MEF在這邊扮演的就是一個代理人,只要我們將插座與插頭交給它,插頭就會插在正確的插座上,它自行會依照一些線索(插座與插頭的外觀)下去推斷要做怎樣的組合,不用煩惱它到底是歐規、美規、兩頭、三頭、110v、220v...

 

MEF主要是由下列幾個部分所組成 

  • Composable part
  • Export
  • Import
  • Composition Container
  • Catalog 

 

Composable part 是composable unit,也就是最小的組成單位,可以提供服務給其他組件使用(擴充功能)或是操作其他組件提供的服務(擴充點)。

Export 是指提供的服務,也就是我們所說的擴充功能。透過ExportAttribute來設定。

Import 是指所使用的服務,也就是我們所說的擴充點。透過ImportAttribute來設定。

Composition Container 是組合容器,會去mapping import跟export、組合Composable part。

Catalog 主要是用來探索Composable part用的,可從型別、組件 (Assembly) 或目錄 (Directory)探索到Composable part。

 

了解了MEF的概念以及組成元件後,讓我們來看一下MEF官方的示意圖片。由下圖中我們可以很清楚的看到Composable part內具有Import與Export,而Export與Import可以透過MEF做組合,表示著透過MEF我們可以很容易的在程式撰寫時透過Attribute去指定擴充點與擴充功能,並透過MEF將它組合起來達到我們想要的效果。簡單的一張圖帶出了上面提到的Import、Export、Composable Part、以及Composition等概念。

image

 

而下圖則是表示著Catalog可以幫我們探索Composable part,並且透過Composition Container元件我們可以將之組合在一起。

image

 

這邊讓我們實際來寫個簡易的MEF程式。為了便於比較,這邊筆者直接拿使用反射(Reflection)實現應用程式擴充元件機制這篇的範例稍作修改,從反射叫用改用MEF下去實作。

 

首先我們必須將System.ComponentModel.Composition.dll加入參考。

image

 

然後在程式中引用System.ComponentModel.Composition命名空間:


using System.ComponentModel.Composition;

 

接著將PlugIn.Core的內的Interface做些調整。


namespace PlugIn.Core
{
    public interface IHost
    {
		IEnumerable<IModule> Modules { get; set; }
    }
}

 

並在PlugIn.Core.IModule上附加InheritedExportAttribute(前面所提到的Export),用以指定所有實作該介面的類別都視為我們程式中的擴充功能。


namespace PlugIn.Core
{
	[InheritedExport]
    public interface IModule
    {
        String Name { get; }
        IHost Host { get; set; }
        void Execute();
    }
}

 

最後要調整的是PlugIn.Host.MainForm,將之實作IHost介面,在Modules成員屬性上附加ImportManyAttribute(前面提到的Import),用以指定Modules成員屬性為我們程式中的擴充點,並在建構子中透過DirectoryCatalog(前面提到的Catalog)找出當前目錄下有哪些可擴充的元件(Composable part),然後用CompositionContainer將擴充點與擴充功能組合就可以了。


namespace Host
{
    public partial class MainForm : Form, IHost
    {
        ...
        [ImportMany]
        public IEnumerable<IModule> Modules { get; set; }

        public MainForm()
        {
            InitializeComponent();

            var catalog = new DirectoryCatalog(Environment.CurrentDirectory, "*.dll");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);

            foreach (IModule module in Modules)
            {
                module.Host = this;
                模組MToolStripMenuItem.DropDownItems.Add(module.Name, null, Module_Click).Tag = module;
            }
        }
        ...
   }
}

 

程式撰寫起來比使用反射來說簡單了許多,使用反射(Reflection)實現應用程式擴充元件機制這篇範例內用到的PlugInController整個都可以拿掉了。

 

運行結果如下:

image

image

 

Download

PlugInDemo.zip

 

Link