学无先后达者为师!
不忘初心,砥砺前行。

都 2022 年了,.NET Core WinForms 设计器怎么样了?

在过去的几个Visual Studio发布周期中,Windows Forms(WinForms)团队一直在努力使.NET应用程序的WinForms设计器与.NET Framework设计器平价。如您所知,需要一个新的 WinForms 设计器来支持 .NET Core 3.1 应用程序和更高版本的 .NET 5+ 应用程序。这项工作需要对设计师进行近乎完整的重新架构,因为我们回应了.NET和基于.NET Framework的WinForms设计师之间的差异,每个人都知道和喜欢。这篇博文的目的是让您深入了解新架构以及我们所做的更改。当然,在创建自定义控件和 .NET WinForms 应用程序时,这些更改可能会对您产生怎样的影响。

阅读此博客文章后,您将熟悉新的 WinForms 设计器要解决的潜在问题,并对这种新方法中的主要组件有较高的了解。享受这个对设计师架构的看法,并继续关注未来的博客!

一点历史

WinForms于2001年与.NET和Visual Studio的第一个版本一起推出。WinForms本身可以被认为是围绕复杂的Win32 API的包装器。它的构建使得企业开发人员无需成为 ace C++开发人员即可创建数据驱动的业务线应用程序。WinForms因其所见即所得的设计师而立即受到欢迎,即使是新手开发人员也可以在几分钟内将应用程序组合在一起以满足他们的业务需求。

在我们添加对 .NET Core 应用程序的支持之前,只有一个进程 devenv.exe,Visual Studio 环境和正在设计的应用程序都在其中运行。但是.NET Framework和.NET Core不能在devenv.exe中一起运行,因此我们必须将设计器从流程中移除,因此我们将新设计师称为WinForms Out of Process Designer(或简称OOP designer)。

我们今天在哪里?

虽然我们的目标是在发布Visual Studio 2022时实现OOP设计器和.NET Framework设计器之间的完全对等,但我们的积压工作仍然存在一些问题。也就是说,OOP设计器在其当前迭代中已经在所有重要级别上进行了大多数重大改进:

  • 性能:从Visual Studio 2019 v16.10开始,OOP设计器的性能得到了相当大的提高。我们致力于缩短项目加载时间,并改进了与设计图面上的控件交互的体验,例如选择和移动控件。
  • 数据绑定支持:Visual Studio 2022 中的 WinForms 为在 OOP 设计器中管理数据源提供了一种简化的方法,主要侧重于对象数据源。这种新方法对于 OOP 设计器和基于 .NET 的应用程序来说是独一无二的。
  • WinForms Designer Extensibility SDK:由于 OOP 设计器和 .NET Framework 设计器之间的概念差异,.NET 的第三方控件的提供程序将需要使用专用的 WinForms Designer SDK 来开发在 OOP 设计器上下文中运行的自定义控件设计器。我们上个月发布了 SDK 的预发布版本作为 NuGet 包,你可以在此处下载。我们将更新此软件包,使其在 2022 年第一季度提供 IntelliSense。未来几周还将有一篇关于 SDK 的专门博客文章。

WinForms设计师的引擎盖下的外观

