.Net中通过反射技术的应用----插件程序的开发入门
转载:http://www.cnblogs.com/winloa/archive/2012/03/25/2416355.html
再开始之前,先学习基本基本的概念.
程序集:所有.Net类都是定义在某个Assembly(程序集)中的,.Net基本类是定义在mscorlib.dll中。exe也可以看做是类库,也可以引用。.net的exe也是Assembly,
.net中的exe和dll的区别就是exe中包含入口函数,其他没有区别,exe也可以当成dll那样引用、也可以反编译。
GAC:全局程序集缓存。公用的Assembly放到GAC中,我们新建一个项目,会发现引用的程序集,如system,找不到这个dll放在哪里,实质上系统已注册到全局GAC中
程序集包含描述它们自己的内部版本号和它们包含的所有数据和对象类型的详细信息的元数据。
程序集具有以下特点: 程序集作为 .exe 或 .dll 文件实现。 通过将程序集放在全局程序集缓存中,可在多个应用程序之间共享程序集。 要将程序集包含在全局程序集缓存中,必须对程序集进行强命名。
有关更多信息,请参见具有强名称的程序集。
程序集仅在需要时才加载到内存中。 可以使用反射以编程方式获取关于程序集的信息。
如果加载程序集的目的只是对其进行检查,应使用诸如 ReflectionOnlyLoadFrom 的方法。 可以在单个应用程序中使用相同程序集的两个版本
OK,编写以下代码
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
static void Main(string[] args)
{
//获取当前程序已经加载的所有程序集
System.Reflection.Assembly [] arry= AppDomain.CurrentDomain.GetAssemblies();
foreach (var item in arry)
{
string str = item.FullName;
Console.WriteLine(str.Substring(0,str.IndexOf(',')));
}
Console.Read();
}
运行结果:
上述是获取程序已经加载的程序集,如何动态从文件加载Assembly,不需要在编译的时候引用该文件。
首先新建一个类库项目 CLib
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
namespace CLib
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void Say()
{
Console.WriteLine("{0}{1}岁",Name,Age);
}
}
}
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
namespace CLib
{
interface IFace
{
void ShowPic();
}
}
编译CLib项目,将CLib.dll拷贝到D盘根目录下.
编写以下代码,获取CLib.dll中的信息
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
System.Reflection.Assembly asm=System.Reflection.Assembly.LoadFile(@"D:\CLib.dll");
//获取程序集中所有的数据类型
Type[] typeArray = asm.GetTypes();
foreach (var item in typeArray)
{
Console.WriteLine(item.Name);
}
程序运行结果:
到这里已经能够从文件加载Assembly,并且能获取到Assembly中的数据类型,那么下一步可以如何调用CLib中Person类中的Say()方法呢?
编写以下代码:
标注出来的4个方法,是因为Person中的Name属性和Age属性的get;set;编译而成。这个可以通过反编译工具进行查看.
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
System.Reflection.Assembly asm=System.Reflection.Assembly.LoadFile(@"D:\CLib.dll");
//获取程序集中所有的数据类型
Type[] typeArray = asm.GetTypes();
foreach (var item in typeArray)
{
Console.WriteLine(item.FullName);
}
//这里的参数是完全限定名称.
Type typePerson = asm.GetType("CLib.Person");
if (typePerson != null)
{
//获取类型中所有的成员(属性+方法)
System.Reflection.MemberInfo [] memberArray= typePerson.GetMembers();
foreach (var item in memberArray)
{
Console.WriteLine(item.Name);
}
//获取say方法
System.Reflection.MethodInfo methodSay = typePerson.GetMethod("Say");
//程式中并没有添加CLib的引用,所以这里不能通过直接new的方式来创建Person的实例
object objPerson = Activator.CreateInstance(typePerson);
//调用Say方法,第一个参数表述say方法是哪个对象的,第二个参数是这个方法的参数
methodSay.Invoke(objPerson, null);
}
Console.Read();
到这里,已经能够从文件加载Assembly,并且调用其方法,再介绍几个方法
Type类可以叫做“类的类”,一个类型对应一个Type类的对象,通过Type对象可以获得类的所有的定义信息,比如类有哪些属性、哪些方法等。Type就是对类的描述。
获得Type对象的方法:
1、通过类获得Type:Type t = typeof(Person)
2、通过对象获得类的Type:Type t = p.GetType()
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
typeArray= asm.GetExportedTypes();
在CLib项目中,新增一个类Chinese,继承与Person类
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
public class Chinese:Person
{
public void HouseholdRegister()
{
Console.WriteLine("景德镇的镇户口超牛13。");
}
}
在AssemblyDemo中编写
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
Assembly asm = Assembly.LoadFile(@"D:\CLib.dll");
Type typePerson = asm.GetType("CLib.Person");
Type typeChinese = asm.GetType("CLib.Chinese");
#region IsAssignableFrom 判断是否是"父子关系",这里可以是类,也可以是接口
//判断能否把typeChinese赋值给typePerson
typePerson.IsAssignableFrom(typeChinese);
typeChinese.IsAssignableFrom(typePerson);
#endregion
#region IsInstanceOfType 判断对象是否是某类型,当前类可以是o的类、父类、接口
//创建Person类型
object obj = Activator.CreateInstance(typePerson);
//判断obj是typePerson类型
typePerson.IsInstanceOfType(obj);
#endregion
#region IsSubclassOf(c) 判断调用者是否是c的子类
typePerson.IsSubclassOf(typeChinese);
#endregion
到这里,学习和复习了简单的反射,有了这些技术点,就可以开始插件程序的编写。
插件程序实现功能如下:
开发一个简单的“记事本”程序,此程序的功能可通过插件进行扩展.
我们现在开发这一款“记事本”程序,并不知道将来以后别人会开发出怎样的功能,所以主程序开发者需要确定几个固定的方法,以后的插件开发者必须在插件中提供这几个方法给主程序使用。用什么方式来实现这个呢?---接口。so.先定义好接口
新建一个类库项目IEditorPro,在IEditorPro中新增接口
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
namespace IEditorPro
{
interface IEditor
{
/// <summary>
/// 插件名称
/// </summary>
string Name
{
get;
set;
}
/// <summary>
/// 插件执行程式,为了简单起见,这里直接把记事本的内容传递过来
/// </summary>
void Execute(TextBox txt);
}
}
新建一个Winform项目Notepad,在主窗体上拖一个TextBox和一个菜单栏
在Notepad新建Addins文件夹,规定用于存放插件,程序在启动时,检测Addins文件夹下的插件,如果有,则进行加载
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
private void FrmMain_Load(object sender, EventArgs e)
{
string dir = Assembly.GetEntryAssembly().Location;
dir = Path.GetDirectoryName(dir);
dir = Path.Combine(dir, "Addins");
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
//扫描下所有的dll文件
string[] dlls = Directory.GetFiles(dir, "*.dll");
foreach (var dllName in dlls)
{
Assembly asm = Assembly.LoadFile(dllName);
Type[] typeArray = asm.GetExportedTypes();
foreach (var item in typeArray)
{
Type typeIEditor=typeof(IEditor);
//必须是实现了IEditor接口,并且不能是抽象类
if (typeIEditor.IsAssignableFrom(item)
&& !item.IsAbstract)
{
IEditor editor=Activator.CreateInstance(item) as IEditor;
if (editor != null)
{
ToolStripItem tsmiItem= tsmiTools.DropDown.Items.Add(editor.Name);
//在这里传递editor。在Click事件中调用
tsmiItem.Tag = editor;
tsmiItem.Click += new EventHandler(tsmiItem_Click);
}
}
}
}
}
void tsmiItem_Click(object sender, EventArgs e)
{
ToolStripItem item = sender as ToolStripItem;
IEditor editor = item.Tag as IEditor;
editor.Execute(this.txtValue);
}
主程序就开发完成了,此时运行程序,可见工具栏下无任何功能.
下面,我们开始为这个程序开发新的插件.首先来开发一个转换大小写的插件.
新建一个类库项目PluginToUpper,添加IEditorPro.dll的引用,新建一个类ChangeToUpper,实现接口IEditor
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
public class ChangeToUpper:IEditor
{
public string Name
{
get
{
return "大小写转换";
}
set
{
}
}
public void Execute(TextBox txt)
{
txt.Text = txt.Text.ToUpper();
}
}
编译PluginToUpper项目,将PluginToUpper.dll拷贝到Notepad的Addins文件夹下.
再次运行Notepad时,插件已经OK,并且可以正常使用
再编写一个稍微复杂的插件,改变字体和字号的插件
新建一个类库项目,PluginToChangeStyle,,添加IEditorPro.dll的引用,新建一个类ChangeStyle,实现接口IEditor
因为我们要弹出窗体,让用户选择字体和字号,so.. 在PluginToChangeStyle新增一个窗口
用户选择字体和字号以后,通过委托方式来传递用户选择的字体和字号
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
namespace PluginToChangeStyle
{
public delegate void SetFontDelegate(string family,float size);
public partial class FrmSetFont : Form
{
public SetFontDelegate setFont;
public FrmSetFont()
{
InitializeComponent();
}
private void FrmSetFont_Load(object sender, EventArgs e)
{
//获取计算机中所有的安装字体
InstalledFontCollection fc = new InstalledFontCollection();
foreach (FontFamily font in fc.Families)
{
this.cbFontFamily.Items.Add(font.Name);
}
}
private void button1_Click(object sender, EventArgs e)
{
string family = cbFontFamily.Text;
float size = float.Parse(cbFontSize.Text);
setFont(family, size);
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
}
}
ChangeStyle类的代码:
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
namespace PluginToChangeStyle
{
public class ChangeStyle:IEditor
{
public string Name
{
get
{
return "修改字体";
}
set
{
}
}
private TextBox txtBox;
public void Execute(TextBox txt)
{
txtBox = txt;
FrmSetFont frm = new FrmSetFont();
//通过委托方式在两个窗体之间传递内容
frm.setFont = SetTextBoxFont;
frm.ShowDialog();
}
/// <summary>
/// 设置字体、字号
/// </summary>
/// <param name="family"></param>
/// <param name="size"></param>
public void SetTextBoxFont(string family, float size)
{
txtBox.Font = new System.Drawing.Font(family, size);
}
}
}
编译PluginToChangeStyle项目,将PluginToChangeStyle.dll拷贝Notepad的Addins文件夹下.
再次运行Notepad时,我们编写的改变字体字号插件已经OK,并且可以正常使用
相关的代码下载:
http://115.com/file/ani80ym1#插件Demo.zip
代码写得很粗糙,主要是方便初学者的理解.主程序在扫描插件的时候,还需要考虑很多因素,也可以使用linq方式来获取.
这个插件还可以应用于MVC,实现补丁式开发.主要原理是将注册视图的代码分离,实现无缝补丁升级模式.
有空再再写.