關於C#外掛程式設計和外掛宿主資料傳遞的一些方法
最近做了一個C#的外掛式程式設計的專案,涉及到了在winform下外掛和宿主的資料傳遞。希望實現外掛主動向宿主傳遞資料而不是宿主通過執行一個方法去索要資料。
具體實現是Form1為宿主,在textbox裡輸入字串可以傳到外掛裡。
另外是可以在外掛裡通過滑鼠繪圖,單擊確定將圖傳遞到宿主。
考慮到外掛程式設計,必然涉及到介面,這裡我定義介面
namespace interclass { public delegate void ChangeDataHandler(List<List<Point>> ptlists); //定義委託 public interface inter { DataTransfer DataTransfer { get; set; } string WhoAmI(DataTransfer DT); //傳遞DataTransfer進入外掛同時獲得外掛的名字 void Action(object sender, EventArgs e); //外掛程式入口 List<List<Point>> PointLists { get; } event ChangeDataHandler ChangeData; //定義事件 } public class DataTransfer { private string testdata; public string Testdata { get => testdata; set => testdata = value; } } }
參考跨執行緒和跨窗體傳遞資料的相關資料,通過委託可以實現外掛向宿主主動傳遞。
在宿主外掛中新增動態選單,繫結單擊事件到Action,以開始外掛的執行。
下面是宿主窗體載入外掛的程式碼,包含動態選單的操作。
private List<inter> _plugIns = new List<inter>(); //外掛列表
int _countPlugAdded = 0; //外掛成功載入計數
int _countPlugAddError = 0; //外掛載入失敗計數
DataTransfer MainDT; //資料傳遞的例項
private void Form1_Load(object sender, EventArgs e) { List<string> list = new List<string>(); 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<List<Point>> 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<List<Point>> _myPointLists = new List<List<Point>>();
記錄滑鼠的位置並依次連線。若擡起滑鼠則另外新建一個表。實現在外掛裡的繪圖的儲存。
外掛裡實現介面:
namespace Plugin3
{
public class Class1 : inter
{
private DataTransfer myDT;
public DataTransfer DataTransfer { get => myDT; set => myDT = value; }
private List<List<Point>> _pointLists;
public List<List<Point>> 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<List<Point>>();
myDT = DT;
return "JustForFun";
}
private void picChanged(List<List<Point>> ptlists)
{
_pointLists.Clear();
foreach (List<Point> pl in ptlists)
{
_pointLists.Add(new List<Point>());
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<List<Point>> ptlist); //定義委託
public partial class FormE : Form
{
bool _mouseLeftDown = false;
List<List<Point>> _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事件的發生和其他操作由外掛定義,這也就是前面第一個只接收宿主資料不用實現這個事件的原因。宿主可以不關心外掛是否要向宿主傳遞資料,何時傳遞資料。