https://msdn.microsoft.com/zh-cn/library/ff576068.aspx
http://blogs.msdn.com/b/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx
随着 Microsoft .NET Framework 4 的即将推出,您会发现手头上多了一种令人激动的新技术,这项技术将大大简化应用程序开发。如果您一直为如何设计更易于维护和扩展的应用程序而绞尽脑汁,请往下读。
托管可扩展性框架 (MEF) 是 .NET Framework 4 和 Silverlight 4 中新增的一个库,用于简化在部署后可由第三方进行扩展的可组合系统的设计。MEF 可使您的应用程序具有开放性,从而允许应用程序开发人员、框架编写者以及第三方扩展程序不断引入新功能。
几年前,在 Microsoft 内部,一些小组致力于为一个问题找到解决方案,即如何基于可重用的组件构建可动态发现、重用和组合的应用程序:
此问题并不是 Microsoft 所特有的。多年来,客户一直实现其自己的自定义可扩展性解决方案。显然,这是很好的机会,平台可以步入这一领域,提供更通用的解决方案,有助于 Microsoft 和客户实现双赢。
无论如何,MEF 不是此问题的第一种解决方案。人们提出过许多解决方案 — 跨越平台边界的尝试数不胜数,涉及的工作包括 EJB、CORBA、Eclipse 的 OSGI 实现以及 Java 端的 Spring 等等。在 Microsoft 的平台上,.NET Framework 自身内部包含组件模型和 System.Addin。同时存在若干种开源解决方案,包括 SharpDevelop 的 SODA 体系结构和“控制反转”容器(如 Castle Windsor、Structure Map 以及模式和实践的 Unity)。
既然目前已有这些方法,为何还要引入新事物?这是因为我们意识到,我们当前的所有解决方案对于常规第三方可扩展性都不理想。这些解决方案要么规模过大,不适合常规用途,要么需要主机或扩展开发人员一方完成过多工作。MEF 在最大程度上秉承了所有这些解决方案的优点,尝试解决刚才所提及的令人头痛的问题。
让我们来看看 MEF 的核心概念,如图 1 所示。
图 1 托管可扩展性框架中的核心概念
MEF 有几个基本核心概念:
可组合的部件(或简称“部件”)— 一个部件向其他部件提供服务,并使用其他部件提供的服务。MEF 中的部件可来自任何位置(应用程序内部或外部);从 MEF 的角度来看,这并无区别。
导出 — 导出是部件提供的服务。某个部件提供一个导出时,称为该部件导出 该服务。例如,部件可以导出记录程序(对于 Visual Studio 而言则是导出编辑器扩展)。虽然大多数部件只提供一个导出,但也有部件可提供多个导出。
导入 — 导入是部件使用的服务。某个部件使用一个导入时,称为该部件导入 该服务。部件可导入一个服务(如记录程序),也可导入多个服务(如编辑器扩展)。
约定 — 约定是导出或导入的标识符。导出程序指定其提供的字符串约定,导入程序指定其需要的约定。MEF 从要导出和导入的类型派生约定名称,因此在大多数情况下,您不必考虑这一点。
组合 — 部件由 MEF 组合,MEF 将部件实例化,然后使导出程序与导入程序相匹配。
开发人员可通过编程模型使用 MEF。通过编程模型,可将组件声明为 MEF 部件。MEF 提供了一个现成可用的特性化编程模型,这将是本文的重点内容。该模型只是 MEF 支持的众多可能的编程模型之一。MEF 的核心 API 完全与特性无关。
在特性化编程模型中,部件(称为“特性化部件”)使用 System.ComponentModel.Composition 命名空间中的一组 .NET 特性进行定义。在下面几节中,我将使用此模型尝试构建一个可扩展的 Windows Presentation Foundation (WPF) 销售订单管理应用程序。使用此应用程序,客户只需在 bin 文件夹中部署一个二进制文件,便可在其环境中添加新的自定义视图。我们将了解一下如何通过 MEF 实现此功能。我将在尝试过程中逐步改善设计,并对 MEF 的功能以及特性化编程模型在该过程中所起到的作用进行更多说明。
该订单管理应用程序允许插入新视图。若要向 MEF 中导出某些内容,可使用 Export 特性进行导出,如下所示:
[Export] public partial class SalesOrderView : UserControl { public SalesOrderView() { InitializeComponent(); } }
上面的部件导出 SalesOrderView 约定。默认情况下,Export 特性将成员(在此例中为类)的具体类型用作约定。您还可以通过向特性构造函数传递参数来显式指定约定。
特性化部件可通过对属性或字段使用 import 特性来表示其需求。应用程序可导出 ViewFactory 部件,其他部件可使用该部件来访问视图。该 ViewFactory 使用属性导入来导入 SalesOrderView。导入属性仅表示使用 Import 特性修饰属性:
[Export] public class ViewFactory { [Import] public SalesOrderView OrderView { get; set; } }
部件也可使用 ImportingConstructor 特性,通过构造函数进行导入(通常称为构造函数注入),如下所示。使用导入构造函数时,MEF 会假设所有参数都是导入,从而不必使用 import 特性:
[Export] public class ViewFactory { [ImportingConstructor] public ViewFactory(SalesOrderView salesOrderView) { } }
一般来说,通过构造函数而不是属性进行导入属于个人喜好问题,尽管有时适合使用属性导入,尤其是当存在并非由 MEF 实例化的部件(如 WPF 应用程序示例中)时。构造函数参数也不支持重新组合。
随着 SalesOrderView 和 ViewFactory 准备就绪,现在便可以启动组合。不会自动发现或创建 MEF 部件。而是需要编写一段进行组合的启动代码。实现此功能的常见位置为应用程序入口点处,在本例中为 App 类。
启动 MEF 涉及以下几个步骤:
正如您在此处所见,我已对 App 类添加了 ViewFactory 导入。然后我创建了指向 bin 文件夹的 DirectoryCatalog,并创建了使用该目录的容器。最后,我调用了 Composeparts,该方法组合 App 实例并满足 ViewFactory 导入:
public partial class App : Application { [Import] public ViewFactory ViewFactory { get; set; } public App() { this.Startup += new StartupEventHandler(App_Startup); } void App_Startup(object sender, StartupEventArgs e) { var catalog = new DirectoryCatalog(@".\"); var container = new CompositionContainer(catalog); container.Composeparts(this); } }
在组合期间,容器会创建 ViewFactory 并满足其 SalesOrderView 导入。这会创建 SalesOrderView。最后,Application 类会满足其 ViewFactory 导入。这样,MEF 便基于声明性信息组成整个对象图,而不需要手动执行命令性代码来组成该图。
将 MEF 集成到现有应用程序中或与其他框架集成时,通常会发现需要提供给导入程序使用的非 MEF 相关类实例(这表示这些类不是部件)。这些实例可能是密封框架类型(如 System.String)、应用程序范围的单例(如 Application.Current)或从工厂检索的实例(如从 Log4Net 检索的记录程序实例)。
为对此提供支持,MEF 允许进行属性导出。若要使用属性导出,请使用以导出修饰的属性创建中间部件。该属性实质上是一个工厂,执行检索非 MEF 值所需的任何自定义逻辑。在下面的代码示例中,可以看到 Loggerpart 导出 Log4Net 记录程序,从而使其他部件(如 App)可导入该记录程序,而不是依赖于访问静态访问器方法:
public class Loggerpart { [Export] public ILog Logger { get { return LogManager.GetLogger("Logger"); } } }
属性导出在其功能方面如同瑞士军刀,使 MEF 可与其他对象配合良好。您会发现,在将 MEF 集成到现有应用程序中时以及与旧系统交互时,属性导出十分用处。
返回到 SalesOrderView 示例,在 ViewFactory 和 SalesOrderView 之间已形成了紧密耦合关系。工厂需要一个具体的 SalesOrderView,用于限制可扩展性选项以及工厂本身的可测试性。MEF 允许将接口用作约定,从而将导入与导出程序实现分离:
public interface ISalesOrderView{} [Export(typeof(ISalesOrderView))] public partial class SalesOrderView : UserControl, ISalesOrderView { ... } [Export] public class ViewFactory { [Import] ISalesOrderView OrderView{ get; set; } }
在上面的代码中,我更改了 SalesOrderView,以实现 ISalesOrderView 并将其显式导出。我还更改了导入程序端的工厂以导入 ISalesOrderView。请注意,导入程序不必显式指定类型,因为 MEF 可从属性类型 ISalesOrderView 派生该类型。
这就带来一个问题:ViewFactory 是否还应实现 IViewFactory 这样的接口?虽然这可能会对模拟有些作用,但并不要求这样做。在本例中,我不希望任何人更换 ViewFactory,并且它是采用可测试的方式而设计的,因此工作正常。在一个部件上可以具有多个导出,以使该部件能通过多个约定进行导入。例如,SalesOrderView 可通过拥有另一个 export 特性,来导出 UserControl 和 ISalesOrderView:
[Export (typeof(ISalesOrderView))] [Export (typeof(UserControl))] public partial class SalesOrderView : UserControl, ISalesOrderView { ... }
开始创建约定时,需要能够将这些约定部署到第三方。实现此目的的常用方法是使某个约定程序集包含扩展程序将实现的约定的接口。该约定程序集成为部件将引用的 SDK 形式。常见模式是采用“应用程序名称 + .Contracts”的形式命名约定程序集,如 SalesOrderManager.Contracts。
ViewFactory 当前只导入一个视图。对每个视图的一个成员(属性参数)进行硬编码适用于不会频繁更改且预定义类型数量很少的视图。但是使用这种方法时,添加新视图需要重新编译工厂。
如果需要很多类型的视图,则 MEF 提供了更好的方法。您可创建所有视图都导出的通用 IView 接口,而不是使用特定的视图接口。工厂随后导入所有可用 IView 的集合。若要在特性化模型中导入集合,请使用 ImportMany 特性:
[Export] public class ViewFactory { [ImportMany] IEnumerable<IView> Views { get; set; } } [Export(typeof(IView))] public partial class SalesOrderView : UserControl, IView { } //in a contract assembly public interface IView{}
在此处,您可看到 ViewFactory 现在导入了 IView 实例的集合,而不是特定视图。SalesOrder 实现 IView,并将其导出(而不是 ISalesOrderView)。通过此重构,ViewFactory 现在可支持开放视图集。
MEF 还支持使用具体集合(如 ObservableCollection<T> 或 List<T>)以及提供默认构造函数的自定义集合来进行导入。
默认情况下,容器中的所有部件实例都是单例,因而由在容器中导入它们的所有部件共享。因此,SalesOrderView 和 ViewFactory 的所有导入程序都将获得同一实例。在很多情况下需要这样,因为这样便无需拥有其他组件所依赖的静态成员。但是,有时每个导入程序都需要获取自己的实例,例如用于同时在屏幕上查看多个 SalesOrderView 实例。
MEF 中的部件创建策略可以是以下三个值之一:CreationPolicy.Shared、CreationPolicy.NonShared 或 CreationPolicy.Any。若要指定部件的创建策略,请使用 partCreationPolicy 特性修饰部件,如下所示:
[partCreationPolicy(CreationPolicy.NonShared)] [Export(typeof(ISalesOrderView))] public partial class SalesOrderView : UserControl, ISalesOrdderView { public SalesOrderView() { } }
通过对导入设置 RequiredCreationPolicy 属性,也可在导入程序端指定 PartCreationPolicy。
ViewFactory 现在使用一个开放视图集,但是我无法区分各个视图。我可以向 IView 添加名为 ViewType 的成员(视图会提供该成员),然后根据该属性进行筛选。另一种方法是使用 MEF 的导出元数据工具,以通过 ViewType 对视图进行注释。使用元数据具有另一个好处,即视图实例化可延迟到需要时进行,这可节约资源并提高性能。
若要对导出定义元数据,请使用 ExportMetadata 特性。下面的 SalesOrderView 已更改为导出 IView 标记接口作为它的约定。它随后会添加“ViewType”的其他元数据,以便可以放在共享同一约定的其他视图间:
[ExportMetadata("ViewType", "SalesOrder")] [Export(typeof(IView)] public partial class SalesOrderView : UserControl, IView { }
ExportMetadata 有两个参数,即一个字符串形式的键和一个类型对象值。如前面的示例中那样使用魔幻字符串可能会存在问题,因为这在编译时并不安全。我们可以为键提供常量并为值提供枚举(而不使用魔幻字符串):
[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)] [Export(typeof(IView)] public partial class SalesOrderView : UserControl, IView { ... } //in a contract assembly public enum ViewTypes {SalesOrderView} public class ViewMetadata { public const string ViewType = "ViewType"; }
使用 ExportMetadata 特性可提供很大的灵活性,但是使用该特性时需要注意一些事项:
MEF 提供了解决方案来解决以上的问题:自定义导出。
MEF 允许创建包括其自己的元数据的自定义导出。创建自定义导出包括创建还指定元数据的派生 ExportAttribute。我们可使用自定义导出来创建包含 ViewType 元数据的 ExportView 特性:
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)] public class ExportViewAttribute : ExportAttribute { public ExportViewAttribute() :base(typeof(IView)) {} public ViewTypes ViewType { get; set; } }
ExportViewAttribute 指定它通过调用 Export 的基本构造函数来导出 IView。它使用 MetadataAttribute 进行修饰,这指定该特性提供元数据。此特性告知 MEF 查看所有公共属性,并通过将属性名称用作键,对导出创建相关联的元数据。在这种情况下,唯一的元数据为 ViewType。
最后需要记住的重要一点是,ExportView 特性使用 AttributeUsage 特性进行修饰。这指定该属性仅对类有效,且只能存在一个 ExportView 特性。
一般来说,AllowMultiple 应设置为 false;如果为 true,则导入程序将传递一组值而不是单个值。当多个导出具有同一成员的同一约定的不同元数据时,AllowMultiple 应保留为 True。
将新的 ExportViewAttribute 应用于 SalesOrderView 现在会产生如下结果:
[ExportView(ViewType = ViewTypes.SalesOrder)] public partial class SalesOrderView : UserControl, IView { }
如您所见,自定义导出可确保为特定导出提供正确的元数据。这些导出还可减少代码中的干扰信息,更加容易通过 IntelliSense 进行发现,并且可通过特定于域来更好地表达意图。
既然已对视图定义了元数据,ViewFactory 便可将其导入。
为了允许访问元数据,MEF 使用 .NET Framework 4 的一个新 API,即 System.Lazy<T>。使用该 API 可延迟实例的实例化,直至访问 Lazy 的 Value 属性。MEF 使用 Lazy<T,TMetadata> 进一步扩展 Lazy<T>,以允许在不实例化基础导出的情况下访问导出元数据。
TMetadata 是元数据视图类型。元数据视图是接口,用于定义对应于所导出元数据中的键的只读属性。访问元数据属性时,MEF 将动态实现 TMetadata,且将基于导出提供的元数据来设置值。
这是 View 属性使用 Lazy<T,TMetadata> 更改为导入时,ViewFactory 展示的内容:
[Export] public class ViewFactory { [ImportMany] IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; } } public interface IViewMetadata { ViewTypes ViewType {get;} }
导入了包含元数据的延迟导出集合后,可使用 LINQ 对该集合进行筛选。在下面的代码段中,我对 ViewFactory 实现了 GetViews 方法,以检索指定类型的所有视图。请注意,它会访问 Value 属性,以便仅为与筛选器匹配的视图生成实际视图实例:
[Export] public class ViewFactory { [ImportMany] IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; } public IEnumerable<View> GetViews(ViewTypesviewType) { return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value); } }
通过这些更改,ViewFactory 现在可发现在 MEF 组合工厂时 可用的所有视图。如果在该初始组合后容器或目录中出现了新的实现,则 ViewFactory 无法发现这些新实现,因为它已经组合。不仅如此,MEF 实际上会通过引发 CompositionException 来阻止将视图添加到目录,也就是说,除非启用重新组合,否则无法添加视图。
重新组合是 MEF 的一项功能,此功能允许部件在系统中出现新的匹配导出时自动更新其导入。重新组合在某些方案中十分有用,例如从远程服务器下载部件时。SalesOrderManager 可以进行更改,以便在其启动时,可启动多个可选视图的下载。这些视图显示时,会出现在视图工厂中。为了使 ViewFactory 可重新组合,我们在 Views 属性的 ImportMany 特性上将 AllowRecomposition 属性设置为 true,如下所示:
[Export] public class ViewFactory { [ImportMany(AllowRecomposition=true)] IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; } public IEnumerable<View>GetViews(ViewTypesviewType) { return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value); } }
进行重新组合时,Views 集合将立刻替换为包含一组更新过的视图的新集合。
启用重新组合后,应用程序可从服务器下载其他程序集并将这些程序集添加到容器。可通过 MEF 的目录执行此操作。MEF 提供了多个目录,其中有两个目录可重新组合。DirectoryCatalog(您已看到过)是可通过调用其 Refresh 方法来重新组合的目录。另一个可重新组合的目录是 AggregateCatalog,这是目录的目录。您可使用 Catalogs 集合属性向该目录添加目录,这会启动重新组合。我将使用的最后一个目录是 AssemblyCatalog,该目录接受一个它随后将在其之上构建目录的程序集。图 2 演示一个示例,说明如何结合使用这些目录进行动态下载。
图 2 使用 MEF 目录进行动态下载
void App_Startup(object sender, StartupEventArgs e) { var catalog = new AggregateCatalog(); catalog.Catalogs.Add(newDirectoryCatalog((@"\."))); var container = new CompositionContainer(catalog); container.Composeparts(this); base.MainWindow = MainWindow; this.DownloadAssemblies(catalog); } private void DownloadAssemblies(AggregateCatalog catalog) { //asynchronously downloads assemblies and calls AddAssemblies } private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog) { var assemblyCatalogs = new AggregateCatalog(); foreach(Assembly assembly in assemblies) assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly)); catalog.Catalogs.Add(assemblyCatalogs); }
图 2 中的容器是使用 AggregateCatalog 创建的。该容器随后将 DirectoryCatalog 添加到其中,以在 bin 文件夹中获取本地部件。聚合目录会传递到 DownloadAssemblies 方法,该方法异步下载程序集,然后调用 AddAssemblies。该方法会创建新的 AggregateCatalog,向该目录为每个下载程序集添加 AssemblyCatalogs。然后,AddAssemblies 添加包含主要聚合的程序集的 AggregateCatalog。它之所以采用这种方式进行添加,是为了一次性完成重新组合,而不是反复进行(在直接添加程序集目录时会出现这种情况)。
进行重新组合时,集合会立即更新。结果因集合属性类型而异。如果属性类型是 IEnumerable<T>,则它将替换为新实例。如果它是继承自 List<T> 或 ICollection 的具体集合,则 MEF 将对每一项依次调用 Clear 和 Add。无论是哪一种情况,都意味着在使用重新组合时必须考虑线程安全。重新组合不仅与添加有关,也与删除有关。如果从容器中移除目录,则也会移除这些部件。
有时,一个部件可能指定一个缺少的导入,因为该导入在目录中不存在。发生这种情况时,MEF 会阻止发现缺少依赖关系的部件(或依赖于其的任何对象)。MEF 这样做是为了稳定系统,并防止在创建了部件时一定会发生的运行时故障。
此处的 SalesOrderView 已更改,以便在即使不存在记录程序实例时也导入 ILogger:
[ExportView(ViewType = ViewTypes.SalesOrder)] public partial class SalesOrderView : UserControl, IView { [Import] public ILogger Logger { get; set; } }
因为没有可用 ILogger 导出,所以不会向容器显示 SalesOrderView 的导出。这不会引发异常,而仅仅忽略 SalesOrderView。如果您检查 ViewFactory 的 Views 集合,则会发现该集合是空的。
如果有多个导出可用于单个导入,则还会发生拒绝。在这些情况下,会拒绝导入单个导出的部件:
[ExportView(ViewType = ViewTypes.SalesOrder)] public partial class SalesOrderView : UserControl, IView { [Import] public ILogger Logger { get; set; } } [Export(typeof(ILogger))] public partial class Logger1 : ILogger { } [Export(typeof(ILogger))] public partial class Logger2 : ILogger { }
在上面的示例中,会拒绝 SalesOrderView,因为存在多个 ILogger 实现,但只导入单个实现。MEF 提供了允许默认导出在多处存在的工具。有关此方面的详细信息,请参阅codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior-part-2-of-n-defaults.aspx。
您可能会问,MEF 为何不创建 SalesOrderView 并引发异常。在开放的可扩展系统中,如果 MEF 引发异常,则应用程序会非常难以处理异常或使上下文知道要进行何种操作,因为可能缺少部件,或导入可能深深嵌套在组合中。如果不正确处理,则应用程序就会处于无效状态并且不可用。MEF 会拒绝部件,从而确保保持应用程序稳定性。有关稳定组合的详细信息,请参阅:blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx。
拒绝是一项非常强大的功能,但有时可能难以进行诊断,尤其是在拒绝整个依赖关系图时。在先前的第一个示例中,ViewFactory 直接导入 SalesOrderView。假设 MainWindow 导入了 ViewFactory,而 SalesOrderView 被拒绝。随后 ViewFactory 和 MainWindow 也会被拒绝。如果您看到这种情况发生,可能会百思不得其解,因为您知道 MainWindow 和 ViewFactory 是实际存在的;拒绝的原因是缺少依赖关系。
MEF 不会让您束手无措。为了帮助诊断此问题,它提供了跟踪。在 IDE 中,可从输出窗口跟踪所有拒绝消息,不过也可以从任何有效的跟踪侦听器跟踪这些消息。例如,当应用程序尝试导入 MainWindow 时,会输出图 3中的跟踪消息。
图 3 MEF 跟踪消息
System.ComponentModel.Composition Warning: 1 : The ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information. 1) No valid exports were found that match the constraint '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected. Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger (ContractName="Mef_MSDN_Article.ILogger")' on part 'Mef_MSDN_Article.SalesOrderView'. Element: Mef_MSDN_Article.SalesOrderView.logger (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow, Mef_MSDN_Article.SalesOrderView, ...').
跟踪输出会显示问题的根本原因:SalesOrderView 需要 ILogger,但无法找到。然后,我们可以看到拒绝 SalesOrderView 会导致拒绝工厂,最终拒绝 MainWindow。
您可以更进一步,实际检查目录中的可用部件,我将在有关托管的小节中对此进行讨论。在图 4 中,您可以在监视窗口中查看可用部件(在绿色圈中)以及所需的 ILogger 导入(在蓝色圈中)。
图 4 监视窗口中显示的可用部件和所需 ILogger
MEF 的一个目标是支持静态可分析性,从而允许在运行时环境之外分析组合。我们在 Visual Studio 中还未提供这种工具支持,但是 Nicholas Blumhardt 编写了 MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536),这是实现此功能的一个命令行工具。MEFX 可分析程序集并确定要拒绝的部件以及拒绝的原因。
如果在命令行处运行 MEFX.exe,则您将看到很多选项;您可列出特定导入、导出或可用的所有部件。例如,在此处可以看到如何使用 MEFX 显示部件列表:
C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts SalesOrderManagement.SalesOrderView SalesOrderManagement.ViewFactory SalesOrderManagement.MainWindow
这对于获取部件清单十分有用,但是 MEFX 也可向下跟踪拒绝,这是我们现在所关注的问题,如图 5 所示。
图 5 使用 MEFX.exe 向下跟踪拒绝
C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected /verbose [part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog (Path="C:\SalesOrderManagement\bin\debug") [Primary Rejection] [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView") [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView") [Import] SalesOrderManagement.SalesOrderView.logger (ContractName="SalesOrderManagement.ILogger") [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected. at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicCompositionatomicComposition) at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition) at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)
仔细分析图 6 中的输出可了解问题的根本原因:找不到 ILogger。如您所见,在具有许多部件的大型系统中,MEFX 是非常有用的工具。有关 MEFX 的详细信息,请参阅blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx。
图 6 IronRuby 中的部件示例
概括来说,特性化模型具有以下几个优势:
我现在将带您快速了解一下体系结构,并看看它能提供什么功能。在较高级别,MEF 体系结构分为以下几层:编程模型层、托管层和基元层。
特性化模型只是将特性用作发现方法的基元的一种实现。基元可表示非特性化部件,甚至可表示未静态类型化的部件,如在动态语言运行时 (DLR) 中一样。在图 6 中,您可以看到导出 IOperation 的 IronRuby 部件。请注意,它使用 IronRuby 的本机语法来声明部件而不是特性化模型,因为 DLR 中不支持特性。
IronRuby 编程模型不附带 MEF,尽管我们以后可能会增加动态语言支持。
您可以在以下博客系列中阅读有关构建 Ruby 编程模型的实验的详细信息:blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx。
编程模型定义部件、导入和导出。为了实际创建实例和对象图,MEF 提供了托管 API,这些 API 主要位于 System.ComponentModel.Composition.Hosting 命名空间中。托管层提供很大的灵活性、可配置性和可扩展性。MEF 中的很多“工作”都在这一层进行,MEF 中的发现也是从这一层开始。仅仅编写部件的大多数人永不会接触此命名空间。但是,如果您是托管方,则您会如同我之前所做那样使用它们,以启动组合。
目录提供描述可用导出和导入的部件定义 (ComposablepartDefinition)。它们是 MEF 中用于发现的主要单元。MEF 在 System.ComponentModel.Composition 命名空间中提供了多个目录(您已看到过其中一些目录),包括扫描目录的 DirectoryCatalog、扫描程序集的 AssemblyCatalog 以及扫描特定类型集的 TypeCatalog。这些目录中的每个目录都特定于特性化编程模型。但是,AggregateCatalog 与编程模型无关。目录从 ComposablepartCatalog 继承,并且是 MEF 中的扩展点。自定义目录有许多用途:从提供全新的编程模型到封装和筛选现有目录。
图 7 显示一个已筛选目录的示例,该目录接受谓词,以对将返回部件的内部目录进行筛选。
图 7 已筛选的目录
public class FilteredCatalog : ComposablepartCatalog, { private readonly composablepartcatalog _inner; private readonly IQueryable<ComposablepartDefinition> _partsQuery; public FilteredCatalog(ComposablepartCatalog inner, Expression<Func<ComposablepartDefinition, bool>> expression) { _inner = inner; _partsQuery = inner.parts.Where(expression); } public override IQueryable<ComposablepartDefinition> parts { get { return _partsQuery; } } }
CompositionContainer 进行组合,这表示它会创建部件并满足其他部件的导入。在满足导入时,它将从可用导出的池中进行获取。如果这些导出也有导入,则容器将首先满足它们。这样,容器将按需组成整个对象图。导出池的主要来源是目录,但是容器也可以直接将现有部件实例添加到其中并进行组合。虽然在大多数情况下部件来自于目录,但手动将入口点类以及从目录提取的部件一起添加到容器,这也十分常见。
容器也可嵌套在层次结构中,以支持确定范围方案。默认情况下,子容器会查询父级,但是也可以提供其自己的子部件目录,这些子部件会在子容器中创建:
var catalog = new DirectoryCatalog(@".\"); var childCatalog = new DirectoryCatalog(@".\Child\"; var rootContainer = new CompositionContainer(rootCatalog)); var childContainer = new CompositionContainer(childCatalog, rootContainer);
在上面的代码中,childContainer 安排为 rootContainer 的子级。rootContainer 和 childContainer 都提供其自己的目录。有关在应用程序中使用容器托管 MEF 的详细信息,请参阅codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx。
位于 System.ComponentModel.Composition.Primitives 处的基元是 MEF 中的最低级别。可以说,它们是 MEF 及其上层扩展点的量子世界。到现在为止,我已讨论完特性化编程模型。但是,MEF 的容器根本不会绑定到特性;而是绑定到基元。基元定义部件的抽象表示形式,这包括如 ComposablepartDefinition、ImportDefinition 和 ExportDefinition 这样的定义以及表示实际实例的 Composablepart 和 Export。
讨究基元本身是另一个主题,我可能会在以后的文章中进行讨论。目前,您可在blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef-primitives.aspx 处找到有关基元的详细信息。
MEF 也作为 Silverlight 4 的一部分提供。我在此处讨论的所有内容都与开发可扩展的富 Internet 应用程序有关。在 Silverlight 中,我们甚至更进一步,引入了其他 API,以简化在 MEF 上构建应用程序的过程。这些增强功能最后会加入到 .NET Framework 中。
您可在下面的帖子中找到有关 Silverlight 4 中的 MEF 的详细信息:codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight-4-we-come-in-the-name-of-extensibility.aspx。
我只是简要介绍了使用 MEF 可以实现的功能。这是一个强大、稳健而灵活的工具,您可将其添加到工具集中,以帮助您将应用程序向一个充满各种可能性的全新世界开放。我期待看到您使用该工具所完成的工作!
Glenn Block 是负责 .NET Framework 4 中新增的托管可扩展性框架 (MEF) 的项目经理。在 MEF 之前,他是模式和实践工作中的产品计划员,负责 Prism 以及其他客户指南。Block 在内心里是个奇客,他花费了大量时间在各种会议和小组(如 ALT.NET)中不遗余力地传播着他的这份狂热。您可在codebetter.com/blogs/glenn.block 阅读他的博客。
衷心感谢以下技术专家审阅了本文:Ward Bell、Nicholas Blumhardt、Krzysztof Cwalina、Andreas Håkansson、Krzysztof Kozmic、Phil Langeberg、Amanda Launcher、Jesse Liberty、Roger Pence、Clemens Szypierski、Mike Taulty、Micrea Trofin 和 Hamilton Verissimo