• 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()]注解中加入名称标识,从而识别某个具体的对象,然而这种方法只是用于页面初始化的时候就行过滤,页面打开后没有导入的就再也导入不了了,就是说我们不能在导入的集合中分辨各自的不同,所有导入的类都是没有标识的。

    待续...

  • 相关阅读:
    CVE-2019-16278:Nostromo Web服务器的远程命令执行
    内网渗透一(信息收集)
    Apache Flink 任意jar包上传漏洞
    Apache ---- Solrl漏洞复现
    linux内核过高导致vm打开出错修复脚本
    lvm拓展
    文件时间进度扫描控制,可回溯,空闲扫描,系统时间调整不影响
    Raid 管理
    curl 工具使用
    docker 入门
  • 原文地址:https://www.cnblogs.com/yk123/p/5350133.html
Copyright © 2020-2023  润新知