The idea. Instead of one heavy base class handing every control a uniform (and mostly unused) feature set, each capability is a tiny opt-in interface. This is the WPF-idiomatic, industry-standard pattern (WpfUI's IIconControl/IAppearanceControl are the direct analogues). Interfaces live in Controls/Theming/IBeep*.cs; IBeepValidatable lives at the project root.

The four interfaces

Pick the interfaces a control needs; leave the rest off the implements list. Most controls implement IBeepThemeable; the others are added only when the control genuinely hosts that capability.

InterfaceShapeImplement when the control…
IBeepThemeableMarkershould re-resolve theme tokens when the global theme/skin changes — nearly every control.
IBeepAppearanceMarkerhas a template that responds to a BeepAppearance preset (Primary, Secondary, Danger, …).
IBeepIconHostIcon membersexposes leading/trailing icon slots — satisfied implicitly by public members.
IBeepValidatableValidation membersparticipates in the field-presenter validation flow (required, editable, error text, validate hook).

Two of these — IBeepThemeable and IBeepAppearance — are pure markers: they declare no members. Their work happens in XAML. The template of an IBeepAppearance control binds Background/BorderBrush through Triggers on the appearance value; an IBeepThemeable control simply lets its DynamicResource chrome tokens re-resolve when the skin swaps. The interface is the signal ("this control participates"); the behaviour is data-driven.

IBeepThemeable

The base capability — a marker that a control participates in the theming pipeline. It has no members because theming is entirely token-driven: the control's template reads {DynamicResource BeepName.*} tokens, and when BeepChrome.ControlStyle or Theme changes those resources re-resolve automatically.

namespace TheTechIdea.Beep.WPF.Controls.Theming;

/// <summary>Implemented by every control that needs to re-resolve
/// theme-bound resources when the global theme changes.</summary>
public interface IBeepThemeable
{
}
MemberMeaning
(none)Marker interface. Participation is declared by implementing it; the re-skin work is done by BeepChrome + DynamicResource.
When to implement: almost always. Any control with themed chrome should carry IBeepThemeable. Pair it with the BeepChrome CLR wrappers (Theme, ControlStyle) so consumers can set the skin per-control.

IBeepAppearance

A marker that the control's template understands a BeepAppearance preset. Each appearance value maps to a pair of brushes defined by the active skin, selected via template triggers — so the C# side stays empty while the XAML side switches Background/Foreground per appearance.

namespace TheTechIdea.Beep.WPF.Controls.Theming;

/// <summary>Shared appearance surface for controls that expose a
/// BeepAppearance plus hover/pressed state brushes. The XAML template binds
/// Background/BorderBrush via Triggers on the appearance value.</summary>
public interface IBeepAppearance
{
}
MemberMeaning
(none)Marker interface. The appearance value drives a <Trigger Property="Appearance" …> chain in the template; each BeepAppearance case retargets the chrome brushes.
When to implement: for controls with semantic colour presets — buttons, badges, chips, notifications — where a single Appearance value should switch the whole colour treatment (Primary → accent, Danger → error, and so on).

IBeepIconHost

Declares that a control hosts leading and/or trailing icon slots. The icons are plain strings resolved by the icon pipeline (embedded SVG name, pack URI, file path, or inline path geometry) — see Icons & BeepImage. The template renders them with BeepImage inside the chrome border.

// Implemented with PUBLIC members — the interface is satisfied implicitly.
public partial class BeepTextBox : System.Windows.Controls.TextBox,
                                   IBeepValidatable, IBeepIconHost
{
    public string LeadingIcon  { get => (string)GetValue(LeadingIconProperty);  set => SetValue(LeadingIconProperty, value); }
    public string TrailingIcon { get => (string)GetValue(TrailingIconProperty); set => SetValue(TrailingIconProperty, value); }
    public double IconSize     { get => (double)GetValue(IconSizeProperty);     set => SetValue(IconSizeProperty, value); }
}
MemberTypeMeaning
LeadingIconstringIcon shown before the content. Empty string (the default) means "no icon" — the template collapses the slot via a string-to-visibility converter.
TrailingIconstringIcon shown after the content (e.g. a clear/search affordance). Same empty-string convention.
IconSizedoubleEdge length in DIPs for both icon slots.
When to implement: for any control with icon affordances beside its content — text inputs, buttons, list rows. Declare the three members as public dependency-property wrappers; that satisfies IBeepIconHost implicitly, so no explicit interface implementation is required. Icons must sit inside the themed RootBorder, not as siblings, or they render with no chrome.

IBeepValidatable

The capability that lets every input control expose the same validation surface — required flag, editable flag, error state, tooltip/hint, and a ValidateData hook — without forcing a common base class. The field-presenter flow drives all of it: it flips IsEditable off for read-only fields, calls ValidateData, and pushes the message into ValidationError / HasError.

public interface IBeepValidatable
{
    string ComponentName    { get; set; }
    bool   IsRequired       { get; set; }
    bool   IsEditable       { get; set; }
    string ToolTipText      { get; set; }
    bool   HasError         { get; set; }
    string ValidationError  { get; set; }
    bool   ValidateData(out string message);
}
MemberTypeMeaning
ComponentNamestringUnique component id the field-presenter flow uses to map the editor back to a data-source field name.
IsRequiredboolTrue when the host marks the field required. Bindable so the template can show a marker (asterisk, red outline).
IsEditableboolTrue when the user may edit the value. The host flips it off for read-only / identity fields.
ToolTipTextstringHint / prompt text surfaced via the tooltip (and, for native-text editors, the watermark).
HasErrorboolTrue when ValidationError is non-empty; the template binds it to render an error state.
ValidationErrorstringUser-facing error message, set by the host after ValidateData reports a failure.
ValidateData(out string message)boolComputes validity. Returns true with an empty message on success; false and a user-facing reason on failure.
When to implement: for input editors that participate in the field-presenter validation flow — BeepTextBox, BeepDatePicker, BeepNumericUpDown, BeepComboBox, BeepCheckBox. The ContentControl-based text editors inherit the implementation; the native-primitive editors (ComboBox, CheckBox) implement the members directly on the class.

Explicit vs public implementation — the IsEditable clash

Most capability members are declared as ordinary public dependency-property wrappers, which satisfy the interface implicitly. The one trap is IBeepValidatable.IsEditable: some native bases already have an IsEditable property, so re-declaring a public one shadows the base member (a CS0108 warning) and can bind to the wrong thing.

ControlNative baseHow IsEditable is satisfied
BeepTextBox, BeepDatePicker, BeepNumericUpDown TextBox / ContentControlPublic DP wrapper — no native IsEditable, so the plain public member satisfies the interface.
BeepComboBoxComboBoxThe base already has ComboBox.IsEditable. The control does not re-declare it — the inherited native member satisfies IBeepValidatable.IsEditable, and only the other members (IsRequired, HasError, …) get public DPs.
BeepCheckBoxCheckBoxCheckBox has no native IsEditable, so the control declares its own public IsEditable DP.
The rule. When a native base already provides an interface member, let the inherited member satisfy the interface rather than shadowing it — or, if you must add a distinct property, use an explicit interface implementation (bool IBeepValidatable.IsEditable { get; set; }) for the one clashing member and keep the rest public. If a static DependencyProperty field name collides, new silences CS0108 (static fields are not interface members). The same discipline applies to IBeepIconHost: prefer public members so the interface is satisfied implicitly, and only reach for explicit implementation when a name genuinely clashes with the native base.