托管可扩展性框架 (MEF) 简介 - 第一部分






4.92/5 (75投票s)
本文将讨论 MEF,如何开始使用它,等等。
目录
- 引言
- 背景
- 什么是MEF?
- MEF 所在的程序集
- MEF 的组成部分
- MEF 相关术语
- MEF 的工作原理
- 当前程序集中仅包含单个导出部分的简单工作示例
- 当前程序集中包含多个导出部分的简单工作示例
- 使用聚合目录和目录的多个导出部分的简单工作示例
- MEF 的优势
- 结论
- 历史
引言
尽管托管可扩展性框架 (MEF) 已经存在很长时间了,但我仍然认为应该有一篇简单的文章供初学者入门。它现在已成为 .NET Framework 4.0 的一个组成部分,并且可以用于任何类型的应用程序。这是我将继续撰写的 MEF 系列文章的第一部分。在第一部分中,我们将了解基础知识并体验 MEF,到最后,我们应该能够了解 MEF 的好处、它如何帮助开发活动以及在什么情况下我们会采用它。本文的撰写考虑到了许多开发者尚未开始使用它,本文或系列文章将让他们从头开始体验使用它的感觉。那么,让我们开始这段旅程吧。
背景
在 MEF 上工作了几个月,真是非常有趣。因此,我想与那些即将开始循序渐进地学习 MEF 的人分享,也与那些已经进行了一些 MEF 编程但接触不多并想学得更多的人分享。我们将从最基本的示例开始我们的旅程,并随着时间的推移不断前进。这将是一个 n 部分的系列文章,所以在每一部分中,我们将学习一些新功能。
什么是MEF?
托管可扩展性框架是微软公司用于构建可扩展应用程序的新框架。它的基本目的是将组件插件化到一个已运行的应用程序中。它是 .NET 4.0 Framework 的一个组成部分,适用于任何类型的 .NET 应用程序。
MEF 所在的程序集
- System.ComponentModel.Composition.dll
MEF 的组成部分
- Import(导入)、Export(导出)和 Compose(组合)
MEF 相关术语
在更深入地讨论 MEF 之前,让我们先了解一些有助于我们更好地理解 MEF 的基本术语。
- Part(部分): 一个 Part 是一个可以导入或导出到应用程序的对象(例如,类、方法或属性)。
- Catalog(目录): 一个对象,用于帮助发现程序集或目录中可组合的可用部分。
- Contract(契约): 导入和导出的部分需要通过某种契约(例如,接口或预定义的数据类型,如
string
)进行通信。 - Import Attribute(导入属性): 它定义了一个部分的需求。它仅适用于单个 Export Attribute。
- ImportMany Attribute(多重导入属性): 它类似于 Import Attribute,但支持多个 Export Attributes。
- Export Attribute(导出属性): Import Attribute 创建了需求,Export Attribute 满足了这些需求。它暴露了将参与组合的部分。
- Compose(组合): 在 MEF 的术语中,Compose 是将导出的部分与导入的部分组装在一起的区域。
此时,这些术语可能还不太清楚。为了使这些术语能够被理解,让我们先尝试了解 MEF 的工作原理。
MEF 的工作原理
MEF 的工作原理基于需求/供给的原则。可供供给的部分必须被发现,并且需求和供给的部分必须在契约上达成一致。当应用程序需要插入某些部分时,MEF 将从某个位置(我们称之为 Catalog)动态发现这些部分。只要满足契约,组合引擎就会将这些部分组装起来。
当前程序集中仅包含单个导出部分的简单工作示例
让我们创建一个简单的计算器来演示。我们的示例应用程序将具有四个基本算术运算,即加、减、乘和除。考虑到这一点,让我们按以下方式创建项目结构:
解决方案文件 CalculatorDemoUsingMEF 包含三个项目:
CompositionHelper
CalculatorContract
CalculatorUI
a) CalculatorContract 项目描述
在这里,我们主要定义了 Calculator 的具体组件(如 Add
、Subtract
等)必须实现的契约或接口。ICalculator
接口非常简单,如下所述:
namespace CalculatorContract
{
public interface ICalculator
{
int GetNumber(int num1, int num2);
}
}
b) CompositionHelper 项目描述
在这个项目中,我们将创建 Calculator 所需的各种组件,如加法、减法等。Add.cs 文件是用于加法运算的组件。
让我们看一下 Add.cs 文件:
using System.ComponentModel.Composition;
using CalculatorContract;
namespace CompositionHelper
{
[Export(typeof(ICalculator))]
public class Add:ICalculator
{
#region Interface members
public int GetNumber(int num1, int num2)
{
return num1 + num2;
}
#endregion
}
}
首先,我们需要将 System.ComponentModel.Composition
程序集引用添加到项目中。
同时,我们还添加了 CalculatorContract .dll 的引用。
该类用 Export
属性进行修饰,这表明该类将被导出到组合模型,并且它只遵循 ICalculator
契约/接口类型。
CalcliComposition.cs 文件是发生组合的地方。
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using CalculatorContract;
namespace CompositionHelper
{
public class CalciCompositionHelper
{
[Import(typeof(ICalculator))]
public ICalculator CalciPlugin { get; set; }
/// <summary>
/// Assembles the calculator components
/// </summary>
public void AssembleCalculatorComponents()
{
try
{
//Step 1: Initializes a new instance of the
// System.ComponentModel.Composition.Hosting.AssemblyCatalog
// class with the current executing assembly.
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
//Step 2: The assemblies obtained in step 1 are added to the
//CompositionContainer
var container = new CompositionContainer(catalog);
//Step 3: Composable parts are created here i.e.
//the Import and Export components
// assembles here
container.ComposeParts(this);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Sends the result back to the client
/// </summary>
/// <param name="num1"></param>
/// <param name="num2"></param>
/// <returns></returns>
public int GetResult(int num1, int num2)
{
return CalciPlugin.GetNumber(num1, num2);
}
}
}
AssembleCalculatorComponent
函数需要一些注意。在此函数中,我们首先查找部分来自的目录。一旦确定,目录就被保存在一个容器中,最后对部分进行组合。
c) CalculatorUI 项目描述
在此项目中,显示了 UI。它添加了对 CalculatorComponentComposition dll 的引用。
让我们看一下 Add 按钮点击事件代码:
objCompHelper = new CalciCompositionHelper();
//Assembles the calculator components that will participate in composition
objCompHelper.AssembleCalculatorComponents();
//Gets the result
var result = objCompHelper.GetResult(Convert.ToInt32(txtFirstNumber.Text),
Convert.ToInt32(txtSecondNumber.Text));
//Display the result
txtResult.Text = result.ToString();
首先,我们正在组合将参与组合的计算器组件。然后调用加法组件来执行所需的操作。
最终输出如下图所示:
当前程序集中包含多个导出部分的简单工作示例
到目前为止,我们已经学习了 MEF 的一些知识,了解了它的工作原理,并且成功地在我们的应用程序中添加了一个组件。现在,让我们增强我们的应用程序,使其能够处理所有其他组件,如减法、乘法和除法。
因此,让我们在应用程序中添加各种组件部分。项目结构现在将变为:
正如可以发现的,预期不会有太大变化,我们添加了三个新的部分,它们被高亮显示。
现在让我们再次查看 Add.cs 文件:
using System.ComponentModel.Composition;
using CalculatorContract;
namespace CompositionHelper
{
[Export(typeof(ICalculator))]
[ExportMetadata("CalciMetaData", "Add")]
public class Add:ICalculator
{
#region Interface members
public int GetNumber(int num1, int num2)
{
return num1 + num2;
}
#endregion
}
}
我们发现了一个名为 ExportMetadata
的新属性。这个新属性将帮助我们在运行时确定使用哪个实现。它是一种键值对的形式。该属性的第一个参数是元数据的名称。它的类型是 string
。下一个参数是一个对象,它保存元数据的价值。通过它,我们将决定调用哪个操作。其余可导出部分遵循类似的实现。
接下来,让我们看一下 CalciCompositionHelper.cs 文件:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using CalculatorContract;
namespace CompositionHelper
{
public class CalciCompositionHelper
{
[ImportMany]
public System.Lazy<ICalculator,
IDictionary<string, object>>[] CalciPlugins { get; set; }
/// <summary>
/// Assembles the calculator components
/// </summary>
public void AssembleCalculatorComponents()
{
try
{
//Step 1: Initializes a new instance of the
// System.ComponentModel.Composition.Hosting.AssemblyCatalog
// class with the current executing assembly.
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
//Step 2: The assemblies obtained in step 1 are added to the
//CompositionContainer
var container = new CompositionContainer(catalog);
//Step 3: Composable parts are created here i.e.
//the Import and Export components
// assemble here
container.ComposeParts(this);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Sends the result back to the client
/// </summary>
/// <param name="num1"></param>
/// <param name="num2"></param>
/// <returns></returns>
public int GetResult(int num1, int num2, string operationType)
{
int result = 0;
foreach (var CalciPlugin in CalciPlugins)
{
if ((string)CalciPlugin.Metadata["CalciMetaData"] == operationType)
{
result = CalciPlugin.Value.GetNumber(num1, num2);
break;
}
}
return result;
}
}
}
我们观察到的第一个变化是,现在我们有 ImportMany
属性而不是 Import
属性。Import
属性始终由一个 Export
属性填充,而 ImportMany
属性可以由任意数量的 Export
属性填充。
当我们运行应用程序时,我们可以找出参与组合的各种部分。
另一个值得注意的点是 Lazy<T>
类,它基本上将大型对象的创建推迟到我们真正需要它的时候。在示例代码中,第二个参数是由 MEF 在运行时处理的元数据。
相应地,GetResult
函数已稍作修改。它现在接受第三个参数,指定操作类型,并基于此调用导出的部分。
最后,在具体的“操作”点击事件中,我们传递了操作类型为:
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
DoCalciOperation("Add");
}
一旦我们运行应用程序,输出将如下所示(仅显示乘法运算的输出):
希望这与上一部分同样有帮助。在下一部分中,我们将探索一些新的目录类型和 MEF 的更多功能。
使用聚合目录和目录的多个导出部分的简单工作示例
到目前为止,我们看到我们的可组合部分都驻留在当前程序集中。但如果它们驻留在不同的程序集或不同的位置呢?
嗯,我们也有解决方案。我们将看到聚合目录和目录在这些情况下如何帮助我们。
让我们现在看一下项目结构。
我们可以发现,解决方案文件已添加了一个新项目,即 Export Components。在该项目中,我们有一个名为 Components 的文件夹,其中存储了我们以程序集形式导出的部分(.dll 文件)。
我们只在那里添加了两个 DLL,即 Add 和 Subtraction,并从 CompositionHelper
项目中删除了 Add.cs 和 Subtract.cs 文件。
现在让我们再次访问 AssembleCalculatorComponent
函数。
public void AssembleCalculatorComponents()
{
try
{
//Creating an instance of aggregate catalog. It aggregates other catalogs
var aggregateCatalog = new AggregateCatalog();
//Build the directory path where the parts will be available
var directoryPath =
string.Concat(Path.GetDirectoryName
(Assembly.GetExecutingAssembly().Location)
.Split('\\').Reverse().Skip(3).Reverse().Aggregate
((a, b) => a + "\\" + b)
, "\\", "ExportComponents\\Components");
//Load parts from the available DLLs in the specified path
//using the directory catalog
var directoryCatalog = new DirectoryCatalog(directoryPath, "*.dll");
//Load parts from the current assembly if available
var asmCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
//Add to the aggregate catalog
aggregateCatalog.Catalogs.Add(directoryCatalog);
aggregateCatalog.Catalogs.Add(asmCatalog);
//Crete the composition container
var container = new CompositionContainer(aggregateCatalog);
// Composable parts are created here i.e.
// the Import and Export components assembles here
container.ComposeParts(this);
}
catch (Exception ex)
{
throw ex;
}
}
正如所见,最初我们创建了一个 AggregateCatalog
,它将聚合其他目录。然后我们从 Components 目录中获取可导出的组件。
如果我们运行应用程序,我们可以发现 **Directory Catalog 有两个部分(Add 和 Subtract)。**
而 **AssemblyCatalog 有两个部分(Divide 和 Multiply)。**
在发现部分后,它们被添加到 AggragateCatalog
中,然后添加到 CompositionContainer
中。最后,部分被组合。
运行应用程序会产生预期的输出(仅显示加法运算的输出)。
优点
正如我们到目前为止所看到的,MEF 有许多优点。然而,一些常见的优点列在下面:
- MEF 打破了应用程序中紧耦合的依赖关系,但尊重松耦合部分的类型检查。
- 应用程序可以扩展。
- 组件可以在运行时添加。
- 组件的动态发现。
- 极大的可重用性。
结论
所以,在本系列的 MEF 第一部分中,我们学习了 MEF 的基础知识、它的适用性、Export
、Import
、ImportMany
属性的重要性、Assembly
和 Directory
目录、动态组件发现等等。但关于 MEF 还有很多东西需要学习。在下一部分或后续部分中,我们将研究一些属性、目录、使用 MEF 的不同类型应用程序的更多实际示例。感谢阅读。
历史
- 2011年4月28日:初始发布