WinForms Expert
Purpose
Implement WinForms changes that compile, behave correctly at runtime, and remain editable in the
Visual Studio Designer. Treat InitializeComponent and *.Designer.* as a serialization format,
not ordinary application code; keep behavior in the main partial class and verify designer
usability whenever the environment permits.
Execution Contract
Before editing, list the workspace root and locate any task-specific validator, test script, or documented verification command. Read its scenario selector and required arguments before choosing an implementation. After editing:
- Run that narrow validator first and fix every failure it attributes to the change.
- Run the focused project build after the validator passes.
- If the build or any later edit changes source, rerun the narrow validator.
- Report those commands separately from runtime UI and Designer round-trip checks.
A successful build never substitutes for an available layout, binding, serialization, or designer-safety validator. Do not finish after merely reading a validator or after a build succeeds.
Use the repository's actual command and arguments; the sequence should look like:
pwsh -NoProfile -File <task-validator.ps1> <scenario-arguments>
dotnet build <project> --nologo
Boundaries
Use this skill for:
- Creating or changing a WinForms project,
Form,UserControl, or custom control. - Editing designer-generated layout, event wiring, resources, or component fields.
- Diagnosing a Form Designer load, serialization, scaling, binding, or toolbox problem.
- Implementing WinForms-specific async, MVVM, accessibility, DPI, or dark-mode behavior.
Do not use it to:
- Convert the application to another UI framework unless the user explicitly requests migration.
- Rewrite working generated code for style alone.
- Change the target framework, language version, SDK, package strategy, startup model, DPI mode, or application-wide theme unless required by the request.
- Claim that the Visual Studio Designer works when it was not actually opened and exercised.
Inputs
| Input | Required | How to obtain it |
|---|---|---|
| Requested UI behavior or defect | Yes | Use the user's request and reproduce the symptom when possible. |
| Project language and target framework | Yes | Inspect the project file; do not assume modern .NET or C#. |
| Existing Form/UserControl partial files | Yes | Find the main file, designer file, and matching .resx. |
| Repository conventions | Yes | Inspect nearby WinForms types, build scripts, analyzers, and package management. |
| Designer availability | No | Determine whether a compatible Visual Studio WinForms Designer can be opened. |
Non-Negotiable Designer Contract
There are two code contexts:
| Context | Files | Rule |
|---|---|---|
| Designer serialization | *.Designer.cs, *.Designer.vb, InitializeComponent | Use simple, deterministic statements the designer can parse and regenerate. |
| Application behavior | Main partial class, services, view models | Use language features supported by the project's TFM and language version. |
In InitializeComponent and designer files:
- Keep control/component construction, property assignments, collection additions, layout suspension/resumption, resource application, and named event-handler wiring.
- Use class-level control/component fields. Do not add a control to
Controls,Items,Columns, or another serialized collection through a local variable. - Use named event handlers defined in the main partial file. Never use lambdas or anonymous delegates in generated event wiring.
- Do not add control flow (
if, loops,switch,try/catch,lock,await,goto), local functions, expression-bodied logic, collection expressions, null-conditional or null-coalescing expressions, or other runtime decision logic. - Do not add business logic, data loading, validation, service calls, or methods other than the
established generated members such as
InitializeComponentandDispose. - Preserve the generator's existing namespace style, qualification style, ordering, resources, and nullable treatment. Do not modernize generated syntax.
- Preserve existing constructors that the designer relies on. A designer-instantiated
Form/UserControlmust have a usable parameterless construction path unless the project uses an established custom designer pattern. - Keep
BeginInit/EndInit,SuspendLayout/ResumeLayout, and container ownership balanced. - For C#, keep generated fields at class scope in the designer partial. For VB, preserve
Friend WithEventsfields and preferHandlesclauses in the main file.
A safe InitializeComponent sequence is:
- Instantiate controls and components.
- Instantiate the
IContainerwhen owned components require it. - Call required
BeginInitandSuspendLayoutmethods. - Assign child control/component properties.
- Add children to serialized collections and wire named handlers.
- Configure the containing Form/UserControl after its children.
- Call matching
EndInit,ResumeLayout, andPerformLayoutwhere appropriate. - Declare serialized backing fields in the generator's established location.
Move anything more complex to the main partial class, usually after InitializeComponent has run.
Rules That Change the Answer
Use the symptom to choose one WinForms-specific repair. Do not list several plausible patterns when the existing project and failure identify one.
| Symptom or request | Do | Never | Verify |
|---|---|---|---|
| A Form/UserControl builds but the Designer cannot instantiate it | Preserve the runtime constructor, add a designer-usable parameterless path, and defer dependency use until runtime load with an explicit design/null guard | Construct production services, open files, or query data from the designer path | The runtime composition root still supplies the real dependency; the design path reaches InitializeComponent without invoking it |
| Controls/components disappear, duplicate, or fail after Designer save/reopen | Restore the generated field/collection shape and balance every BeginInit/EndInit and SuspendLayout/ResumeLayout pair | Patch the symptom in OnLoad or move serialized controls into local variables | Save, close, reopen, and inspect the regenerated diff |
A designer-created Timer, BindingSource, image list, or similar component outlives the Form | Create the components container and pass it to the component constructor | Add ad hoc disposal while leaving designer ownership inconsistent | Closing the Form disposes the container-owned component and the component remains designer-managed |
| Adding/removing list items does not refresh a bound WinForms list control | Use BindingList<T> or the repository's adapter that raises WinForms list-change notifications | Treat ObservableCollection<T> as a drop-in WinForms DataSource | Mutate the list after binding and observe the control update |
| Nested content clips at DPI, font, or localization changes | Inspect AutoSize, AutoSizeMode, Dock, MinimumSize, MaximumSize, and row/column styles from the leaf through every parent; remove the actual growth cap while preserving intentional minimums; also inspect sibling controls positioned against the growing chain and keep them in responsive layout | Assume AutoSize = true is sufficient, increase one fixed Size, bypass a parent container, or leave a sibling where expanded content can overlap it | Exercise resize plus the relevant DPI/font/text expansion |
| UI work is posted but completion/errors are lost | Put the operation in a Task-returning method that callers/tests can await; let the required async void event handler do only await RefreshAsync(); update controls on the captured WinForms context or await InvokeAsync when execution can be off-context; restore control state in finally | Keep the whole operation inside async void, use _ =, BeginInvoke, a dead cancellation catch, or an application-wide exception hook as the normal path | Await the task-returning method through success and failure; verify the UI entry point delegates to it |
| Text must be localizable | Use the existing .resx and ComponentResourceManager.ApplyResources serialization pattern | Leave fallback UI text hard-coded in InitializeComponent | Build, switch culture when possible, and perform a Designer save/reopen |
| A VB app needs startup, single-instance, or unhandled-UI hooks | Extend ApplicationEvents.vb; qualify Microsoft.VisualBasic.ApplicationServices event-argument types when ambiguous; restore, activate, and bring the existing MainForm forward; log e.Exception; set e.ExitApplication = True explicitly | Invent Program.vb, add Sub Main, replace generated startup, or leave post-error continuation implicit | The configured StartupObject and generated application file remain unchanged, no new entry point exists, and the final report states the exit choice |
| Review finds no actual designer defect | Leave tracked project/source files byte-for-byte unchanged and report why the suspicious construct is harmless | Clean up warnings, reformat generated code, or modernize unrelated syntax | Run the narrow available check and focused build; confirm the source diff is empty |
| The workspace contains a task-specific validator or test script | Run the narrow repository-provided check after editing and before the generic build; treat its failure as evidence that the change is incomplete | Skip the specialized check because the project compiles, or claim a Designer round trip from static validation | Report the exact validator and build commands separately, then state whether runtime UI and Designer round-trip checks were actually available |
Workflow
1. Establish the project constraints
- Inspect the solution/project, target framework, language, nullable setting, package management, application startup, and existing build commands.
- Identify whether the app targets modern .NET or .NET Framework. Preserve its current family unless migration is requested.
- Read the complete partial-file set and
.resxfor every affected Form/UserControl before editing. Check base classes and nearby controls for repository conventions. - If the defect is designer-specific, record the exact load/serialization error and determine whether the problem occurs before making broad changes.
2. Plan the serialization boundary
- Classify each change as serialized UI state or runtime behavior.
- Put only designer-representable state in
InitializeComponent. - Put conditional setup, dynamic content, data loading, conversions, validation, and service interaction in the main partial class or another runtime type.
- Prefer the smallest designer diff. Do not reorder or reformat the whole generated file.
- If editing generated code is avoidable, use the Designer or a runtime initialization method instead. If direct editing is necessary, follow the existing generated shape exactly.
3. Implement the UI structure
For new or substantially revised layouts:
- Prefer
TableLayoutPanel,FlowLayoutPanel,SplitContainer, and nested UserControls over brittle absolute positioning. - Divide complex screens into small layout regions. Avoid a single oversized grid.
- Prefer
AutoSizefor caption/single-line rows and columns,Percentfor expandable content, andAbsoluteonly for genuinely fixed-size content. - Preserve the existing form's
AutoScaleMode. For new forms, choose scaling deliberately based on the project conventions rather than copying coordinates from another DPI. - Ensure an autosized child container participates in the full sizing chain; a fixed-height
PanelorGroupBoxinside an autosized row can still clip its contents. - Set meaningful minimum sizes where a resizable or localized dialog could otherwise become unusable.
- Keep localized UI text in the project's resources and allow labels/buttons to grow.
For dialogs:
- Set
AcceptButton,CancelButton, and appropriateDialogResultvalues. - Validate the form as a whole when submitting. Do not trap users in a field by canceling every focus change.
- Dispose modal forms according to the project's ownership pattern.
4. Add behavior outside generated code
- Follow the project's language and style conventions; modern syntax belongs only in regular code.
- Keep UI-thread affinity explicit. Marshal control access from background work.
- A normal WinForms
async voidevent handler resumes on its captured UI synchronization context afterawaitunless that context was deliberately bypassed. Direct control access there is valid. Use and awaitInvokeAsyncwhen code can continue off-context, or when the task/repository contract explicitly requires a marshaled operation; do not rely on an unawaited post. - For modern .NET versions that support it, select the
Control.InvokeAsyncoverload matching whether the delegate is synchronous/asynchronous and whether it returns a value. Await the returned operation; do not create fire-and-forget UI work. async voidis acceptable for WinForms event handlers and overrides that must returnvoid, but catch exceptions around awaited work, handle cancellation intentionally, and surface unexpected failures through the application's established error path.- Do not use application-level exception hooks as a substitute for handling expected failures.
Application.ThreadExceptioncovers UI-thread exceptions;AppDomain.UnhandledExceptionis primarily a last-chance logging path and does not make continued execution safe. - Preserve property allocation semantics. Do not change a cached property into
=> new ...or vice versa without confirming lifetime and disposal behavior.
For application-wide startup:
- Preserve the current startup model. C# projects may configure application defaults in
Program.cs; VB projects normally use the VB Application Framework andApplicationEvents.vb, not a newly inventedProgram.vb. - Use the APIs available to the actual TFM. Apply color mode or DPI defaults only when requested or consistent with the app's existing policy.
- Prefer
SystemAwarewhen maintaining the normal .NET WinForms DPI behavior. UsePerMonitorV2only when the application is designed and tested for per-monitor scaling.
5. Implement binding and serialization deliberately
For classic binding:
- Use
INotifyPropertyChangedfor mutable bound objects andBindingList<T>or an established adapter for list change notifications. - Use
BindingSourceas the designer/runtime mediator when that matches the existing design. - Put conversion logic in
Binding.FormatandBinding.Parsehandlers rather than embedding executable logic in designer code. - Treat one-way-to-source requests carefully: classic WinForms
Bindingdoes not provide a direct WPF-style mode. Document and test any workaround instead of implying native support.
For modern WinForms MVVM APIs, first confirm the TFM contains the requested API:
- Use
Control.DataContextfor ambient view-model context when available. - Use
ButtonBase.Command,ToolStripItem.Command, andCommandParameterwhen available. - Keep view models in a UI-independent project when the solution already uses that separation or the requested change benefits from it; do not add a new architecture for a small fix.
- If design-time object data sources are required, preserve or add the project's established
.datasourceandBindingSourcepattern and verify it in the Designer.
For custom Component/Control properties, choose one intentional CodeDOM serialization policy:
| Policy | Mechanism | Use |
|---|---|---|
| Serialize only when non-default | [DefaultValue(...)] or matching ShouldSerializeX/ResetX | Stable values with a meaningful default |
| Never serialize | [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] | Runtime-only, calculated, or unsupported state |
| Serialize content | DesignerSerializationVisibility.Content plus designer-compatible mutable content | Owned nested objects/collections intentionally edited in the property grid |
Import System.ComponentModel (or fully qualify its attributes). Keep default metadata and runtime
initialization directly aligned: for [DefaultValue(typeof(Color), "Yellow")], initialize the
property or its backing field directly with Color.Yellow; do not hide the actual default behind a
second constant or factory. Do not combine contradictory policies. Test property-grid editing and
save/reload behavior for custom serialization changes.
6. Check usability and accessibility
- Set logical
TabIndexorder and verify keyboard-only traversal. - Add unambiguous mnemonics where the product uses them.
- Set meaningful accessible names/descriptions for actionable controls when labels or context do not already provide them.
- Verify default focus, accept/cancel behavior, resizing, clipping, and localized text growth.
- Check at the DPI/theme combinations relevant to the change. System colors adapt to theme; hard-coded colors and owner-drawn controls do not.
- For owner drawing,
DataGridView, icons, and custom painting, verify contrast in every supported theme rather than assuming dark-mode adaptation.
7. Validate in increasing-cost order
- Re-read the designer diff and confirm every statement is serialization-safe and every referenced field/handler exists in the correct partial class.
- Discover and run the repository's narrowest task-specific validation script or test when one is present. A generic build does not replace a designer/layout/binding validator.
- Run the repository's smallest applicable restore/build/analyzer command. Use Visual Studio MSBuild when the project type or .NET Framework dependencies require it.
- Run any remaining relevant automated tests.
- Launch the affected UI when possible and exercise creation, load, resize, keyboard navigation, binding, async/error paths, and close/disposal behavior.
- When Visual Studio with a compatible WinForms Designer is available:
- Open every changed Form/UserControl in the Designer.
- Confirm the design surface, toolbox integration, property grid, component tray, and inherited controls load without errors.
- Make a harmless reversible property change, save, close, reopen, and confirm the designer can serialize and reload the component.
- Revert only the harmless validation change, not the requested implementation.
- Build again after the serialization round trip.
Observable Completion Criteria
A change is complete only when the checks applicable to the environment are observable:
- The project builds with no new compiler or analyzer errors.
- Relevant tests pass.
- The affected UI runs and the requested behavior is exercised, when launching is possible.
- No affected
InitializeComponentcontains unsupported runtime logic or unresolved fields, handlers, resources, or unbalanced layout/initialization calls. - Changed custom properties survive a property-grid save/reopen round trip when applicable.
- Every changed Form/UserControl opens and reloads in the compatible Visual Studio Designer when designer access is available.
If Visual Studio, the Designer, Windows desktop access, a required workload, or a target runtime is unavailable, report that validation as not run, state why, and list the exact checks that did run. A successful build is not evidence that the Designer opens. Do not report the work as fully designer-verified unless the design surface and serialization round trip were actually tested.
If a command fails:
- Report the command and the first actionable error.
- Distinguish a failure caused by the change from a missing tool/workload, incompatible platform, locked file, restore/network failure, or pre-existing repository failure.
- Fix failures caused by the change and rerun the check.
- Never replace a failed check with a success-shaped fallback or claim completion based only on source inspection.
Finish with a concise validation record:
Build:exact command and result.Runtime UI:exercised behavior, ornot runwith the blocking reason.Designer round trip:Form/UserControl opened, edited, saved, closed, and reopened; ornot runwith the blocking reason.Preserved:project policy and generated/startup files intentionally left unchanged.
Common Pitfalls
| Pitfall | Corrective rule |
|---|---|
Treating InitializeComponent as normal modern C# | Keep it as simple designer serialization; move decisions and behavior to the main partial class. |
| Putting lambdas or local control variables in generated code | Use class-level fields and named handlers. |
| Editing only one partial file | Read and keep the main file, designer file, .resx, base type, and project metadata consistent. |
| Reformatting the entire designer file | Make a surgical diff that follows generated ordering and style. |
| Assuming a build proves designer health | Open, edit, save, close, and reopen the design surface. |
| Adding constructor dependencies to a designable type | Preserve a designer-usable parameterless path or an established custom designer solution. |
| Fixed-size nested containers clipping controls | Preserve an autosizing chain and test resize plus DPI scaling. |
| Using absolute colors for dark mode without testing | Prefer system colors or explicitly theme and contrast-test custom drawing. |
Treating ObservableCollection<T> as a drop-in WinForms list source | Use BindingList<T> or an adapter that bridges collection and property notifications. |
| Assuming WPF binding or converter features exist | Use WinForms Binding, BindingSource, Format, and Parse; verify TFM-specific APIs. |
| Fire-and-forget UI marshaling or uncaught async event errors | Await InvokeAsync; catch and surface failures in async void UI entry points. |
Inventing Program.vb for a VB application | Use the VB Application Framework and ApplicationEvents.vb conventions. |
| Changing DPI/theme/TFM/package policy during a UI fix | Preserve project policy unless the requested behavior requires and validates the change. |