ok,好像很久很久没有写博客了,
emmmm,一个是没时间,另一个,应该是感觉自己知道的太少了吧,不敢写了
原本是打算写一个系列,结果发现很多地方自己都是有些不够的,所以就一直放着了,
这次趁着国庆,补上一篇吧,算是一个小工具的实例,文末会提供源码下载。
就是不知道大佬们有没有遇到过这种情况啊,
可能有时候要批量处理一些东西,可能是文件,可能是数据,总之就是处理量非常大,
正常人吧,要嘛是分发给很多人处理,要嘛就是一个人哭唧唧的弄上好几天,
容易出现错漏不说,一旦要求改了,ok,重新来过吧,
我们程序员就不一样了,不会偷懒的程序员不是一个好的死肥宅,
噼里啪啦写好一个小工具,然后就可以喝茶了,
待处理的文件或者数据,无论是一千还是一万,对我们来说只是一个数字而已,
哪怕你要求改了,ok,我工具改一下,同样可以施施然的跑去休息。
我应为工作原因,经常写一些工具代码,
看同事写工具代码的话,他们一般都是新建一个控制台程序,
然后要么写类,要么写方法,在Main中调用就好,
这个时候,问题就来了:
可能一个Main里面,全是被注释的其他工具调用代码,时间长了谁也不知道这些是干啥的,
用方法去区分工具的话,一个工具往往可能衍生出多个方法,调用的寻找都极不方便,
新建控制台项目的话,花销太大,很多方法可以重复使用,复制来复制去也是相当难以管理,
用类区分倒是不错,不过,调用起来也是麻烦,得先去Main中注释掉其他工具,只留下待执行的工具,
如果要执行多个工具,还得停下来去修改Main,体验感同样极不友好,
比如说这样,上面的代码全是以前写的工具,真正要执行的是81行的方法,
可以想象,长此以往,这里谁看到了都要头疼,
为了方便自己,所以抽空做了一个小项目,用于管理这些小工具,看图说话,
先说说思路吧,倒是蛮简单,就是反射,
先定义一个父类BaseFun,所有封装的小工具类都继承它,
1 public class TestClass : BaseFun2 {3 4 }
另外有三个参数公用
1 ///2 /// 获取当前程序集 3 /// 4 Assembly _assembly = Assembly.GetExecutingAssembly(); 5 6 ///7 /// 当前选择的类,此处不应这样写,仅作参考 8 /// 9 Type selType = _assembly.GetType(cmb_Class.SelectedValue.ToString());10 11 ///12 /// 当前选择的方法,此处不应这样写,仅作参考13 /// 14 MethodInfo selMethod = selType.GetMethod(cmb_Fun.SelectedValue.ToString());
然后通过反射找到所有父类是BaseFun的类,加载至第一个下拉框里面,
这里简单用到了反射和委托,不熟的童鞋可以多瞅几遍,大佬勿喷,
1 ///2 /// 窗体加载时执行 3 /// 4 /// 5 /// 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),// 父类是BaseFun16 c => new ComBoxItem() { Display = c.Name, Value = c.FullName });17 }18 19 ///20 /// 绑定下拉框选项21 /// 22 ///23 /// 待绑定的下拉框24 /// 数据集25 /// 过滤条件,委托26 /// 返回下拉项,委托27 public void BindCom (ComboBox cmb, ICollection dataList, Func funcWhere, Func func)28 {29 List list = new List ();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 /// 51 /// 下拉框选项52 /// 53 public class ComBoxItem54 {55 ///56 /// 值57 /// 58 public string Value { get; set; }59 ///60 /// 文本61 /// 62 public string Display { get; set; }63 }
然后就是去找每个类里面的方法了,这里我考虑到可能会要求弹框提示一下运行结束,或者展示一些运行信息什么的,
所以就很果断的限定了返回值,只有当返回值为Result类型的时候,它才会去绑定到第二个下拉框中,话说这个Result类的命名好像不太好,啧,再说吧
1 ///2 /// 选项更改时执行,重新绑定方法列表 3 /// 4 /// 5 /// 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 ///22 /// 返回结果23 /// 24 public class Result25 {26 ///27 /// 消息28 /// 29 public string Msg { get; set; }30 31 ///32 /// 运行时间,ms33 /// 34 public long RunTime { get; set; }35 36 }
找到了方法之后,就应该开始绑定参数了,
1 ///2 /// 选项更改时执行,重新绑定参数列表 3 /// 4 /// 5 /// 6 private void cmb_Fun_SelectedIndexChanged(object sender, EventArgs e) 7 { 8 selMethod = selType.GetMethod(cmb_Fun.SelectedValue.ToString()); 9 BindPara();10 }11 ///12 /// 绑定参数列表13 /// 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 SkinLabel33 {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 SkinTextBox45 {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 SkinLabel56 {57 Location = new Point(x, y + 2),58 TextAlign = ContentAlignment.MiddleLeft,59 Size = new Size(70, 20),60 Text = item.ParameterType.Name61 };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,写好寻找代码就可以了
////// 返回注释信息 /// /// 名称 /// 参数 ///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 ///2 /// 按钮单击时执行,执行选中的指定方法 3 /// 4 /// 5 /// 6 private void bt_Exec_Click(object sender, EventArgs e) 7 { 8 List
最后要注意的是写这些工具方法入口的规则,只要返回值为Resule,就能找到,加载,然后运行,
但同时我也提供了一个更好的入口,
内部更多的实现就不展示了,我会提供源码下载地址,大概思路便是如此,
public Result Fun1() { // 推荐写法,自动计算方法运行时间,自动拼装日志路径,自动记录每一次的执行 // logPath:日志文件路径 return RunFun((logPath) => { // 写入日志文件 base.WriteLog(logPath, "lalal"); // 方法运行结束后,在弹出的对话框中展示 Res.Msg += logPath; return Res; }); } ////// 运行 /// /// ///public Result RunFun(Func 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; }
还有很多想做的啊,
比如说默认值这个玩意儿我不知道应该如何绑定到文本框里,
现在还没做链接数据库,
我还想记录每一次运行的参数,弄个下拉框,选一下就可以直接绑定一起曾经输入过的参数,这样也是很方便的,
以后再慢慢加吧,欢迎大佬们指出不足之处,
码云地址: