.NET应用程序的Windows窗体设计器的特点展示

417 阅读15分钟

在过去的几个Visual Studio发布周期中,Windows Forms(WinForms)团队一直在努力使.NET应用程序的WinForms设计器与.NET框架设计器相匹配。你可能知道,需要一个新的WinForms设计器来支持.NET Core 3.1应用程序,以及后来的.NET 5+应用程序。这项工作需要对设计器进行近乎完全的重构,因为我们要应对.NET和基于.NET框架的WinForms设计器之间的差异,每个人都知道并喜欢。这篇博文的目的是让你对新的架构有一些了解,以及我们做了哪些改变。当然,当你创建自定义控件和.NET WinForms应用程序时,这些变化会对你产生什么影响。

读完这篇博文后,你将熟悉新的WinForms设计器所要解决的基本问题,并对这个新方法中的主要组件有一个高层次的理解。请欣赏这篇关于设计器架构的文章,并请继续关注未来的博客。

一段历史

WinForms是在2001年随第一个版本的.NET和Visual Studio一起推出的。WinForms本身可以被认为是对复杂的Win32 API的一种包装。它的建立是为了让企业开发人员不需要成为王牌C++开发人员来创建数据驱动的业务线应用程序。WinForms立即受到欢迎,因为它的所见即所得的设计器,即使是新手也能在几分钟内为他们的业务需求制作一个应用程序。

在我们增加对.NET核心应用程序的支持之前,只有一个单一的进程,devenv.exe,Visual Studio环境和正在设计的应用程序都在其中运行。但是.NET Framework和.NET Core不能同时在devenv.exe中运行,因此我们不得不将设计器从进程中分离出来,因此我们将新的设计器称为WinForms Out of Process Designer(简称OOP设计器)。

我们今天的情况如何?

虽然我们的目标是在Visual Studio 2022的发布中实现OOP设计器和.NET框架设计器之间的完全平等,但我们的积压问题仍有一些。也就是说,OOP设计器在其当前的迭代中已经在所有重要的层面上有了大部分的重大改进。

  • 性能。从Visual Studio 2019 v16.10开始,OOP设计器的性能已经有了很大的提高。我们致力于减少项目的加载时间,并改善了与设计表面的控件互动的体验,如选择和移动控件。
  • **数据绑定支持。**Visual Studio 2022中的WinForms为OOP设计器中的数据源管理带来了一种简化的方法,主要关注对象数据源。这种新方法对于OOP设计器和基于.NET的应用程序来说是独一无二的。
  • **WinForms Designer Extensibility SDK。**由于OOP设计器和.NET框架设计器之间的概念差异,.NET的第三方控件供应商将需要使用专门的WinForms设计器SDK来开发在OOP设计器上下文中运行的自定义控件设计器。我们在上个月以NuGet包的形式发布了SDK的预发布版本,你可以在这里下载它。我们将在2022年的第一季度更新这个包,使其提供IntelliSense。在未来几周内,也会有一篇关于SDK的专门博文。

看一下WinForms设计器的内部结构

使用WinForms设计器设计表单和UserControls,对于第一次看设计器引擎的人来说有几个惊喜:

  1. 设计器并没有将布局 "保存"(序列化)在某种XML或JSON中。它直接将表单/用户控件的定义序列化为代码--在新的OOP设计器中,它是C#或Visual Basic .NET。当用户在表单上放置一个Button时,创建这个Button和分配其属性的代码被生成到表单的一个名为 "InitializeComponent "的方法。当表单在设计器中被打开时,"InitializeComponent "方法被解析,一个影子.NET程序集正在从该代码中快速创建。这个程序集包含`InitializeComponent'的可执行版本,在设计器的上下文中被加载。然后,`InitializeComponent'方法被执行,设计器现在能够显示所有控制定义和指定属性的结果表单。我们称这种序列化为代码文档对象模型序列化,或简称为CodeDOM序列化。这就是为什么,你不应该直接编辑`InitializeComponent':当你在Form上直观地编辑一些东西并保存时,这个方法会被覆盖,你的编辑就会丢失。

  2. 所有的WinForms控件都有两个代码层。首先是控件的代码,它在运行时运行,然后是控件设计器,它在设计时控制行为。每个控件的设计器功能并不在设计器本身中实现。相反,一个专门的控制设计器与Visual Studio的服务和功能进行交互。让我们看一下`SplitContainer`作为一个例子。An animated gif of the various control designer features of the SplitContainer control

    SplitContainer的设计时行为是在一个相关的设计器中实现的,在这个例子中是`SplitContainerDesigner`。该类为`SplitContainer`控件的设计时体验提供了关键功能:

    • 鼠标点击时选择外部面板和内部面板的方式。
    • 分割条可以被移动以调整内部面板的大小。
    • 提供Designer Action Glyph,允许使用该控件的开发者通过各自的快捷菜单管理Designer Actions。

