最近做了一个C#的插件式编程的项目,涉及到了在winform下插件和宿主的数据传递。希望实现插件主动向宿主传递数据而不是宿主通过运行一个方法去索要数据。
具体实现是Form1为宿主,在textbox里输入字符串可以传到插件里。
另外是可以在插件里通过鼠标绘图,单击确定将图传递到宿主。
考虑到插件编程,必然涉及到接口,这里我定义接口
namespace interclass
{
public delegate void ChangeDataHandler(List> ptlists); //定义委托
public interface inter
{
DataTransfer DataTransfer { get; set; }
string WhoAmI(DataTransfer DT); //传递DataTransfer进入插件同时获得插件的名字
void Action(object sender, EventArgs e); //插件程序入口
List> PointLists { get; }
event ChangeDataHandler ChangeData; //定义事件
}
public class DataTransfer
{
private string testdata;
public string Testdata { get => testdata; set => testdata = value; }
}
}
参考跨线程和跨窗体传递数据的相关资料,通过委托可以实现插件向宿主主动传递。
在宿主插件中添加动态菜单,绑定单击事件到Action,以开始插件的运行。
下面是宿主窗体载入插件的代码,包含动态菜单的操作。
private List _plugIns = new List(); //插件列表
int _countPlugAdded = 0; //插件成功载入计数
int _countPlugAddError = 0; //插件加载失败计数
DataTransfer MainDT; //数据传递的实例
private void Form1_Load(object sender, EventArgs e)
{
List list = new List();
MainDT = new DataTransfer();
//读取插件
string path = System.IO.Path.Combine(System.Environment.CurrentDirectory + @"\dlls\");
DirectoryInfo dir = new DirectoryInfo(path);
FileInfo[] fil = dir.GetFiles();
foreach (FileInfo f in fil)
{
if (f.Extension.Equals(".dll"))
{
list.Add(f.FullName);
}
}
//载入插件
if (list.Count != 0)
{
foreach (string tp in list)
{
try
{
inter tempI;
Assembly asd = Assembly.LoadFile(tp);
Type[] t = asd.GetTypes();
foreach (Type tin in t)
{
if (tin.GetInterface("inter") != null)
{
tempI = (inter)Activator.CreateInstance(asd.GetType(tin.FullName));
_plugIns.Add(tempI);
tempI.ChangeData += new ChangeDataHandler(myDataChanged);
//创建菜单
ToolStripMenuItem newItem = new ToolStripMenuItem();
newItem.Text = tempI.WhoAmI(MainDT);
newItem.Click += tempI.Action;
((ToolStripMenuItem)(mainMenu.Items[1])).DropDownItems.Add(newItem);
//计数累加
_countPlugAdded++;
//break;
}
}
}
catch (Exception)
{
_countPlugAddError++;
}
}
}
else
{
ToolStripMenuItem newItem = new ToolStripMenuItem();
newItem.Text = "No Plug-In";
newItem.Enabled = false;
((ToolStripMenuItem)(mainMenu.Items[1])).DropDownItems.Add(newItem);
}
MessageBox.Show(_countPlugAdded.ToString() +
"个插件成功载入," + _countPlugAddError.ToString() +
"个插件加载失败。");
}
然后在插件里实现接口。首先说接收宿主数据的插件。
大致思路是传递DataTransfer实例进入插件(在载入插件并实例化的时候已经传递进去了),然后在插件里通过定时器扫描DataTransfer实例。当然也可以通过事件进行,我这里偷懒使用了定时器。
因为接口定义并没有和Form有关联,所以在Action,也就是插件程序的入口点,要实例化子窗体显示出来。
namespace plugin1
{
public class Class1 : inter
{
private DataTransfer dataTransfer;
public DataTransfer DataTransfer { get => dataTransfer; set => dataTransfer = value; }
public List> PointLists => throw new NotImplementedException();//因为这个插件不向宿主传递数据所以不实现这个。
public event ChangeDataHandler ChangeData;//因为这个插件不会产生向宿主传递数据所以不实现这个。
public void Action(object sender, EventArgs e)
{
MessageBox.Show("Plugin1");//测试用,表示在Class1里面,Action已经开始运行。
FormT formT = new FormT();//实例化窗体
formT.SetDT(dataTransfer);//SetDT是在FormT里面定义的方法,用于把DataTransfer传递进FormT里面。
formT.Show();//显示插件的子窗体
}
public string WhoAmI(DataTransfer DT)
{
dataTransfer = new DataTransfer();
dataTransfer = DT;//把宿主的DataTransfer传入插件
return "plugin1";//返回自己的名字
}
}
}
这样在宿主里单机菜单里的plugin1就可以运行这个插件的Action方法并显示子窗体。
在插件的窗体里面写:
namespace plugin1
{
public partial class FormT : Form
{
private int _counter = 0;
DataTransfer _dataTransfer;//传入数据用
public void SetDT(DataTransfer inputDT) => _dataTransfer = inputDT;
public FormT()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)//通过定时器扫描DataTransfer实现宿主数据的同步显示。
{
this.Text = "这是一个插件创建的窗体,由插件控制。" + _counter.ToString();
_counter++;
Monitor.Enter(_dataTransfer);
label1.Text = _dataTransfer.Testdata;
Monitor.Pulse(_dataTransfer);
Monitor.Exit(_dataTransfer);
}
private void FormT_Load(object sender, EventArgs e)
{
this.Text = "这是一个插件创建的窗体,由插件控制。" + _counter.ToString();
timer1.Enabled = true;
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(_dataTransfer.Testdata,"我是插件创建的窗体");//通过点击按钮实现数据的传入。
}
}
}
这里我尝试了两种方法, 一个是通过定时器定时查看,另外是借助button_click实现对DataTransfer扫描。
宿主运行接口定义的方法确实可以实现读取插件的信息,但是若插件需要主动传递数据到宿主这样便不好用。假设现在需要在插件里通过鼠标绘图,单击按钮将这个图传递到宿主里进行显示。
需要在宿主里定义一个成员
List> _myPointLists = new List>();
记录鼠标的位置并依次连线。若抬起鼠标则另外新建一个表。实现在插件里的绘图的保存。
插件里实现接口:
namespace Plugin3
{
public class Class1 : inter
{
private DataTransfer myDT;
public DataTransfer DataTransfer { get => myDT; set => myDT = value; }
private List> _pointLists;
public List> PointLists { get => _pointLists; }
public event ChangeDataHandler ChangeData;
public void Action(object sender, EventArgs e)
{
FormE formE = new FormE();
formE.ChangePic += new ChangePicHandler(picChanged);
formE.Show();
}
public string WhoAmI(DataTransfer DT)
{
_pointLists = new List>();
myDT = DT;
return "JustForFun";
}
private void picChanged(List> ptlists)
{
_pointLists.Clear();
foreach (List pl in ptlists)
{
_pointLists.Add(new List());
foreach (Point p in pl)
{
_pointLists[_pointLists.Count-1].Add(new Point());
_pointLists[_pointLists.Count - 1][_pointLists[_pointLists.Count - 1].Count-1] = p;
}
}
ChangeData?.Invoke(_pointLists);//执行委托实例 将数据从Class1传递到宿主
}
}
}
和之前一样,同样使用Action作为入口,在Action中将formE里面的ChangePic事件加上picChanged事件处理。
因为我们是在formE里面绘图,而不是在Class1中,所以我们要定义另外一个事件,也就是ChangePic来将数据从窗体传递到Class1中。这个和跨窗体传递数据的方法相同。
最后执行ChangeData委托实例。接口定义了ChangeData事件,在上面的宿主加载插件的代码中,通过
tempI = (inter)Activator.CreateInstance(asd.GetType(tin.FullName));
_plugIns.Add(tempI);
tempI.ChangeData += new ChangeDataHandler(myDataChanged);
实现了ChangeData事件和myDataChanged的事件处理。其中myDataChanged处理方法由宿主定义,而事件的发生由插件定义,实现了宿主和插件的分离。
在formE中,除了绘图的相关方法外,在单击“确定”按钮中产生了ChangePic事件
namespace Plugin3
{
public delegate void ChangePicHandler(List> ptlist); //定义委托
public partial class FormE : Form
{
bool _mouseLeftDown = false;
List> _drawPoints;
int _drawCount = 0;
int pointNumber = 0;
Point myMousePosition;
public event ChangePicHandler ChangePic; //定义事件
public FormE()
{
InitializeComponent();
}
public void SetForm()
{
}
private void FormE_Load(object sender, EventArgs e)
{
}
private void FormE_MouseDown(object sender, MouseEventArgs e)
{
}
private void FormE_MouseUp(object sender, MouseEventArgs e)
{
}
private void FormE_MouseMove(object sender, MouseEventArgs e)
{
}
private void FormE_Paint(object sender, PaintEventArgs e)
{
}
private void FormE_FormClosed(object sender, FormClosedEventArgs e)
{
}
//Clear
private void button1_Click(object sender, EventArgs e)
{
_drawPoints.Clear();
GC.Collect();
_drawCount = 0;
pointNumber = 0;
Text = "点数:" + pointNumber.ToString();
Invalidate();
}
private void button2_Click(object sender, EventArgs e)
{
ChangePic?.Invoke(_drawPoints);//执行委托实例 将_drawPoints传递到上一级的Class1中
}
}
}
FormE中的_drawPoints即是随鼠标移动不断记录当前鼠标位置产生的列表。用于记录在插件窗体里面的绘图。
由此便通过两个事件将数据主动传递到了宿主中。这个过程里,只有宿主通过接口定义的ChangeData事件和宿主内实现的这个事件的处理方法是由宿主定义,至于DataChanged事件的发生和其他操作由插件定义,这也就是前面第一个只接收宿主数据不用实现这个事件的原因。宿主可以不关心插件是否要向宿主传递数据,何时传递数据。