MEF框架学习之旅(五)导入导出(下)
导入导出的方式:
一、类
当一个部件需要导出自身时一个组成部件级别的导出经常用。为了让部件导出自己,简单的方法是用[System.ComponentModel.Composition.ExportAttribute]特性修饰一个组成部件,正如下面所示一样:
代码段
[Export] public class ConsoleLogger { public void Write(string message) { Console.WriteLine(message); } }
主程序(导入部分)代码如下:
代码段
class Program { [Import] public ConsoleLogger consoleLogger { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); p.consoleLogger.Write("简单的导入导出实例。"); Console.Read(); } }
二、属性
部件也可以导出属性。属性导出有下面几个优点:
他们允许导出密封类型例如核心CLR类型,或者其它第三方类型。
他们允许从怎样创建导出中解耦导出。例如导出运行环境为你创建的现有的HttpContext。
他们允许有导出同一个组成部件关系的成员,例如一个DefaultSendersRegistry组成部件导出一个sender默认设置作为属性。
举例一个StringProvider类要导出一个”Message "契约,导出部分正如下面代码一样:
代码段
public class StringProvider { [Export("Message")] public string Output { get { return "Hello World"; } } }
主程序(导入部分)代码也需要添加一个”Message "契约,这样导入与导出部件才能组合,代码如下:
代码段
class Program { [Import("Message")] public string Input { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); Console.Write(p.Input); Console.Read(); } }
三、字段
MEF也支持直接导入字段,用法与属性相似。
代码段
[Import] public ConsoleLogger consoleLogger;
注意:注意导入或导出私有成员(字段,属性和方法)当在完全信任状态下支持,在中间/局部信息状态可能有问题。
四、构造函数(构造函数导入导出)
部件也可使用 ImportingConstructor 特性,通过构造函数进行导入(通常称为构造函数注入),如下所示。使用导入构造函数时,MEF 会假设所有参数都是导入,从而不必使用 import 特性。
你也可以通过构造函数的参数指定导入。这意味着不必给每个导入添加属性,你只需要为导入给构造函数添加参数。为了使用这个,按照下面的步骤进行:
1.添加一个[System.ComponentModel.Composition.ImportingConstructorAttribute] 特性到要被MEF使用的构造函数。
2.为每个导入添加参数到构造函数。
例如下面的代码在Program类的构造函数导入一个ConsoleLogger
代码段
[ImportingConstructor] public Program(ConsoleLogger consoleLogger) { //……… }
一般来说,通过构造函数而不是属性进行导入属于个人喜好问题,尽管有时适合使用属性导入,尤其是当存在并非由 MEF 实例化的部件时。构造函数参数也不支持重新组合。
有几个不同的方法在构造函数定义导入
1、隐式导入-默认容器会使用参数的类型去识别契约。例如上面的例子。
2、显式导入-如果你想要指定被导入的契约,添加[System.ComponentModel.Composition.ImportAttribute]特性到参数。
代码段
[ImportingConstructor] public Program([Import(typeof(ConsoleLogger))]ConsoleLogger consoleLogger) { //……… }
五、方法
方法导出必须将协定类型或协定名称指定为类型,并且无法推断。指定的类型可以为自定义委托或泛型类型。
方法导出有下面几个好处:
他们允许好的细致控件作为导出。例如,一个规则引擎要导入一个插入式设置的方法导出。
他们从任务知识的类型串保护调用者。
他们可以通过轻量的代码情报中产生,你不能用其它导出做得到。
注意:由于framework的局限性,方法导出不可以超过4参数。
(一)、无返回值方法导入导出:
在下面的例子里,ConsoleLogger类导出它的Write方法作为Action<string>委托。Processor导入相同的委托。
导出部分代码:
代码段
public class ConsoleLogger { [Export(typeof(Action<string>))] public void Write(string message) { Console.WriteLine(message); } }
导入部分代码:
代码段
class Program { [Import(typeof(Action<string>))] public Action<string> LoggerWrite { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); p.LoggerWrite("无参方法导入导出。"); Console.Read(); } }
你也可以通过使用一个简单的字符串契约导出和导入方法。例如下面所用的“Logger"契约
代码段
public class ConsoleLogger { [Export("Logger")] public void Write(string message) { Console.WriteLine(message); } }
代码段
class Program { [Import("Logger")] public Action<string> LoggerWrite { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); p.LoggerWrite("无返回值方法导入导出。"); Console.Read(); } }
注意:当用方法导出时,你需要提供一个类型或者一个字符串契约名,并且不能留空。
(二)、有返回值方法导入导出:
还是刚才的类,将方法改为有返回值后。
代码段
public class ConsoleLogger { [Export(typeof(Func<string, bool>))] public bool Write(string message) { Console.WriteLine(message); return true; } }
在此类中,Write方法采用单个 string参数,并返回 bool。 若要匹配此导出,导入部件必须适当的成员。 下面的类导入 Write方法。
代码段
class Program { [Import(typeof(Func<string, bool>))] public Func<string, bool> LoggerWrite { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); bool bl=p.LoggerWrite("有返回值方法导入导出。"); Console.Read(); } }
六、导入多个对象
只有当 Import 特性与一个导出匹配并且仅与一个导出匹配时,才能成功构成该特性。 其他类将生成组合错误。若要导入与同一协定匹配的多个导出,请使用 ImportMany 特性。 用此特性标记的导入始终是可选的。 例如,如果不存在匹配导出,组合将失败。 下面的类将导入任意数量类型为 IMyAddin 的导出。
代码段
public class MyClass { [ImportMany] public IEnumerable<IMyAddin> MyAddin { get; set; } }
可以使用普通 IEnumerable<T> 语法和方法访问导入的数组。 也可以改用普通数组 (IMyAddin[])。
当您将此模式与 Lazy<T> 语法结合使用时,它可能非常重要。 例如,通过使用 ImportMany、IEnumerable<T> 和 Lazy<T>,您可以导入对任意数量的对象的间接引用,并仅实例化必要的对象。 下面的类演示此模式。
代码段
public class MyClass { [ImportMany] public IEnumerable<Lazy<IMyAddin>> MyAddin { get; set; } }
多个对象导入筛选的几种方式:
(一).通过聚合类
通过聚合类进行多个对象的筛选是一种非常常用的方式。
导出部分:Export有两个参数,一个是名称另一个是类型,通过这两个参数的组合完成导入导出的契约功能。
代码段
[Export("Console", typeof(ILogger))] public class ConsoleLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } } [Export("Debug", typeof(ILogger))] public class DebugLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } }
聚合类:
分别将两个不同的日志记录组件都通过[ExportAttribute]进行标注为导出部件,并分别为其设置好通信契约,那么在导入他们的地方就直接通过契约确定分别导入什么类型的日志记录实现部件。通过提供一个统一的服务类以供系统中统一调用
代码段
[Export] public class LoggerService { [Import("Console")] public ILogger ConsoleLogger { get; set; } [Import("Debug")] public ILogger DebugLogger { get; set; } }
主程序(导入)部分:
如上代码就将LoggerService也进行了[ExportAttribute]标注,表示此门面类也是一个可装配的部件,那么在主程序中同样可以通过[ImportAttribute]进行导入的。
代码段
class Program { [Import] public LoggerService Logger { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); p.Logger.ConsoleLogger.Write("ConsoleLogger"); p.Logger.DebugLogger.Write("DebugLogger"); Console.Read(); } }
(二).通过目录过滤
导出部分:
MEF中提供了一个专门用于目录过滤筛选的元数据特性PartMetadata,要进行目录部件的筛选过滤就需要通过PartMetadata特性的标注,MEF容器才能进行正确的装配。
代码段
[PartMetadata("Logger", "Console")] [Export(typeof(ILogger))] public class ConsoleLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } } [PartMetadata("Logger", "Debug")] [Export(typeof(ILogger))] public class DebugLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } }
目录筛选部分:
通过自定义Catalog来实现这一功能,前面已经说过了。
代码段
public class FilteredCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged { private readonly ComposablePartCatalog _inner; private readonly INotifyComposablePartCatalogChanged _innerNotifyChange; private readonly IQueryable<ComposablePartDefinition> _partsQuery; public FilteredCatalog(ComposablePartCatalog inner, Expression<Func<ComposablePartDefinition, bool>> expression) { _inner = inner; _innerNotifyChange = inner as INotifyComposablePartCatalogChanged; _partsQuery = inner.Parts.Where(expression); } public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed { add { if (_innerNotifyChange != null) _innerNotifyChange.Changed += value; } remove { if (_innerNotifyChange != null) _innerNotifyChange.Changed -= value; } } public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing { add { if (_innerNotifyChange != null) _innerNotifyChange.Changing += value; } remove { if (_innerNotifyChange != null) _innerNotifyChange.Changing -= value; } } public override System.Linq.IQueryable<ComposablePartDefinition> Parts { get { return _partsQuery; } } }
主程序(导入)部分:
两个日志类的元数据名称都为" Logger ",其值分别是Console、Debug,那么就可以通过下面代码的方式实现对目录中部件的筛选,下面是代码块演示了如何从目录中筛选出元数据名称为" Logger ",其值为"Console"的部件。
代码段
class Program { static void Main(string[] args) { Program p = new Program(); var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); var parent = new CompositionContainer(catalog); var filteredCat = new FilteredCatalog(catalog, item => item.Metadata.ContainsKey("Logger") && item.Metadata["Logger"].ToString() == "Console"); var child = new CompositionContainer(filteredCat, parent); var control = child.GetExportedValue<ILogger>(); control.Write("Console"); Console.Read(); } }
(三).通过特性CreationPolicy筛选:
这个特性在后面章节将具体讲解,这里就不具体说了,直接贴代码了,这种方式一般不用来做筛选。
导出部分:
代码段
[PartCreationPolicy(CreationPolicy.Shared)] [Export(typeof(ILogger))] public class ConsoleLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } } [PartCreationPolicy(CreationPolicy.NonShared)] [Export(typeof(ILogger))] public class DebugLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } }
主程序(导入)部分:
代码段
class Program { [ImportMany(RequiredCreationPolicy = CreationPolicy.Shared)] public List<ILogger> Service { get; set; } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); container.ComposeParts(this); } static void Main(string[] args) { Program p = new Program(); p.Compose(); Console.WriteLine(p.Service.Count); Console.Read(); } }
七、继承导入导出
MEF支持基类/基接口去定义导出的功能,这样它就可以被继承。这是综合遗产框架的目标,它利用MEF去发现但不需要修改现存的自定义代码。为了提供这个功能,使用System.ComponentModel.Composition.InheritedExportAttribute.例如下面的ILogger有一个InheritedExport。ConsoleLogger、DebugLogger继承ILogger所以它们自动导出ILogger。
代码段
[InheritedExport] public interface ILogger { void Write(string message); } [Export(typeof(ILogger))] public class ConsoleLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } } [Export(typeof(ILogger))] public class DebugLogger : ILogger { void ILogger.Write(string message) { Debug.WriteLine(message); } }
如果某个类继承自部件,则该类也可能会成为部件。 导入始终由子类继承。 因此,部件的子类将始终为部件,并具有与其父类相同的导入。
通过使用 Export 特性的声明的导出不会由子类继承。 但是,部件可通过使用 InheritedExport 特性继承自身。 部件的子类将继承并提供相同的导出,其中包括协定名称和协定类型。 与 Export 特性不同,InheritedExport 只能在类级别(而不是成员级别)应用。 因此,成员级别导出永远不能被继承。
下面四个类演示了导入和导出继承的原则。 NumTwo 继承自 NumOne,因此 NumTwo 将导入 IMyData。 普通导出不会被继承,因此 NumTwo 将不会导出任何内容。 NumFour 继承自 NumThree。 由于 NumThree 使用了 InheritedExport,因此 NumFour 具有一个协定类型为 NumThree 的导出。 成员级别导出从不会被继承,因此不会导出 IMyData。
代码段
[Export] public class NumOne { [Import] public IMyData MyData { get; set; } } public class NumTwo : NumOne { //Imports are always inherited, so NumTwo will //import IMyData. //Ordinary exports are not inherited, so //NumTwo will NOT export anything. As a result it //will not be discovered by the catalog! } [InheritedExport] public class NumThree { [Export] Public IMyData MyData { get; set; } //This part provides two exports, one of //contract type NumThree, and one of //contract type IMyData. } public class NumFour : NumThree { //Because NumThree used InheritedExport, //this part has one export with contract //type NumThree. //Member-level exports are never inherited, //so IMyData is not exported. }
如果存在与 InheritedExport 特性关联的元数据,该元数据也将被继承。子类无法修改继承的元数据。 但是,通过使用相同协定名称和协定类型但使用新元数据重新声明 InheritedExport 特性,子类可以将继承的元数据替换为新元数据。 下面的类演示此原则。 MegaLogger 部件继承自 Logger 并包括 InheritedExport 特性。 由于 MegaLogger 重新声明名为 Status 的新元数据,因此它不会从 Logger 中继承 Name 和 Version 元数据。
代码段
[InheritedExport(typeof(IPlugin)), ExportMetadata("Name", "Logger"), ExportMetadata("Version", 4)] public class Logger : IPlugin { //Exports with contract type IPlugin and //metadata "Name" and "Version". } public class SuperLogger : Logger { //Exports with contract type IPlugin and //metadata "Name" and "Version", exactly the same //as the Logger class. } [InheritedExport(typeof(IPlugin)), ExportMetadata("Status", "Green")] public class MegaLogger : Logger { //Exports with contract type IPlugin and //metadata "Status" only. Re-declaring //the attribute replaces all metadata. }
在重新声明 InheritedExport 特性以重写元数据时,请确保协定类型相同。 (在前面的示例中,IPlugin 为协定类型。)如果协定类型不同,则第二个特性将创建另一个独立于部件的导出(而不是重写)。 通常,这意味着您必须在重写 InheritedExport 特性时显式指定协定类型,如前面的示例所示。
由于无法直接实例化接口,因此通常无法用 Export 或 Import 特性来修饰接口。 不过,可以在接口级别用 InheritedExport 特性修饰接口,并且任何实现类将随任何关联的元数据一起继承该导出。 但是,接口本身将不可用作部件。
八、自定义导出特性
可以对基本导出特性 Export 和 InheritedExport 进行扩展,以包括元数据作为特性属性。 在将类似的元数据应用于多个部件或创建元数据特性的继承树时,此方法十分有用。
自定义特性可以指定协定类型、协定名称或任何其他元数据。 为了定义自定义特性,必须使用 MetadataAttribute 特性来修饰继承自 ExportAttribute(或 InheritedExportAttribute)的类。详细请看下面一个章节。
相关阅读: