MEF 与 ASP.NET - "Hello World!"
基础的 MEF (Managed Extensibility Framework) 与 ASP.NET。极简版的 "Hello World!" 示例。
引言
这是一个非常简单、最基础的 ASP.NET MEF "Hello World!" 示例。
该示例展示了在 ASP.NET 中开始使用 MEF 所需的基本设置步骤。
我希望这些内容能够为那些想开始使用 MEF 的 ASP.NET 开发者提供必要的“启动”支持,帮助他们掌握该框架的基本概念,从而更好地深入研究更复杂的方面。
它为人们提供了一个可以随意摆弄、试验和扩展的基础设置。
我自己也很希望在几天前就能找到一些像这样非常简单的例子。
这可以为我节省成百上千次的谷歌搜索,以及我无数小时的徒劳阅读。
或者,也许我的谷歌搜索技巧还不够强?
请在评论区提供有用的(初学者)链接!
背景
MEF (Managed Extensibility Framework) 是微软开发的一个框架。
codeplex 上的 MEF 社区网站 提供了更多信息和示例 - 但遗憾的是,对于 ASP.NET(非 MVC)来说,没有简单易懂的入门内容。
简而言之:MEF 是一个用于将插件连接到应用程序的框架(以便您可以访问它们提供的任何服务)。
与微软同样开发的 MAF (Managed Addin Framework) 相比,MEF 使这一切变得非常容易。普遍的共识是,除非有特殊需求,否则应远离 MAF。主要原因在于 MAF 在代码层面非常复杂,难以启动和运行。
两者之间的区别:“MEF 关注‘可扩展性’,而 MAF 关注‘隔离性’(征服或启用)”。
大多数 MEF 示例都是针对控制台应用程序、Silverlight/WPF 的。有一些纯 Web 应用程序的示例,但它们似乎都适用于 ASP.NET MVC。
我几乎花费了数天时间 scouring the net,都没有找到一个简单、直接、不花哨的 ASP.NET 示例,我希望通过这篇文章能让我的 fellow wannabe MEF ASP.NET 开发者避免这种沮丧。
我能找到的*极少数*纯 ASP.NET 示例,对于那些只想尝试一下、看到一些东西运行而无需理解海量代码的人来说,都显得过于复杂。
这些示例之所以“过于复杂”,是有原因的,因为最终如果你想认真对待 ASP.NET 中的 MEF,你就需要“深入研究”。
但首先需要登上船 - 因此,我们在这里。
MEF 如何工作
基本思路是
- 使用
[Import]
和[Export]
属性来(连接)您的代码(供 MEF 匹配) - 将您的插件加载到一个 **目录(catalog)** 中(我使用的是
DirectoryCatalog
,但还有其他几种类型的目录) - 将您的目录放入一个 **容器(container)**,例如
CompositionContainer
(有几种实现方式) - 让容器的
ComposeParts
方法完成连接工作(也有几种实现方式)
然后您就完成了。
一旦您掌握了以上内容(并且了解了连接部分 - 见下文),MEF 本身就会变得相当容易。
我不得不承认,我花了比我想承认的更多的时间才从 MEF 的各种资料中提炼出以上非常简单的步骤,因为它从未被明确地说明过,而且往往淹没在各种花哨的 MEF 功能演示中。
现在是时候看代码了...
Using the Code
要从头开始创建您自己的基础示例,您需要执行以下操作
- 在 Visual Studio 2008/2010 中设置一个空的 Web 应用程序项目。您需要 .NET 4(MEF 已包含)。
- 向项目中“添加引用”到
System.ComponentModel.Composition
。 - 向项目中添加一个 WebForm 页面(
aspnetMEFBasic.aspx
)。 - 向页面添加一个 div 标签(用于输出)
<div id="div1" runat="server"></div>
- 在代码隐藏文件(aspnetMEFBasic.aspx.cs)中,添加
using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting;
- 将代码替换为
namespace aspnetMEFBasic { public partial class aspnetMEFBasic : System.Web.UI.Page { //Import our "plugin" [Import] Class1 c1; protected void Page_Load(object sender, EventArgs e) { //Step 1: //Find the assembly (.dll) that has the stuff we need //(i.e. [Export]ed stuff) and put it in our catalog DirectoryCatalog catalog = new DirectoryCatalog (System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")); //Step 2: //To do anything with the stuff in the catalog, //we need to put into a container (Which has methods to do the magic stuff) CompositionContainer container = new CompositionContainer(catalog); //Step 3: //Now lets do the magic bit - Wiring everything up container.ComposeParts(this); //Step4: //Lets see if it works div1.InnerText = c1.s1; } } }
注意
[Import]
属性(请阅读注释)。 - 向项目中添加一个类文件(Code.cs)(暂时将其放在根目录下)。向文件中添加以下内容
using System.ComponentModel.Composition;
- 将代码替换为
namespace aspnetMEFBasic { [Export] public class Class1 { [Import] public string s1; //Gets it's value ("Hello World!") from "somewhere" - //MEF takes care of this. } public class Class2 { private class Class3 { [Export] public string s3 = "Hello World!"; //Not as "private" as one would think- //Not to MEF at least } } }
- 注意两个
[Export]
属性和一个[Import]
属性。还要注意Class3
是private
的 - 并且我们通常无法从外部访问s3
。 - 编译并运行代码 - aspnetMEFBasic.aspx 页面应该会显示 "
Hello World!
"
关注点
首先 - 这当然不是我们真正用于页面的“独立”插件,因为 Class1
直接声明在我们的应用程序项目中 - 因此,我们首先可以声明一个 Class1
类型的变量 s1
(但我们确实成功获取了 **私有的** s3
变量)。
我选择这样做纯粹是为了演示目的 - 并使其非常非常简单。
实际上,还可以让它变得更简单。Class2
(因此也包括 Class3
)对于一个基础示例来说并不是真正必需的。您可以删除它们,然后在 Class1
中将 public string s1 = "Hello World!;
替换为(这样 s1
就会有一个值),它会正常工作。
但我这样做是为了演示 MEF 可以变得多么“肮脏”(访问 private
类 Class3
)- 从而让您意识到需要注意!
今天的宿主是
注意代码隐藏文件第 3 步中的 "container.ComposeParts(this);
" 部分。
在这个例子中,“this
”指的是我们当前所在的网页 - 您也可以使用“sender
”(Page_Load
的参数之一)。一旦您开始与 ASP.NET 页面生命周期的事件挂钩,“sender
”通常就是您要使用的。
在 MEF 的世界里,“this
”/“sender
”是“**宿主**”进程,即需要被连接到我们 **目录** 中提供的任何服务的进程,才能利用这些服务。
事实上,您可以传入任何类,只要它设置了某些 [Import]
属性,并且需要与 **目录** 中相应的 [Export]
进行连接。
这里真正重要的收获是
**目录**(以及它被添加到的 **容器**)本身对 **宿主** 是谁(将是谁)完全不知情 - 因此,您需要将 **宿主** 作为参数传递,让 MEF 知道谁需要被连接。
而在 Web 应用程序中(与控制台应用程序不同),当您开始编写不在代码隐藏文件(宿主很明显)内的代码时,情况可能会变得有些棘手,因为此时弄清楚如何获取正确的 **宿主** 并将其作为参数传递以进行组合,并不总是那么直接。
连接 - [Import]/[Export]
连接由 [Import]
和 [Export]
属性控制。MEF 确保正确的 [Import]
会接收到相应的 [Export]
。MEF 通过推断导入/导出的项的类型来做到这一点 - 如果一个 [Import]
的类型与一个 [Export]
的类型匹配,它们就会被连接起来。
我也可以自己提供类型(通常情况下,您会这样做):对于简单的 string
,是 [Import(typeof(string))]
/[Export(typeof(string))]
;对于 aspnetMEFBasic.aspx.cs 页面顶部的 [Import]
,则是 [Import(typeof(Class1))]
(在 Class1
的顶部有一个相应的 [Export(typeof(Class1))]
)。
这将来会很有用!
如果一个 [Import(typeof(string))]
有两个 [Export(typeof(string))]
会怎样?
事实上,一个 [Import]
可以接收一个 [Export]
列表。但我在这里不介绍它是如何工作的(当然,对于简单的 [Import]
类型 - int
, string
等,这是不可能的)。
大多数控制台示例都很好地演示了这一点 - 例如 计算器示例。
MSDN 页面是查找更多关于 MEF 信息的一个很好的起点。
如果你有多个 [Export(typeof(string))]
,并且每个都需要去不同的 [Import(typeof(string))]
,该怎么办?
然后我们需要帮助 MEF 一点,让它知道哪个 [Export(typeof(string))]
去哪个 [Import(typeof(string))]
。
您可以通过在属性中添加一个名称值来实现,例如[Import("TheString1")]
以及相应的 [Export("TheString1")]
,以及[Import("TheString2")]
以及相应的 [Export("TheString2")]
。
请注意,这些("TheString1
" 和 "TheString2
")仅仅是 string
s - 它们不是类型!
这样,即使每个 [Export]
在类型上都可以与每个 [Import]
匹配,MEF 也知道哪个 string
[Import]
/[Export]
连接到哪里。
如果我们再细心一点,还可以像这样添加类型[Import("TheString1", typeof(string))]
以及相应的 [Export("TheString1", typeof(string))]
,以及[Import("TheString2", typeof(string))]
以及相应的 [Export("TheString2", typeof(string))]
。
(如前所述,这将来会很有用 - 所以养成随时添加类型的习惯是个好主意)。
扩展连接(可实现真正插件的连接)
通常,您应该*避免*在 [Import]
/[Export]
属性中使用具体类的类型(如 Class1
),对于像 string
和 int
等简单类型也是如此(通常情况下)。
相反,您应该使用 Interface
类型 - 因为这才是真正的魔力开始的地方!
(小插曲)
在上面的代码示例中,唯一真正的魔力在于我们设法获取了私有的 s3 变量。
这更多是反射(MEF 使用的)的魔力,而不是 MEF 本身的魔力。
通过反射,您可以做到那种“肮脏的技巧”。
危险在于,如果您在 [Import]
/[Export]
属性的命名上不小心,可能会错误地连接到不相关的项。
将某个东西命名为 [Import("Sendmail")]
可能不是个好主意,因为每个人都这么做... 因此,当您开始导入第三方插件时,您可能会连接到您意想不到的东西。
(结束插曲)
回到真正的魔力 - 让我们创建一个真正的插件。也就是说,一段我们的 Web 应用程序事先不知道其工作原理的代码(插件)- 除了,您猜对了,就是那个插件的
Interface
。Interface
s 在这个上下文和其他上下文中通常被称为“契约”。这是插件签名(通过实现 Interface
)的契约,因此同意提供契约中定义的内容,反之,它告诉应用程序可以从同意该契约的给定插件那里期望获得哪些“服务”。
当然,应用程序和插件都需要了解契约(Interface
)- 这是将它们绑定在一起的粘合剂。契约本身没有任何功能(这很快就会变得很明显)。
为了设置真正的应用程序/插件结构,我们将需要三个不同的项目(而不是像之前那样只有一个项目)
- Web 应用程序项目
- 接口项目
- 插件项目
您可以为每个项目创建一个 Visual Studio 解决方案,以真正演示“真实世界”的示例如何与不同方创建主应用程序,而其他人创建插件(以及接口/契约)协同工作。
或者 - 只需在一个解决方案中有 3 个独立的项目,这样在更改代码时更容易将它们连接起来 - 因为它可以自动化(而且您将同时创建主应用程序和插件)。
我们需要做的第一件事是 Interface
项目。它非常简单 - 如前所述,它本身没有任何功能。
Interface
项目只包含一个类文件(aspnetMEFInterface.cs
),内容如下
namespace aspnetMEF {
public interface IHelloWorld {
string s { get; }
}
}
就是这样 - 真的。没有技巧。这只是一个关于提供(或接收)string
的契约。仅此而已。
插件(aspnetMEFPlugin.cs
)也同样简单。
using System.ComponentModel.Composition;
namespace aspnetMEF {
public class aspnetMEFPlugin : IHelloWorld {
private string _s = "HelloWorld!";
#region IHelloWorld Members
[Export("aspnetMEFHelloWorld", typeof(string))]
public string s {
get { return _s; }
}
#endregion
}
}
记住要“添加引用”到项目中的 System.ComponentModel.Composition
。
同样,对于aspnetMEFwebapp.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
namespace aspnetMEF {
public partial class aspnetMEF : System.Web.UI.Page {
[Import("aspnetMEFHelloWorld", typeof(string))]
string s;
protected void Page_Load(object sender, EventArgs e) {
var catalog = new DirectoryCatalog(System.IO.Path.Combine
(AppDomain.CurrentDomain.BaseDirectory, "bin"));
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
div1.InnerText = s;
}
}
}
记住要“添加引用”到项目中的 System.ComponentModel.Composition
。
而 aspnetMEFwebapp.aspx 只需添加 div1
<div id="div1" runat="server">
</div>
现在如何让它工作 - 假设每个都是独立的解决方案。
将 aspnetMEFInterface.dll 放入 aspnetMEFPlugin
和 aspnetMEFwebapp
的 bin 文件夹中。在两个项目中“添加引用”(请注意,实际上,您需要在编写引用接口的代码之前完成此操作)。
就这样。
启动 aspnetMEFwebapp
,向世界问好 :-)
只导入/导出简单的
string
当然有点乏味(且有限)。扩展这个例子很简单(读者练习)。
在 aspnetMEFPlugin.cs 中,执行以下操作
using System.ComponentModel.Composition;
namespace aspnetMEF {
[Export("aspnetMEFHelloWorld", typeof(IHelloWorld))]
public class aspnetMEFPlugin : IHelloWorld {
private string _s = "HelloWorld!";
#region IHelloWorld Members
//[Export("aspnetMEFHelloWorld", typeof(string))]
public string s {
get { return _s; }
}
#endregion
}
}
(完整展示文件,以使其完全清晰地说明了什么已更改。)
注意 [Export]
属性已移至类声明。
注意 [Export]
的 **类型** 已更改为 IHelloWorld
(这就是真正的强大之处)。其他所有内容都相同。
在 aspnetMEFwebapp.aspx.cs 中,更改 [Import]
以匹配,并自然地将 s
变量的类型更改为匹配(我也更改了名称 - 为 hw
)。
[Import("aspnetMEFHelloWorld", typeof(IHelloWorld))]
IHelloWorld hw;
...
div1.InnerText = hw.s;
其他一切都一样。
对 Interface
本身没有更改。
注意我们现在是如何导入整个类的实例的。这是最后的魔法。
现在我们可以访问我们插件的 **方法**,而不仅仅是一个简单的 string
getter。
更多读者练习
- 在插件中添加一些方法
- 也把这些方法添加到接口中(否则 WebApp 就不知道它们的存在了)
您现在拥有一个功能齐全的插件生态系统!!!
尽情享受 :-)
关于 WebApp 和 MEF 的一些说明
到目前为止我们所做的都是最简单的。与控制台应用程序相比,WebApp 和 MEF 的棘手之处在于状态。每次提供网页时,我们都会丢失之前所做的所有工作(而控制台应用程序则始终将所有内容保留在内存中)。
我们需要在每次提供页面时都进行完整的 **目录/容器/组合(catalog/container/compose)** 操作。而且这很昂贵... 如前所述,MEF 依赖反射来完成其魔力,而反射嘛... 很昂贵。
应用程序可以帮忙 - 对吗?有,也有否... 因为这会引发并发(或 MEF 中的 Parts Lifetime)的问题 - 看这里。
更不用说,我们可能需要访问(动态)控件(用户控件和服务器控件)、母版页等等...
因此,我们需要将 **目录/容器/组合** 部分钩入 aspnet 页面生命周期。而将所有这些结合起来,就变得有些棘手了:看这里。
这是可行的,但开始会有点吓人 :-)
历史
- 2011.03.11:添加了“今天的宿主是”部分