65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (75投票s)

2011年4月28日

CPOL

9分钟阅读

viewsIcon

259713

downloadIcon

5958

本文将讨论 MEF,如何开始使用它,等等。

目录

  1. 引言
  2. 背景
  3. 什么是MEF?
  4. MEF 所在的程序集
  5. MEF 的组成部分
  6. MEF 相关术语
  7. MEF 的工作原理
  8. 当前程序集中仅包含单个导出部分的简单工作示例
  9. 当前程序集中包含多个导出部分的简单工作示例
  10. 使用聚合目录和目录的多个导出部分的简单工作示例
  11. MEF 的优势
  12. 结论
  13. 历史

引言

尽管托管可扩展性框架 (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 的基本术语。

  1. Part(部分): 一个 Part 是一个可以导入或导出到应用程序的对象(例如,类、方法或属性)。
  2. Catalog(目录): 一个对象,用于帮助发现程序集或目录中可组合的可用部分。
  3. Contract(契约): 导入和导出的部分需要通过某种契约(例如,接口或预定义的数据类型,如 string)进行通信。
  4. Import Attribute(导入属性): 它定义了一个部分的需求。它仅适用于单个 Export Attribute。
  5. ImportMany Attribute(多重导入属性): 它类似于 Import Attribute,但支持多个 Export Attributes。
  6. Export Attribute(导出属性): Import Attribute 创建了需求,Export Attribute 满足了这些需求。它暴露了将参与组合的部分。
  7. Compose(组合): 在 MEF 的术语中,Compose 是将导出的部分与导入的部分组装在一起的区域。

此时,这些术语可能还不太清楚。为了使这些术语能够被理解,让我们先尝试了解 MEF 的工作原理。

MEF 的工作原理

MEF 的工作原理基于需求/供给的原则。可供供给的部分必须被发现,并且需求和供给的部分必须在契约上达成一致。当应用程序需要插入某些部分时,MEF 将从某个位置(我们称之为 Catalog)动态发现这些部分。只要满足契约,组合引擎就会将这些部分组装起来。

当前程序集中仅包含单个导出部分的简单工作示例

让我们创建一个简单的计算器来演示。我们的示例应用程序将具有四个基本算术运算,即加、减、乘和除。考虑到这一点,让我们按以下方式创建项目结构:

1.jpg

解决方案文件 CalculatorDemoUsingMEF 包含三个项目:

  1. CompositionHelper
  2. CalculatorContract
  3. CalculatorUI

a) CalculatorContract 项目描述

在这里,我们主要定义了 Calculator 的具体组件(如 AddSubtract 等)必须实现的契约或接口。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 程序集引用添加到项目中。

2.jpg

同时,我们还添加了 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();

首先,我们正在组合将参与组合的计算器组件。然后调用加法组件来执行所需的操作。

最终输出如下图所示:

3.jpg

当前程序集中包含多个导出部分的简单工作示例

到目前为止,我们已经学习了 MEF 的一些知识,了解了它的工作原理,并且成功地在我们的应用程序中添加了一个组件。现在,让我们增强我们的应用程序,使其能够处理所有其他组件,如减法、乘法和除法。

因此,让我们在应用程序中添加各种组件部分。项目结构现在将变为:

4.jpg

正如可以发现的,预期不会有太大变化,我们添加了三个新的部分,它们被高亮显示。

现在让我们再次查看 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 属性填充。

当我们运行应用程序时,我们可以找出参与组合的各种部分。

5.jpg

另一个值得注意的点是 Lazy<T> 类,它基本上将大型对象的创建推迟到我们真正需要它的时候。在示例代码中,第二个参数是由 MEF 在运行时处理的元数据。

相应地,GetResult 函数已稍作修改。它现在接受第三个参数,指定操作类型,并基于此调用导出的部分。

最后,在具体的“操作”点击事件中,我们传递了操作类型为:

private void btnAdd_Click(object sender, RoutedEventArgs e)
{
            DoCalciOperation("Add");
}

一旦我们运行应用程序,输出将如下所示(仅显示乘法运算的输出):

6.jpg

希望这与上一部分同样有帮助。在下一部分中,我们将探索一些新的目录类型和 MEF 的更多功能。

使用聚合目录和目录的多个导出部分的简单工作示例

到目前为止,我们看到我们的可组合部分都驻留在当前程序集中。但如果它们驻留在不同的程序集或不同的位置呢?

嗯,我们也有解决方案。我们将看到聚合目录和目录在这些情况下如何帮助我们。

让我们现在看一下项目结构。

7.jpg

我们可以发现,解决方案文件已添加了一个新项目,即 Export Components。在该项目中,我们有一个名为 Components 的文件夹,其中存储了我们以程序集形式导出的部分(.dll 文件)。

我们只在那里添加了两个 DLL,即 Add 和 Subtraction,并从 CompositionHelper 项目中删除了 Add.csSubtract.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)。**

8.jpg

而 **AssemblyCatalog 有两个部分(Divide 和 Multiply)。**

9.jpg

在发现部分后,它们被添加到 AggragateCatalog 中,然后添加到 CompositionContainer 中。最后,部分被组合。

运行应用程序会产生预期的输出(仅显示加法运算的输出)。

10.jpg

优点

正如我们到目前为止所看到的,MEF 有许多优点。然而,一些常见的优点列在下面:

  1. MEF 打破了应用程序中紧耦合的依赖关系,但尊重松耦合部分的类型检查。
  2. 应用程序可以扩展。
  3. 组件可以在运行时添加。
  4. 组件的动态发现。
  5. 极大的可重用性。

结论

所以,在本系列的 MEF 第一部分中,我们学习了 MEF 的基础知识、它的适用性、ExportImportImportMany 属性的重要性、Assembly Directory 目录、动态组件发现等等。但关于 MEF 还有很多东西需要学习。在下一部分或后续部分中,我们将研究一些属性、目录、使用 MEF 的不同类型应用程序的更多实际示例。感谢阅读。

历史

  • 2011年4月28日:初始发布
© . All rights reserved.