Brad Abrams's blog Chinese version

Design Guidelines, Managed code and the .NET Framework: http://blogs.msdn.com/brada/default.aspx
随笔 - 44, 评论 - 18, 引用 - 1

导航

工具

关于

These postings are provided "AS IS" with no warranties, and confer no rights.

标签

每月存档

广告



访客

使用托管扩展性框架(MEF)创建可扩展应用的简介

【原文地址】Simple Introduction to Extensible Applications with the Managed Extensions Framework

【原文发表日期】29 September 08 08:21

 

近期我的团队一直致力于托管扩展性框架(Managed Extensions Framework)(MEF)的工作……现在我获得了一个机会向各位解释它的概念,而且我认为我已经发现一种新方法来讲解MEF,以使各位能更容易地理解接受它。因为我想花一点点时间来浏览一个非常简单MEF示例,以这种方式为大家介绍一般的扩展应用的能力,以及MEF独特的功能。

顺便说一句,你可以下载当前CTP版本的MEF最终可运行的示例

 

背景

 

让我们从最简单的例子开始:Hello World!

   1: using System;
   2:  
   3: class Program
   4: {
   5:     public void Run()
   6:     {
   7:         Console.WriteLine("Hello World!");
   8:         Console.ReadKey();
   9:     }
  10:     static void Main(string[] args)
  11:     {
  12:         Program p = new Program();
  13:         p.Run();
  14:     }
  15: }

现在,你也许不想永远打出相同的字符串,那么让我们稍稍地重构一下,将字符串抽取出来……

   1: public string Message { get; set; }
   2:  
   3: public void Run()
   4: {
   5:     Console.WriteLine(Message);
   6:     Console.ReadKey();
   7: }

看起来不错,现在我们要添加message……好吧,实际的文本内容是一项独立的关注点,它应该来自另外的类。比如:

   1: public class SimpleHello 
   2: {
   3:     public string Message
   4:     {
   5:         get
   6:         {
   7:             return "hello world!!";
   8:         }
   9:     }
  10: }

现在我们只需简单地把它们拼装起来:

   1: public void Run()
   2: {
   3:     SimpleHello hello = new SimpleHello();
   4:     Message = hello.Message;
   5:  
   6:     Console.WriteLine(Message);
   7:     Console.ReadKey();
   8: }

这已经能够运行了,但第3和第4行看上去有点怪怪的……我们带来了紧耦合的问题……我们真正想做的是将第3和第4行外部化,在不改变程序逻辑的其它部分的情况下控制它们。

 

进入MEF

添加对System.ComponentModel.Composition.dll程序集的引用,它可在MEF压缩包的bin目录下找到。

添加

   1: using System.ComponentModel.Composition;

现在,在Program类里,我们需要导入Message的值 -- 就是说,我们要指定程序外部的某段代码必须提供message。随后我们就要移除紧耦合。注意第4和第5行,我们指定了要导入Message的值。这里我将展示一下如何根据类型(字符串)引入……基础的类型,例如字符串可能会显得太通用了,因此可以考虑使用一个具名的import,例如[Import(“Message”)]。

   1: class Program
   2: {
   3:  
   4:     [Import]
   5:     public string Message { get; set; }
   6:  
   7:     public void Run()
   8:     {
   9:        // SimpleHello hello = new SimpleHello();
  10:         //Message = hello.Message;
  11:  
  12:         Console.WriteLine(Message);
  13:         Console.ReadKey();
  14:     }

现在我们需要在SimpleHello类中导出Message这个属性,它通知系统它能够满足需求。注意第3和第4行添加了一个Export属性(attribute)……同样地,它根据类型(在此为字符串)进行导出。同上,你也许会在实际运用中添加一个显式的名称[Export(“Message”)]。

   1: public class SimpleHello 
   2: {
   3:     [Export]
   4:     public string Message
   5:     {
   6:         get
   7:         {
   8:             return "hello world!!";
   9:         }
  10:     }
  11: }

现在我们需要告诉MEF为我们拼装这些内容。

   1: public void Run()
   2: {
   3:     //SimpleHello hello = new SimpleHello();
   4:     //Message = hello.Message;
   5:     var catalog = new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly());
   6:     var container = new CompositionContainer(catalog.CreateResolver());
   7:     container.AddPart(this);
   8:     container.Compose();
   9:  
  10:  
  11:     Console.WriteLine(Message);
  12:     Console.ReadKey();
  13: }

在第5行,我们创建了一个catalog——它告诉MEF在何处寻找导入与导出。在此,我们指定了当前运行的程序集。共有成千上万种不同的catalog,我们稍后再来看看,当然你也可以自行创建catalog。

