小工具?不,这是小工具的集合!

时间:2022-11-18 16:06:48

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