前一篇我们说到了如何利用应用程序域的相关技术实现热升级的目的。下面我来介绍另一种场景,如下图所示:
主程序仅提供作为MdiContainer的窗体框架,所有的功能都以单独的子窗体形式加载。每个子窗体对应的是一个单独的功能模块(dll文件)。
比如管理公司结构的时候,员工管理模块和部门管理模块就分别以单独的dll文件的形式加载到主窗体中,我们今天要做的就是对这样一个单独的子窗体功能模块进行升级。
我们先评估一下利用上一篇所说创建新应用程序域的方式能否实现。
首先,主程序Mdi窗体在默认应用程序域中启动,然后要显示某个功能界面的时候,通过创建应用程序域加载对应的功能模块。OK,这一步能够实现,如下效果:
不过,还没有达到我们想要的效果,我们想实现的是功能模块1这个窗体是作为应用程序框架这个窗体的子窗体显示的。还要继续做。
要达到上述效果,只要设置功能模块1的MdiParent属性为主程序窗体就行了。
不过问题就在这了,主窗体和子窗体分别属于不同的应用程序域!上一篇我们介绍过,不同应用程序域中的成员是密封的,任何程序域中都不能直接访问其他程序域中的成员!只能使用按引用封送或按值封送的技术,才能间接地访问。显然窗体对象不是值类型,如果要按引用封送的话,不管是把主窗体封送进子窗体所在程序域,还是把子窗体封送进入主窗体所在程序域,都必须保证一点,窗体类型必须继承自MarshalByRefObject,查看Form类的继承关系:
public class Form : ContainerControl
public class ContainerControl : ScrollableControl, IContainerControl
public class ScrollableControl : Control, IComponent, IDisposable
public class Control : Component, IDropTarget, ISynchronizeInvoke, IWin32Window, IBindableComponent, IComponent, IDisposable
public class Component : MarshalByRefObject, IComponent, IDisposable
最终“惊喜地”发现,Form是继承自MarshalByRefObject,那么就可以使用按引用封送的技术了?
可是,当我使用MemberwiseClone方法将主窗体对象封成MarshalByRefObject发送到子窗体所在程序域,并设置子窗体的MdiParent属性时,
运行却产生如下异常:
看来即使封送Form类,也只能传送诸如Name, Text之类的简单属性,像ControlCollection这样的并没有做可序列化实现。
既然使用新应用程序域无法实现,那就只能看在同一个应用程序域中能不能有办法实现。
在上一篇中也说过,无法在程序运行期间更新文件的根本原因,就是运行中的程序“霸占”着文件的句柄,直到卸载程序域或者退出进程的时候才被释放。那么只要在加载完模块后,同时使用某种方式释放句柄应该就可以了!
如果不需要使用AppDomain,那么一般我们加载程序集使用Assembly的相关静态方法,调用Assembly方法生成的对象就处在调用所在的域中,这样子窗体和主窗体对象处于同一个程序域中,也就很方便地设置子窗体的MdiParent属性了。
关于加载程序集的相关主要方法(略去重载方法)如下:
public static Assembly Load(AssemblyName assemblyRef); public static Assembly Load(byte[] rawAssembly); public static Assembly Load(string assemblyString); public static Assembly LoadFile(string path); public static Assembly LoadFrom(string assemblyFile);
观察这些方法发现,不管是assemblyRef,还是assemblyString, path, assemblyFile这些都跟文件名有关,通过调用测试发现,这些方法加载完文件后并没有释放文件句柄。
唯独方法
public static Assembly Load(byte[] rawAssembly);
加载的是一组字节流!调用这个方法,需要先把文件读入内存字节流,然后再从这个字节流加载,已经跟硬盘上的文件没有关系了,也就是说当文件被读入内存字节流中后,句柄会被释放,这个不就是我们希望的么!
OK,既然找到了这样一个方法,那么我们建一个解决方案来验证一下。
如下所示:
其中,Modules.xml作为配置文件,用来描述主程序需要加载的功能模块信息
<?xml version="1.0" encoding="utf-8" ?> <Modules> <Module name="Module1" file="Modules\\Module1.dll" interface_name="Module1.Loader" /> </Modules>
功能模块中有个简单的窗体
点击显示后,会弹出该模块的版本号,后面我们以这个消息判断是否升级成功。
主程序启动后,首先读取Modules.xml文件,在工具栏中生成对应的按钮,表示已发现对应功能模块
当点击按钮时加载并显示子窗体:
void ctl_Click(object sender, EventArgs e) { string name = ((ToolStripButton)sender).Text; Form frm = ModuleManager.LoadModule(name); frm.MdiParent = this; frm.Show(); }
public static Form LoadModule(string moduleName) { if (!_modules.ContainsKey(moduleName)) return null; ModuleInfo mInfo = _modules[moduleName]; if (mInfo.Frm_Module != null && !mInfo.Frm_Module.IsDisposed) return mInfo.Frm_Module;
byte[] bFile = File.ReadAllBytes(mInfo.File); Assembly amy = Assembly.Load(bFile); ILoader loader = (ILoader)amy.CreateInstance(mInfo.Interface_Name); Form frm = loader.LoadModule(); mInfo.Frm_Module = frm; return frm; }
通过File.ReadAllBytes方法将dll文件读入字节流,这时候文件句柄就已经被释放了,也就可以在运行中进行升级操作。
其中,ModuleInfo保存Modules.xml中读取的模块信息。
class ModuleInfo { string name; /// <summary> /// 模块唯一标识,也作为应用程序域的名称 /// </summary> public string Name { get { return name; } } string file; /// <summary> /// 模块对应的文件名 /// </summary> public string File { get { return file; } set { file = value; } } string interface_name; /// <summary> /// 用于程序框架加载模块的入口 /// </summary> public string Interface_Name { get { return interface_name; } set { interface_name = value; } } /// <summary> /// 功能模块窗体 /// </summary> public Form Frm_Module; public ModuleInfo(string name, string file, string interface_name) { this.name = name; this.file = file; this.interface_name = interface_name; } }
逻辑很简单,我们直接运行看一下
如何判断这时候磁盘上的文件和正在运行的子窗体没有关系呢,为了便于演示,我在功能模块1的窗体上再加一个按钮用于删除相应的dll文件。
再次运行,点击删除
删除成功了!说明没有问题,句柄已经被释放,这时候我们重新生成一下Module1,把版本修改为1.0.0.1
[assembly: AssemblyVersion("1.0.0.1")] [assembly: AssemblyFileVersion("1.0.0.1")]
关闭子窗体,(只是关闭子窗体,并不关闭主窗体),再次点击Module1按钮
加载成功,并且版本已经更改为最新的1.0.0.1!
至此,整个程序升级的工作完成。
完整的解决方案 点击下载
注:我在blog中使用的代码都是为了演示使用的精简的代码,并不适合拿来直接使用,只是希望大家能理解解决的方法,再以自己理解的方式实现。