Rationale
WinForms developers commonly need to perform a burst of UI mutations: adding many items, moving many child controls, changing layout-affecting properties, or showing a form whose initial background and child controls are not ready at the same time. Today, applications rely on a mix of control-specific BeginUpdate / EndUpdate, SuspendLayout / ResumeLayout, hand-written WM_SETREDRAW, and direct Win32 DeferWindowPos calls.
Those techniques work, but they are inconsistent, easy to misbalance, and often require application code to understand HWND-level details. This proposal adds a small set of composable WinForms APIs for the common cases:
- suspend painting for a control while a synchronous mutation runs;
- suspend relocation/layout work for a control while a synchronous mutation runs;
- batch child HWND relocation through
DeferWindowPos while composing painting and layout suspension; and
- defer top-level form reveal to reduce the initial default-background flash, especially noticeable in dark-mode applications.
The goal is not to make the WinForms layout engine faster. The goal is to reduce intermediate visible states and collapse avoidable work during an already synchronous UI-thread mutation.
API Proposal
namespace System.Windows.Forms;
public interface ISupportSuspendPainting
{
void BeginSuspendPainting();
void EndSuspendPainting();
}
public interface ISupportSuspendRelocation
{
void BeginSuspendRelocation();
void EndSuspendRelocation();
}
namespace System.Windows.Forms;
public readonly ref struct SuspendPaintingScope
{
public SuspendPaintingScope(ISupportSuspendPainting? target);
public void Dispose();
}
public readonly ref struct SuspendRelocationScope
{
public SuspendRelocationScope(ISupportSuspendRelocation? target);
public void Dispose();
}
public static class ControlMutationExtensions
{
public static SuspendPaintingScope SuspendPainting(this ISupportSuspendPainting? target);
public static SuspendRelocationScope SuspendRelocation(this ISupportSuspendRelocation? target);
}
namespace System.Windows.Forms;
public partial class Control : ISupportSuspendPainting, ISupportSuspendRelocation
{
public virtual void BeginSuspendPainting();
public virtual void EndSuspendPainting();
public virtual void BeginSuspendRelocation();
public virtual void EndSuspendRelocation();
public DeferLocationChangeScope DeferLocationChange();
public DeferLocationChangeScope DeferLocationChange(bool suppressRender);
public DeferLocationChangeScope DeferLocationChange(bool suppressRender, bool suspendLayout);
}
namespace System.Windows.Forms;
public readonly ref struct DeferLocationChangeScope
{
public DeferLocationChangeScope(Control parent);
public DeferLocationChangeScope(Control parent, bool suppressRender);
public DeferLocationChangeScope(Control parent, bool suppressRender, bool suspendLayout);
public void Defer(Control control, int x, int y);
public void Defer(Control control, int x, int y, int width, int height);
public void Defer(Control control, Rectangle bounds);
public void Dispose();
}
namespace System.Windows.Forms;
public partial class ListView
{
public override void BeginSuspendPainting();
public override void EndSuspendPainting();
}
public partial class ListBox
{
public override void BeginSuspendPainting();
public override void EndSuspendPainting();
}
public partial class ComboBox
{
public override void BeginSuspendPainting();
public override void EndSuspendPainting();
}
public partial class TreeView
{
public override void BeginSuspendPainting();
public override void EndSuspendPainting();
}
public partial class RichTextBox
{
public override void BeginSuspendPainting();
public override void EndSuspendPainting();
}
namespace System.Windows.Forms;
public enum FormAppearanceMode
{
Classic = 0,
Deferred = 1,
}
public partial class Application
{
public static FormAppearanceMode FormAppearanceMode { get; }
public static void SetFormAppearanceMode(FormAppearanceMode mode);
}
API Usage
using (buttonPanel.SuspendPainting())
using (buttonPanel.SuspendRelocation())
{
foreach (Button button in buttonPanel.Controls.OfType<Button>())
{
button.Text = GetUpdatedText(button);
button.Width += 20;
}
}
using DeferLocationChangeScope scope = parent.DeferLocationChange(
suppressRender: true,
suspendLayout: true);
for (int i = 0; i < parent.Controls.Count; i++)
{
Control child = parent.Controls[i];
scope.Defer(child, x: 0, y: i * child.Height, width: parent.Width, height: child.Height);
}
Application.SetFormAppearanceMode(FormAppearanceMode.Deferred);
Application.SetColorMode(SystemColorMode.Dark);
Application.Run(new MainForm());
Application.SetFormAppearanceMode(FormAppearanceMode.Classic);
Application.Run(new SplashScreenForm());
Alternative Designs
Heap-allocated IDisposable scope classes
The scope APIs could return IDisposable class instances. This is more flexible for helper libraries that store scopes, pass them through interfaces, or compose them with APIs that require IDisposable.
The downside is that leaked scopes become runtime bugs. A leaked painting or relocation suspension can leave a control visually stale or with deferred layout state. Because these APIs are intended for synchronous "mutate now" UI-thread code, this proposal recommends readonly ref struct scopes so the compiler prevents storing, boxing, capturing, and crossing async or iterator boundaries.
Default interface implementations
ISupportSuspendPainting could provide default implementations and keep Control untouched. That would still require per-instance refcount state. A ConditionalWeakTable or similar side table would be more indirect and more expensive than storing state on Control itself. It would also not help controls that need native BeginUpdate / EndUpdate behavior.
Only expose extension methods, not interfaces
Extension methods on Control would cover today's HWND controls, but would not give future custom controls or HWND-less visual implementations a clear contract to implement. The interfaces keep the contract independent of Control.
Use only SuspendLayout
SuspendLayout prevents layout computation but does not suppress painting and does not batch native window-position changes. It is necessary but not sufficient for flicker-free relocation-heavy mutations.
Make Deferred the enum zero value
Deferred is the desired .NET 11 runtime default when the API is never called, but Classic = 0 keeps default(FormAppearanceMode) conservative and preserves the usual .NET enum guidance. This mirrors a deliberate distinction between CLR default value and WinForms runtime default.
Risks
- Ref-count imbalance remains possible when callers invoke
Begin* / End* directly. The scope APIs make the ergonomic path safer, but the public methods must still tolerate nesting and direct calls.
WM_SETREDRAW suppresses painting for HWND-backed controls but does not make arbitrary custom drawing atomic. Controls can still invalidate or repaint after the scope exits.
- Existing controls with public
BeginUpdate / EndUpdate methods must keep source and binary compatibility. Their suspension overrides should route through existing update behavior rather than changing those public APIs.
DeferWindowPos has sharp lifetime semantics. Every DeferWindowPos call returns the current HDWP and failure returns NULL, losing the batch. The implementation must never call EndDeferWindowPos with NULL and must define whether it falls back to individual SetWindowPos calls or cleanly aborts the lost batch.
DeferLocationChange improves native move throughput and collapses layout/paint churn, but it does not improve the layout engine algorithm itself.
FormAppearanceMode.Deferred depends on DWM support. On unsupported systems or unsupported window kinds, it must be inert and preserve classic behavior.
- Deferred form reveal can reduce the default-background flash, but a deep tree of late-painting child controls can still update visibly after reveal.
- Uncloak timing is the hardest part of the form behavior. Uncloaking too early preserves the flash; uncloaking too late makes startup feel slower.
- Special cases such as MDI children, owned/tool windows, splash screens, layered/opacity windows, per-monitor DPI changes during creation, and handle recreation need conservative handling.
- All APIs are UI-thread-oriented and do not make WinForms controls thread-safe.
Will this feature affect UI controls?
Yes. The painting and relocation APIs are implemented by Control and selected existing controls. They affect rendering and layout timing but do not change designer serialization, accessibility names/roles, localization, or existing layout results.
FormAppearanceMode.Deferred affects top-level form startup presentation. It should be documented as a visual startup behavior, not an accessibility feature. It should preserve accessibility/UIA object creation and not hide forms from accessibility clients longer than necessary. It has no localization requirements.
The new APIs should be available to designer-generated code because InitializeComponent lives in the user's assembly. The scope-based APIs are runtime helpers and should not require CodeDOM property serialization.
Status Checklist
Rationale
WinForms developers commonly need to perform a burst of UI mutations: adding many items, moving many child controls, changing layout-affecting properties, or showing a form whose initial background and child controls are not ready at the same time. Today, applications rely on a mix of control-specific
BeginUpdate/EndUpdate,SuspendLayout/ResumeLayout, hand-writtenWM_SETREDRAW, and direct Win32DeferWindowPoscalls.Those techniques work, but they are inconsistent, easy to misbalance, and often require application code to understand HWND-level details. This proposal adds a small set of composable WinForms APIs for the common cases:
DeferWindowPoswhile composing painting and layout suspension; andThe goal is not to make the WinForms layout engine faster. The goal is to reduce intermediate visible states and collapse avoidable work during an already synchronous UI-thread mutation.
API Proposal
API Usage
Alternative Designs
Heap-allocated
IDisposablescope classesThe scope APIs could return
IDisposableclass instances. This is more flexible for helper libraries that store scopes, pass them through interfaces, or compose them with APIs that requireIDisposable.The downside is that leaked scopes become runtime bugs. A leaked painting or relocation suspension can leave a control visually stale or with deferred layout state. Because these APIs are intended for synchronous "mutate now" UI-thread code, this proposal recommends
readonly ref structscopes so the compiler prevents storing, boxing, capturing, and crossing async or iterator boundaries.Default interface implementations
ISupportSuspendPaintingcould provide default implementations and keepControluntouched. That would still require per-instance refcount state. AConditionalWeakTableor similar side table would be more indirect and more expensive than storing state onControlitself. It would also not help controls that need nativeBeginUpdate/EndUpdatebehavior.Only expose extension methods, not interfaces
Extension methods on
Controlwould cover today's HWND controls, but would not give future custom controls or HWND-less visual implementations a clear contract to implement. The interfaces keep the contract independent ofControl.Use only
SuspendLayoutSuspendLayoutprevents layout computation but does not suppress painting and does not batch native window-position changes. It is necessary but not sufficient for flicker-free relocation-heavy mutations.Make
Deferredthe enum zero valueDeferredis the desired .NET 11 runtime default when the API is never called, butClassic = 0keepsdefault(FormAppearanceMode)conservative and preserves the usual .NET enum guidance. This mirrors a deliberate distinction between CLR default value and WinForms runtime default.Risks
Begin*/End*directly. The scope APIs make the ergonomic path safer, but the public methods must still tolerate nesting and direct calls.WM_SETREDRAWsuppresses painting for HWND-backed controls but does not make arbitrary custom drawing atomic. Controls can still invalidate or repaint after the scope exits.BeginUpdate/EndUpdatemethods must keep source and binary compatibility. Their suspension overrides should route through existing update behavior rather than changing those public APIs.DeferWindowPoshas sharp lifetime semantics. EveryDeferWindowPoscall returns the currentHDWPand failure returnsNULL, losing the batch. The implementation must never callEndDeferWindowPoswithNULLand must define whether it falls back to individualSetWindowPoscalls or cleanly aborts the lost batch.DeferLocationChangeimproves native move throughput and collapses layout/paint churn, but it does not improve the layout engine algorithm itself.FormAppearanceMode.Deferreddepends on DWM support. On unsupported systems or unsupported window kinds, it must be inert and preserve classic behavior.Will this feature affect UI controls?
Yes. The painting and relocation APIs are implemented by
Controland selected existing controls. They affect rendering and layout timing but do not change designer serialization, accessibility names/roles, localization, or existing layout results.FormAppearanceMode.Deferredaffects top-level form startup presentation. It should be documented as a visual startup behavior, not an accessibility feature. It should preserve accessibility/UIA object creation and not hide forms from accessibility clients longer than necessary. It has no localization requirements.The new APIs should be available to designer-generated code because
InitializeComponentlives in the user's assembly. The scope-based APIs are runtime helpers and should not require CodeDOM property serialization.Status Checklist
api-suggestionlabelapi-ready-for-reviewblockinglabel to expedite the review appointmentapi-approved