上篇介绍了如何利用反射实现动态调用Dll,学会这个技术我们就不怕软件大而臃肿了,无论多复杂的系统我们可以把它们模块化,技术简单用处很大。然而Demo是简单的应用是复杂的,我们的WinForm应用平台要考虑的事情很多,不但要考虑功能还要考虑容错性。下面几个问题是我们首先要解决的:
1、Dll配置信息的存放,是放在配置文件中还是数据库中?
2、配置Dll的时候能否通过选择Dll文件的方式来完成,类名输入要求比较精确,手动输入容易出错,能否提供选择输入的方式?
3、Dll可能由水平层次不一的人来编写,很难保证谁的Dll不会出现致命的错误,有的程序员做了友好处理,有的没有,如何避免弹出.Net自带的错误对话框,能否提供一个统一的错误捕获机制,来处理错误提示?
4、Dll的窗体打开方式,是鼠标单击执行还是双击执行,是在新窗口中打开还是在原有窗口中打开,是模态还是非模态,如何避免重复?
解决这四个问题我们的应用平台雏形基本可以完成了。本篇我们一一解决这些问题。
问题1、Dll配置信息的存放
作为大型的应用系统最好使用数据库来管理数据,所以我们把Dll的配置信息存放到数据库中。在数据库中建一张配置表。
DllFileName | Dll文件名,默认放到主程序目录下下 |
DllClassName | 引用的类名,带命名空间 |
FormName | 窗体的名称 |
BlankWindows | 是否在新窗口中打开 |
MouseIsClick | 是否是单击执行,否则双击执行 |
SDIWindows | SDI类型的窗体是否模态显示 |
DllMethodName | 方法名,调用窗体就不调用方法,这里只能一个是有效的 |
NodeType | 应用平台左侧是导航树,需要指明节点调用的是窗体还是功能 |
下一步建一个Dll配置的界面,如下图:
问题2、选择输入类信息
如果手动输入dll文件名或者类名很容易出错,我们提供一种选择输入的方式,基本上从软件设计角度来讲容易出错的信息都通过选择来输入。这里我们用的技术还是反射调用,从程序集中取出类名和方法名,.Net提供了这样的方法,可以取到所有的类信息和方法,不管是静态方法还是动态方法。
我们把上一篇的例子稍微改动一下,在TestDll中增加一个窗体Form2,这个窗体是一个内部窗体不需要对外调用的,Form1是需要外部调用的。下面的代码可以把TestDll.dll中的类取出来。
/// <summary> /// 获取程序集中的类信息 /// </summary> /// <param name="AssemblyFileName">全路径的程序集文件名称</param> /// <returns>类列表</returns> public static List<string> GetAssemblyClassList(string AssemblyFileName) { List<string> result = new List<string>(); if (File.Exists(AssemblyFileName) == false) { return result; } Assembly myAss = Assembly.LoadFrom(AssemblyFileName); //加载程序集文件 Type[] types = myAss.GetTypes();//得到程序集中的所有类型 foreach (Type each in types) { if (each.IsClass == false) continue;//判断是否是类,如果不是类过滤掉 result.Add(each.FullName); } return result; }
运行后得到类信息,如下图:
下面的代码返回对应类中的方法名:
/// <summary> /// 获取程序集中对应类的方法 /// </summary> /// <param name="AssemblyFileName">全路径的程序集文件名称</param> /// <param name="className">类的全名</param> /// <returns>方法名列表</returns> public static List<string> GetAssemblyMethodList(string AssemblyFileName, string className) { List<string> result = new List<string>(); if (File.Exists(AssemblyFileName) == false) { return result; } Assembly myAss = Assembly.LoadFrom(AssemblyFileName); //获取程序集显示名称 Type[] types = myAss.GetTypes(); foreach (Type each in types) { if (each.IsClass == false) continue; if (each.FullName == className) { MethodInfo[] myMethods = each.GetMethods(); foreach (MethodInfo m in myMethods) { if (m.IsPublic)//如果是公共方法 result.Add(m.Name); } } } return result; }
运行后得到类中的方法,如下图:
现在可以实现选择输入了,是不是功能提高了不少。但是问题又出来了,我们看到有很多我们不想看到的类名和方法名,Net提供的过滤手段比较有限,我试了几个判断方法,是否是public,好像public的方法也很多,包括基类的都显示出来了,虽然可以过滤掉父类的方法,但是也不能明确的显示我们指定的。有没有办法只显示我们指定输出的类名和方法呢?这样我们就不会在那么多类型中查找了,只要思想不滑坡,办法总比困难多。我们可以利用自定义属性,在要输出的类或者方法上增加一个自定义属性,在获取这些信息的时候判断是否有自定义属性,如果有就输出如果没有就不输出,这样不就可以了吗。我们来试试看,首先定义一个输出的自定义属性,代码如下:
public class OutMethodsAttribute : Attribute { protected string _name; public string Name { get { return this._name; } } public OutMethodsAttribute(string name) { _name = name; } }
然后在要输出类上加上这个自定义属性。把Form1窗体代码修改如下:
namespace TestDll { [OutMethods("Form1")] public partial class Form1 : Form { string _userName = ""; public Form1() { InitializeComponent(); } public Form1(string userName) { InitializeComponent(); _userName = userName; label1.Text = _userName; } private void button1_Click(object sender, EventArgs e) { MessageBox.Show(_userName ",调用Ok了!"); } [OutMethods("Add")] public int Add(int a, int b) { return a b; } } }
我们再来修改获取类信息的方法和获取方法的代码,代码如下:
/// <summary> /// 获取程序集中的类信息 /// </summary> /// <param name="AssemblyFileName">全路径的程序集文件名称</param> /// <returns>类列表</returns> public static List<string> GetAssemblyClassList(string AssemblyFileName) { List<string> result = new List<string>(); if (File.Exists(AssemblyFileName) == false) { return result; } Assembly myAss = Assembly.LoadFrom(AssemblyFileName); //加载程序集文件 Type[] types = myAss.GetTypes();//得到程序集中的所有类型 object[] objClass = null; foreach (Type each in types) { if (each.IsClass == false) continue;//判断是否是类,如果不是类过滤掉 objClass = each.GetCustomAttributes(true);//获取自定属性 foreach (Attribute clAttribute in objClass) { if (clAttribute is OutMethodsAttribute)//如果有输出标记 { result.Add(each.FullName); } } } return result; } /// <summary> /// 获取程序集中有输出标记类的方法 /// </summary> /// <param name="AssemblyFileName">全路径的程序集文件名称</param> /// <param name="className">类的全名</param> /// <returns>方法名列表</returns> public static List<string> GetAssemblyMethodList(string AssemblyFileName, string className) { OutMethodsAttribute outAttribute; List<string> result = new List<string>(); if (File.Exists(AssemblyFileName) == false) { return result; } Assembly myAss = Assembly.LoadFrom(AssemblyFileName); //获取程序集显示名称 Type[] types = myAss.GetTypes();//得到程序集中的所有类型 object[] objMethod = null; foreach (Type each in types) { if (each.IsClass == false) continue; if (each.FullName == className) { MethodInfo[] myMethods = each.GetMethods(); foreach (MethodInfo m in myMethods) { objMethod = m.GetCustomAttributes(true);//获取自定属性 foreach (Attribute attr in objMethod) { if (attr is OutMethodsAttribute)//如果有输出标记 { outAttribute = (attr as OutMethodsAttribute); result.Add(outAttribute.Name); } } } } } return result; }
运行修改后的代码,果然只显示了我们定义输出标志的信息,如下图:
这样就太完美了,这是我想到的过滤方式,不知道大家有没有更好的方法。
问题3、应用平台统一的错误捕获机制
我们先来让Test.dll触发一个异常,我们不做任何处理。下面的代码会抛出一个异常。
private void button2_Click(object sender, EventArgs e)
{
throw new Exception("触发一个异常!");
}
弹出的界面如下图:
这种界面是很不友好的,如果一个系统运行中弹出这样的界面可以说是很大的失败。现实中每个模块是有不同的程序员开发的,有的人代码习惯很好,做了完善的错误处理,有的人则不重视,所以很难保证抛出这种异常,那么我们作为一个平台怎么办呢。我们能否接管抛出的所有异常,由平台统一处理?还是那句话只要思想不滑坡办法总比困难多。Winform的应用程序可以提供一个单独的线程来捕获应用程序的所有异常,我们可以利用这个线程来处理。下面的代码就可以完成统一的异常处理:
using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using System.Threading; namespace WinAppDynamicDemo { static class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); ThreadExceptionHandler handler = new ThreadExceptionHandler();//捕获异常的线程 Application.ThreadException = new ThreadExceptionEventHandler(handler.Application_ThreadException);//为应用程序指定捕获异常的线程 Application.Run(new Form1()); } } /// /// 一个处理异常的线程类 /// internal class ThreadExceptionHandler { public void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { try { DialogResult result = ShowThreadExceptionDialog(e.Exception); if (result == DialogResult.Abort) Application.Exit(); } catch (Exception ex) { string errorStr = "未知错误!" ex.Message.ToString(); try { MessageBox.Show(errorStr, "出错了"); } finally { Application.Exit(); } } } /// /// 调用一个统一的错误处理窗口 /// private DialogResult ShowThreadExceptionDialog(Exception ex) { string InnerError = "";//前一个异常,便于进步一查找错误原因 if (ex.InnerException != null) { InnerError = ex.InnerException.Message; } else { InnerError = ex.Message; } string errorMessage = "错误信息:\n" ex.Message "\n错误原因:\n" InnerError "\n错误来源:\n" ex.Source "\n产生错误的方法:\n" ex.TargetSite "\n应用程序运行时类型:\n" ex.GetType(); fmShowError infoform = new fmShowError(errorMessage); return infoform.ShowDialog(); } } }
再次执行触发异常的代码,我们发现所有的异常都被我们统一的错误处理线程接管了,界面如下图:
这个功能有点类似WebForm中统一的错误处理页面。这里处理的不够美观,我们再把他处理的好看一些。下图是HF2.0中的错误处理截图:
问题4、窗体打开方式
对MDI类型的窗体我们防止重复打开,可以利用主窗体的MdiChildren属性,遍历所有打开的子窗体看是否已经存在,如果已经存在那么显示到最前面。检查窗体是否存在代码如下:
/// <summary> /// 根据子窗体名字判断窗体是否打开 /// </summary> /// <param name="ParentForm">主窗体</param> /// <param name="formtitle">子窗体的标题</param> /// <returns>窗体</returns> private System.Windows.Forms.Form FormExists(System.Windows.Forms.Form ParentForm,string formname) { System.Windows.Forms.Form bl=null; foreach(Form objForm in ParentForm.MdiChildren) { if (objForm.Name.Equals(formname)) { bl = objForm; break; } } return bl; }
至于单击打开还是双击执行,我们只要把代码放在鼠标单击事件或是双击事件里面就可以,这里不赘述了。
到此为止,我们的应用平台基本很完善了。下面有本篇demo的源码,这些代码是实现的核心,并不是一个完整的应用,读者可以自己去完善它。在该系列文章的结束我会提供一个完整的应用下载,HF1.0学习版的下载,这是一个完整的工作流应用平台,包含平台涉及到的所有部分。这里我们还是一步一步来,下一篇我们介绍工作流平台数据库访问层,这不是一个普通的访问层,也许你自己也写过数据库访问层,但是我们提供的这个不但支持多数据库,而且支持本地和远程访问,远程不是简单的链接远程数据库,而是通过wcf的方式链接远程数据库,而且我们使用了数据代理层,这样业务模块的丝毫不用变化,只要更换代理层就可以实现本地和远程访问的切换!
本篇demo下载地址: http://files.cnblogs.com/legweifang/WinAppDynamicDemo20120725.rar