MEF學習筆記


之前公司里用到了一個叫MEF的東西,說來慚愧一直只管寫代碼卻不曾理解MEF框架為何物,今天就來學習一下,這是一篇遲到了不知多久的博客。

--------------------------------------------------------進入正題-------------------------------------------------------------------

1.MEF概念

MEF,全稱Managed Extensibility Framework(托管可擴展框架)。單從名字我們不難發現:MEF是專門致力於解決擴展性問題的框架,MSDN中對MEF有這樣一段說明:

  Managed Extensibility Framework 或 MEF 是一個用於創建可擴展的輕型應用程序的庫。 應用程序開發人員可利用該庫發現並使用擴展,而無需進行配置。 擴展開發人員還可以利用該庫輕松地封裝代碼,避免生成脆弱的硬依賴項。 通過 MEF,不僅可以在應用程序內重用擴展,還可以在應用程序之間重用擴展。

2.MEF使用

  MEF的使用范圍廣泛,在Winform、WPF、Win32、Silverlight中都可以使用,我們就從控制台說起,看看控制台下如何實現MEF,下面先新建一個win32控制台項目MEF_1的Demo,添加一個IBookService接口一個繼承這個接口的類MusicBook ,如圖:

首先需要手動添加對System.ComponentModel.Composition命名空間的引用,由於在控制台程序中沒有引用這個DLL,所以要手動添加:

 

我們再回到program類中寫如下代碼:

class Program
    {
        [Import]
        public IBookService Service { get; set; }

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Service != null)
            {
                Console.WriteLine(pro.Service.GetBookName());
            }
            Console.Read();
        }
       //宿主MEF並組合部件
         private void Compose() { 
//獲取包含當前執行的代碼的程序集
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
CompositionContainer container = new CompositionContainer(catalog);
      //將部件(part)和宿主程序添加到組合容器
      container.ComposeParts(this); } 
}

  這個方法表示添加當前Program這個類到組合容器,為什么要添加到組合容器?是因為只要添加到組合容器中之后,如果該類里面有Import,MEF才會自動去尋找對應的Export。這也就是為什么使用MEF前必須要組合部件的原因。

代碼運行情況:

可以看到調用了MusicBook類的GetBookName方法,但是我們並沒有實例化MusicBook類,是不是很神奇呢???

這一就是實現了主程序和類之間的解耦,大大提高了代碼的擴展性和易維護性!

看到這里可能有人就會說這個Import是多此一舉了,既然我們可以new,為什么非要用這種奇怪的語法呢,怪別扭的。其實如果我們站在架構的層面,它的好處就是可以減少dll之間的引用。

------------------------------------------------MEF中的Import(導入)和Export(導出)-------------------------------------

上面的測試小Demo我們對MEF有了一個初步的了解,下面我們來細看一下MEF中的Import(導入)和Export(導出)。

 class Program
    {
        [Import("MusicBook")]
        public IBookService Service { get; set; }

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Service != null)
            {
                Console.WriteLine(pro.Service.GetBookName());
            }
            Console.Read();
        }
        //宿主MEF並組合部件
        private void Compose()
        {
            //獲取包含當前執行的代碼的程序集
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            CompositionContainer container = new CompositionContainer(catalog);
            //將部件(part)和宿主程序添加到組合容器
            container.ComposeParts(this);
        }
    }
}
 ////Export(typeof(IBookService)) 這句話將類聲明導出為IBookService接口類型
    //[Export(typeof(IBookService))]
    [Export("MusicBook",typeof(IBookService))]
    class MusicBook:IBookService
    {

        public string BookName { get; set; }

        public string GetBookName()
        {
            return "音樂書本";
        }
    }

注意,標紅的是改動過的地方,其他地方的代碼沒有變,上一次我們使用的是Export的方法是[Export(typeof(IBookService))],這次前面多了一個參數,沒錯,這個就是一個契約名,名字可以隨便起,而且可以重復,但是如果名字亂起,和其他DLL中的重復,到時候會導致程序出現很多Bug,最好按照一定的規范去起名字。

這里有了契約名以后,導入(Import)時就要指定的契約名,否則將無法找到MusicBook,Export還有一個方法是[Export("Name")],這個方法只指定了契約名,沒有指定導出類型,那么默認的導出類型是object類型,在導入時導出到的對象就要為object類型,否則將匹配不到那個組件。

  到現在,我們只寫了一個接口和一個實現類,導出的也是一個類,下面我們多添加幾個類來看看會怎么樣,為了方便大家測試,我把實現接口的類寫在一個文件里面,新加幾個類后,的MusicBook類文件代碼如下:

[Export("MusicBook",typeof(IBookService))]
    public class MusicBook:IBookService
    {

        public string BookName { get; set; }

        public string GetBookName()
        {
            return "音樂書本";
        }
    }
    [Export("MusicBook", typeof(IBookService))]
    public class MathBook : IBookService
    {


        public string BookName { get; set; }

        public string GetBookName()
        {
            return "數學課本";
        }
    }
    [Export("MusicBook", typeof(IBookService))]
    public class HistoryBook:IBookService
    {

        public string BookName { get; set; }

        public string GetBookName()
        {
            return "歷史課本";
        }
    }

