Managed Extensibility Framework 简介
概述
Managed Extensibility Framework (MEF) 使开发人员能够在其 .NET 应用程序中提供挂钩,以供用户和第三方扩展。可以将 MEF 看成一个通用应用程序扩展实用工具。
MEF 使开发人员能够动态创建扩展,无需扩展应用程序,也不需要任何特定于扩展内容的知识。这可以确保在编译时两者之间不存在耦合,使应用程序能够在运行时扩展,不需要重新编译。MEF 还可以在将扩展加载到应用程序之前,查看扩展程序集的元数据,这是一种速度更快的方法。
本实验中包含几个与可扩展性相关的关键概念:
• Composition(复合)是将几个带有不同功能的对象组合为一个或多个复杂对象的过程。复合不是从父类中继承功能,而是将几个不同的对象组合为一个对象。例如,Wing、Propeller、Fuselage 和 VerticalStablizer 对象可以组成 Aircraft 对象的一部分。
• ComposableParts 是 MEF 的关键构建块。ComposableParts 支持应用程序通过 Exports 和 Imports 公开和使用组件扩展。
• Contracts 是 Export 和 Import 组件之间的通信途径。Contract 通常通过 Interface 类实现。Contracts 支持 MEF ComposableParts 以避免依赖关系或者与其他组件之间的紧密耦合。
• Conditional Binding 允许加载满足特定元数据标准的组件。以上述示例为例,您可以选择加载 VerticalStabilizer 组件,这些组件仅由复合石墨 (composite graphite) 组成。
实现扩展的主要方式是,在应用程序的扩展点添加 Import 属性并向扩展添加相应的 Export 属性。Import 和 Export 可以看做是供应商和消费者的关系:Export 组件提供了一些价值;Import 组件消费这些价值。其他扩展选项对于开发人员是开放的,包括完全自定义的扩展方法;但是,本实验仅关注上文提到的主要方法。
目标
在本次动手实验中,您将学习如何:
• 定义组件的可扩展性选项
• 执行条件绑定和组件创建
• 在应用程序运行时导入扩展的程序集
系统要求
您必须拥有以下工具才能完成本实验:
• Microsoft Visual Studio 2010
• .NET Framework 4
安装
使用 Configuration Wizard 验证本实验的所有先决条件。要确保正确配置所有内容,请按照以下步骤进行。
注意: 要执行安装步骤,您需要使用管理员权限在命令行窗口中运行脚本。
1. 如果之前没有执行,运行 Training Kit 的 Configuration Wizard。要做到这一点,运行位于 %TrainingKitInstallationFolder%\Labs\IntroToMEF\Setup 文件夹下的 CheckDependencies.cmd 脚本。安装先决条件中没有安装的软件(如有必要请重新扫描),并完成向导。
注意:为了方便,本实验中管理的许多代码都可用于 Visual Studio 代码片段。CheckDependencies.cmd 文件启动 Visual Studio 安装程序文件安装该代码片段。
练习
本次动手实验由以下练习组成:
1. 使用 MEF 向应用程序动态添加模块
2. 动态扩展窗体
初始材料
这次动手实验包括以下初始材料。
• Visual Studio 解决方案。您将发现可以用作练习起点的 Visual Studio 解决方案,具体解决方案取决于练习。
如果我束手无策了怎么办?
该动手实验中的源代码包括一个最终文件夹,如果完成了每个练习的每一步,您可以在该文件夹中找到应该获取的 Visual Studio 解决方案。如果需要其他帮助来完成练习,您可以使用该解决方案作为指南。
完成本实验的估计时间:30 分钟。
下一步
练习 1:使用 MEF 向应用程序动态添加模块
练习 1:使用 MEF 向应用程序动态添加模块
Managed Extensibility Framework 的一个实际应用是在运行时向应用程序添加模块。这在以下场景非常有用:用户选择购买或初始安装特定模块,之后可能需要添加更多模块。使用 MEF,您可以配置应用程序来监控众所周知的目录,并在该目录中添加找到的任何模块程序集。将程序集放入目录可以使应用程序加载这些程序集,无需明确设置对它们的引用。
任务 1 –更新主窗体以加载支持的模块
在本任务中,您将向现有窗体添加代码,以创建扩展挂钩并动态导入稍后在本练习中创建的类。仅导入那些与配置元数据匹配的类。
MainForm.cs 中定义的主窗体需要一种方法来从文件系统中读取模块。为了本例的简便起见,您将在窗体初始加载过程中导入模块。
1. 从 Start | All Programs | Microsoft Visual Studio 2010 | Microsoft Visual Studio 2010 打开 Microsoft Visual Studio 2010。
2. 打开 MefLab.sln 解决方案文件。默认情况下,该文件位于以下文件夹:%TrainingKitInstallFolder%\Labs\IntroToMEF\Source\Ex01-DynamicallyAddModules\begin\C#。
3. 在代码视图中打开 MainForm(右键单击解决方案资源管理器中的 MainForm,然后选择 View Code)。
4. 更新窗体以使用 MEF 库。为此,在 MainForm 类定义中的顶部添加以下语句。
C#
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
注意:MEF 框架通过契约导入和导出。这些契约由接口指定,用于定义哪些可扩展点 (Imports) 与哪些导出组件相关。您的 MainForm 将导入实现 IMainFormContract 的模块,后者在 MefCommon 项目中定义。要导入这些模块,您需要定义一个集合来通过属性保存和公开它们。MEF 的复合阶段将解析适用的 Exports 和 Imports 并加载您的集合。
5. 将以下代码添加到 MainForm.cs 文件中构造函数的上方。
(代码片段– MEF 简介实验 - 练习 1 ImportedMainFormContracts)
C#
[ImportMany]
public Lazy<IMainFormContract, IDictionary<string, object>>[] ImportedMainFormContracts { get; set; }
6. MEF 的 CompositionContainer 用于匹配组件与导出和导入值。CompositionContainer 支持加载、绑定和检索组件及其值。通过在 MainForm 类的上方添加以下变量声明创建全局 CompositionContainer。
C#
private CompositionContainer _container;
7. 您现在必须创建一个帮助函数从文件夹导入对象,您已经使用 MainForm.cs 文件中的 _extensionDir 变量指定了该文件。在 MainForm.cs 文件 MainForm 类的下方添加以下代码。
(代码片段 – MEF 简介实验 - 练习 1 GetContainerFromDirectory)
C#
private CompositionContainer GetContainerFromDirectory()
{
var catalog = new AggregateCatalog();
var thisAssembly =
new AssemblyCatalog(
System.Reflection.Assembly.GetExecutingAssembly());
catalog.Catalogs.Add(thisAssembly);
catalog.Catalogs.Add(
new DirectoryCatalog(_extensionDir));
var container = new CompositionContainer(catalog);
return container;
}
8. 接下来,您需要使用已加载的容器。您必须遵从常用 MEF 模式,这需要使用 Compose 方法让 MEF 解析所有适用的导入和导出操作。在 GetContainerFromDirectory 方法下方添加以下方法。
(代码片段 – MEF 简介实验 - 练习 1 Compose)
C#
private bool Compose()
{
_container = GetContainerFromDirectory();
try
{
_container.ComposeParts(this);
}
catch (CompositionException compException)
{
MessageBox.Show(compException.ToString());
return false;
}
return true;
}
9. 您将调用窗体构造函数中的 Compose。添加代码,构造函数应该如下所示:
(代码片段 – MEF 简介实验 - 练习 1 Compose 调用)
C#
public MainForm()
{
InitializeComponent();
bool successfulCompose = Compose();
if (!successfulCompose)
{
this.Close();
}
}
任务 2 –创建一个扩展模块
在本任务中,您将使用窗体处理现有的 Windows Forms 项目以维护员工数据。您将使用 MEF 标记窗体,表示其可用于导出到其他应用程序。MefEmployeeModule 项目预建了几个类,以节省时间。
1. 在继续下一步前,您将需要引用 MEF 库。将对 MEF 库的引用添加到 MefEmployeeModule 项目。
a. 选择 Solution Explorer 中的 MefEmployeeModule 项目并选择 Project | Add Reference 。将出现 Add References 对话框。
b. 选择 .NET 选项卡。
图 1
MEF 引用
c. 选择 System.ComponentModel.Composition 组件。单击 OK 按钮添加对该库的引用。
注意: 如果您没有找到 System.ComponentModel.Composition 组件,则转到 Browse 选项卡,在 %SystemRoot%\Microsoft.net\Framework\v4.0.20704 文件夹或相应的 .NET Framework 4.0 文件夹中查询该程序集。
2. 您还需要使用合适的 using 语句更新类文件。在代码视图中打开 EmployeeMaintenance 窗体,并在文件顶部 using 块中添加以下行。
C#
using System.ComponentModel.Composition;
3. EmployeeMaintenance 窗体就是需要添加到主窗体的模块。记住,您已经在 MainForm 中使用了 ImportMany 属性,以便仅拉入实现 IMainFormContract 契约的组件。您需要根据该契约指定 EmployeeMaintenance 窗体。类声明之后的代码如下:
C#
[Export(typeof(IMainFormContract))]
public partial class EmployeeMaintenance :Form, IMainFormContract
4. IMainFormContract 指定必须实现的两个属性。将以下代码添加到 EmployeeMaintenance 构造函数上方。
(代码片段 – MEF 简介实验 - 练习 1 Implement IMainFormContract)
C#
public string MenuItemText
{
get { return "&Employees"; }
}
public string SubFormTitle
{
get { return "Employee Pane"; }
}
5. 您还需要使用将用于导入 MainForm 类的一些元数据来修饰该类。MEF 允许您在各个位置查询该元数据。添加 ExportMetaData 属性如下:
(代码片段 – MEF 简介实验 - 练习 1 ExportMetaData attributes)
C#
[Export(typeof(IMainFormContract))]
[ExportMetadata("Name", "Employee Pane")]
[ExportMetadata("MenuText", "&Employees")]
public partial class EmployeeMaintenance :Form, IMainFormContract
6. 保存窗体并编译 MefEmployeeModule 项目。选择 Build | Build MefEmployeeModule。MefEmployeeModule 程序集现在已经编译,并可使用 MEF 导入到其他应用程序中。
任务 3 –导入 Employee Maintenance 窗体
在本任务中,您将更新主应用程序窗体,以在运行时用户单击菜单项时导入可用的模块。
1. 在 Solution Explorer 的 MefLabMain 项目中双击 MainForm.cs,在设计视图下打开 MainForm。
2. 现在需要浏览 CompositionContainer 中的组件并确定导出菜单文本信息的组件。您需要调整窗体,以根据导出的元数据加载菜单项。Compose 方法加载支持 IMainFormContract 的模块列表,因此您需要浏览该集合并收集将添加到菜单中的项。为窗体的 Load 事件创建一个事件处理程序,以便可以根据需要调整菜单。在窗体中央双击。将出现代码视图窗口,并带有定义的新 MainForm_Load 方法。添加以下代码以对集合进行迭代,根据需要更新菜单项。注意,您还要与事件处理程序挂钩以处理每个新的菜单项。
(代码片段– MEF 简介实验 - 练习 1 填充菜单)
C#
private void MainForm_Load(object sender, EventArgs e)
{
foreach (var export in this.ImportedMainFormContracts)
{
var exportedMenuText = export.Metadata["MenuText"] as string;
if (String.IsNullOrEmpty(exportedMenuText))
{
return;
}
ToolStripItem menuItem =
modulesToolStripMenuItem.DropDownItems.Add(exportedMenuText);
menuItem.Click += new System.EventHandler(this.LaunchModule_Click);
}
}
注意:如下所示,将逻辑混合到应用程序表示层不是一种好的软件工程实践。很难测试、混合问题,以及打破许多其他可靠的设计原则。
您将打破该原则,在 MainForm_Load 等窗体方法中显示逻辑混合,以保证示例的简明扼要。在实际的应用程序中,该逻辑应该分离到另一个类,最好使用 Model View Presenter 或类似方法。我们建议您浏览 MVP 及其相关项目作为设计原则,帮助您编写更多灵活、可维护、可测试的应用程序。
3. 下一步是添加从菜单操作启动组件的功能。您将使用与之前相同的方法:查看导入组件列表中加载的组件,通过查看元数据找到适用的项。Value 属性允许您启动特定的组件。在 MainForm 类定义的底部添加以下方法。该方法与 MainForm_Load 方法结合可处理动态添加菜单的单击事件。
(代码片段– MEF 简介实验 - 练习 1 菜单单击处理程序)
C#
private void LaunchModule_Click(object sender, EventArgs e)
{
ToolStripItem thisItem = sender as ToolStripItem;
if (thisItem == null) { return; }
string thisItemTitle = thisItem.Text;
foreach (var export in this.ImportedMainFormContracts)
{
string menuTitle = export.Metadata["MenuText"] as string;
if (String.IsNullOrEmpty(menuTitle))
{
return;
}
if (menuTitle == thisItemTitle)
{
Form frm = export.Value as Form;
if (frm == null) { return; }
frm.Show(this);
return;
}
}
}
下一步
练习 1:验证
练习 1:验证
在本验证中,您将分别使用和不使用导入模块来运行应用程序。
1. 添加以下 Post-Build 命令以生成附加输出文件夹。为此,右键单击 MefEmployeeModule 项目并选择 Properties。单击 Build Events 选项卡并将以下内容剪切到 Post-build 事件命令行字段中。确定包含了引号,以正确处理带空格的目录。
Post-Build Command
if not exist "$(SolutionDir)MefLabMain\bin\Debug\ExtModules" mkdir "$(SolutionDir)MefLabMain\bin\Debug\ExtModules"
2. 编译解决方案 (CTRL+SHIFT+B)。
3. 设置 MefLabMain 作为启动项目。在 Solution Explorer 中,右键单击 MefLabMain 并选择 Set as startup project。
4. 按 F5 运行应用程序。应该会出现父窗口,其顶部应有一个菜单。Modules 菜单下方应该没有任何项目。
图 2
空的 Modules 菜单
5. 退出应用程序。选择 File | Exit。
6. 您已经构建了该应用程序来监控 MefLabMain\bin\Debug\ExtModules 目录的更改。您可以更新 MefEmployeeModule 项目,以自动通过项目的 Post Build 事件将其输出二进制文件保存到该文件夹。将以下粗体显示的行添加到 Post-Build 命令行中。
Post-Build Command
if not exist "$(SolutionDir)MefLabMain\bin\Debug\ExtModules" mkdir "$(SolutionDir)MefLabMain\bin\Debug\ExtModules"
copy "$(TargetPath)" "$(SolutionDir)MefLabMain\bin\Debug\ExtModules"
7. 按 F5 运行应用程序。应该会出现父窗口,其顶部应有一个菜单。Employees 菜单项应该在 Modules 菜单下方出现。
图 3
加载了模块的 Modules 菜单
8. 单击 Employees 菜单项。应该出现一个 Employee Maintenance 窗体。
图 4
Employee Maintenance 窗体
注意: 加载模块时抛出 CompositionExceptions 通常表示您忘了将所需的某个文件复制到 ExtModules 文件夹中。
9. 注意,将从父项目调用 Employee 模块,无需明确设置任何引用。MEF 处理了对所有程序集的加载操作,允许您创建一个完全解耦的组件集合。
10. 单击 EmployeeMaintenance 窗体右上方的关闭按钮 ( ) 关闭它。
11. 选择 File | Exit 关闭父窗体。
下一步
练习 2:动态扩展窗体
练习 2:动态扩展窗体
在最后一个练习中,您将程序集放入众所周知的目录中,向应用程序动态添加一个新的 Employee Maintenance 窗体。现在,您将使用 MEF 扩展这个新添加的窗体,动态加载可以为窗体提供更多功能的窗体控件。
无论有没有扩展,应用程序都必须能够正常工作,因此需要通过 Employee Maintenance 窗体来松散耦合扩展的功能。两者都不需要了解另一方的内在工作机制。您将通过 Interface 使用 Contract 支持此功能。
本练习将使用几个已经预建的类,使您能关注本实验的关键方面。
我们的计划是扩展 EmployeeMaintenance 窗体,动态添加操作员工列表的命令按钮。您将使用现有接口 ICmdButtonInfo,该接口包含命令按钮和一个 CompanyInfo 对象,该对象可以从导出的对象继承而来。
任务 1 –将 Add Employee Button 标记为可导出
MEFEmployeeExtender 项目包含可以在运行时动态加载到 Employee Maintenance 窗体的控件。对于本实验,您将修改两个控件– AddEmployeeButton 和 DeleteEmployeeButton,它们都实现 ICmdButtonInfo 契约。
在本任务中,您将修改 AddEmployeeButton,通过 ICmdButtonInfo 契约导出其信息。
1. 从 Start | All Programs | Microsoft Visual Studio 2010 | Microsoft Visual Studio 2010 打开 Microsoft Visual Studio 2010。
2. 打开 MefLab.sln 解决方案文件。默认情况下,该文件位于以下文件夹:%TrainingKitInstallFolder%\Labs\IntroToMEF\Source\Ex02-DynamicallyExtendAForm\begin\C#。您也可以继续使用上一个练习完成时获得的解决方案。
3. 您还需要使用 EmployeeMaintenance 窗体中的 AddEmployee 按钮,因此需要将其标记为可通过契约导出。修改 AddEmployeeButton.cs 类,使用 Export 属性将该类标记为可导出。
C#
[Export(typeof(ICmdButtonInfo))]
public class AddEmployeeButton :ICmdButtonInfo
4. 保存文件。
任务 2 –将 DeleteEmployee Button 标记为可导出
1. 与 AddEmployeeButton 一样,您需要通知 MEF 通过 ICmdButtonInfo 契约导出 DeleteEmployeeButton。修改 DeleteEmployeeButton.cs 类,使用 Export 属性将该类标记为可导出。
C#
[Export(typeof(ICmdButtonInfo))]
public class DeleteEmployeeButton:ICmdButtonInfo
2. 保存文件。
任务 3 –向 Employee Maintenance 窗体添加一个 Panel 控件
在本任务中,向 Employee Maintenance 窗体添加一个 Panel 控件。之后,您可以动态向窗体添加按钮。将它们放在该面板上将更容易将这些按钮对齐。
1. 双击 Solution Explorer 中 MEFEmployeeModule 项目下方的 EmployeeMaintenance.cs,在设计器中打开 EmployeeMaintenance 窗体。
2. 如果 Toolbox 面板不可见,单击 View 菜单的 Toolbox。
3. 从工具箱 Containers 选项卡中将 Panel 控件拖到窗体上。
a. 在 DataGridView 下找到 Panel。
b. 拖动 Panel 的边缘,将其宽度拉伸到 DataGridView 的宽度。
c. 将 Panel 的名称更改为“ButtonsPanel”。
任务 4 –向 Employee Maintenance 窗体添加代码以加载扩展的对象
在本任务中,您将向 Employee Maintenance 窗体的 Load 事件处理程序添加代码。该代码将查看包含导出控件的众所周知的程序集目录。这些控件将在运行时自动添加到窗体中。
1. 在 Solution Explorer 中右键单击并选择 View Code,以打开 EmployeeMaintenance.cs。
2. 添加一个集合来保存扩展的对象。您将引用该集合作为导入对象,因为您将把这些对象导入到这个特定类中。注意,您只接受满足 ICmdButtonInfo 契约的导入。在 EmployeeMaintenance 类的顶部添加以下代码。
(代码片段– MEF 简介实验 - 练习 2 ImportedButtons)
C#
[ImportMany]
public Lazy<ICmdButtonInfo>[] ImportedButtons
{
get;
set;
}
3. 现在,您需要迭代该集合,并加载导入的对象,本例中为按钮。将代码添加到 MainForm 之后,您需要使用 Value 属性访问导出对象本身。在 EmployeeMaintenance 类的底部添加以下方法。
(代码片段– MEF 简介实验 - 练习 2 CreateButtons)
C#
private void CreateButtons()
{
int left = 10;
foreach (var importedButton in this.ImportedButtons)
{
var btnInfo = importedButton.Value;
btnInfo.CompanyInfo = this.CompanyInfo;
btnInfo.CompanyInfo.EmployeeListChanged +=
new EventHandler(CompanyInfo_EmployeeListChanged);
Button btn = btnInfo.CommandButton;
this.ButtonsPanel.Controls.Add(btn);
btn.Left = left;
left += btn.Width;
left += 20;
}
}
4. 更新窗体的加载方法以调用 CreateButtons 方法。
C#
private void EmployeeMaintenance_Load(object sender, EventArgs e)
{
GetAllEmployees();
CreateButtons();
}
5. 当用户选择 DataGridView 中的行时,您需要更新 CompanyInfo 对象的 SelectedEmployee 字段。这样一来,松散耦合的 CompanyInfo 对象中的按钮可以了解要对哪个员工进行操作。在 EmployeeMaintenance 类中添加以下方法。
(代码片段– MEF 简介实验 - 练习 2 empDataGridView_SelectionChanged)
C#
private void empDataGridView_SelectionChanged(object sender, EventArgs e)
{
if (empDataGridView.SelectedRows.Count > 0)
{
Employee selectedEmployee
= (Employee)empDataGridView.SelectedRows[0].DataBoundItem;
this.CompanyInfo.SelectedEmployee = selectedEmployee;
}
else
{
this.CompanyInfo.SelectedEmployee = null;
}
}
6. 将该方法连接到 DataGridView 的 SelectionChanged 事件。在设计模式中打开 EmployeeMaintenance 窗体。右键单击 DataGridView 控件并从上下文菜单中选择 Properties。在 Properties 表单上,单击 Event 图标列出事件。在列表中找到 SelectionChanged 事件,并使用下拉列表选择 empDataGridView_SelectionChanged 方法。
图 5
连接 SelectionChanged 事件
7. 编译解决方案。选择 Build | Build Solution。
下一步
练习 2:验证
练习 2:验证
在本验证中,您将运行带有新 MEF 扩展的解决方案。
1. 如果它尚未打开,可以从 Start | All Programs | Microsoft Visual Studio 2010 | Microsoft Visual Studio 2010 中打开 Microsoft Visual Studio 并打开 MEFLab 解决方案。
2. 测试带有新扩展的应用程序。MefEmployeeExtender 程序集已经通过预配置的 Post-Build 事件复制到 ExtModules 文件夹中。
a. 按 F5 运行应用程序。选择 Modules | Employees。
b. 应该出现一个 Employee Maintenance 窗体。这一次,它应该包含两个新按钮:Delete Employee 和 Add Employee。
图 6
EmployeeMaintenance 窗体
c. 单击 Add Employee 按钮。DataGridView 中将显示一个新行。
d. 单击任何行左边的灰色框,突出显示 DataGridView 该行。单击 Delete Employee 按钮。该行应该会消失。
图 7
删除行
e. 在 Employee Maintenance Load 方法中设置断点,每个按钮的单击方法都可以在该代码中步进,以查看发生了什么。
f. 关闭 Employee Maintenance 窗体和主窗体
下一步
总结
总结
在本实验中,您使用 Microsoft Managed Extensiblity Framework 为应用程序添加了几个可扩展性点,并构建了扩展来插入这些可扩展性点。
使用 MEF,您可以设置导出属性,无需明确引用即可加载外部程序集,还可以在运行时动态扩展应用程序。您了解了导入和导出的交互,并通过接口使用契约指定了要处理的导出组件。