关于 winforms-expert
winforms-expert 是用于实现和诊断 Windows Forms 改动的技能,强调保持构建正确并兼容 Visual Studio Designer。它指导开发者先检查项目与验证流程,并区分设计器序列化代码和运行时逻辑。
主要功能
- 01设计器安全的序列化指导
- 02发现项目与验证器
- 03先运行针对性验证再构建
- 04WinForms 专项修复指导
适用场景
- 01新增或修改 Form 和控件
- 02诊断 Designer 加载或保存问题
- 03修复布局、绑定或 DPI 行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill winforms-expert安装后在 Claude Code 中输入/winforms-expert确认已加载。
详细介绍
WinForms 专家
用途
实现能够成功编译、运行时行为正确,并且仍可在 Visual Studio 设计器中编辑的 WinForms 更改。将 InitializeComponent 和 *.Designer.* 视为序列化格式,而不是普通应用程序代码;将行为逻辑保留在主分部类中,并在环境允许时验证设计器是否可用。
执行约定
编辑之前,列出工作区根目录,并找到任何针对任务的验证器、测试脚本或文档说明的验证命令。选择实现方案前,先阅读其场景选择器和必需参数。编辑之后:
- 先运行范围最小的验证器,并修复它归因于本次更改的所有失败。
- 验证器通过后,再运行针对性的项目构建。
- 如果构建或之后的任何编辑更改了源代码,则重新运行范围最小的验证器。
- 将这些命令与运行时 UI 和设计器往返检查分别报告。
成功构建不能替代现有的布局、绑定、序列化或设计器安全性验证器。不要只读过验证器或只在构建成功后就结束。
使用仓库实际的命令和参数;执行顺序应如下所示:
pwsh -NoProfile -File <task-validator.ps1> <scenario-arguments>
dotnet build <project> --nologo
限制
适用于:
- 创建或更改 WinForms 项目、
Form、UserControl或自定义控件。 - 编辑设计器生成的布局、事件连线、资源或组件字段。
- 诊断窗体设计器加载、序列化、缩放、绑定或工具箱问题。
- 实现 WinForms 特有的异步、MVVM、无障碍、DPI 或深色模式行为。
不适用于:
- 除非用户明确要求迁移,否则不要将应用程序转换为其他 UI 框架。
- 不要仅为改善代码风格而重写正常工作的生成代码。
- 除非请求需要,否则不要更改目标框架、语言版本、SDK、包管理策略、启动模型、DPI 模式或应用程序级主题。
- 如果没有实际打开并操作 Visual Studio 设计器,不要声称它可以正常工作。
输入
| 输入 | 必需 | 获取方式 |
|---|---|---|
| 所请求的 UI 行为或缺陷 | 是 | 使用用户的请求,并在可能时复现症状。 |
| 项目语言和目标框架 | 是 | 检查项目文件;不要假设使用现代 .NET 或 C#。 |
| 现有的 Form/UserControl 分部文件 | 是 | 查找主文件、设计器文件及对应的 .resx。 |
| 仓库约定 | 是 | 检查附近的 WinForms 类型、构建脚本、分析器和包管理方式。 |
| 设计器是否可用 | 否 | 确定是否能够打开兼容的 Visual Studio WinForms 设计器。 |
不可妥协的设计器约定
有两种代码上下文:
| 上下文 | 文件 | 规则 |
|---|---|---|
| 设计器序列化 | *.Designer.cs、*.Designer.vb、InitializeComponent | 使用简单、确定性的语句,以便设计器解析并重新生成。 |
| 应用程序行为 | 主 partial 类、服务、视图模型 | 使用项目 TFM 和语言版本支持的语言特性。 |
在 InitializeComponent 和设计器文件中:
- 保留控件/组件的构造、属性赋值、集合添加、布局暂停/恢复、资源应用以及命名事件处理程序的绑定。
- 使用类级别的控件/组件字段。不要通过局部变量将控件添加到
Controls、Items、Columns或其他序列化集合中。 - 使用在主 partial 文件中定义的命名事件处理程序。生成的事件绑定中绝不要使用 lambda 或匿名委托。
- 不要添加控制流(
if、循环、switch、try/catch、lock、await、goto)、局部函数、表达式主体逻辑、集合表达式、空条件或空合并表达式,或其他运行时决策逻辑。 - 不要添加业务逻辑、数据加载、验证、服务调用,或
InitializeComponent和Dispose等既定生成成员以外的方法。 - 保留生成器现有的命名空间风格、限定方式、顺序、资源和可空性处理。不要更新生成的语法。
- 保留设计器所依赖的现有构造函数。除非项目采用既定的自定义设计器模式,否则由设计器实例化的
Form/UserControl必须提供可用的无参数构造路径。 - 保持
BeginInit/EndInit、SuspendLayout/ResumeLayout和容器所有权配对平衡。 - 对于 C#,在设计器 partial 类的类作用域中保留生成的字段。对于 VB,保留
Friend WithEvents字段,并优先在主文件中使用Handles子句。
安全的 InitializeComponent 顺序如下:
- 实例化控件和组件。
- 如果所拥有的组件需要,则实例化
IContainer。 - 调用所需的
BeginInit和SuspendLayout方法。 - 赋值子控件/组件的属性。
- 将子控件添加到序列化集合,并绑定命名处理程序。
- 在子控件配置完成后,配置包含它们的 Form/UserControl。
- 在适当情况下,调用匹配的
EndInit、ResumeLayout和PerformLayout。 - 在生成器既定的位置声明序列化的后备字段。
将任何更复杂的内容移到主 partial 类中,通常放在 InitializeComponent 运行之后。
会改变答案的规则
根据症状选择一种 WinForms 专用修复方式。当现有项目和故障已经明确指出应采用哪种方式时,不要列出多种可能的模式。
| 症状或需求 | 应做 | 切勿 | 验证 |
|---|---|---|---|
| Form/UserControl 可以构建,但 Designer 无法实例化它 | 保留运行时构造函数,添加可供 Designer 使用的无参数路径,并在运行时加载时再使用依赖项,同时显式检查设计模式和空值 | 在 Designer 路径中构造生产服务、打开文件或查询数据 | 运行时组合根仍提供真正的依赖项;设计路径会执行 InitializeComponent,且不会调用该依赖项 |
| Designer 保存/重新打开后,控件/组件消失、重复或失效 | 恢复生成字段/集合的结构,并确保每个 BeginInit/EndInit 和 SuspendLayout/ResumeLayout 调用都成对出现 | 在 OnLoad 中修补症状,或将序列化控件移入局部变量 | 保存、关闭、重新打开,并检查重新生成的差异 |
Designer 创建的 Timer、BindingSource、图像列表或类似组件的生命周期超过了 Form | 创建 components 容器,并将其传递给组件构造函数 | 在 Designer 所有权不一致的情况下添加临时释放逻辑 | 关闭 Form 时会释放容器所拥有的组件,且该组件仍由 Designer 管理 |
| 添加/移除列表项后,绑定的 WinForms 列表控件没有刷新 | 使用 BindingList<T>,或使用仓库中可触发 WinForms 列表更改通知的适配器 | 将 ObservableCollection<T> 当作可直接替换的 WinForms DataSource | 绑定后修改列表,并观察控件是否更新 |
| DPI、字体或本地化变化时,嵌套内容被裁切 | 从叶级控件开始,沿所有父级检查 AutoSize、AutoSizeMode、Dock、MinimumSize、MaximumSize 和行/列样式;移除实际的增长限制,同时保留有意设置的最小值;还要检查相对于增长链定位的同级控件,并确保它们处于响应式布局中 | 认为 AutoSize = true 就足够,只增大一个固定的 Size,绕过父容器,或让同级控件留在扩展内容可能与之重叠的位置 | 测试调整大小以及相关的 DPI/字体/文本扩展情况 |
| 已投递 UI 工作,但完成状态/错误丢失 | 将操作放入返回 Task 的方法中,以便调用方/测试等待;让必需的 async void 事件处理程序只执行 await RefreshAsync();在捕获的 WinForms 上下文中更新控件,或在执行可能不在该上下文中时等待 InvokeAsync;在 finally 中恢复控件状态 | 将整个操作放在 async void 中,使用 _ =、BeginInvoke、无实际作用的取消异常捕获,或将应用级异常钩子作为常规处理路径 | 等待返回 Task 的方法,验证成功和失败情况;确认 UI 入口会委托给该方法 |
| 文本必须支持本地化 | 使用现有的 .resx 和 ComponentResourceManager.ApplyResources 序列化模式 | 在 InitializeComponent 中硬编码回退 UI 文本 | 构建、在可能时切换区域性,并执行 Designer 保存/重新打开 |
| VB 应用需要启动、单实例或未处理 UI 异常钩子 | 扩展 ApplicationEvents.vb;当事件参数类型存在歧义时,使用 Microsoft.VisualBasic.ApplicationServices 限定类型;还原、激活并将现有 MainForm 置于前台;记录 e.Exception;显式设置 e.ExitApplication = True | 臆造 Program.vb、添加 Sub Main、替换生成的启动代码,或让错误后的继续执行行为保持不明确 | 配置的 StartupObject 和生成的应用程序文件保持不变,不存在新的入口点,并在最终报告中说明退出选择 |
| 审查未发现实际的 Designer 缺陷 | 让已跟踪的项目/源文件逐字节保持不变,并说明可疑结构为何无害 | 清理警告、重新格式化生成的代码,或现代化无关语法 | 运行范围最小的可用检查和针对性构建;确认源文件差异为空 |
| 工作区包含针对任务的验证器或测试脚本 | 编辑后、通用构建前运行仓库提供的针对性检查;将其失败视为更改尚未完成的证据 | 因项目可以编译就跳过专用检查,或声称静态验证证明了 Designer 往返检查 | 分别报告准确的验证器和构建命令,然后说明实际是否能够进行运行时 UI 和 Designer 往返检查 |
工作流
1. 明确项目约束
- 检查解决方案/项目、目标框架、语言、可空设置、包管理、应用程序启动方式以及现有构建命令。
- 确定应用面向的是现代 .NET 还是 .NET Framework。除非明确要求迁移,否则保留当前系列。
- 编辑之前,读取每个受影响的 Form/UserControl 对应的全部分部文件和
.resx。检查基类和相邻控件,了解仓库约定。 - 如果缺陷特定于设计器,请记录确切的加载/序列化错误,并确定问题是否在进行大范围更改前就已发生。
2. 规划序列化边界
- 将每项更改归类为序列化 UI 状态或运行时行为。
- 仅将设计器可表示的状态放入
InitializeComponent。 - 将条件设置、动态内容、数据加载、转换、验证和服务交互放入主分部类或其他运行时类型中。
- 尽量缩小设计器差异。不要对整个生成文件重新排序或重新格式化。
- 如果可以避免编辑生成的代码,请改用设计器或运行时初始化方法。如果必须直接编辑,请严格遵循现有的生成代码结构。
3. 实现 UI 结构
对于新布局或经过大幅调整的布局:
- 优先使用
TableLayoutPanel、FlowLayoutPanel、SplitContainer和嵌套的 UserControl,而不是脆弱的绝对定位。 - 将复杂屏幕划分为较小的布局区域。避免使用单个过大的网格。
- 标题/单行行列优先使用
AutoSize,可扩展内容使用Percent,只有确实固定尺寸的内容才使用Absolute。 - 保留现有窗体的
AutoScaleMode。对于新窗体,应根据项目约定审慎选择缩放方式,而不是照搬其他 DPI 下的坐标。 - 确保自动调整大小的子容器参与完整的尺寸调整链;自动调整大小的行中如果包含固定高度的
Panel或GroupBox,其内容仍可能被裁切。 - 对可调整大小的对话框或本地化对话框设置合理的最小尺寸,避免其变得无法使用。
- 将本地化 UI 文本保存在项目资源中,并允许标签和按钮扩展。
对于对话框:
- 设置
AcceptButton、CancelButton和适当的DialogResult值。 - 提交时验证整个窗体。不要通过取消每次焦点更改来困住用户。
- 按照项目的所有权模式释放模态窗体。
4. 在生成的代码之外添加行为
- 遵循项目的语言和风格约定;现代语法仅用于常规代码。
- 明确保持 UI 线程亲和性。从后台任务访问控件时,应封送调用。
- 普通 WinForms
async void事件处理程序在await后会恢复到捕获的 UI 同步上下文,除非刻意绕过了该上下文。在此处直接访问控件是有效的。如果代码可能在上下文之外继续执行,或任务/仓库约定明确要求封送操作,请使用并等待InvokeAsync;不要依赖未等待的投递操作。 - 对于支持此功能的现代 .NET 版本,请根据委托是同步还是异步,以及是否返回值,选择匹配的
Control.InvokeAsync重载。等待返回的操作;不要创建即发即弃的 UI 工作。 - 对于必须返回
void的 WinForms 事件处理程序和重写方法,可以使用async void,但应在等待的工作周围捕获异常,妥善处理取消,并通过应用程序既定的错误处理路径报告意外故障。 - 不要将应用程序级异常钩子用作处理预期故障的替代方案。
Application.ThreadException处理 UI 线程异常;AppDomain.UnhandledException主要用于最后阶段的日志记录,并不能确保程序可以安全地继续运行。 - 保留属性的分配语义。未经确认对象的生命周期和释放行为,不要将缓存属性改为
=> new ...,也不要反向更改。
对于应用程序范围的启动配置:
- 保留当前的启动模型。C# 项目可以在
Program.cs中配置应用程序默认值;VB 项目通常使用 VB Application Framework 和ApplicationEvents.vb,而不是另行创建Program.vb。 - 使用实际 TFM 支持的 API。仅在有请求或符合应用程序现有策略时,才设置颜色模式或 DPI 默认值。
- 维持常规 .NET WinForms DPI 行为时,优先使用
SystemAware。仅当应用程序针对每显示器缩放进行了设计和测试时,才使用PerMonitorV2。
5. 有意识地实现绑定和序列化
对于经典绑定:
- 对可变绑定对象使用
INotifyPropertyChanged,并使用BindingList<T>或成熟的适配器来通知列表更改。 - 在符合现有设计的情况下,使用
BindingSource作为设计器/运行时中介。 - 将转换逻辑放在
Binding.Format和Binding.Parse处理程序中,而不是嵌入设计器代码中。 - 谨慎处理单向到源的请求:经典 WinForms
Binding不提供直接的 WPF 式模式。应记录并测试任何变通方案,而不要暗示原生支持。
对于现代 WinForms MVVM API,首先确认 TFM 包含所请求的 API:
- 在可用时,使用
Control.DataContext提供环境视图模型上下文。 - 在可用时,使用
ButtonBase.Command、ToolStripItem.Command和CommandParameter。 - 如果解决方案已采用 UI 无关的项目来存放视图模型,或所请求的更改因此受益,则继续采用这种方式;不要为了小修复引入新的架构。
- 如果需要设计时对象数据源,请保留或添加项目现有的
.datasource和BindingSource模式,并在设计器中验证。
对于自定义 Component/Control 属性,选择一种明确的 CodeDOM 序列化策略:
| 策略 | 机制 | 用途 |
|---|---|---|
| 仅在非默认值时序列化 | [DefaultValue(...)] 或相匹配的 ShouldSerializeX/ResetX | 具有明确默认值的稳定值 |
| 从不序列化 | [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] | 仅运行时使用、计算得出或不受支持的状态 |
| 序列化内容 | DesignerSerializationVisibility.Content,以及与设计器兼容的可变内容 | 有意在属性网格中编辑的所属嵌套对象/集合 |
导入 System.ComponentModel(或完全限定其属性)。确保默认元数据与运行时初始化直接一致:对于 [DefaultValue(typeof(Color), "Yellow")],直接用 Color.Yellow 初始化属性或其后备字段;不要通过第二个常量或工厂隐藏实际默认值。不要组合相互矛盾的策略。对自定义序列化更改测试属性网格编辑和保存/重新加载行为。
6. 检查可用性和无障碍性
- 设置合理的
TabIndex顺序,并验证仅使用键盘时的遍历效果。 - 在产品中使用助记键的地方,添加明确无歧义的助记键。
- 如果标签或上下文尚未提供相关信息,为可操作控件设置有意义的无障碍名称/描述。
- 验证默认焦点、确认/取消行为、调整大小、裁剪以及本地化文本增长情况。
- 检查与此次更改相关的 DPI/主题组合。系统颜色会随主题调整;硬编码颜色和所有者绘制控件则不会。
- 对所有者绘制、
DataGridView、图标和自定义绘制,验证其在每种受支持主题下的对比度,不要假定它们会自动适配深色模式。
7. 按成本从低到高进行验证
- 重新阅读设计器差异,确认每条语句都可安全序列化,且每个引用的字段/处理程序都存在于正确的分部类中。
- 查找并运行仓库中最精简的特定任务验证脚本或测试(如果有)。通用构建不能替代设计器/布局/绑定验证器。
- 运行仓库中适用范围最小的还原/构建/分析器命令。如果项目类型或 .NET Framework 依赖项有要求,请使用 Visual Studio MSBuild。
- 运行其余相关自动化测试。
- 在可能的情况下启动受影响的 UI,并测试创建、加载、调整大小、键盘导航、绑定、异步/错误路径以及关闭/释放行为。
- 如果可以使用兼容的 WinForms Designer 的 Visual Studio:
- 在设计器中打开每个已更改的 Form/UserControl。
- 确认设计图面、工具箱集成、属性网格、组件托盘和继承控件均能正常加载,没有错误。
- 做出无害且可撤销的属性更改,保存、关闭、重新打开,并确认设计器能够序列化并重新加载该组件。
- 仅还原这项无害的验证更改,不要还原请求的实现。
- 序列化往返后再次构建。
可观察的完成标准
只有在环境适用的检查均可观察时,更改才算完成:
- 项目构建成功,且没有新增编译器或分析器错误。
- 相关测试通过。
- 在可以启动的情况下,受影响的 UI 能正常运行,并且请求的行为已得到测试。
- 受影响的
InitializeComponent中没有不受支持的运行时逻辑,也没有无法解析的字段、处理程序、资源或不平衡的布局/初始化调用。 - 在适用的情况下,自定义属性更改在属性网格保存/重新打开往返后仍然保留。
- 在设计器可用时,每个已更改的 Form/UserControl 都能在兼容的 Visual Studio Designer 中打开并重新加载。
如果 Visual Studio、Designer、Windows 桌面访问、必需的工作负载或目标运行时不可用,请将该验证报告为未运行,说明原因,并列出实际运行的准确检查项。构建成功并不能证明 Designer 可以打开。除非实际测试了设计图面和序列化往返,否则不要声称已完成全面的设计器验证。
如果命令失败:
- 报告命令和第一个可采取行动的错误。
- 区分失败是由更改引起,还是由缺少工具/工作负载、平台不兼容、文件被锁定、还原/网络故障或仓库中原有的问题引起。
- 修复由更改引起的失败,并重新运行检查。
- 绝不要用看似成功的替代方案代替失败的检查,也不要仅凭源代码检查就声称已完成。
最后提供简洁的验证记录:
Build:准确命令和结果。Runtime UI:已测试的行为;如果未运行,则说明阻碍原因。Designer round trip:Form/UserControl 已打开、编辑、保存、关闭并重新打开;如果未运行,则说明阻碍原因。Preserved:有意保持不变的项目策略文件和生成/启动文件。
常见陷阱
| 陷阱 | 修正规则 |
|---|---|
将 InitializeComponent 当作现代 C# 代码 | 将其保留为简单的设计器序列化代码;将决策和行为移到主分部类中。 |
| 在生成的代码中放入 lambda 或局部控件变量 | 使用类级字段和具名处理程序。 |
| 只编辑一个分部文件 | 阅读并确保主文件、设计器文件、.resx、基类型和项目元数据保持一致。 |
| 重新格式化整个设计器文件 | 参照生成的顺序和风格进行最小范围的修改。 |
| 认为构建成功就证明设计器正常 | 打开、编辑、保存、关闭并重新打开设计界面。 |
| 为可设计类型添加构造函数依赖项 | 保留设计器可用的无参数构造路径,或采用既有的自定义设计器方案。 |
| 固定尺寸的嵌套容器裁切控件 | 保留自动调整大小链,并测试窗口调整大小和 DPI 缩放。 |
| 未经测试便使用绝对颜色适配深色模式 | 优先使用系统颜色,或明确为自定义绘制设置主题并测试对比度。 |
将 ObservableCollection<T> 当作可直接替换的 WinForms 列表数据源 | 使用 BindingList<T>,或使用能够桥接集合通知和属性通知的适配器。 |
| 认为 WPF 绑定或转换器功能也存在于 WinForms 中 | 使用 WinForms 的 Binding、BindingSource、Format 和 Parse;核实特定 TFM 的 API。 |
| 对 UI 调度采用不等待的方式,或未捕获异步事件错误 | 等待 InvokeAsync;在 async void UI 入口中捕获并报告失败。 |
为 VB 应用凭空创建 Program.vb | 使用 VB Application Framework 和 ApplicationEvents.vb 约定。 |
| 在 UI 修复期间更改 DPI、主题、TFM 或包策略 | 除非所需行为要求更改且更改经过验证,否则保持项目策略不变。 |