這里添加兩個類,HistoryBook和MathBook,都繼承自IBookService接口,注意他們的契約名都相同,都為MusicBook,后面再詳細的說這個問題,修改后的program的代碼如下:

class Program
    {
        [ImportMany("MusicBook")]
        public IEnumerable<IBookService> Services { get; set; }

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Services != null)
            {
                foreach (var service in pro.Services) { Console.WriteLine(service.GetBookName()); }
            }
            Console.Read();
        }
        //宿主MEF並組合部件
        private void Compose()
        {
            //獲取包含當前執行的代碼的程序集
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            CompositionContainer container = new CompositionContainer(catalog);
            //將部件(part)和宿主程序添加到組合容器
            container.ComposeParts(this);
        }
    }

這里需要注意的是標紅的兩行代碼,[ImportMany("MusicBook")]還有下面的聲明變成了IEnumerable<>,因為要導出多個實例,所以要用到集合,下面采用foreach遍歷輸出,運行的結果如下圖:

現在三個類的契約名都不相同了,其他的代碼不動,再次運行程序看看,是不是現在只輸出MusicBook了,同理,修改[Import("Name")]中的契約名稱,就會導入指定含有名稱的類,契約名可以重復,這一以來,我們就可以用契約名給類進行分類,導入時可以根據契約名來導入。

注意:IEnumerable<T>中的類型必須和類的導出類型匹配,如類上面標注的是[Exprot(typeof(object))],那么就必須聲明為IEnumerable<object>才能匹配到導出的類。

例如:我們在類上面標注[Export("Book")],我們僅僅指定了契約名,而沒有指定類型,那么默認為object,此時還用IEnumerable<IBookService>就匹配不到。

那么,這種情況就要在輸出是進行強制類型轉換,代碼如下:

 [Export("MusicBook")]
    public class MusicBook:IBookService
    {

        public string BookName { get; set; }

        public string GetBookName()
        {
            return "音樂書本";
        }
    }

如果我們這樣寫了那樣結果就只能匹配到后兩個:

此時我們需要強制轉換一下輸出的類型:

[ImportMany("MusicBook")]
        public IEnumerable<object> Services { get; set; }

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Services != null)
            {
                foreach (var service in pro.Services)
                {
                    var ss = (IBookService)service; Console.WriteLine(ss.GetBookName());
                }
            }
            Console.Read();
        }

結果就匹配到了所有類的輸出:

------------------------------------------------MEF中方法和屬性能不能導出呢?---------------------------------------------

前面說完了導入和導出的幾種方法,如果大家細心的話會注意到前面我們導出的都是類,那么方法和屬性能不能導出呢???答案是肯定的,下面就來說下MEF是如何導出方法和屬性的:

首先是導出屬性:

 [Export("MusicBook", typeof(IBookService))]
    public class MathBook : IBookService
    {
        //導出私有的屬性
        [Export(typeof(string))]
        private string _privateBookName = "私有的屬性BookName";
        //導出公用的屬性
        [Export(typeof(string))]
        public string _publicBookName = "公有的屬性BookName";

        public string BookName { get; set; }

        public string GetBookName()
        {
            return "數學課本";
        }
    }
 class Program
    {
        [ImportMany("MusicBook")]
        public IEnumerable<object> Services { get; set; }

        //導入屬性(這里不區分public還是private)
        [ImportMany]
        public List<string> InputString { get; set; } 

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Services != null)
            {
                foreach (var service in pro.Services)
                {
                    var ss = (IBookService)service;
                    Console.WriteLine(ss.GetBookName());
                }
                foreach (var str in pro.InputString) { Console.WriteLine(str); }
            }
            Console.Read();
        }
        //宿主MEF並組合部件
        private void Compose()
        {
            //獲取包含當前執行的代碼的程序集
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            CompositionContainer container = new CompositionContainer(catalog);
            //將部件(part)和宿主程序添加到組合容器
            container.ComposeParts(this);
        }
    }

最后的輸出結果為:

下面說導出方法吧,同理無論是公有方法還是私有方法都是可以導出的,MusicBook代碼如下:

        //導出公用方法(無參數)
        [Export(typeof (Func<string>))]
        public string PublicGetBookName()
        {
            return "PublicMathBook";
        }
        //導出私有方法(有參數)
        [Export(typeof (Func<int,string>))]
        public string PrivateGetBookName(int count)
        {
            return string.Format("我看到一個好書,我買了{0}本", count);
        }
