小工具?不,這是小工具的集合!
ok,好像很久很久沒有寫部落格了,
emmmm,一個是沒時間,另一個,應該是感覺自己知道的太少了吧,不敢寫了
原本是打算寫一個系列,結果發現很多地方自己都是有些不夠的,所以就一直放著了,
這次趁著國慶,補上一篇吧,算是一個小工具的例項,文末會提供原始碼下載。
就是不知道大佬們有沒有遇到過這種情況啊,
可能有時候要批量處理一些東西,可能是檔案,可能是資料,總之就是處理量非常大,
正常人吧,要嘛是分發給很多人處理,要嘛就是一個人哭唧唧的弄上好幾天,
容易出現錯漏不說,一旦要求改了,ok,重新來過吧,
我們程式設計師就不一樣了,不會偷懶的程式設計師不是一個好的死肥宅,
噼裡啪啦寫好一個小工具,然後就可以喝茶了,
待處理的檔案或者資料,無論是一千還是一萬,對我們來說只是一個數字而已,
哪怕你要求改了,ok,我工具改一下,同樣可以施施然的跑去休息。
我應為工作原因,經常寫一些工具程式碼,
看同事寫工具程式碼的話,他們一般都是新建一個控制檯程式,
然後要麼寫類,要麼寫方法,在Main中呼叫就好,
這個時候,問題就來了:
可能一個Main裡面,全是被註釋的其他工具呼叫程式碼,時間長了誰也不知道這些是幹啥的,
用方法去區分工具的話,一個工具往往可能衍生出多個方法,呼叫的尋找都極不方便,
新建控制檯專案的話,花銷太大,很多方法可以重複使用,複製來複制去也是相當難以管理,
用類區分倒是不錯,不過,呼叫起來也是麻煩,得先去Main中註釋掉其他工具,只留下待執行的工具,
如果要執行多個工具,還得停下來去修改Main,體驗感同樣極不友好,
比如說這樣,上面的程式碼全是以前寫的工具,真正要執行的是81行的方法,
可以想象,長此以往,這裡誰看到了都要頭疼,
為了方便自己,所以抽空做了一個小專案,用於管理這些小工具,看圖說話,
先說說思路吧,倒是蠻簡單,就是反射,
先定義一個父類BaseFun,所有封裝的小工具類都繼承它,
1 public class TestClass : BaseFun 2 { 3 4 }
另外有三個引數公用
1 /// <summary>2 /// 獲取當前程式集 3 /// </summary> 4 Assembly _assembly = Assembly.GetExecutingAssembly(); 5 6 /// <summary> 7 /// 當前選擇的類,此處不應這樣寫,僅作參考 8 /// </summary> 9 Type selType = _assembly.GetType(cmb_Class.SelectedValue.ToString()); 10 11 /// <summary> 12 /// 當前選擇的方法,此處不應這樣寫,僅作參考 13 /// </summary> 14 MethodInfo selMethod = selType.GetMethod(cmb_Fun.SelectedValue.ToString());
然後通過反射找到所有父類是BaseFun的類,載入至第一個下拉框裡面,
這裡簡單用到了反射和委託,不熟的童鞋可以多瞅幾遍,大佬勿噴,
1 /// <summary> 2 /// 窗體載入時執行 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void F_Main_Load(object sender, EventArgs e) 7 { 11 // 繫結類的資訊 12 BindCom( 13 cmb_Class,// 待繫結的下拉框 14 _assembly.GetTypes(),// 獲取程式集中所有的類 15 c => c.BaseType == typeof(BaseFun),// 父類是BaseFun 16 c => new ComBoxItem() { Display = c.Name, Value = c.FullName }); 17 } 18 19 /// <summary> 20 /// 繫結下拉框選項 21 /// </summary> 22 /// <typeparam name="T"></typeparam> 23 /// <param name="cmb">待繫結的下拉框</param> 24 /// <param name="dataList">資料集</param> 25 /// <param name="funcWhere">過濾條件,委託</param> 26 /// <param name="func">返回下拉項,委託</param> 27 public void BindCom<T>(ComboBox cmb, ICollection<T> dataList, Func<T, bool> funcWhere, Func<T, ComBoxItem> func) 28 { 29 List<ComBoxItem> list = new List<ComBoxItem>(); 30 31 if (!dataList.HasItems()) return; 32 33 // 迴圈資料集 34 foreach (var item in dataList) 35 { 36 // 執行條件 37 if (funcWhere.Invoke(item)) 38 list.Add(func.Invoke(item)); 39 } 40 41 if (!list.HasItems()) return; 42 43 // 繫結資料集 44 ComBoxItem option = list[0]; 45 cmb.ValueMember = nameof(option.Value); 46 cmb.DisplayMember = nameof(option.Display); 47 cmb.DataSource = list; 48 } 49 50 /// <summary> 51 /// 下拉框選項 52 /// </summary> 53 public class ComBoxItem 54 { 55 /// <summary> 56 /// 值 57 /// </summary> 58 public string Value { get; set; } 59 /// <summary> 60 /// 文字 61 /// </summary> 62 public string Display { get; set; } 63 }
然後就是去找每個類裡面的方法了,這裡我考慮到可能會要求彈框提示一下執行結束,或者展示一些執行資訊什麼的,
所以就很果斷的限定了返回值,只有當返回值為Result型別的時候,它才會去繫結到第二個下拉框中,話說這個Result類的命名好像不太好,嘖,再說吧
1 /// <summary> 2 /// 選項更改時執行,重新繫結方法列表 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void cmb_Class_SelectedIndexChanged(object sender, EventArgs e) 7 { 9 // 獲取當前選擇的類 10 selType = _assembly.GetType(cmb_Class.SelectedValue.ToString()); 11 12 if (selType == null) return; 13 14 // 繫結方法的資訊 15 BindCom( 16 cmb_Fun, 17 selType.GetMethods(),// 返回類中所有公開方法 18 c => c.ReturnType == typeof(Result) && c.DeclaringType == selType,// 返回型別為Result,且是由當前類定義,而不是繼承自父類的方法 19 c => new ComBoxItem() { Display = c.Name, Value = c.Name }); 20 } 21 /// <summary> 22 /// 返回結果 23 /// </summary> 24 public class Result 25 { 26 /// <summary> 27 /// 訊息 28 /// </summary> 29 public string Msg { get; set; } 30 31 /// <summary> 32 /// 執行時間,ms 33 /// </summary> 34 public long RunTime { get; set; } 35 36 }
找到了方法之後,就應該開始繫結引數了,
1 /// <summary> 2 /// 選項更改時執行,重新繫結引數列表 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void cmb_Fun_SelectedIndexChanged(object sender, EventArgs e) 7 { 8 selMethod = selType.GetMethod(cmb_Fun.SelectedValue.ToString()); 9 BindPara(); 10 } 11 /// <summary> 12 /// 繫結引數列表 13 /// </summary> 14 private void BindPara() 15 { 16 // 清空所有控制元件 17 flp_Para.Controls.Clear(); 18 19 string name = $"M:{selType.FullName}.{selMethod.Name}"; 20 21 int y = 5; 22 23 if (selMethod.GetParameters().Length > 0)// 拼接尋找方法註釋的name屬性值 24 name += $"({string.Join(",", selMethod.GetParameters().Select(c => c.ParameterType.FullName))})"; 25 26 // 迴圈方法所需的所有引數 27 foreach (var item in selMethod.GetParameters()) 28 { 29 int x = 0; 30 31 // 載入引數 32 SkinLabel paraName = new SkinLabel 33 { 34 Location = new Point(x, y + 2), 35 TextAlign = ContentAlignment.MiddleRight, 36 Size = new Size(80, 20), 37 Text = item.Name + ":" 38 }; 39 paraName.MouseMove += Form_MouseDown; 40 41 x += paraName.Size.Width + 5; 42 43 // 載入文字框 44 SkinTextBox text = new SkinTextBox 45 { 46 Name = item.Name, 47 Size = new Size(150, 20), 48 Location = new Point(x, y), 49 WaterText = GetNote(name, item.Name)// 新增水印註釋 50 }; 51 52 x += text.Size.Width + 5; 53 54 // 載入引數型別 55 SkinLabel paraType = new SkinLabel 56 { 57 Location = new Point(x, y + 2), 58 TextAlign = ContentAlignment.MiddleLeft, 59 Size = new Size(70, 20), 60 Text = item.ParameterType.Name 61 }; 62 paraType.MouseMove += Form_MouseDown; 63 64 y += 27; 65 66 flp_Para.Controls.Add(paraName); 67 flp_Para.Controls.Add(text); 68 flp_Para.Controls.Add(paraType); 69 } 70 }
考慮到可讀性,所以我把引數的註釋也找了出來,繫結到文字框的水印中去了,
Winform自帶的文字框是沒有水印這個功能的,所以我用了第三方的水印控制元件CSkin,
那麼,這時候肯定有人問了,C#程式碼的註釋怎麼整,
程式碼編譯後的Dll裡面是沒有註釋的,所以反射也找不到註釋,總不能去讀.cs檔案吧,
其實簡單設定一下,VS就會自動幫我們生成一份註釋文件,
最後在bin\Debug目錄下,就會有一個XML註釋文件,我們直接讀取它就可以了,所有類和方法的節點都是member,寫好尋找程式碼就可以了
/// <summary> /// 返回註釋資訊 /// </summary> /// <param name="name">名稱</param> /// <param name="para">引數</param> /// <returns></returns> private string GetNote(string name, string para) { // 讀取XML XDocument document = XDocument.Load(_assembly.GetName().Name + ".xml"); // 根據name尋找節點 var item = document.Descendants("member").Where(c => c.Attribute("name").Value == name).FirstOrDefault(); if (item == null) return ""; // 若引數名稱為空 if (string.IsNullOrWhiteSpace(para)) return (item.Element("summary")?.Value + "").Replace("\n", "").Trim(); // 返回引數註釋 return (item.Elements("param").Where(c => c.Attribute("name").Value == para).FirstOrDefault()?.Value + "").Replace("\n", "").Trim(); }
到這基本方法都能找對了,引數也能加載出來,接下來就是執行方法了,
1 /// <summary> 2 /// 按鈕單擊時執行,執行選中的指定方法 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void bt_Exec_Click(object sender, EventArgs e) 7 { 8 List<object> list = new List<object>(); 9 10 // 迴圈方法的所有引數 11 foreach (var item in selMethod.GetParameters()) 12 { 13 // 尋找和引數名稱相同的控制元件 14 Control con = flp_Para.Controls.Find(item.Name, false).FirstOrDefault(); 15 object obj = null; 16 17 #region 引數校驗 18 19 if (con == null) 20 { 21 MessageBox.Show($"缺少引數:{item.Name}"); 22 return; 23 } 24 if (string.IsNullOrWhiteSpace(con.Text)) 25 { 26 MessageBox.Show($"{item.Name}:值為空"); 27 return; 28 } 29 30 try 31 { 32 obj = Convert.ChangeType(con.Text, item.ParameterType); 33 } 34 catch (Exception) 35 { 36 MessageBox.Show($"{item.Name}:型別錯誤 ({item.ParameterType.Name})"); 37 return; 38 } 39 40 #endregion 41 42 list.Add(obj); 43 44 } 45 46 Result res = (Result)selMethod.Invoke(_assembly.CreateInstance(selType.FullName), list.ToArray()); 47 48 if (cb_Log.Checked) 49 { 50 res.Msg += $"\n{selType.Name}\t{selMethod.Name}\t執行時間:{res.RunTime} ms\n"; 51 MessageBox.Show(res.Msg); 52 } 53 }
最後要注意的是寫這些工具方法入口的規則,只要返回值為Resule,就能找到,載入,然後執行,
但同時我也提供了一個更好的入口,
內部更多的實現就不展示了,我會提供原始碼下載地址,大概思路便是如此,
public Result Fun1() { // 推薦寫法,自動計算方法執行時間,自動拼裝日誌路徑,自動記錄每一次的執行 // logPath:日誌檔案路徑 return RunFun((logPath) => { // 寫入日誌檔案 base.WriteLog(logPath, "lalal"); // 方法執行結束後,在彈出的對話方塊中展示 Res.Msg += logPath; return Res; }); } /// <summary> /// 執行 /// </summary> /// <param name="func"></param> /// <returns></returns> public Result RunFun(Func<string, Result> func) { Res = new Result(); // 拼裝日誌檔案路徑 string logPath = LogStarPath + GetMethodName(2) + ".log"; WriteLog(logPath, "==========Star=========="); // 定時器 Stopwatch watch = new Stopwatch(); // 開始計時 watch.Start(); // 執行方法 func.Invoke(logPath); // 停止計時 watch.Stop(); // 返回執行時間 Res.RunTime = watch.ElapsedMilliseconds; WriteLog(logPath, "==========End ==========\t" + Res.RunTime + " ms\n"); return Res; }
還有很多想做的啊,
比如說預設值這個玩意兒我不知道應該如何繫結到文字框裡,
現在還沒做連結資料庫,
我還想記錄每一次執行的引數,弄個下拉框,選一下就可以直接繫結一起曾經輸入過的引數,這樣也是很方便的,
以後再慢慢加吧,歡迎大佬們指出不足之處,