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

MEF 与 ASP.NET - "Hello World!"

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (18投票s)

2011年3月11日

CPOL

12分钟阅读

viewsIcon

67567

downloadIcon

1218

基础的 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")仅仅是 strings - 它们不是类型!
这样,即使每个 [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

Interfaces 在这个上下文和其他上下文中通常被称为“契约”。这是插件签名(通过实现 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:添加了“今天的宿主是”部分
© . All rights reserved.