当你使用 Visual Studio 中的 WinForms Designer 来创建一个 WinForms 表单或用户控件时,它并没有像 XML 或 HTML 那样的特殊定义或文件格式来表示用户界面。从一开始,WinForms 使用的唯一格式就是程序代码。在 WinForms Visual Basic 项目中定义的表单或用户控件会被保存到 VB 代码中。在 C# 项目中,这就是 C# 代码。这些代码将被放置在一个专用的 Designer 文件中,该文件位于实际表单代码文件后面,也包含控制 UI 的代码。
当你的表单或用户控件需要在 WinForms Designer 中再次打开时,该代码将被解释并根据结果对象图在 Designer 中重新创建表单/用户控件。这就是我们把保存表单的过程称为 CodeDOM 序列化的原因。这里的 CodeDOM 指的是一种对象模型(Code Document object model,代码文档对象模型),它允许开发人员通过特定类型的对象来定义程序的各个方面或程序的一部分。
虽然 CodeDOM 很灵活,可以比较容易地进行扩展,并且支持比 Visual Basic 或 C# 更多的语言,但是从现有的代码文件生成 CodeDOM 图是一件完全不同的事情。虽然 CodeDOM 可以通过现有的编译器实现为特定的语言编写代码文件,但生成的代码风格仍然是 .NET 框架刚开始时的风格,在许多情况下已经不再符合当前的编码标准。
在 WinForms 中,当你设计一个表单的时候,所有相关的内容都是在每个表单或用户控件的一个方法中生成的。这个方法(还有一些基础结构和初始化代码)叫做 InitializeComponent。
这个方法会被表单的构造函数无条件地调用。在 C# 中,这是非常明显的,你添加到项目中的新表单总是具有构造函数和所需的调用:
public partial class Form1 : Form
{
public Form2()
{
InitializeComponent();
}
}
在 Visual Basic 中,如果你不显式地添加构造函数 Sub New,Visual Basic 编译器会在后台自动插入对 InitializeComponent 的调用。但如果你在代码文件中添加了一个构造函数,编辑器也会在 VB 代码中插入对 InitializeComponent 的调用:
Public Class Form1
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
End SubEnd Class
注意,在 Visual Basic 中,Inherits 语句允许新表单类继承 System.Windows.Forms.Form 基类,与 C# 不同的是,它只是 Designer 代码隐藏文件的一部分。在 VB 中,分部类只需在其中一个分部类的代码文件中声明 partial 关键字即可。这就是为什么 Visual Basic WinForms 表单代码文件默认不包含任何东西,只包含表单的类定义的原因。
直到最近,WinForms Designer 使用 CodeModel 接口来解释不同编码语言的源代码,以构建所需的内部 CodeDOM 图,以便 Designer 保存表单或用户控件的定义。但是我们改变了这一点。
Enter Roslyn
WinForms 在 Visual Studio 2022 17.5版本中引入了一种现代化的方式,来读取和生成 WinForms 进程外 Designer 的 InitializeComponent 代码。它通过使用 .NET 编译器平台的 API(通常称为 Roslyn SDK)来完成所有相关任务。Roslyn 编译器是一套针对 .NET 语言的开源编译器和代码分析 API。它允许开发人员使用现代语言特性在 C# 和 Visual basic .NET 中编写、分析和操作代码。它还提供了一套丰富的诊断和代码重构功能,以提高代码质量和开发人员的工作效率。它是 C# 和 VB 代码生成的黄金标准和当前的最佳实践。而且,由于它与 Visual Studio 中用于编译和构建任何 C# 或 Visual Basic 项目的工具相同,因此其代码生成结果完全符合当前的编码标准。
此外,由于 Roslyn 编译器提供了某些 API,不仅知道特定语句、命令或方法的正确语法,而且还知道在 WinForms 设计时代码块的语义,WinForms Designer 可以比以前更早、更精确地指出 InitializeComponent 内部代码潜在的问题。因此,它不仅知道你什么时候拼错了“Buttne”——它还知道在 InitializeComponent 内部定义的拼写错误的变量将是一个未知的符号,并且能够指出这一点。
但是还有一系列额外的好处:
-
以前,基于 CodeModel 构建 CodeDOM 只能在 UI Thread 上运行。这不仅是一种阻塞操作,而且无法充分发挥现代多核处理器的潜力。使用 Roslyn 编译器,我们将可以通过使用并行化来优化构建的过程。
-
旧的系统没有一种简单的方法来解释最近引入的语言特性。而使用 Roslyn,我们可以选择引入像 NameOf 这样的语言特性来生成更优的代码,特别是用于数据绑定的时候。此外,它为未来 InitializeComponent 内部更复杂的代码生成开辟了新的道路,这将有助于优化和均衡在具有不同 HighDPI 设置的机器上的 HighDPI 场景的代码生成。
-
Roslyn 编译器支持许多方面的 .editorconfig 配置,因此在 InitializeComponent 中生成的代码接近于您和您的团队通过自定义 .editorconfig 定义强制执行的编码标准。
总而言之,有一些编码元素与之前的有本质上的不同。C# 中 this 或 Visual Basic 中 Me 的省略就是一个例子。下面的截图显示了 Roslyn 在 InitializeComponent 中 Button 的代码生成的区别:
如果你对将 WinForms Designer 中的代码生成转移到 Roslyn 或如何使用 .editorconfig 配置 InitializeComponent 代码生成的更多技术背景感兴趣,请查看 WinForms 库中的这篇技术文章,它更详细地解释了所有这些内容。
关于这个主题的反馈对我们来说真的很重要,请在文章下方留言告诉我们你在 WinForms 代码生成方面的想法或意见。如果你对 WinForms Designer 有任何建议,或者发现了一个 bug,请随时在 WinForms Github 库中提交新的 issue。编码愉快!