上一篇《分享在winform下實現模塊化插件編程》已經實現了模塊化編程,但我認為不夠完美,存在以下幾個問題: 1.IAppContext中的CreatePlugInForm方法只能依據完整的窗體類型名稱formTypeName來動態創建窗體對象,調用不夠方便,且該方法創建的窗體不受各模塊註冊窗體類型 ...
上一篇《分享在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下來用,當然其實模塊化插件編程還有其它的細節,比如:各模塊組件的更新,各模塊組件的安全性問題等,這些大家有興趣也可以研究一下,本文若有不足,歡迎指出,謝謝!