分享在winform下实现模块化插件编程-优化版

简介:

上一篇《分享在winform下实现模块化插件编程》已经实现了模块化编程,但我认为不够完美,存在以下几个问题:

1.IAppContext中的CreatePlugInForm方法只能依据完整的窗体类型名称formTypeName来动态创建窗体对象,调用不够方便,且该方法创建的窗体不受各模块注册窗体类型AppFormTypes限制,也就是可以创建任何FORM,存在不确定性;

2.动态创建的窗体对象无法直接对其公共属性或公共方法进行调用

3.主应用程序中的LoadComponents方法是通过指定文件夹对所有的DLL文件全部进行获取然后再进行TYPE解析最终才找到实现了ICompoentConfig的类,这个过程比较繁锁效率低下;

4.编译后的应用程序根目录混乱,许多的DLL都与主应用程序EXE在一起;

 下面就针对上述问题进行一一解决。

1.为IAppContext增加几个CreatePlugInForm的扩展方法,同时AppContext实现这几个方法,代码如下:

IAppContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/// <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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/// <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(动态执行公共方法(含公共静态方法)),弥补动态创建的窗体无法对公共成员进行操作的问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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配置节点如下:

若需实现从CONFIG文件配置,那么就需要增加自定义节点配置,如:compoents,当然如果为能省事也可以直接用appSettings节点,要增加自定义节点配置,就需要定义与自定义节点相关的类,具体的实现方式,百度搜索一下就知道了,我这里也直接给出一个参考地址:http://www.cnblogs.com/lichaoliu/archive/2010/11/03/1868245.html,如下是我实现的compoents节点相关的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/// <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; }
     }
}

最终实现的配置示例如下:

1
2
3
4
5
6
7
8
<? 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方法,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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,如下:

1
2
3
4
5
<runtime>
     <assemblyBinding xmlns= "urn:schemas-microsoft-com:asm.v1" >
     <probing privatePath= "Libs" />
      </assemblyBinding>
</runtime>

而我这里采用另一种方法:通过订阅AssemblyResolve事件(该事件是加载程序失败时触发)然后在订阅的事件中动态加载缺失的程序集来实现的,好处是安全,不用担心路径被改造成程序无法正常运行的情况,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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下来用,当然其实模块化插件编程还有其它的细节,比如:各模块组件的更新,各模块组件的安全性问题等,这些大家有兴趣也可以研究一下,本文若有不足,欢迎指出,谢谢!

本文转自 梦在旅途 博客园博客,原文链接:http://www.cnblogs.com/zuowj/p/5383672.html  ,如需转载请自行联系原作者

相关文章
|
3月前
|
存储 JavaScript 前端开发
第四章 模块和组件、模块化和组件化的理解
第四章 模块和组件、模块化和组件化的理解
|
4月前
|
设计模式
二十三种设计模式全面解析-桥接模式的高级应用:构建灵活的跨平台UI框架
二十三种设计模式全面解析-桥接模式的高级应用:构建灵活的跨平台UI框架
|
5月前
|
监控 数据可视化 前端开发
一个.NetCore前后端分离、模块化、插件式的通用框架
一个.NetCore前后端分离、模块化、插件式的通用框架
102 0
|
前端开发 JavaScript 开发者
封装库/工具库中重要概念之UI框架
UI(User Interface)框架是前端开发中十分重要的一部分,它提供了各种组件和样式,用于构建页面和用户界面。在前端开发中,封装库/工具库可以帮助我们更加高效地使用 UI 框架。
147 0
|
开发框架 Oracle 关系型数据库
C/S架构Winform插件化框架,Winform通用界面框架
插件化框架特点: 1. 开发框架以模块化形式在逻辑上解耦 2. 开发框架模块以动态链接库(DLL文件)形式独立部署。 3. 模块主界面(frmBaseModule)用来分割系统功能菜单与功能按钮,作为各模块的入口界面。 4. 插件化框架核心功能-动态加载模块技术。
2059 0
|
图形学
如何理解Unity组件化开发模式
Unity的开发模式核心:节点和组件,组件可以加载到任何节点上,每个组件都有 gameobject 属性,可以通过这个属性获取到该节点,即游戏物体。 也就是说游戏物体由节点和组件构成,每个组件表示物体的一种特性(能力)。
2404 0
|
存储 Java Android开发
Android插件化开发之动态加载技术学习
Android插件化开发之动态加载技术学习 为什么要插件化开发和动态加载呢?我认为原因有三点: 可以实现解耦 可以解除单个dex函数不能超过65535的限制 可以给apk瘦身,比如说360安全卫士,整个安装包才13.
2082 0