当我们决定在原始设计器中支持建立在.NET Core 3.1和.NET 5+上的应用程序时,我们面临着一个重大挑战。Visual Studio是建立在.NET框架上的,但需要通过序列化和反序列化这些代码来为针对不同运行时间的项目往返设计器代码。虽然有一些限制,你可以在.NET Core/.NET 5+应用程序中运行基于.NET框架的类型,但反过来就不一样了。这个问题被称为 "类型解析问题"。在TextBox控件中可以看到一个很好的例子:在.NET Core 3.1中,我们添加了一个新的属性,叫做`PlaceholderText`。在.NET框架中,该属性在`TextBox`上并不存在。因此,如果基于.NET框架的CodeDom串行器(在Visual Studio中运行)遇到`PlaceholderText`属性,就会失败。

此外,一个拥有所有控件和组件的表单在设计时就会在设计器中显示出来。因此,实例化窗体并在设计器窗口中显示它的代码也必须在.NET中执行,而不是在.NET框架中,这样,只有在.NET中可用的较新的属性也反映了控件、组件以及最终整个窗体或UserControl的实际外观和行为。

因为我们计划在未来继续创新和增加新的功能,所以这个问题只会随着时间的推移而增加。所以我们必须设计一种机制,支持WinForms设计器和Visual Studio之间的这种跨框架互动。

进入DesignToolsServer

开发人员需要在设计器中看到他们的表单与运行时的样子完全一样(所见即所得)。无论是前面例子中的 "PlaceholderText "属性,还是带有所需默认字体的表单布局,CodeDom序列化器都必须在项目所针对的.NET版本中运行。如果CodeDom序列化与Visual Studio在同一进程中运行,我们自然无法做到这一点。为了解决这个问题,我们在一个名为DesignToolsServer的新.NET(核心)进程中运行设计器(因此称为进程外设计器)。DesignToolsServer进程与你的应用程序运行相同的.NET版本和相同的bitness(x86或x64)。

现在,当你双击Solution Explorer中的Form或UserControl时,Visual Studio的设计器加载器服务会确定目标.NET版本并启动DesignToolsServer进程。然后设计器加载器将 "InitializeComponent "方法中的代码传递给DesignToolsServer进程,它现在可以在所需的.NET运行时下执行,现在能够处理该运行时提供的每一种类型和属性。

Screenshot showing each Form of projects with different bitness and the entries of DesignToolsServer in TaskManager

虽然脱离进程解决了类型解决的问题,但它在Visual Studio内部的用户交互方面引入了一些其他的挑战。例如,属性浏览器,它是Visual Studio的一部分(因此也是基于.NET框架)。它应该显示.NET类型,但由于CodeDom序列化器不能(去)序列化.NET类型的同样原因,它无法做到这一点。

自定义属性描述符和控制代理

为了促进与Visual Studio的交互,DesignToolsServer为表单上的组件和控件引入了代理类,这些代理类在Visual Studio进程中与表单上的真实组件和控件一起在DesignToolsServer.exe进程中创建。对于表单上的每一个控件,都会创建一个对象代理。当真正的控件存在于DesignToolsServer进程中时,对象代理实例存在于客户端--Visual Studio进程中。如果你现在在表单上选择一个实际的.NET WinForms控件,从Visual Studio的角度来看,一个对象代理就是被选中的。而这个对象代理并不具有它在服务器端的对应控件的相同属性。它是用自定义的代理属性描述符来1:1映射控件的属性,Visual Studio可以通过它与服务器进程对话。

所以,现在点击窗体上的一个按钮控件,会导致以下(有点简化)的事件链,以使属性显示在属性浏览器中:

  1. 鼠标点击发生在Visual Studio进程中的特殊窗口,称为*输入屏蔽。*如果你愿意的话,它的作用就像一个防喷嚏装置,它纯粹是为了拦截鼠标信息,并将其发送给DesignToolsServer进程。
  2. DesignToolsServer接收鼠标点击并将其传递给行为服务。行为服务找到控件并将其传递给选择服务,后者采取必要的步骤来选择该控件。
  3. 在这个过程中,行为服务也找到了相关的控制设计器,并启动了必要的步骤,让控制设计器为该控件渲染它需要渲染的任何装饰和字形。想想设计器的动作字形或前面SplitPanel例子中的特殊选择标记。
  4. 选择服务将控件的选择报告给Visual Studio的选择服务。
  5. Visual Studio现在知道,什么对象代理映射到DesignToolsServer中的选定控件。Visual Studio的选择服务会选择那个对象代理。这再次触发了属性浏览器中所选控件(对象代理)的值的更新。
  6. 属性浏览器现在反过来查询所选对象代理的属性描述符,这些描述符被映射到DesignToolsServer进程中的实际控件的代理描述符上。因此,对于属性浏览器需要更新的每个属性,属性浏览器都会调用各自代理的属性描述符上的GetValue,这导致了对服务器的跨进程调用,以检索该控件属性的实际值,最终显示在属性浏览器中。

