小工具?不,这是小工具的集合!
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; }
还有很多想做的啊,
比如说默认值这个玩意儿我不知道应该如何绑定到文本框里,
现在还没做链接数据库,
我还想记录每一次运行的参数,弄个下拉框,选一下就可以直接绑定一起曾经输入过的参数,这样也是很方便的,
以后再慢慢加吧,欢迎大佬们指出不足之处,
码云地址:https://gitee.com/StepDest/FunctionAction