四,设计支持加载项的应用程序
构建可扩展应用程序时,接口是中心。可以利用基类来代替接口,但接口通常是首选,因为它允许加载项开发人员选择他们自己的基类。下面描述了如何构建这样的应用程序。
1,创建一个“宿主SDK”(Host SDK)程序集。
它定义一个接口,接口的方法作为宿主应用程序和加载项应用程序之间的通信机制使用。接口的参数和返回类型,尝试使用MSCorLib.dll中定义的类型或接口。如果要传递并返回自己定义的数据类型,也应该在这个“宿主SDK”程序集中定义。一旦搞定接口的定义,就可以为这个程序集赋一个强名称(版本控制),然后把它打包并部署到合作伙伴或用户那里。一旦发布就应避免对该程序集的做任何重大的改动。例如,不应以任何方式更改接口。如果定义了新的数据类型,对数据类型添加新的成员是可以的。如果对程序集做了任何修改,可能需要用一个发布者策略文件来部署它。(关于发布者策略文件可以参考程序集的部署相关文章)。
2,加载项开发人员在他们自己的程序集中定义自己的类型。
他们的加载项程序集引用了你的“宿主SDK”程序集中的类型。加载项开发人员可以按照他们的希望频率推出新版本,而宿主应用程序能正常的使用这些版本,不会出现任何问题。
3,创建一个单独的“宿主应用程序”(Host Application)程序集。
在其中包含你的应用程序的类型。显然,这个程序集要引用“宿主SDK”程序集,并使用其中定义的类型。
这样,加载方和宿主方都只依赖于“宿主SDK”这个程序集了。两方可以独立开发,发布,互不影响,实现了“解耦”。
下面来做一个简单的例子:
首选定义HostSDK.dll。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace HostSDK { public interface IAddIn { string DoSomething(Int32 x); } }
其次做加载项AddInTypes.dll
using System; using System.Collections.Generic; using System.Linq; using System.Text; using HostSDK; namespace AddInTypes { public sealed class AddIn_A : IAddIn { #region IAddIn public string DoSomething(int x) { return "AddIn_A: " + x.ToString(); } #endregion } public sealed class AddInB : IAddIn { #region IAddIn public string DoSomething(int x) { return "AddIn_B: " + x.ToString(); } #endregion } }
最后定义一个简单的宿主程序集Host.exe
private void ShowPlugin() { //假设所有Dll都放在exe同目录中 string directory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); string[] dllFiles = Directory.GetFiles(directory, "*.dll"); List<Type> types = new List<Type>(); //找出所有实现了IAddIn类型的成员 foreach (var file in dllFiles) { Assembly ass = Assembly.LoadFrom(file); foreach (Type t in ass.GetExportedTypes()) { if (t.IsClass && typeof(IAddIn).IsAssignableFrom(t)) { types.Add(t); } } } //调用加载类 foreach (var t in types) { IAddIn addIn = (IAddIn)Activator.CreateInstance(t); Console.WriteLine(addIn.DoSomething(5)); } }
4,关于程序集部署的几个值得注意的地方。
App.config配置节点:
<runtime> <disableCachingBindingFailures enabled="1" /> </runtime>
Assembly的Load或LoadFrom方法加载程序集有缓存,当第一次加载程序集后,不管是成功还是失败,下次都是直接取缓存。有时我们需要在加载失败的情况下(比如重新拷贝新的dll以解决失败的问题,但又不希望关闭当前的主程序)重新加载程序集,就需要将这个配置节点启用。
将dll加入到GAC中的命令是gacutil /i “程序集路径”;卸载的命令是gacutil /u “程序集名称”。另外gacutil有版本限制,vs2010建的工程默认选中了.net framework 4.0,如果你用2.0或3.5版本的gacutil就会失败。我机器上没有4.0的gacutil,所以只有将工程属性platform需要的.net版本调到3.5。这样就可以正常使用了。
5,版本策略文件的使用
先截一个图,在没有升级HostSDK.dll时,它的版本号是1.1.0.0
现在我们由于需求变更,需要对这个HostSDK进行修改,修改后的版本号定为1.2.0.0。为了不影响其它用户正常使用,我们对这个dll进行更新发布,需要使用发布者策略文件来配置它。来看看具体是怎么做的。
- 先定义一个发布者策略文件HostSDK.config,内容如下:
<?xml version="1.0"?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="HostSDK" publicKeyToken ="6f6864489080509e" cultrue="neutral"/> <bindingRedirect oldVersion="1.1.0.0" newVersion="1.2.0.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
- 然后将HostSDK.config,HostSDK.dll(1.2.0.0版),mykey.snk(签名文件)放到同一个目录,接着利用程序集连接器al.exe生成发布者策略程序集。
al /out:“C:\testAssembly\Policy.1.1.HostSDK.dll” /version:1.0.0.0 /keyfile:”C:\ testAssembly\mykey.snk” /linkresource:” C:\ testAssembly \ HostSDK.config”
最后会生成Policy.1.1.HostSDK.dll文件,注意其中的1.1,这个代表这个发布者策略程序集对应于major是1,minor是1的曾经发布过的HostSDK.dll文件的版本号,不能随意想写什么就写什么,否则后果就是找不到要应用策略的1.1版本的HostSDK.dll,策略应用会失败(其实我在这就摔了跟头,唉。。)
- 做完这一步,需要将Policy.1.1.HostSDK.dll安装到GAC中,命令如下:
gacutil /i “C:\ testAssembly \Policy.1.1.HostSDK.dll”
- 最后需要部署1.2.0.0版的HostSDK.dll文件,你可以将其加入到GAC中,但不是必须的。也可以直接将其拷贝到应用程序的基目录,但切记最好不要覆盖以前的dll,否则,升级后的程序如果有问题也不能“回滚”了。如果以前的dll就在基目录下,建议用codeBase指定新版本dll的存放路径。配置到codeBase指定的目录中,如下:
<?xml version="1.0"?> <configuration> <startup> <supportedRuntime version="v2.0.50727"/> </startup> <runtime> <disableCachingBindingFailures enabled="1" /> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="HostSDK" publicKeyToken ="6f6864489080509e" cultrue="neutral"/> <codeBase version="1.2.0.0" href="newDll\HostSDK.dll"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
结果显示,版本已经是1.2.0.0的了。
如果1.2.0.0版的dll出现问题,我们希望恢复到以前的dll,只需简单的修改一下当前应用程序的配置文件。
加上一句<publisherPolicy apply="no"/>,如下:
<?xml version="1.0"?> <configuration> <startup> <supportedRuntime version="v2.0.50727"/> </startup> <runtime> <disableCachingBindingFailures enabled="1" /> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <publisherPolicy apply="no"/> <assemblyIdentity name="HostSDK" publicKeyToken ="6f6864489080509e" cultrue="neutral"/> <codeBase version="1.2.0.0" href="newDll\HostSDK.dll"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
在实际应用中,可能需要将每一个加载项程序集加载到一个单独的AppDomain中,这样,卸载这个AppDomain就将加载项的对象移除了。如果需要跨AppDomain的跨边界访问,需要告诉加载项的开发人员从MarshalRefObject派生他们的加载项类型。此时的AddInTypes.dll可能是这样。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using HostSDK; using System.Runtime.Remoting; namespace AddInTypes { public sealed class AddIn_A : MarshalByRefObject, IAddIn { #region IAddIn public string DoSomething(int x) { return AppDomain.CurrentDomain.FriendlyName + " AddIn_A: " + x.ToString(); } #endregion } public sealed class AddInB : MarshalByRefObject, IAddIn { #region IAddIn public string DoSomething(int x) { return AppDomain.CurrentDomain.FriendlyName + " AddIn_B: " + x.ToString(); } #endregion } }
调用方类似于如下调用:
AppDomain ad2 = AppDomain.CreateDomain("ad #2"); foreach (var t in types) { ObjectHandle oh = Activator.CreateInstance(ad2, t.Assembly.FullName, t.FullName); IAddIn addIn = (IAddIn)oh.Unwrap(); msg += addIn.DoSomething(5) + Environment.NewLine; }
但这样的做法强迫了加载项开发方实现MarshalByRefObject类型。一种更常见的做法是宿主应用程序定义自己的从MarshalByRefObject派生的类型。宿主要在新的AppDomain中创建它自己的MarshalByRefObject派生类型,在这个派生类型里加载程序集,并创建加载项类型的实例。例子:
定义一个OperPlugIn类。
public class OperPlugIn : MarshalByRefObject { public string GetMsg() { string directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string[] dllFiles = Directory.GetFiles(directory, "*.dll"); List<Type> types = new List<Type>(); string msg = ""; //Find all the types inplement IAddIn foreach (var file in dllFiles) { Assembly ass = Assembly.LoadFrom(file); foreach (Type t in ass.GetExportedTypes()) { if (t.IsClass && typeof(IAddIn).IsAssignableFrom(t)) { types.Add(t); } } msg += ass.ToString() + Environment.NewLine; } msg += typeof(IAddIn).Assembly.ToString() + Environment.NewLine; //Disply all the Addin //add AppDomain foreach (var t in types) { IAddIn addIn = (IAddIn)Activator.CreateInstance(t); msg += addIn.DoSomething(5) + Environment.NewLine; } return msg; } }
调用这个包装类:
private void ShowPlugin() { AppDomain ad2 = AppDomain.CreateDomain("ad #2"); OperPlugIn MyHelper = (OperPlugIn)ad2.CreateInstanceAndUnwrap("Host", "Host.OperPlugIn"); MessageBox.Show(MyHelper.GetMsg()); }
结果如下:
可以看出代码都是在"ad #2"这个AppDomain中运行的。