上一篇《分享在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 ,如需转载请自行联系原作者