寫在前面的話
對於大神,Winform這種“古董玩具”,實在沒太多“技術性”可言了,然而『好用才是王道』,本文不以技術為賣點,純屬經驗之談,歡迎交流拍磚
朴素版UI


開發初衷
由於本人所在公司不定時需要開發各種OA、數據處理小工具,需求各式各樣,雜七雜八,有臨時性需求開發的,有長期使用且要不定時更新的,功能一般只有一兩個。又因應用不通用,所以不利於統一整合到某單一系統中,如此導致個別使用者電腦里裝了玲琅滿目的“小程序”。
隨着應用數目的增加,維護管理變得越來越棘手[1]。嘗試從網上下載過一兩個插件框架來用,使用起來雖不是很理想但也湊合[2]。后來某用戶提出想要“可實時卸載、加載插件”的需求時,改造那些框架就變得很麻煩,所以干脆自己開發一個。經過幾個版本的迭代,運行穩定,代碼也變得簡潔了。到現在也使用了好一段時間,代碼也給重構了一番,所以拿出來和大家分享下。
設計與實現

框架簡單明了,主體功能就在插件管理器上。插件是UserControl格式,采用.Net的反射機制進行加載。
如此設計,出於兩個目的:
1)插件功能高內聚,與框架低耦合。開發人員根據規范[3]開發並測試好后,直接接入框架即可。也可單獨編譯成單一程序
2)方便將原來的應用通過簡單改造變成插件加載到框架中
插件加載流程