class Program
    {
        [ImportMany("MusicBook")]
        public IEnumerable<object> Services { get; set; }

        //導入屬性(這里不區分public還是private)
        [ImportMany]
        public List<string> InputString { get; set; }

        //導入無參數方法
        [Import]
        public Func<string> methodWithoutPara { get; set; }
        //導入有參數的方法
        [Import]
        public Func<int, string> methodWithPara { get; set; }

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            if (pro.Services != null)
            {
                foreach (var service in pro.Services)
                {
                    var ss = (IBookService)service;
                    Console.WriteLine(ss.GetBookName());
                }
                foreach (var str in pro.InputString)
                {
                    Console.WriteLine(str);
                }
            }
            //調用無參數的方法
            if (pro.methodWithPara != null)
            {
                Console.WriteLine(pro.methodWithoutPara());
            }
            
            //調用有參數的方法
            if (pro.methodWithoutPara!=null)
            {
                Console.WriteLine(pro.methodWithPara(5)); }
            Console.Read();
        }
        //宿主MEF並組合部件
        private void Compose()
        {
            //獲取包含當前執行的代碼的程序集
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            CompositionContainer container = new CompositionContainer(catalog);
            //將部件(part)和宿主程序添加到組合容器
            container.ComposeParts(this);
        }
    }

導入導出方法用到了Func<T>委托,當然沒有返回值的話可以用Action<T>委托。

------------------------------------------------MEF在分層架構中的使用---------------------------------------------

前面講了MEF的基礎和基本到導入導出方法,下面就是見證MEF真正魅力所在的時刻。如果沒有看過前面的文章,請到我的博客首頁查看。

  前面我們都是在一個項目中寫了一個類來測試的,但實際開發中,我們往往要采用分層架構,就拿最簡單的三層架構來說吧,我們通常把業務邏輯寫在DLL中,現在就來寫一個例子,看看如何在不編譯整個項目的情況下,輕松的實現擴展。先透露一下,我們只要添加一個DLL就可以了。

  這里就以銀行為例子吧,首先新建一個控制台項目,叫MEF_2吧,然后建一個類庫寫接口,然后再建一個類庫實現接口。項目結構如下:

其中MEF_2和BankOfChina都只引用接口項目,MEF_2不需要引用BankOfChina。

BankInterface的代碼如下,做個簡單實例,寫幾個方法測試一下:

public interface ICard
    {
       //賬戶金額
        double Money { get; set; }
        //獲取賬戶信息
        string GetCountInfo();
        //存錢
        void SaveMoney(double money);
        //取錢
        void CheckOutMoney(double money);
    }

這里添加一個中國銀行卡,實現接口,引用命名空間什么的不再重復說了,不懂看前面的文章,代碼如下:

namespace BankOfChina
{
    [Export(typeof(ICard))]
    public class ZHCard : ICard
    {
        public string GetCountInfo()
        {
            return "中國銀行";
        }
        public double Money { get; set; }

        public void SaveMoney(double money)
        {
            this.Money += money;
        }

        public void CheckOutMoney(double money)
        {
            this.Money -= money;
        }

    }
}

主程序中的代碼:

namespace MEF_2
{
    class Program
    {
        [ImportMany(typeof(ICard))]
        public IEnumerable<ICard> cards { get; set; } 

        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            foreach (var c in pro.cards)
            {
                Console.WriteLine(c.GetCountInfo());
            }
            Console.Read();
        }

        private void Compose()
        {
            var catalog = new DirectoryCatalog("MyCards");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }
}

現在,我們知道只有一種銀行卡,及中國銀行的,注意我標紅的代碼,這里是一個目錄,及主程序所在目錄的Cards(bin-debug-Cards)文件夾(我們需要提前建立好),我們把生成的BankOfChian.dll拷貝到這個文件夾下,然后運行才可以正確輸出信息(畢竟我們沒有引用那個項目),如圖:

 

此時我們看到輸出結果為"中國銀行"。

到了這里相信大家已經明白了,如果現在需求改變了,需要支持建行、農行等銀行卡,怎么辦呢?通常我們要改項目,把整個項目都編譯再重新發布。但是現在不需要這么做了,我們只需要添加一個類庫項目,把生成的dll拷貝到Cards目錄下即可。

我們在這個解決方案下繼續添加一個類庫項目,實現ICard接口,代碼如下:

namespace NongHang
{
    [Export(typeof(ICard))]
    public class NHCard:ICard
    {
        public double Money { get; set; }

        public string GetCountInfo()
        {
            return "中國農業銀行";
        }

        public void SaveMoney(double money)
        {
            this.Money += money;
        }

        public void CheckOutMoney(double money)
        {
            this.Money -= money;
        }
    }
}

點擊右鍵編譯,把生成的dll拷貝到Cards目錄下面,運行看到如下結果:

再看看Cards目錄中,現在你添加幾個dll,就會顯示多少條信息了。

------------------------------------------------MEF的高級用法 MEF中如何訪問某個具體的對象-----------------------------------

前面我們講過在導出的時候,可以在[Export()]注解中加入名稱標識,從而識別某個具體的對象,然而這種方法只是用於頁面初始化的時候就行過濾,頁面打開后沒有導入的就再也導入不了了,就是說我們不能在導入的集合中分辨各自的不同,所有導入的類都是沒有標識的。

待續...

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM