在不修改代碼的情況下無限擴展應用項目
在許多需要分模塊開發,較為復雜的應用項目(如ERP之類)中,如何做到輕松擴展,往往是一個頭疼的問題。
在傳統條件下,我們會把各個功能分布在不同的類庫中,每添加一個功能就引用一個程序集,而這種方法,我們會發現,當你每添加一個新擴展後,都要對新增的程序集進行引用,這樣也意味著,你每次都要重新編譯一次主應用程序,這一來一往,維護成本也是有的。
到了.NET 3.5時代,你可能會想到Addin,但這個方法也會帶來擴展成本。
而我們所追求的最完美解決方案是:
如果我od 們編寫完應用程序後,可以在原有程序不變的情況下,無限添加擴展就好了。也就是說,我把應用A在用戶的機器上安裝好了,後來我做了一點擴展,這個新功能我已經編譯到fc.dll類庫中了,可我不想每次升級都要把EXE文件和所有組件重新編譯,我只需要把新的fc.dll復制到應用安裝目錄下就可以了。
也許這樣一來,維護成本可以大大降低了,到了.NET 4.0時代,利用MEF框架確實可以做到以上要求,但前提條件是:
1、在開發項目前,對整個項目的結構和規範必須明確,至少整個應用程序是什麽樣子的,你必須在大腦裏面有個底。
2、編寫一個公共類庫,裏面包含所有將來要進行擴展組件的行為規範,也就是在這個公共類庫中定義所有將來可能被實現的接口,後面所有擴展的程序集都必須實現這些接口。
本文涉及的內容可能有些深奧,但願大家可以接受,接受不了也沒關系,畢竟許多東西是需要時間去掌握的。
我弄個簡單的例子吧,比如,我現在要開發一個應用,在一個窗口上點擊按鈕後,顯示對應球類的名字,如“足球”、“皮球”、“排球”等。但是,可能當初我只給出兩個選項——足球和排球,這是我在把程序給客戶前就擴展的兩個程序集,但過了幾天,我突然想起,還有“羽毛球”、“籃球”等,於是,我要為應用程序再加兩個dll,但我希望應用程序擴展後不用修改代碼,無論我將來擴展100個還是10000個dll我都不需要重新生成主程序,我只要把新的dll扔到應用程序中的Ext文件夾中就可以了。
我們來看看如何實現。
1、新建一個公共類庫,寫兩個接口,IBall接口就是與球類信息有關的類,提供擴展時實現該接口。
[csharp] view plain copy- public interface IBall
- {
- string GetInformation();
- }
它有一個公共方法GetInformation,返回對應球類的名字,如“足球”.
另一個接口是用來描述元數據的。
[csharp] view plain copy- public interface IMetaData
- {
- string BallType { get; }
- }
為什麽要定義這個元數據的接口呢?就是為了識別我們應用程序調用了哪個擴展。
比如,FootBall(足球)類擴展實現了IBall接口,VolleyBall(排球)類擴展也實現了IBall接口,BasketBall(籃球)類擴展也實現了IBall接口,可能以後會更多,所有的擴展都實現IBall,那麽,我們怎麽知道我們正在調用的足球?而不是籃球呢?所以,就需要這個IMetaData類,在進行擴展的導出類時,我們為每一個類型定義一下IMetaData的BallType屬性,例如,我在定義足球類時,我定義BallType為“foot ball”,在定義排球類時,把BallType設置為“volley ball”,這樣,在我們的應用程序中,就可以通過這個來判斷我們正在調用哪個擴展,當然,如果你不需要明確知道調用哪個擴展,這個元數據就可以忽略。
2、分別編寫兩個類庫,符合以下兩個條件:
a、實現IBall接口。
b、用ExportAttribute標識為導出類型。
MEF的類都是來自System.ComponentModel.Composition程序集,在需要的地方引用就行了,如何引用程序集,我就不說了,這是基礎知識。這些類分布在以下三個命名空間。
System.ComponentModel.Composition
System.ComponentModel.Composition.Hosting
System.ComponentModel.Composition.Primitives
[csharp] view plain copy
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using System.ComponentModel.Composition;
- using System.ComponentModel.Composition.Hosting;
- namespace BallLibA
- {
- /// <summary>
- /// 足球
- /// </summary>
- [Export(typeof(CommonLib.IBall))]
- [ExportMetadata("BallType","Foot Ball")]
- public class FootBall:CommonLib.IBall
- {
- public string GetInformation()
- {
- return "足球";
- }
- }
- }
在MEF中,我們不需要實現提供元數據的接口,只需要在ExportMetadata特性中直接為屬性設置值就行,運行時會自動生成實現元數據(本例是IMetaData接口)的類。
接照同樣的方法,我們再做一個類庫。
[csharp] view plain copy- /// <summary>
- /// 排球
- /// </summary>
- [Export(typeof(CommonLib.IBall))]
- [ExportMetadata("BallType", "Volley Ball")]
- public class VolleyBall : CommonLib.IBall
- {
- public string GetInformation()
- {
- return "排球";
- }
- }
現在,我們的項目已經有兩個擴展了。
3、我們來實現我們的主應用程序,我們只需引用我們前面編寫的公共類庫中的接口即可,而擴展的dll我們不需要引用,MEF會自動尋找。因此,把所有擴展的程序集都生成dll文件,然後統一扔到與exe文件同一位置的Ext文件夾中就行了,你有1000個dll就全部扔到文件夾裏就行,MEF會自動尋找。
我們用一個WinForm程序作為主程序,如下圖所示。
在程序運行時,會根據Ext目錄下的所有擴展的dll自動發現所有程序集,然後顯示在ComboBox中,我們選擇對應的球類,然後點擊按鈕,這樣在文本框中就會對應地顯示球類的名稱。
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- using System.ComponentModel.Composition;
- using System.ComponentModel.Composition.Hosting;
- namespace TestApp
- {
- public partial class Form1 : Form
- {
- CompositionContainer myContainer = null;
- // 引入的組合類型
- [ImportMany]
- IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;
- public Form1()
- {
- InitializeComponent();
- DirectoryCatalog catl = new DirectoryCatalog("Ext");
- myContainer = new CompositionContainer(catl);
- try
- {
- myContainer.ComposeParts(this);//組合組件
- }
- catch (Exception ex)
- {
- MessageBox.Show(ex.Message);
- }
- var resBalls = (from t in mBalls
- select t.Metadata.BallType).ToArray();
- this.comboBox1.DataSource = resBalls;
- }
- private void button1_Click(object sender, EventArgs e)
- {
- if (this.comboBox1.SelectedIndex == -1)
- {
- MessageBox.Show("請選擇一個擴展。"); return;
- }
- string ballName = this.comboBox1.SelectedItem.ToString();
- // 取出要執行哪個擴展程序集
- var ballInstance = mBalls.FirstOrDefault(x => x.Metadata.BallType == ballName);
- if (ballInstance != null)
- {
- this.txtResult.Text = ballInstance.Value.GetInformation();
- }
- }
- }
- }
從上面的代碼中,可以總結出MEF的用法,這方法你有興趣的話可以背下來,因為無論你用到什麽項目,思路都是一樣的。
1、聲明一個CompositionContainer變量是必須的,因為它可以用來指示當前應用程序與哪些擴展程序集進行合並。
2、在實例化CompositionContainer時,我使用DirectoryCatalog類,為什麽?因為這個類好用,你只需要告訴它你擴展的dll放在哪個文件夾就行了。它會在你指定的文件夾裏面自動找到導出的擴展類。
3、有導出類,自然就有導入類,因為我們的所有擴展都是實現IBall接口的,所以,擴展的類的導出類型應使用IBall,這樣,凡是聲明為導出類的都會被MEF發現並自動加載。
所以,導出是針對擴展的程序集而言的,那導入就好理解了,就是針對我們的主應用程序而言,像本例,WinForm應用作為主程序,所有擴展都是在這個WinForm中使用的,所以這個WinForm就必須對類型進行導入。因此才有了以下代碼。
[csharp] view plain copy- // 引入的組合類型
- [ImportMany]
- IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;
使用Lazy來延遲實例化的好處是提高性能,記住,加了Import的導入類型是不用new的,因為DirectoryCatalog在Ext文件夾下找到所有的dll都會自動實例化,這就是要用延遲實便化的原因,只有在用的時候才new,不然,如果我的擴展目錄下有100000個dll,350000000個類,那你一運行就全部實例化,那這性能估計要把內存用爆。
前面我說過,IMetaData用於標識元數據,我們不必自己去實現,而我們也不必指事實上哪個接口,因為上面代碼中,Lazy<T, TMetadata>就有兩個泛型參數,看到沒?
T是我們要導入的類型,本例中是IBall,註意,我們這裏的類型一定要是公共的接口,不是擴展的具體類,不然就實現不了無限擴展的目的,接口用途就是它有通用性。
TMetadata就是用來標識元數的類型,本例是IMetaData接口,所以,前面我為什麽不用指定IMetaData的原因,因為這裏會指定,MEF會沿著這個線索自動搜索它的屬性BallType。
在實例化CompositionContainer容器後,要記得調用擴展方法ComposeParts,告訴MEF,所有擴展的程序集將和當前實例進行組合,不然你將無法調用。
現在,你運行一個這個WinForm,你就明白了。
看到了吧,FootBall和VolleyBall類所在的兩個程序集我並沒有在項目中,引用,只是把它們扔到Ext目錄下,應用程序就自動識別了。
我們的WinForm程序不用修改一行代碼。
如果你還不信的話,我們接下來再增加一個dll,定義一個BasketyBall(籃球類),然後,把這個籃球類庫也生成一個dll,同樣扔到Ext目錄下,而WinForm程序我根本不需要改動。
[csharp] view plain copy- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using System.ComponentModel.Composition;
- using System.ComponentModel.Composition.Hosting;
- namespace BallLibC
- {
- /// <summary>
- /// 籃球
- /// </summary>
- [Export(typeof(CommonLib.IBall))]
- [ExportMetadata("BallType","Basket Ball")]
- public class BasketBall:CommonLib.IBall
- {
- public string GetInformation()
- {
- return "籃球";
- }
- }
- }
同樣道理,把這個類庫編譯成dll,然後扔到Ext文件下,然後你再運行一下WinForm程序看看。
看到了吧,我沒有對WinForm做任何修改,只是在Ext目錄下多放了一個dll而已,運行後,程序就自動識別並找到對應的類型了。下拉列表框中就自動多了一個Basket Ball的選項了。選擇它,並單擊按鈕,這個BadketBall類就被執行了,輸出“籃球”。
以此類推,你再添加一千個一萬個dll,只要它符合IBall接口規範並設置導出,然後把這一千個一萬個dll全放到Ext目錄下,應用程序不需要做任何修改,運行後就會自動找到一千個一萬個擴展類了。
這樣一來,是不是節約了不少維護和升級成本了?MEF(Managed Extensibility Framework)強大吧?
在不修改代碼的情況下無限擴展應用項目