主代碼
/// <summary>
/// 加載PlugIns插件目錄下的dll
/// </summary>
public static List<UserControlBase> GetPlugIns()
{
List<UserControlBase> lUc = new List<UserControlBase>();
foreach (var dllFile in Directory.GetFiles(PlugInsDir))
{
FileInfo fi = new FileInfo(dllFile);
if (!fi.Name.EndsWith(".dll")) continue;
foreach (var _uc in CreatePluginInstance(fi.FullName))
{
if (_uc != null)
{
lUc.Add(_uc);
}
}
}
return lUc;
}
/// <summary>
/// 根據全名和路徑構造對象
/// </summary>
/// <param name="sFilePath">程序集路徑</param>
/// <returns></returns>
public static List<UserControlBase> CreatePluginInstance(string sFilePath, Type hostType = null)
{
List<UserControlBase> lUc = new List<UserControlBase>();
try
{
lUc = CreateInstance(sFilePath, new string[] { "Uc" }, hostType);
}
catch (Exception ex)
{
Console.WriteLine("CreateInstance: " + ex.Message);
}
return lUc;
}
/// <summary>
/// 反射創建實例
/// </summary>
/// <param name="sFilePath"></param>
/// <param name="typeFeature"></param>
/// <param name="hostType"></param>
/// <param name="dynamicLoad"></param>
/// <returns></returns>
public static List<UserControlBase> CreateInstance(string sFilePath, string[] typeFeature, Type hostType = null, bool dynamicLoad = true)
{
var lUc = new List<UserControlBase>();
Assembly assemblyObj = null;
if (!dynamicLoad)
{
#region 方法一:直接從DLL路徑加載
assemblyObj = Assembly.LoadFrom(sFilePath);
#endregion
}
else
{
#region 方法二:先把DLL加載到內存,再從內存中加載(可在程序運行時動態更新dll文件,比借助AppDomain方便多了!)
using (FileStream fs = new FileStream(sFilePath, FileMode.Open, FileAccess.Read))
{
using (BinaryReader br = new BinaryReader(fs))
{
byte[] bFile = br.ReadBytes((int)fs.Length);
br.Close();
fs.Close();
assemblyObj = Assembly.Load(bFile);
}
}
#endregion
}
if (assemblyObj != null)
{
#region 讀取dll內的所有類,生成實例(這樣可省去提供 命名空間 的步驟)
// 程序集(命名空間)中的各種類
foreach (Type type in assemblyObj.GetTypes())
{
try
{
if (type.ToString().Contains("<>")) continue;
if (typeFeature != null)
{
bool invalidInstance = true;
foreach (var tf in typeFeature)
{
if (type.ToString().Contains(tf))
{
invalidInstance = false;
break;
}
}
if (invalidInstance) continue;
}
var uc = (UserControlBase)assemblyObj.CreateInstance(type.ToString()); //反射創建
lUc.Add(uc);
if (hostType != null)
{
AssemblyInfoHelper aih = new AssemblyInfoHelper(hostType);
}
}
catch (InvalidCastException icex)
{
Console.WriteLine(icex);
}
catch (Exception ex)
{
throw new Exception("Create " + sFilePath + "(" + type.ToString() + ") occur " + ex.GetType().Name + ":\r\n" + ex.Message + (ex.InnerException != null ? "(" + ex.InnerException.Message + ")" : ""));
}
}
#endregion
}
return lUc;
}
/// <summary>
/// 加載插件
/// </summary>
void LoadPlugIns()
{
// 整理UI
tvPlugins.Nodes.Clear();
lPlugIn.Clear();
dicLoadedUCs.Clear();
#region 逐一加載UC
string[] DllFiles = Directory.GetFiles(LoadPlugInManager.PlugInsDir);
string dllFile = "";
for (int f = 0; f < DllFiles.Length; f++)
{
dllFile = DllFiles[f];
FileInfo fi = new FileInfo(dllFile);
if (!fi.Name.EndsWith(".dll")) continue;
ThreadHelper.RunInAdditionalThread(new DlgtVoidMethod(() =>
{
// 該部分在另一線程中完成,所以不會卡住當前窗體
foreach (var uc in CreatePluginInstance(fi.FullName, this.GetType()))
{
if (uc != null)
{
// 保存到已加載UC字典
if (!dicLoadedUCs.ContainsKey(uc.UCName))
{
dicLoadedUCs.Add(uc.UCName, new List<UserControlBase>());
dicLoadedUCs[uc.UCName].Add(uc);
lPlugIn.Add(uc);
// 這里通知窗體線程,加載到插件樹控件中(供用戶點擊選擇相應控件)
ThreadHelper.RunInAdditionalThread(new DlgtVoidMethod_withParam((Object obj) =>
{
UserControlBase _uc = obj as UserControlBase;
TreeNode _tn_ = null;
foreach (TreeNode n in tvPlugins.Nodes)
{
if (n.Text == _uc.UCTpye)
{
_tn_ = n;
break;
}
}
if (_tn_ == null)
{
_tn_ = new TreeNode(_uc.UCTpye);
tvPlugins.Nodes.Add(_tn_);
}
TreeNode _n_ = new TreeNode(_uc.UCName);
_n_.ToolTipText = _uc.Recommend;
_tn_.Nodes.Add(_n_);
tvPlugins.ExpandAll();
Log("App", "成功加載:" + _uc.UCName);
})
, uc
, new DlgtVoidMethod_withParam(delegate (Object oEx)
{
MessageBox.Show((oEx as Exception).Message);
})
, tvPlugins);
}
}
}
})
, new DlgtVoidMethod_withParam(delegate (Object oEx)
{
MessageBox.Show((oEx as Exception).Message);
}));
}
#endregion
}
這里,最重要的插件“熱拔插”功能,就是使用CreateInstance中方法二來將dll加載到內存,然后再進行實例化,如此,dll文件在程序加載插件完畢后,就可完美“脫身”,又可在程序運行時,重新加載(指定dll)。
用戶在使用本地應用時,往往想要有比Web應用更“順滑”的操作預期,比如點擊后的實時響應性、信息反饋、進度顯示、程序不要被卡死等,所以在功能滿足需求的前提下,照顧用戶使用感受,也是開發人員需要多注意的(用戶反饋好,說不定就有褒獎哦~)。
謝謝閱讀~
*[1] 較早開發的程序,通用功能沒有封裝;通用功能封裝好后,有改動,又要一個一個程序更新等
*[2] 網上下載的框架存在冗余功能、代碼,或者對某一業務針對性太強,需要進行改造
*[3] 插件需集成自PlugInProgram.UserControlBase,類名以Uc開頭——UcXXXX,使用抽象類中的ucName字段給插件命名
附錄:
【主源碼】
『插件示例』