Diagram which shows the process chain how Visual Studio communicates with the DesignToolsServer

自定义控件与DesignToolsServer的兼容性

有了这些新概念的知识,显然需要对现有的针对.NET的自定义控件设计者进行调整。调整的必要程度完全取决于自定义控件对典型的自定义控件设计器功能的利用程度。

下面是一个简单的简化指南,说明如何决定一个控件是否可能需要对OOP设计器的典型设计器功能进行调整:

  • 每当一个控件带来了特殊的UI功能(如自定义装饰器、快照线、字形、鼠标交互等),该控件将需要为.NET进行调整,至少要针对新的WinForms Designer SDK重新编译。原因是OOP Designer重新实现了很多原来的功能,而这些功能被组织在不同的命名空间中。如果不重新编译,新的OOP设计器就不知道如何处理控件设计器,也无法识别控件设计器的类型。
  • 如果控件带来了自己的类型编辑器,那么需要的调整就更加可观了。这也是团队对标准控件库所经历的过程。控件设计器的模态对话框只能在Visual Studio进程的上下文中工作,而控件设计器的其他部分则在DesignToolServer的进程中运行。这意味着一个带有自定义类型编辑器的控件,在模态对话框中显示,总是需要一个客户端/服务器控件设计器的组合。它需要在Visual Studio进程中的模态UI和DesignToolsServer进程中的控件的实际实例之间进行通信。
  • 由于控件和它的大部分设计器现在都在DesignToolsServer(而不是Visual Studio)进程中,通过处理WndProc代码中的UI交互来对开发者的UI交互做出反应将不再有效。如前所述,我们将发表一篇博文,介绍.NET自定义控件的编写,并深入研究.NET Windows Forms SDK的更多细节。

然而,如果一个控件的属性只实现了一个自定义的转换器,那么就不需要改变,除非这个转换器需要在属性网格中自定义绘画。然而,那些在设计时使用自定义枚举或通过自定义转换器提供标准设置列表的属性,运行起来就很好。

尚未到来的功能和淘汰的功能

虽然我们几乎达到了与.NET框架设计器的同等水平,但OOP设计器仍有一些地方需要努力:

  • Tab Order交互已经实现,目前正在测试中。 该功能将在Visual Studio 17.1 Preview 3中提供。 除了你已经在.NET框架设计器中发现的Tab Order功能外,我们已经计划扩展Tab Order交互,这将使它更容易重新排序,特别是在大型表格或大型表格的一部分。
  • 组件设计器还没有最终确定,我们正在积极地进行这方面的工作。然而,组件的使用是完全被支持的,而且组件托盘与.NET框架设计器具有同等地位。但是请注意,并不是所有在.NET框架的工具箱中默认可用的组件在OOP设计器中都支持。我们已经决定支持OOP设计器中的那些组件,这些组件只能通过.NET平台扩展(见Windows兼容性包)。 当然,如果你仍然需要这些组件,你可以在.NET的代码中直接使用它们。
  • 类型化数据集设计器不是OOP设计器的一部分。对于直接通向.NET框架中的SQL查询编辑器的类型编辑器也是如此(如DataSet组件编辑器)。类型化数据集需要所谓的数据源提供者服务,这不属于WinForms。 虽然我们已经实现了对对象数据源的现代化支持,并鼓励开发者与更现代的ORM(如EFCore)一起使用,但OOP设计器可以在有限的范围内处理现有表单上的类型化数据集,这些表单是由.NET框架项目移植而来的。

总结和主要启示

因此,虽然大多数基本的设计器功能与.NET框架设计器是一致的,但也有一些关键的区别:

  • 我们已经将.NET WinForms设计器从采购中剔除。当Visual Studio 2022只有64位的.NET框架时,新的设计器的服务器进程在项目的各自位数中运行,并作为一个.NET进程。 然而,这也带来了一些突破性的变化,主要是围绕控制设计器的编写。
  • 数据绑定是围绕对象数据源进行的。虽然目前以有限的方式支持维护基于类型DataSet的数据层的传统支持,但对于.NET,我们建议使用现代ORM,如EntityFramework或甚至更好。EFCore。使用DesignBindingPicker和新的Databinding Dialog来设置对象数据源。
  • 控件库作者,他们需要比自定义类型编辑器更多的设计时间支持,需要WinForms Designer Extensibility SDK。 框架控件设计者如果不针对.NET WinForms Designer的新OOP架构进行调整,就无法再工作。

请告诉我们您想从我们这里听到关于WinForms设计器的哪些主题--OOP设计器中新的对象数据源功能和WinForms设计器SDK是已经在制作中的主题,并且在我们的清单上。

请注意,WinForms .NET运行时是开源的,你可以做出贡献!如果你有想法,遇到bug,甚至想围绕WinForms运行时进行PR,请看看WinForms Github repo。 如果你对WinForms Designer有建议,也可以在那里提出新问题。

编码愉快!