上一篇《分享在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下來用,當然其實模塊化插件編程還有其它的細節,比如:各模塊組件的更新,各模塊組件的安全性問題等,這些大家有興趣也可以研究一下,本文若有不足,歡迎指出,謝謝!
