分享在winform下实现模块化插件编程-优化版
上一篇《分享在winform下实现模块化插件编程》已经实现了模块化编程,但我认为不够完美,存在以下几个问题:
1.IAppContext中的CreatePlugInForm方法只能依据完整的窗体类型名称formTypeName来动态创建窗体对象,调用不够方便,且该方法创建的窗体不受各模块注册窗体类型AppFormTypes限制,也就是可以创建任何FORM,存在不确定性;
2.动态创建的窗体对象无法直接对其公共属性或公共方法进行调用
3.主应用程序中的LoadComponents方法是通过指定文件夹对所有的DLL文件全部进行获取然后再进行TYPE解析最终才找到实现了ICompoentConfig的类,这个过程比较繁锁效率低下;
4.编译后的应用程序根目录混乱,许多的DLL都与主应用程序EXE在一起;
下面就针对上述问题进行一一解决。
1.为IAppContext增加几个CreatePlugInForm的扩展方法,同时AppContext实现这几个方法,代码如下:
IAppContext:
/// <summary> /// 应用程序上下文对象接口 /// 作用:用于收集应用程序必备的一些公共信息并共享给整个应用程序所有模块使用(含动态加载进来的组件) /// 作者:Zuowenjun /// 2016-3-26 /// </summary> public interface IAppContext { /// <summary> /// 应用程序名称 /// </summary> string AppName { get; } /// <summary> /// 应用程序版本 /// </summary> string AppVersion { get; } /// <summary> /// 用户登录信息 /// </summary> object SessionUserInfo { get; } /// <summary> /// 用户登录权限信息 /// </summary> object PermissionInfo { get; } /// <summary> /// 应用程序全局缓存,整个应用程序(含动态加载的组件)均可进行读写访问 /// </summary> ConcurrentDictionary<string, object> AppCache { get; } /// <summary> /// 应用程序主界面窗体,各组件中可以订阅或获取主界面的相关信息 /// </summary> Form AppFormContainer { get; } /// <summary> /// 动态创建在注册列表中的插件窗体实例 /// </summary> /// <param name="formType"></param> /// <returns></returns> Form CreatePlugInForm(Type formType,params object[] args); /// <summary> /// 动态创建在注册列表中的插件窗体实例 /// </summary> /// <param name="formTypeName"></param> /// <returns></returns> Form CreatePlugInForm(string formTypeName, params object[] args); /// <summary> /// 动态创建在注册列表中的插件窗体实例 /// </summary> /// <param name="formTypeName"></param> /// <returns></returns> Form CreatePlugInForm<TForm>(params object[] args) where TForm : Form; }
AppContext:
/// <summary> /// 应用程序上下文对象类 /// 作者:Zuowenjun /// 2016-3-26 /// </summary> public class AppContext : IAppContext { internal static AppContext Current; internal Dictionary<string, Type> AppFormTypes { get; set; } public string AppName { get; private set; } public string AppVersion { get; private set; } public object SessionUserInfo { get; private set; } public object PermissionInfo { get; private set; } public ConcurrentDictionary<string, object> AppCache { get; private set; } public System.Windows.Forms.Form AppFormContainer { get; private set; } public AppContext(string appName, string appVersion, object sessionUserInfo, object permissionInfo, Form appFormContainer) { this.AppName = appName; this.AppVersion = appVersion; this.SessionUserInfo = sessionUserInfo; this.PermissionInfo = permissionInfo; this.AppCache = new ConcurrentDictionary<string, object>(); this.AppFormContainer = appFormContainer; } public System.Windows.Forms.Form CreatePlugInForm(Type formType, params object[] args) { if (this.AppFormTypes.ContainsValue(formType)) { return Activator.CreateInstance(formType, args) as Form; } else { throw new ArgumentOutOfRangeException(string.Format("该窗体类型{0}不在任何一个模块组件窗体类型注册列表中!", formType.FullName), "formType"); } } public System.Windows.Forms.Form CreatePlugInForm(string formTypeName, params object[] args) { if (!formTypeName.Contains('.')) { formTypeName = "." + formTypeName; } var formTypes = this.AppFormTypes.Where(t => t.Key.EndsWith(formTypeName, StringComparison.OrdinalIgnoreCase)).ToArray(); if (formTypes == null || formTypes.Length != 1) { throw new ArgumentException(string.Format("从窗体类型注册列表中未能找到与【{0}】相匹配的唯一窗体类型!", formTypeName), "formTypeName"); } return CreatePlugInForm(formTypes[0].Value, args); } public Form CreatePlugInForm<TForm>(params object[] args) where TForm : Form { return CreatePlugInForm(typeof(TForm), args); } }
从AppContext类中可以看出,CreatePlugInForm方法有三个重载,分别支持依据TYPE、泛型、模糊类型名来动态创建窗体对象,同时若窗体类型含有参的构造函数,那么后面的args参数数组赋值即可。
2.为Form类型增加三个扩展方法,分别是:SetPublicPropertyValue(动态给公共属性赋值)、GetPublicPropertyValue(动态获取公共属性的值)、ExecutePublicMethod(动态执行公共方法(含公共静态方法)),弥补动态创建的窗体无法对公共成员进行操作的问题,代码如下:
public static class FormExtension { /// <summary> /// 动态给公共属性赋值 /// </summary> /// <param name="form"></param> /// <param name="propertyName"></param> /// <param name="propertyValue"></param> public static void SetPublicPropertyValue(this Form form, string propertyName, object propertyValue) { var formType = form.GetType(); var property = formType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (property != null) { property.SetValue(form, propertyValue, null); } else { throw new Exception(string.Format("没有找到名称为:{0}的公共属性成员!", propertyName)); } } /// <summary> /// 动态获取公共属性的值 /// </summary> /// <typeparam name="TResult"></typeparam> /// <param name="form"></param> /// <param name="propertyName"></param> /// <param name="defaultPropertyValue"></param> /// <returns></returns> public static TResult GetPublicPropertyValue<TResult>(this Form form, string propertyName, TResult defaultPropertyValue) { var formType = form.GetType(); var property = formType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); var proValue = property.GetValue(form, null); if (property != null) { try { return (TResult)Convert.ChangeType(proValue, typeof(TResult)); } catch { return defaultPropertyValue; } } else { throw new Exception(string.Format("没有找到名称为:{0}的公共属性成员!", propertyName)); } } /// <summary> /// 动态执行公共方法(含公共静态方法) /// </summary> /// <param name="form"></param> /// <param name="methodName"></param> /// <param name="args"></param> /// <returns></returns> public static object ExecutePublicMethod(this Form form, string methodName, params object[] args) { var formType = form.GetType(); var method = formType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); if (method != null) { return method.Invoke(form, args); } else { throw new Exception(string.Format("没有找到名称为:{0}且形数个数有:{1}个的公共方法成员!", methodName, args == null ? 0 : args.Count())); } } }
使用很简单就不再演示说明了。
3.动态加载符合条件的模块组件,之前的LoadComponents效率太低,而我这里想实现类似ASP.NET 的Handler或Module可以动态的从CONFIG文件中进行增减配置,ASP.NET 的Handler、Module配置节点如下:
<httpHandlers> <add path="eurl.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" /> <add path="trace.axd" verb="*" type="System.Web.Handlers.TraceHandler" validate="True" /> <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="True" /> <add verb="*" path="*_AppService.axd" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False" /> <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False"/> <add path="*.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" /> <add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="True" /> <add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="True" /> <add path="*.asmx" verb="*" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False" /> <add path="*.rem" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" /> <add path="*.soap" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" /> <add path="*.asax" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.ascx" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.master" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.skin" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.browser" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.sitemap" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.dll.config" verb="GET,HEAD" type="System.Web.StaticFileHandler" validate="True" /> <add path="*.exe.config" verb="GET,HEAD" type="System.Web.StaticFileHandler" validate="True" /> <add path="*.config" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.cs" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.csproj" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.vb" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.vbproj" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.webinfo" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.licx" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.resx" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.resources" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.mdb" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.vjsproj" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.java" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.jsl" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.ldb" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.ad" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.dd" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.ldd" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.sd" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.cd" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.adprototype" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.lddprototype" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.sdm" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.sdmDocument" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.mdf" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.ldf" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.exclude" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.refresh" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" /> <add path="*.svc" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False"/> <add path="*.rules" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*.xoml" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False"/> <add path="*.xamlx" verb="*" type="System.Xaml.Hosting.XamlHttpHandlerFactory, System.Xaml.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False"/> <add path="*.aspq" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*.cshtm" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*.cshtml" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*.vbhtm" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*.vbhtml" verb="*" type="System.Web.HttpForbiddenHandler" validate="True"/> <add path="*" verb="GET,HEAD,POST" type="System.Web.DefaultHttpHandler" validate="True" /> <add path="*" verb="*" type="System.Web.HttpMethodNotAllowedHandler" validate="True" /> </httpHandlers> <httpModules> <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" /> <add name="Session" type="System.Web.SessionState.SessionStateModule" /> <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" /> <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" /> <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" /> <add name="RoleManager" type="System.Web.Security.RoleManagerModule" /> <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" /> <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" /> <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" /> <add name="Profile" type="System.Web.Profile.ProfileModule" /> <add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <add name="ServiceModel" type="System.ServiceModel.Activation.HttpModule, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" /> <add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/> </httpModules>
若需实现从CONFIG文件配置,那么就需要增加自定义节点配置,如:compoents,当然如果为能省事也可以直接用appSettings节点,要增加自定义节点配置,就需要定义与自定义节点相关的类,具体的实现方式,百度搜索一下就知道了,我这里也直接给出一个参考地址:http://www.cnblogs.com/lichaoliu/archive/2010/11/03/1868245.html,如下是我实现的compoents节点相关的类:
/// <summary> /// 组件配置节点类 /// 作者:Zuowenjun /// 2016-3-30 /// </summary> public class CompoentConfigurationSection : ConfigurationSection { private static readonly ConfigurationProperty s_property = new ConfigurationProperty( string.Empty, typeof(ComponentCollection), null, ConfigurationPropertyOptions.IsDefaultCollection); [ConfigurationProperty("", Options = ConfigurationPropertyOptions.IsDefaultCollection)] public ComponentCollection Components { get { return (ComponentCollection)base[s_property]; } } [ConfigurationProperty("basePath", IsRequired = false)] public string BasePath { get { return ReMapBasePath(this["basePath"].ToString()); } set { this["basePath"] = ReMapBasePath(value); } } private string ReMapBasePath(string basePath) { if (basePath.Trim().StartsWith("~\\")) { basePath = basePath.Replace("~\\", AppDomain.CurrentDomain.BaseDirectory + "\\"); } return basePath; } } /// <summary> /// 组件配置集合类 /// 作者:Zuowenjun /// 2016-3-30 /// </summary> [ConfigurationCollection(typeof(ComponentElement))] public class ComponentCollection : ConfigurationElementCollection { public ComponentCollection():base(StringComparer.OrdinalIgnoreCase) { } protected override ConfigurationElement CreateNewElement() { return new ComponentElement(); } protected override object GetElementKey(ConfigurationElement element) { return (element as ComponentElement).FileName; } new public ComponentElement this[string fileName] { get { return (ComponentElement)base.BaseGet(fileName); } } public void Add(ComponentElement item) { this.BaseAdd(item); } public void Clear() { base.BaseClear(); } public void Remove(string fileName) { base.BaseRemove(fileName); } } /// <summary> /// 组件配置项类 /// 作者:Zuowenjun /// 2016-3-30 /// </summary> public class ComponentElement : ConfigurationElement { [ConfigurationProperty("fileName", IsRequired = true, IsKey = true)] public string FileName { get { return this["fileName"].ToString(); } set { this["fileName"] = value; } } [ConfigurationProperty("entryType", IsRequired = true)] public string EntryType { get { return this["entryType"].ToString(); } set { this["entryType"] = value; } } [ConfigurationProperty("sortNo", IsRequired = false, DefaultValue = 0)] public int SortNo { get { return Convert.ToInt32(this["sortNo"]); } set { this["sortNo"] = value; } } }
最终实现的配置示例如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="compoents" type="WMS.PlugIn.Framework.Configuration.CompoentConfigurationSection,WMS.PlugIn.Framework"/> </configSections> <compoents basePath="~\Libs\"> <add fileName="WMS.Com.CW.dll" entryType="WMS.Com.CW.CompoentConfig" sortNo="1" /> </compoents>
然后主应用程序这边改进LoadComponents方法,具体代码如下:
private void LoadComponents() { var compoents = ConfigurationManager.GetSection("compoents") as CompoentConfigurationSection; if (compoents == null) return; string basePath = compoents.BasePath; if (string.IsNullOrWhiteSpace(basePath)) { basePath = Program.AppLibsDir; } Type targetFormType = typeof(Form); foreach (ComponentElement item in compoents.Components) { string filePath = Path.Combine(basePath, item.FileName); var asy = Assembly.LoadFrom(filePath); var type = asy.GetType(item.EntryType, true); ICompoent compoent = null; var config = (ICompoentConfig)Activator.CreateInstance(type); config.CompoentRegister(AppContext.Current, out compoent);//关键点在这里,得到组件实例化后的compoent if (compoent != null) { foreach (Type formType in compoent.FormTypes)//将符合的窗体类型集合加到AppContext的AppFormTypes中 { if (targetFormType.IsAssignableFrom(formType) && !formType.IsAbstract) { AppContext.Current.AppFormTypes.Add(formType.FullName, formType); } } } } }
对比改进前后的LoadComponents方法,有没有觉得改进后的代码效率更高一些了,我认为效率高在避免了文件夹的扫描及类型的查询,改进后的方法都是通过配置文件的信息直接获取程序集及指定的类型信息。
4.改进了插件编程这块后,最后一个要解决的其实与插件编程无关,但因为我在项目中也同时进行了改进,所以也在此一并说明实现思路。
要想将引用的DLL放到指定的文件夹下,如:Libs,就需要了解程序集的寻找原理,具体了解请参见:C#开发奇技淫巧三:把dll放在不同的目录让你的程序更整洁,说白了只要设置或改变其私有目录privatePath,就能改变程序集加载时寻找的路径,网上大部份是采用如下配置的方式来修改privatePath,如下:
<runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="Libs"/> </assemblyBinding> </runtime>
而我这里采用另一种方法:通过订阅AssemblyResolve事件(该事件是加载程序失败时触发)然后在订阅的事件中动态加载缺失的程序集来实现的,好处是安全,不用担心路径被改造成程序无法正常运行的情况,实现代码如下:
static class Program { public static string AppLibsDir = null; /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main(string[] args) { AppLibsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Libs\"); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; AddEnvironmentPaths(AppLibsDir); } static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { Assembly assembly = null, objExecutingAssemblies = null; objExecutingAssemblies = Assembly.GetExecutingAssembly(); AssemblyName[] arrReferencedAssmbNames = objExecutingAssemblies.GetReferencedAssemblies(); foreach (AssemblyName assmblyName in arrReferencedAssmbNames) { if (assmblyName.FullName.Substring(0, assmblyName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(","))) { string path = System.IO.Path.Combine(AppLibsDir, args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll"); assembly = Assembly.LoadFrom(path); break; } } return assembly; } static void AddEnvironmentPaths(params string[] paths) { var path = new[] { Environment.GetEnvironmentVariable("PATH") ?? string.Empty }; string newPath = string.Join(Path.PathSeparator.ToString(), path.Concat(paths)); Environment.SetEnvironmentVariable("PATH", newPath); } }
里面包括一个动态增加环境路径的方法:AddEnvironmentPaths,其作用网上也讲过了,就是处理通过[DllImport]中的程序集的加载。
这样就完成了将引用的DLL放到指定的目录中:libs,当然在主应用程序引用DLL时,请将复制到本地设为False,这样编译后的程序根目录才会干净如你所愿。
以上就是本文的全部内容,代码也都已贴出来了,大家可以直接COPY下来用,当然其实模块化插件编程还有其它的细节,比如:各模块组件的更新,各模块组件的安全性问题等,这些大家有兴趣也可以研究一下,本文若有不足,欢迎指出,谢谢!