在第6行,我们创建了一个Composition container——这就是实际上将所有不同的部分拼装起来的地方。

在第7行,我们将当前Program类的实例作为依赖项加入到container中。

在第8行,我们调用了Compose,这也就是Program类的Message属性得以赋值之处。

注意,这个例子中拼装是通过类型匹配实现的(字符串到字符串)……显然这不总是正确的方法,我们稍后再看看其它的拼装方法。

运行它,你就会看到期待中的输出“hello world!”。

现在我们再添加一个message,以添加一点乐趣……

   1: public class MoreMessages
   2: {
   3:     [Export]
   4:     public string FunMessage
   5:     {
   6:         get
   7:         {
   8:             return "This is getting fun!";
   9:         }
  10:     }
  11: }

现在运行……程序崩溃了!为什么?好吧,让我们看一看异常:

System.ComponentModel.Composition.CompositionException  Error : Multiple exports were found that match the constraint '(composableItem.ContractName = \"System.String\")'. The import for this contract requires a single export only."

从错误中看起来,似乎是我们为Import提供了太多的选择……MEF不知道该选择哪个了。当然你可以以编程的方式解决它,你也可以移除其中一个message的export……不过更有趣的是,实际上你可以告诉MEF你能够处理零个或多个结果。如下所示改变Program的Message属性。

   1: [Import]
   2: public IEnumerable<string> Messages { get; set; }

注意到我们将返回类型更改为一个字符串集合,而不是仅仅单个字符串。

现在稍稍地改变调用的代码,我们获得了:

   1: class Program
   2: {
   3:  
   4:     [Import]
   5:     public IEnumerable<string> Messages { get; set; }
   6:  
   7:     public void Run()
   8:     {
   9:         //SimpleHello hello = new SimpleHello();
  10:         //Message = hello.Message;
  11:         var catalog = new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly());
  12:         var container = new CompositionContainer(catalog.CreateResolver());
  13:         container.AddPart(this);
  14:         container.Compose();
  15:  
  16:         foreach (var s in Messages)
  17:         {
  18:             Console.WriteLine(s);
  19:         }
  20:  
  21:     
  22:         Console.ReadKey();
  23:     }

Console output 1

哇噢——我们获得了两个message!太酷了!

MEF更多的价值

OK,我想我们都同意,当我们做的工作影响到了同一个程序集的时候,这种方式实际上给我们的代码增加了一点复杂度。当你各自的部门工作于不同的组件时,MEF才最管用。根据定义,这些组件通常在没有交叉引用的独立程序集内。为展示MEF是怎样支持这一点的,让我们添加一个新的Class Library项目到解决方案中,命名为ExternalMessages,并添加对System.ComponentModel.Composition.dll程序集的引用。

添加如下的类。

   1: using System;
   2: using System.ComponentModel.Composition;
   3:  
   4: public class Class1
   5: {
   6:     [Export]
   7:     public string Message
   8:     {
   9:         get
  10:         {
  11:             return "I am starting to get it...";
  12:         }
  13:     }
  14: }

现在我们需要将这个类拼装入catalog……注意第6行,我们将catalog改为在某个目录中寻找所需各部分。

   1: public void Run()
   2:  {
   3:      //SimpleHello hello = new SimpleHello();
   4:      //Message = hello.Message;
   5:      var catalog = new DirectoryPartCatalog(@"..\..\..\ExternalMessages\bin\Debug");
   6:          // new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly());
   7:      var container = new CompositionContainer(catalog.CreateResolver());
   8:      container.AddPart(this);
   9:      container.Compose();
  10:  
  11:      foreach (var s in Messages)
  12:      {
  13:          Console.WriteLine(s);
  14:      }
  15:  
  16:  
  17:      Console.ReadKey();
  18:  }

注意:DirectoryPartCatalog同样支持相对路径,它会在当前的AppDomain.CurrentDomain.BaseDirectory下的路径进行查找,比如说:

new DirectoryPartCatalog(@”.\extensions\”);

运行它,我们就获得了新的message!

酷,但我们也失去了原来的message,而我希望也能得到它们……好吧,幸运的是,我们还有一个聚合部件目录类(aggregate part catalog)能够从多个源获取所需部分。

   1: public void Run()
   2:  {
   3:      //SimpleHello hello = new SimpleHello();
   4:      //Message = hello.Message;
   5:      var catalog = new AggregatingComposablePartCatalog();
   6:         catalog.Catalogs.Add (new DirectoryPartCatalog(@"..\..\..\ExternalMessages\bin\Debug"));
   7:         catalog.Catalogs.Add (new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly()));
   8:      var container = new CompositionContainer(catalog.CreateResolver());
   9:      container.AddPart(this);
  10:      container.Compose();

