(后续) 使用 Builder 模式让右键菜单的创建更上一层楼

本文最后更新于:11 天前

前言

上文 (没看的快去看!),我们讨论了如何以最爽的方式创建右键菜单。但后台有小伙伴留言说那么多方法写在窗体里面显得太冗余且无法在其他窗体中调用。如果全部放在实例类里面的话,又要写个 new() 也显得不优雅;放静态类每次都要写类名,太冗长了。所以这期我们用 Builder 设计模式来使它变得更好用。(以 ContextMenu 为例,ContextMenuStrip 请以此类推)

正文

首先,创建 Builder 实例类,名为 ContextMenuBuilder

1
2
3
4
public class ContextMenuBuilder
{

}

其次,创建 MenuItem 各方法不变,还是这几个,如下:(为了后面调用时简洁明了,我们可以省略方法名的动词)

1
2
3
4
5
6
7
8
9
10
11
public MenuItem Item(string Text)
=> new(Text);

public MenuItem Item(string Text, EventHandler OnClickHandler)
=> new(Text, OnClickHandler);

public MenuItem Menu(string Text, MenuItem[] Items)
=> new(Text, Items);

public MenuItem Separator()
=> new("-");

关键在于 Build() 方法应该怎么写。首先我们要求在使用 Builder 时,不要显式出现 new(),那么第一可以确定的是该方法应该为静态方法,即

1
public static ContextMenu Build() { }

那么参数应该是什么呢?由于我们的 Builder 是个实例对象,所以在静态方法中调用实例对象中的方法,必须要创建对象实例。但是我们又不能直接在方法里面 var 一个变量等于 new ContextMenuBuilder(),并且需要 MenuItem 各方法的返回值来拿出对应的菜单项,以便在其他地方控制状态等。所以我们仍要使用 MenuItem[] 数组来作为最终创建 ContextMenu 的原材料。这时就要动动脑筋了,有什么东西可以作为 Build() 方法的参数,并且可以调用的到 ContextMenuBuilder 的实例方法,还要获得 MenuItem[] 输出?

没错,就是 Func<T, TResult>,先来看看这个方法的签名。

1
public delegate TResult Func<in T, out TResult>(T arg);

这个委托使用泛型逆变和协变来约束指定类型的行为,人话就是输入 T (委托的参数),输出 TResult (委托返回值),这不正是我们想要的吗?所以我们可以这么写:

1
public static ContextMenu Build(Func<ContextMenuBuilder, MenuItem[]> Builder) { }

这表示我们可以传入 ContextMenuBuilder 实例来调用创建 MenuItem 的方法,并且可以获得 MenuItem[] 输出。那这个参数 Builder 怎么使用呢?很简单,委托可以理解为就是一种方法,用它时就像调用方法那样就行,要注意我们需要传入 ContextMenuBuilder 实例,所以 Builder 的参数为 new()

1
Builder(new());

现在我们就通过 Builder 委托拿到了 MenuItem[] 返回值,直接传入 ContextMenu 构造函数,我们就获得了 ContextMenu 实例。(关于 ContextMenuStrip 可以参考上一篇文章,在本文开头跳转)

1
2
public static ContextMenu Build(Func<ContextMenuBuilder, MenuItem[]> Builder)
=> new(Builder(new()));

那我们应该这么使用这个 Build() 方法并且表示 Func<ContextMenuBuilder, MenuItem[]> 呢?这不,Lambda 表达式就来啦,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public partial class Form1 : Form
{
private MenuItem Test;

public Form1()
{
InitializeComponent();

this.ContextMenu = ContextMenuBuilder.Build(b => // b 相当于 ContextMenuBuilder 实例的变量名,名称随意。
[
b.Item("测试"),
Test = b.Item("测试"),

b.Menu("测试",
[
b.Item("测试"),
b.Item("测试"),
b.Item("测试", (_, _) => MessageBox.Show("Test"))
]),

b.Separator(),
b.Item("测试")
]);

Test.Enabled = false;
}
}

怎么样,是不是感觉更爽了呢。最终效果如图:

最后献上完整代码!外加 Merge() 方法,这个方法推荐为静态,用法见上一篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ContextMenuBuilder
{
public MenuItem Item(string Text)
=> new(Text);

public MenuItem Item(string Text, EventHandler OnClickHandler)
=> new(Text, OnClickHandler);

public MenuItem Menu(string Text, MenuItem[] Items)
=> new(Text, Items);

public MenuItem Separator()
=> new("-");

public static ContextMenu Build(Func<ContextMenuBuilder, MenuItem[]> Builder)
=> new(Builder(new()));

public static ContextMenu Merge(ContextMenu Target, ContextMenu Reference)
{
Target.MergeMenu(Reference);
return Target;
}
}

相关链接


(后续) 使用 Builder 模式让右键菜单的创建更上一层楼
https://wanghaonie.github.io/posts/e1fb75c4dbec/
作者
WangHaonie
发布于
2025-05-03 20:01:49
更新于
2025-05-03 20:27:49
许可协议