Class Control
Base class for controls that need background/border decoration without reimplementing it - composes an internal element (Chrome, a Border by default) that fills this control's entire ActualBounds and forwards Background/BorderBrush/ BorderThickness/Padding to it.
public class Control : UIElement, INotifyPropertyChanging, IBindingTarget, INotifyPropertyChanged, INotifyFocusChanged, IContainerLayout
- Inheritance
-
Control
- Implements
- Derived
- Inherited Members
- Extension Methods
Remarks
UIElement itself carries no decoration (per the Border extraction) - this class exists so every decorated control doesn't have to compose its own Border by hand. Subclasses that need child content should derive from ContentControl instead, which exposes Content as Chrome's content.
Setting Template replaces Chrome with markup content of the template author's
choosing instead of the built-in Border - see Template's own remarks. Any
future property added to this class that currently forwards into Chrome the way
Background/BorderBrush/BorderThickness/Padding do
must gain the same dual-path treatment those four have (an untemplated branch forwarding into
(Border)Chrome, a templated branch storing locally) - there is no generic mechanism that does this
automatically, so it's easy to forget.
The untemplated branch of that same dual-path treatment also participates in the value-precedence system
(see PropertyValuePrecedence): a direct C# assignment to
Background/BorderBrush/BorderThickness/Padding
while untemplated now raises this control's own PropertyChanged
(guarded by an equality check, so re-assigning the current value is a no-op), so it registers as a local
value that outranks a later-applied Style/VisualState/animation
setter for the same property - exactly like every other [RegisterReference] property on this class.
A local value blocks every precedence tier below it outright, not just Style - a
control that locally sets one of these four properties in its own constructor (the common pattern for a
custom-themed sub-widget, e.g. a Slider's gradient-filled Background) permanently opts that
property out of its type's own theme, including any VisualState that targets it.
Constructors
Control()
Initializes a new instance of the Control class.
public Control()
Properties
Background
Gets or sets the background brush drawn behind this control's content.
public IBrush Background { get; set; }
Property Value
BorderBrush
Gets or sets the brush used to paint this control's border.
public IBrush? BorderBrush { get; set; }
Property Value
BorderThickness
Gets or sets the thickness of this control's border, and the amount by which its content is inset from ContentBounds.
public Thickness BorderThickness { get; set; }
Property Value
Chrome
Gets the internal element this control composes for its background/border decoration - a Border by default, or Template's instantiated content when one is set.
protected UIElement Chrome { get; }
Property Value
ContentBounds
Gets the area available to this control's decorated content, i.e. this control's own ActualBounds inset by BorderThickness and Padding.
public Rectangle ContentBounds { get; }
Property Value
Remarks
Computed from this control's own ActualBounds, not Chrome's - normally the two agree (Chrome fills ActualBounds exactly), but a subclass like ScrollViewer deliberately arranges Chrome larger than this control's own bounds (so Content can overflow for scrolling) - callers of ContentBounds mean this control's own visible content area, not however big Chrome currently happens to be.
Padding
Gets or sets the padding of this control, i.e. the amount by which its content is inset from Chrome's own bounds (inside BorderThickness).
public Thickness Padding { get; set; }
Property Value
Remarks
Hides (rather than overrides - Padding isn't virtual) Padding to forward it to Chrome when untemplated, the same way Background/BorderBrush/BorderThickness already do - so Background/BorderBrush (drawn across Chrome's full bounds, unaffected by padding - the standard box model) cover this control's whole area including the padding band, and only the actual content ends up inset by it. Measure()'s own generic padding step still applies exactly once when untemplated - to Chrome, when MeasureContent() calls Chrome's Measure() - since this control's own (hidden, always-zero) base Padding field is never written to.
Template
Gets or sets the template that replaces this control's default decorated visual tree (Chrome).
public ControlTemplate? Template { get; set; }
Property Value
Remarks
null (the default) keeps this control's built-in Border-based
decoration, with Background/BorderBrush/BorderThickness/
Padding forwarding into it exactly as always. Setting a template swaps Chrome
for markup content of the template author's choosing instead - those same four properties then store
locally, reflected into the template's content only where it explicitly asks for them via
{TemplateBinding} (see TemplateBindingExtension). Their
values are preserved across the switch either way.
Simple decorated controls like Button/ContentControl/CheckBox/
ToggleButton are always template-safe. Structural controls whose own code reaches into
their visual tree by name - such as Slider (PART_Thumb) or
ProgressBar (PART_Fill) - are safe too, via named template parts: see
OnApplyTemplate()/GetTemplateChild<T>(string). A control that hasn't been
retrofitted with an OnApplyTemplate() override yet - currently ScrollViewer,
TextBox, ColorPicker, and ColorPickerButton - still accepts a
Template without throwing, but its own behavior stays wired to its built-in default parts
regardless of what the template actually provides.
Exceptions
- ArgumentException
value's own target type isn't assignable to this control's runtime type.
Methods
ArrangeContent()
Handles custom arrange logic when overridden in any UIElement instance.
protected override void ArrangeContent()
Remarks
This method is called during the arrange pass to position and size the element's content within its final bounds. The element's position and size are already determined by the layout system and available in the ActualBounds property.
When implementing this method:
- Position child elements within the available space;
- Respect the element's padding when positioning content;
- Handle any content-specific layout requirements;
- Call Arrange() on child elements if needed.
CaptureTemplateState()
Captures whatever subclass-specific state needs to survive a Template swap, beyond the four decoration properties this class already preserves on its own.
protected virtual object? CaptureTemplateState()
Returns
- object
Opaque state later passed back to RestoreTemplateState(object?).
Remarks
Returns null by default. ContentControl overrides this pair to also preserve Content across a swap. This is also the seed of what a future named-template-parts addition is expected to build on: it runs once per swap, at whichever of the two points RestoreTemplateState(object?)'s own remarks describe - exactly where discovering a new template's named parts would need to happen too.
GetHostedElement(object?)
Gets the element, if any, that the old Chrome displays but doesn't own, so a Template swap leaves its bindings alone.
protected virtual UIElement? GetHostedElement(object? state)
Parameters
stateobjectThe state CaptureTemplateState() returned before the swap.
Returns
Remarks
A swap unbinds every element of the old Chrome, so its {TemplateBinding}s stop
following this control. Elements the control only hosts there belong to the application, and so do their
{Binding}s. They move on to the new Chrome and must keep working.
ContentControl returns its Content.
GetTemplateChild<T>(string)
Finds a named element inside the current Template's content - a "template part".
protected T? GetTemplateChild<T>(string name) where T : UIElement
Parameters
namestringThe name to look up, as declared by
x:Nameon an element inside the template's markup. No naming convention is enforced by this method itself, but the established one (mirroring WPF) is aPART_prefix, e.g.PART_Thumb- document it on whichever property/field a subclass exposes the found element through.
Returns
- T
The named element, or null when Template is null, the template has no element by that name, or it isn't a
T.
Type Parameters
TThe type the named element is expected to have.
Remarks
Deliberately does not reuse FindControl<T>(UIElement, string)
- that extension searches from
element.GetRoot(), which would walk straight past Chrome up to whatever document this control itself is attached inside, missing the template's own isolated MarkupNameScope entirely (or, worse, silently matching an unrelated same-named element elsewhere in that outer document). This looks up GetScope(UIElement) directly on Chrome instead, which is exactly the root LoadTemplateContent(XElement, UIElement, string?) attaches the template's own scope to.
GetVisualChildren()
Enumerates this element's immediate visual children, in paint order (back-to-front), for hit-testing and tree traversal.
protected override IEnumerable<UIElement> GetVisualChildren()
Returns
- IEnumerable<UIElement>
The element's immediate children. The base implementation yields none (a leaf element).
Remarks
Container elements (e.g. Panel, Border) override this to expose their children, in the exact order they're drawn in — HitTest(Vector2) depends on this matching actual paint order, or clicks will target the wrong, visually-obscured element.
MeasureContent()
Handles custom measure logic when overridden in any UIElement instance.
protected override Size MeasureContent()
Returns
Remarks
This method is called during the measure pass to determine the desired size of the element. The returned size should represent the natural size of the element's content, without considering any constraints, margins, or padding.
When implementing this method:
- Return the natural size needed to display the content;
- Do not include padding, margins, or borders in the returned size;
- Consider the content's actual dimensions (e.g., text length, image size);
- Return Empty if the element has no content.
OnApplyTemplate()
Called once, every time Template changes (in either direction - applied, cleared, or switched to a different template), right after Chrome and every decoration/content property have been fully re-wired for the new state.
protected virtual void OnApplyTemplate()
Remarks
Does nothing by default. A subclass that reaches into its own visual tree by reference - e.g. Slider wiring its drag logic directly to its thumb element - overrides this to call GetTemplateChild<T>(string) and repoint that reference to whatever the new Template (or, when reverting, the fresh default Border) actually provides, instead of hardcoding it once at construction. This is what actually makes a control "template-safe": until a control overrides this appropriately, applying a custom Template to it silently orphans whatever elements its own code still holds direct references to.
OnAttached()
Raises the Attached event.
protected override void OnAttached()
Remarks
Propagates Canvas to every element in GetVisualChildren() - this is what makes attaching a whole pre-built subtree (the common pattern: build children, then attach the root once via Add(UIElement)) reach every descendant, not just the immediate root. Each child's own Canvas setter recurses into its children the same way, so this walks the full subtree regardless of depth. Container-specific child-add paths (e.g. OnChildAdded(object?, GenericEventArgs<UIElement>)) still assign Canvas directly for children added after the parent is already attached - this cascade is what covers everything built beforehand.
OnDetached()
Raises the Detached event.
protected override void OnDetached()
Remarks
Propagates null to every element in GetVisualChildren(), mirroring OnAttached() - see its remarks.
OnRender(IRenderContext)
Handles custom render logic when overridden in any UIElement instance.
protected override void OnRender(IRenderContext context)
Parameters
contextIRenderContextThe context to render with.
RestoreTemplateState(object?)
Restores state captured by CaptureTemplateState() after a Template swap.
protected virtual void RestoreTemplateState(object? state)
Parameters
stateobjectThe state CaptureTemplateState() returned before the swap.
Remarks
Called exactly once per swap, at one of two different points depending on direction - check Template (already the new value by the time this runs) to tell which:
-
Switching to a template (Template is non-null): called
before the new template's content is built, so a
{TemplateBinding}inside it captures the restored value on its very first read. Restore state directly into a backing field here, never through a public property setter that participates in the value-precedence system (mirrors how this class's own Background/BorderBrush/BorderThickness/ Padding are restored - see Template's setter for why). - Switching away from a template (Template is null): called after Chrome has already been replaced by the fresh default Border - restore state into that Border's own properties directly; it's brand new, so there's no precedence-poisoning risk there.