使用 WinForms 设计器设计窗体和用户控件,对于第一次在设计器引擎盖下查看的人来说,有几个惊喜:

  1. 设计器不会在某种 XML 或 JSON 中”保存”(序列化)布局。它将 Forms/UserControl 定义直接序列化为代码 – 在新的 OOP 设计器(即 C# 或 Visual Basic .NET)中。当用户将按钮放在窗体上时,用于创建此按钮和分配其属性的代码将生成到窗体的一个名为”InitializeComponent”的窗体方法中。在设计器中打开窗体时,将分析”InitializeComponent”方法,并从该代码动态创建影子 .NET 程序集。此程序集包含”InitializeComponent”的可执行版本,该版本在设计器的上下文中加载。然后执行”InitializeComponent”方法,设计器现在能够显示生成的 Form 及其所有控件定义和分配的属性。我们将这种序列化称为代码文档对象模型序列化,简称 CodeDOM 序列化。这就是为什么,您不应该直接编辑”初始化组件”:下次您在表单上直观地编辑某些内容并保存它时,该方法将被覆盖,并且您的编辑将丢失。
  2. 所有 WinForms 控件都有两个代码层。首先是在运行时运行的控件的代码,然后是控件设计器,它在设计时控制行为。每个控件的控件设计器功能不是在设计器本身中实现的。相反,专用的控件设计器与 Visual Studio 服务和功能进行交互。让我们以”SplitContainer”为例:SplitContainer 的设计时行为在关联的设计器中实现,在本例中为”SplitContainerDesigner”。此类为”SplitContainer”控件的设计时体验提供关键功能:
    • 单击鼠标时选择外部面板和内部面板的方式。
    • 移动拆分杆以调整内部面板大小的能力。
    • 提供设计器操作标志符号,该标志符号允许使用控件的开发人员通过相应的快捷方式菜单管理设计器操作。

当我们决定在原始设计器中支持基于 .NET Core 3.1 和 .NET 5+ 构建的应用时,我们面临着一个重大挑战。Visual Studio 是在 .NET Framework 上构建的,但需要通过序列化和反序列化面向不同运行时的项目来往返设计器代码。虽然存在一些限制,您可以在 .NET Core/.NET 5+ 应用程序中运行基于 .NET Framework 的类型,但情况并非如此。此问题称为”类型解析问题“。在 TextBox 控件中可以看到一个很好的例子:在 .NET Core 3.1 中,我们添加了一个名为”PlaceholderText”的新属性。在 .NET Framework 中,该属性在”文本框”上不存在。因此,如果基于 .NET Framework 的 CodeDom Serializer(在 Visual Studio 中运行)遇到 “PlaceholderText” 属性,它将失败。

此外,窗体及其所有控件和组件在设计时在设计器中呈现自身。因此,实例化窗体并在设计器窗口中显示窗体的代码也必须在 .NET 中执行,而不是在 .NET Framework 中执行,以便仅在 .NET 中可用的较新属性也反映控件、组件以及最终整个窗体或 UserControl 的实际外观和行为。

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

进入设计工具服务器

开发人员需要在设计器中看到他们的窗体,精确地查看运行时 (WYSIWYG) 的方式。无论是前面示例中的”占位符文本”属性,还是具有所需默认字体的窗体的布局,CodeDom 序列化程序都必须在项目所针对的 .NET 版本的上下文中运行。我们自然不能这样做,如果CodeDom序列化在与Visual Studio相同的进程中运行。为了解决这个问题,我们在一个名为 DesignToolsServer 的新 .NET(核心)进程中运行进程外设计器(因此名称为”进程外设计器”)。”设计工具服务器”进程运行与应用程序相同版本的 .NET 和相同的位数(x86 或 x64)。

现在,当您在”解决方案资源管理器”中双击窗体或 UserControl 时,Visual Studio 的设计器加载程序服务将确定目标 .NET 版本并启动 DesignToolsServer 进程。然后,设计器加载程序将代码从”InitializeComponent”方法传递到 DesignToolsServer 进程,在该进程中,它现在可以在所需的 .NET 运行时下执行,并且现在能够处理此运行时提供的每个类型和属性。

显示具有不同位数的项目的每个形式的屏幕截图以及 TaskManager 中 DesignToolsServer 的条目

虽然脱离进程解决了类型解析问题,但它在Visual Studio内部引入了一些围绕用户交互的其他挑战。例如,属性浏览器,它是Visual Studio的一部分(因此也是基于.NET Framework的)。它应该显示 .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,这将导致对服务器的跨进程调用以检索该控件属性的实际值,该值最终显示在属性浏览器中。
显示 Visual Studio 如何与 DesignToolsServer 进行通信的流程链的图示

自定义控件与设计工具服务器的兼容性

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

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

  • 每当控件带来特殊的 UI 功能(如自定义装饰器、对齐线、字形、鼠标交互等)时,都需要针对 .NET 调整该控件,并至少针对新的 WinForms Designer SDK 重新编译该控件。这样做的原因是OOP设计器重新实现了许多原始功能,并且该功能在不同的命名空间中组织。如果不重新编译,新的 OOP 设计器将不知道如何处理控件设计器,并且不会识别控件设计器类型。
  • 如果控件带有自己的“类型编辑器”,则所需的调整将更加可观。这与团队使用标准控件库执行的过程相同:控件设计器的模式对话框只能在 Visual Studio 进程的上下文中工作,而控件设计器的其余部分则在 DesignToolServer 进程的上下文中运行。这意味着具有自定义类型编辑器(显示在模式对话框中)的控件始终需要客户端/服务器控件设计器组合。它需要在 Visual Studio 进程中的模式 UI 与 DesignToolsServer 进程中控件的实际实例之间进行通信。
  • 由于控件及其大多数设计器现在都位于 DesignToolsServer(而不是 Visual Studio)进程中,因此通过处理 WndProc 代码中的 UI 交互来对开发人员的 UI 交互做出反应将不再有效。如前所述,我们将发布一篇博客文章,介绍 .NET 自定义控件的创作,并更详细地深入了解 .NET Windows 窗体 SDK。

但是,如果控件的属性仅实现自定义转换器,则无需进行任何更改,除非转换器需要在属性网格中进行自定义绘制。但是,使用自定义枚举或在设计时通过自定义转换器提供标准设置列表的属性运行良好。

功能尚未推出并逐步淘汰 功能

虽然我们几乎与 .NET Framework Designer 平起平坐,但 OOP Designer 仍然需要在几个方面开展工作:

  • Tab 键顺序交互组件已实现,目前正在进行测试。此功能将在 Visual Studio 17.1 Preview 3 中提供。除了在 .NET Framework 设计器中已经找到的 Tab 键顺序功能之外,我们还计划扩展 Tab 键顺序交互,这将使重新排序变得更加容易,尤其是在大型窗体或大型窗体的某些部分中。
  • 组件设计器尚未最终确定,我们正在积极研究这个问题。但是,完全支持组件的使用,并且组件栏与 .NET Framework 设计器具有奇偶校验。但请注意,并非 .NET Framework 中的”工具箱”中默认提供的所有组件在 OOP 设计器中都受支持。我们决定不支持 OOP 设计器中的这些组件,这些组件只能通过 .NET 平台扩展获得(请参阅 Windows 兼容包)。当然,如果您仍然需要这些组件,可以直接在 .NET 中的代码中使用它们。
  • 类型化数据集设计器不是 OOP 设计器的一部分。对于直接指向 .NET Framework 中的 SQL 查询编辑器的类型编辑器(如数据集组件编辑器),情况也是如此。类型化数据集需要所谓的数据源提供程序服务,该服务不属于 WinForms。虽然我们已经对对象数据源的支持进行了现代化改造,并鼓励开发人员将其与更现代的 ORM(如 EFCore)一起使用,但 OOP 设计器可以在有限的范围内处理现有窗体上的类型化数据集,这些窗体已从 .NET Framework 项目移植过来。

总结和关键要点

因此,尽管大多数基本设计器功能与 .NET Framework 设计器相同,但也存在主要区别:

  • 我们已经将.NET WinForms Designer从proc中剔除。虽然 Visual Studio 2022 仅为 64 位 .NET Framework,但新的设计器的服务器进程以项目的相应位数运行,并作为 .NET 进程运行。但是,这带来了一些重大更改,主要是围绕控件设计器的创作。
  • 数据绑定侧重于对象数据源。虽然目前以有限的方式支持维护基于类型化数据集的数据层的传统支持,但对于.NET,我们建议使用现代ORM,如EntityFramework,甚至更好的是EFCore。使用“设计绑定选取器“和新的”数据绑定”对话框设置对象数据源。
  • 控件库作者需要比自定义类型编辑器更多的设计时支持,他们需要 WinForms 设计器扩展性 SDK。如果不针对 .NET WinForms 设计器的新 OOP 体系结构调整框架控件设计器,框架控件设计器将不再工作。

让我们知道您希望从我们这里听到有关 WinForms 设计器的主题 – OOP 设计器和 WinForms 设计器 SDK 中的新对象数据源功能是已经在制作中的主题,并且位于我们列表的顶部。

另请注意,WinForms .NET 运行时是开源的,您可以做出贡献!如果您有想法,遇到错误,甚至想在WinForms运行时周围使用PR,请查看WinForms Github存储库。如果您对 WinForms 设计器有建议,也请随时在那里提交新问题。

祝您编码愉快!

赞(0) 打赏
未经允许不得转载:码农很忙 » 都 2022 年了,.NET Core WinForms 设计器怎么样了?

评论 抢沙发

给作者买杯咖啡

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册