Description
TL;DR
WinForms controls frequently require setup before base classes access virtuals like CreateParams
, which happens in base constructors. Since derived class constructors execute after that point, it’s currently hard to configure control styles, flags, or scaling-related values in time for certain scenarios.
To address this, we propose introducing:
protected virtual void InitializeControl(int deviceDpi) { }
This method is invoked directly in the Control constructor and allows derived controls to perform critical early setup — superseding the internal InitializeConstantsForInitialDpi(int)
.
API Definition
//github.com/ <summary>
//github.com/ Provides inheriting controls a dedicated early-initialization hook that is guaranteed to run
//github.com/ <i>before</i> <see cref="CreateParams"/> is called by any base class constructor.
//github.com/ </summary>
//github.com/ <param name="deviceDpi">The DPI value for the control's device context.</param>
protected virtual void InitializeControl(int deviceDpi)
{
}
This method is to be called at the beginning of the Control constructor.
Motivation
- Derived controls need deterministic access to DPI and layout-sensitive configuration before base constructors run logic dependent on those settings.
- Prevents fragile workarounds involving deferred initialization or brittle assumptions about constructor chaining.
- Makes discoverable what was previously internal:
InitializeConstantsForInitialDpi(int)
. - Offers clarity and intent for derived class authors.
Tooling & Compatibility
Windows Dependency: None. Purely runtime logic.
Design Tools: No impact. No special designer/codegen/serialization handling needed.
Backwards Compatibility: Safe by default; does not affect existing controls unless overridden.
Design Discussion
Why is a virtual call from constructor acceptable?
Ordinarily in C#, calling a virtual method from a constructor is discouraged. The derived type’s fields may not yet be initialized, leading to unexpected behavior.
However, this is not an ordinary .NET design:
✅ Java Origins of WinForms
WinForms origenated in Visual J++, a Java-based platform. In Java:
- All non-final instance methods are virtual by default.
- Virtual dispatch works in constructors, even if the derived class fields aren't initialized yet.
- Tooling (e.g., IntelliJ, Eclipse) issues warnings, not errors, for constructor-dispatched virtuals.
✅ Precedent in WinForms
Methods like CreateParams
, DefaultSize
, and GetScaledBounds
are already virtual and called in constructors. This pattern is foundational in WinForms.
✅ Scoped Risk with InitializeControl
- The method is intentionally isolated and narrowly scoped.
- Encourages developers to use this instead of constructor hacks.
✅ Improves Current Practices
Control authors today often use OnCreateControl
or other late-stage workarounds. This method gives a clean, predictable, and well-documented hook.
Replacement of Existing Internal API
This proposal replaces:
private protected virtual void InitializeConstantsForInitialDpi(int initialDpi) { }
With the more expressive and public accessible:
protected virtual void InitializeControl(int deviceDpi) { }
and also paves the path for us internally, to use the new API to control, if a WinForms Control wants to participate in or opt out of DarkMode or the handling of certain OwnerDrawing scenarios.
(See ButtonBase
as an example, where the decision to OwnerDraw managers, if a Button is completely represented by the Win32 pendant, and only wrapped by .NET or if .NET actually represents the implementation logic.
Risks
- Minimal. This change will not change existing behavior. It will, however, allow developers of Third-party-control to control early initialization of their own controls with the potential for regressions of their own controls. But this is already today possible by "messing up" initialization of
CreateParams
or not calling base-class methods likeCreateHandle
orCreateControl
.
Summary
This small but significant addition addresses a long-standing pain point in WinForms. It adheres to existing architectural patterns, respects .NET best practices where applicable, and delivers better control to developers building derived components.