构建可扩展的应用程序时,接口是中心。可用基类来代替接口,但接口通常是首选的,因为它允许加载项开发人员选择他们自己的基类。例如,假如你要写一个应用程序,它能无缝的加载和使用别人创建的类型。下面描述了如何设计这样的应用程序。
- 创建一个“宿主SDK”(Host SDK)程序集,它定义了一个接口,接口的方法作为宿主应用程序和加载项之间的通信机制使用。为接口方法定义参数和返回值时,尝试使用MSCorLib.dll中定义的其他接口或类型。如果要传递并返回自己定义的数据类型,也在宿主的SDK程序集中定义他们。一旦搞定接口定义,就可以为这个程序集赋予一个强命名,然后把它打包并部署到合作伙伴和用户那里。一旦发布就应避免对程序集做出任何重大的改变。例如,不要以任何方式更改接口。但是,如果定义了任何数据类型,那么在类型中添加新成员时可以的。如果对程序集进行了任何修改,可能需要使用一个发布者策略文件来部署它。
注意: 可以使用MSCorlib.dll中定义的类型,因为CLR总是加载与CLR版本匹配的那个版本的MSCorlib.dll。此外,在一个CLR实例中,只会加载一个版本的MSCorlib.dll。换言之,永远不会出现多个版本的MSCorlib.dll都加载的情况。最后的结果是,绝不会出现类型版本不匹配的情况。这还有助于减少应用程序对内存的需求。
- 当然,加载项开发人员会在他们自己的加载项程序集中定义自己的类型。他们的加载项程序集将引用你宿主程序集中的类型。加载项开发人员可以按他们的希望频率推出程序集的新版本,而宿主应用程序能够正确使用加载项的类型。不会出现任何问题。
- 创建一个单独的“宿主应用程序”程序集,在其中包含你应用程序的类型。这个程序集显然要引用“宿主SDK”程序集,并使用其中定义的类型。可自由修改“宿主应用程序”程序集中的代码。由于加载项开发人员不会引用这个“宿主应用程序”程序集,所以随时都能推出“宿主应用程序”程序集的版本,这样做并不会对加载项开发人员产生任何影响。
本节包含了一些非常重要的信息。跨程序集使用类型时,需要关注程序集的版本控制问题。要花一些时间精心构造,将用于跨程序集边界通信的类型隔离到他们自己的程序集中。要避免以后更改这些类型的定义。但是,如果真的要修改类型的定义,一定要修改程序集的版本号,并为新版本的程序集创建一个发布者策略文件。
下面看一个非常简单的例子,它综合运用了所有这些知识。首先是HostSDK程序集的代码:
namespace HostSDK.dll { public interface IAddIn { string DoSomething(Int32 x); } }
其次是AddInTypes.dll程序集中的代码,该程序集定义了两个公共类型,他们实现了HostSDK.dll的接口。为了生成这些程序集,必须引用HostSDK.dll程序集:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using HostSDK; namespace AddInTypes { public sealed class AddIn_A : IAddIn { public AddIn_A() { } public string DoSomething(Int32 x) { return "AddIn_A:" + x.ToString(); } } public sealed class AddIn_B : IAddIn { public AddIn_B() { } public string DoSomething(Int32 x) { return "AddIn_B:" + x.ToString(); } } }
然后就是一个简单的Host.exe程序集(一个控制台应用程序)的代码。为了生成这个程序集,它必须引用HostSDK.dll程序集。为了发现有哪些可用的加载项类型,以下宿主代码假定类型是在一个以dll文件扩展名的程序集中定义的,而且这个程序集已经部署到和宿主的EXE相同的目录中。Microsoft的“托管可扩展性框架”时在我刚才描素的各种机制的顶部构建的,它提供了加载项注册和发现机制,构建动态可扩展性应用程序时,强烈建议你研究一下MEF,它能简化本章描素的一些操作。
static void Main(string[] args) { String AddInDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); string[] AddInAssemblies = Directory.GetFiles(AddInDir, "*.dll"); List<Type> AddInTypes = new List<Type>(); foreach (String file in AddInAssemblies) { Assembly AddInAssembly = Assembly.LoadFrom(file); foreach (Type t in AddInAssembly.GetExportedTypes()) { //如果类型实现了IAddIn接口的一个类,该类就可以为宿主使用 if (t.IsClass && typeof(IAddIn).IsAssignableFrom(t)) { AddInTypes.Add(t); } } } foreach (Type t in AddInTypes) { IAddIn ai = (IAddIn)Activator.CreateInstance(t); Console.WriteLine(ai.DoSomething(5)); } }
这个简单的宿主和加载项例子没有用到AppDomain。但在实际应用程序中,可能在每个加载项自己的AppDomain中创建他们,每个AppDomain都有自己的安全性和配置设置。当然,如果将加载项从内存中移除,可以卸载相应的AppDomain。为了跨AppDomain边界通信,可告诉加载项开发人员从MashalByRefObject派生出他们自己的加载项类型。但是,另一个更常见的版本是让宿主应用程序定义自己的、从MashalByRefObject派生的内部类型。每个AppDomain创建好之后,宿主要在新的APPdomain中创建他们自己的MashalByRefObject派生类的实例。宿主的代码(位于默认AppDomain中)将和它自己的类型(位于其他AppDomain)通信,让后者载入加载项程序集,并创建和使用加载项程序集。