Claude
Skills
Sign in
Back

make-wpf-custom-control

Included with Lifetime
$97 forever

Generates WPF CustomControl C# class and XAML ControlTemplate style from a control name. Use when creating a new custom control, scaffolding a templated control, or generating CustomControl boilerplate code. Usage: /wpf-dev-pack:make-wpf-custom-control <ControlName>

General

What this skill does


# WPF CustomControl Generation

**If `$0` is empty, use the AskUserQuestion tool to ask: "Enter the CustomControl name (e.g., CircularProgress, RangeSlider)". Do NOT proceed until a valid name is provided. Use the response as the ControlName for all subsequent steps.**

Generate a `$0` CustomControl.

- Replace `{BaseClass}` with the appropriate WPF base class (e.g., Control, Button, ContentControl, ItemsControl) based on the control name and context.
- Replace `{Namespace}` with the project's root namespace detected from csproj or existing code.
- If the host project follows a non-default style convention (e.g. block-scoped namespaces, custom usings), conform to it.

## Workflow

### Step 1: Validate Input

- `$0` must be PascalCase
- Determine the best BaseClass based on `$0` name and intended usage

### Step 2: Generate C# Class File

Create `$0.cs`:

```csharp
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;

namespace {Namespace}.Controls;

/// <summary>
/// $0 - Custom WPF control based on {BaseClass}.
/// </summary>
[TemplatePart(Name = TemplateParts.Root, Type = typeof(Border))]
[TemplateVisualState(GroupName = VisualStates.CommonStates, Name = VisualStates.Normal)]
[TemplateVisualState(GroupName = VisualStates.CommonStates, Name = VisualStates.MouseOver)]
[TemplateVisualState(GroupName = VisualStates.CommonStates, Name = VisualStates.Pressed)]
[TemplateVisualState(GroupName = VisualStates.CommonStates, Name = VisualStates.Disabled)]
public class $0 : {BaseClass}
{
    // Single source of truth for Template Part names.
    // XAML <Border x:Name="…"> literals MUST match these constants.
    private static class TemplateParts
    {
        public const string Root = "PART_Root";
    }

    // Single source of truth for VSM group/state names.
    // XAML <VisualStateGroup x:Name="…"> / <VisualState x:Name="…"> literals
    // MUST match these constants exactly. The compiler will not catch a mismatch
    // and runtime GoToState will return false silently.
    private static class VisualStates
    {
        public const string CommonStates = "CommonStates";
        public const string Normal       = "Normal";
        public const string MouseOver    = "MouseOver";
        public const string Pressed      = "Pressed";
        public const string Disabled     = "Disabled";
    }

    #region Constructors

    static $0()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof($0),
            new FrameworkPropertyMetadata(typeof($0)));
    }

    public $0()
    {
        // IsEnabledChanged is an EVENT — there is no OnIsEnabledChanged to
        // override (Control/UIElement exposes no such virtual). Subscribe here.
        IsEnabledChanged += (_, _) => UpdateVisualState(true);
    }

    #endregion

    #region Dependency Properties

    /// <summary>
    /// Example dependency property.
    /// </summary>
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(
            nameof(Value),
            typeof(int),
            typeof($0),
            new FrameworkPropertyMetadata(
                defaultValue: 0,
                flags: FrameworkPropertyMetadataOptions.AffectsRender,
                propertyChangedCallback: OnValueChanged,
                coerceValueCallback: CoerceValue));

    public int Value
    {
        get => (int)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is $0 control)
        {
            control.OnValueChanged((int)e.OldValue, (int)e.NewValue);
        }
    }

    protected virtual void OnValueChanged(int oldValue, int newValue)
    {
        // Handle property change
    }

    // Multi-constraint coerce: relational constraints first, hard domain LAST.
    // See authoring-wpf-controls §4 "Multi-Constraint Coerce Ordering".
    private static object CoerceValue(DependencyObject d, object baseValue)
    {
        var control = ($0)d;
        var v = (int)baseValue;

        // (Add any relational constraints that depend on other properties first.)

        // Hard domain clamp LAST so transient cross-property states cannot leak
        // a value outside the legal domain.
        v = Math.Clamp(v, 0, 100);

        return v;
    }

    #endregion

    #region Read-only Dependency Property (optional)

    // Read-only DPs expose internal state for binding while preventing external writes.
    // Replace with your real read-only property or remove this region.
    private static readonly DependencyPropertyKey IsBusyPropertyKey =
        DependencyProperty.RegisterReadOnly(
            nameof(IsBusy),
            typeof(bool),
            typeof($0),
            new PropertyMetadata(false));

    public static readonly DependencyProperty IsBusyProperty = IsBusyPropertyKey.DependencyProperty;

    public bool IsBusy
    {
        get => (bool)GetValue(IsBusyProperty);
        private set => SetValue(IsBusyPropertyKey, value);
    }

    #endregion

    #region Routed Event (optional)

    // Replace with your real routed event or remove this region.
    public static readonly RoutedEvent ValueChangedEvent =
        EventManager.RegisterRoutedEvent(
            nameof(ValueChanged),
            RoutingStrategy.Bubble,
            typeof(RoutedPropertyChangedEventHandler<int>),
            typeof($0));

    public event RoutedPropertyChangedEventHandler<int> ValueChanged
    {
        add    => AddHandler(ValueChangedEvent, value);
        remove => RemoveHandler(ValueChangedEvent, value);
    }

    protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<int> e)
        => RaiseEvent(e);

    #endregion

    #region Template Parts

    private Border? _partRoot;
    private bool _isPressed;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        // OnApplyTemplate fires BEFORE Loaded. Avoid duplicating init logic in both —
        // do template-binding setup here, and only do Loaded-time work in Loaded.
        _partRoot = GetTemplateChild(TemplateParts.Root) as Border;

        // Template-Part tolerance: if PART_Root is missing, disable only the
        // feature that depends on it. Do NOT throw — see authoring-wpf-controls §3.1.
        if (_partRoot is null)
        {
            // Optional: log a designer-only warning here if you want.
        }

        UpdateVisualState(false);
    }

    #endregion

    #region Visual States

    private void UpdateVisualState(bool useTransitions)
    {
        // States declared via [TemplateVisualState] MUST be reachable here,
        // otherwise the attribute is a documentation lie and the state never fires.
        string state =
            !IsEnabled  ? VisualStates.Disabled  :
            _isPressed  ? VisualStates.Pressed   :
            IsMouseOver ? VisualStates.MouseOver :
                          VisualStates.Normal;

        VisualStateManager.GoToState(this, state, useTransitions);
    }

    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        UpdateVisualState(true);
    }

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        UpdateVisualState(true);
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);
        _isPressed = true;
        UpdateVisualState(true);
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonUp(e);
        _isPressed = false;
        UpdateVisualState(true);
    }

    // Note: IsEnabled changes are handled via the IsEnabledChanged event
    // subscribed in the constructor (there is no OnIsEnabledChanged to override).

    #endregion
}
```

### Step 3: Generate XAML Style File

Creat

Related in General