太酷了,现在我们获得了所有的message!

Console output 2 

最后,提一下这一点……我创建了一些导出message的不同的程序集……所要做的仅是将catalog指向它们并运行。

Multiple assemblies

   1: public void Run()
   2: {
   3:     //SimpleHello hello = new SimpleHello();
   4:     //Message = hello.Message;
   5:     var catalog = new AggregatingComposablePartCatalog();
   6:        catalog.Catalogs.Add (new DirectoryPartCatalog(@"..\..\..\ExternalMessages\bin\Debug"));
   7:        catalog.Catalogs.Add(new DirectoryPartCatalog(@"..\..\..\ExtraMessages"));
   8:        catalog.Catalogs.Add (new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly()));
   9:     var container = new CompositionContainer(catalog.CreateResolver());
  10:     container.AddPart(this);
  11:     container.Compose();
  12:  

看我在第7行把它们加入的路径……现在只需把程序集拷贝到这个目录下,它们就可以在程序中使用了!注意即使我不断地添加更多的扩展,我也不需要改变任何核心程序的逻辑。

Console output 3

将MEF带入下一层次

 

以上我仅展示了最简单的场景……让我们尝试一些更强大的。如果你仍然吹毛求疵地想在主程序中找到紧耦合的部分,那么Console.WriteLine()将显露出来……如果你想写到日志中呢?如果你想调用一个web service或者是输出到HTML或WPF呢?对Console的紧耦合使这一点不容易做到。我们将如何使用关注分离(separation of concerns)原则与MEF来消除这种紧耦合呢?

首先,我们需要定义一个接口,描述输出字符串的契约。为确保正确的依赖项管理,让我们创建一个新的Library项目并命名为SharedLibrary,添加这个接口,并让其它所有的项目都添加对这个项目的引用。

   1: namespace SharedLibrary
   2: {
   3:     public interface IOutputString
   4:     {
   5:         void OutputStringToConsole(string value);
   6:     }
   7: }

现在回到主程序,我们将能够提取出Console.WriteLine()……

   1: class Program
   2: {
   3:     [Import]
   4:     public IEnumerable<string> Messages { get; set; }
   5:  
   6:     [Import]
   7:     public IOutputString Out { get; set; }
   8:  
   9:     public void Run()
  10:     {
  11:         //SimpleHello hello = new SimpleHello();
  12:         //Message = hello.Message;
  13:         var catalog = new AggregatingComposablePartCatalog();
  14:            catalog.Catalogs.Add (new DirectoryPartCatalog(@"..\..\..\ExternalMessages\bin\Debug"));
  15:            catalog.Catalogs.Add(new DirectoryPartCatalog(@"..\..\..\ExtraMessages"));
  16:            catalog.Catalogs.Add (new AttributedAssemblyPartCatalog(Assembly.GetExecutingAssembly()));
  17:         var container = new CompositionContainer(catalog.CreateResolver());
  18:         container.AddPart(this);
  19:         container.Compose();
  20:  
  21:         foreach (var s in Messages)
  22:         {
  23:             Out.OutputString(s);
  24:         }
  25:  
  26:     
  27:         Console.ReadKey();
  28:     }

在第6、7行中我们定义了Out,并在第23行中将Console.WriteLine()改为Out.OutputString()。

现在在ExternalMessages项目中添加以下类

   1: [Export(typeof(IOutputString))]
   2: public class Class1 : IOutputString
   3: {
   4:     public void OutputString(string value)
   5:     {
   6:         Console.WriteLine("Output=" + value);
   7:     }
   8:  
   9:     

注意我们在这里显式地声明了导出类型为刚才定义的共享接口。现在当程序运行时,我们获得了:

Console output 4 

为了更有趣,让我们添加另一个更有创意的IOutputString的实现。

   1: [Export(typeof(IOutputString))]
   2: public class ReverseOutputter : IOutputString
   3: {
   4:  
   5:     public void OutputString(string value)
   6:     {
   7:         foreach (var s in value.Split().Reverse())
   8:         {
   9:             Console.ForegroundColor = (ConsoleColor)(s.Length % 10);
  10:             Console.Write(s + " ");
  11:         }
  12:         Console.WriteLine();
  13:     }
  14: }

现在就运行的话会给我们一个错误,因为我们告诉了MEF我们只需要一个IOutputString……如果我们改变一下代码以获取多个的话就更有趣了!将第7行改为接受一个IOutputString的集合,而在第19行指定对所有输出设备进行循环。

   1: class Program
   2: {
   3:     [Import]
   4:     public IEnumerable<string> Messages { get; set; }
   5:  
   6:     [Import]