Back to list
christian289

developing-wpf-customcontrols

by christian289

ClaudeCode와 함께하는 .NET 개발 튜토리얼

1🍴 0📅 Jan 25, 2026

SKILL.md


name: developing-wpf-customcontrols description: "Develops WPF CustomControls using Parts and States Model best practices. Use when creating templatable controls with TemplatePart, TemplateVisualState, OnApplyTemplate, or VisualStateManager."

WPF CustomControl Development - Parts and States Model

Workflow for developing WPF CustomControls with appearance customization capability.

Development Flow

1. Control Inheritance Decision

UserControl Selection Criteria:
- Need rapid development
- ControlTemplate customization not required
- Complex theme support not required

Control Inheritance Selection Criteria:
- Need appearance customization via ControlTemplate
- Need various theme support
- Need same extensibility as WPF built-in controls

2. Define Control Contract

Declare TemplatePart and TemplateVisualState attributes on the class:

[TemplatePart(Name = PartUpButton, Type = typeof(RepeatButton))]
[TemplatePart(Name = PartDownButton, Type = typeof(RepeatButton))]
[TemplateVisualState(Name = StatePositive, GroupName = GroupValueStates)]
[TemplateVisualState(Name = StateNegative, GroupName = GroupValueStates)]
[TemplateVisualState(Name = StateFocused, GroupName = GroupFocusStates)]
[TemplateVisualState(Name = StateUnfocused, GroupName = GroupFocusStates)]
public class NumericUpDown : Control
{
    // Define Part/State names as const
    private const string PartUpButton = "PART_UpButton";
    private const string PartDownButton = "PART_DownButton";
    private const string GroupValueStates = "ValueStates";
    private const string GroupFocusStates = "FocusStates";
    private const string StatePositive = "Positive";
    private const string StateNegative = "Negative";
    private const string StateFocused = "Focused";
    private const string StateUnfocused = "Unfocused";
}

3. Template Part Property Pattern

Wrap Part elements as private properties, subscribe/unsubscribe events in setter:

private RepeatButton? _upButton;
private RepeatButton? UpButtonElement
{
    get => _upButton;
    set
    {
        // Unsubscribe from existing element's events
        if (_upButton is not null)
            _upButton.Click -= OnUpButtonClick;

        _upButton = value;

        // Subscribe to new element's events
        if (_upButton is not null)
            _upButton.Click += OnUpButtonClick;
    }
}

4. OnApplyTemplate Implementation

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

    // GetTemplateChild + as cast (null on type mismatch)
    UpButtonElement = GetTemplateChild(PartUpButton) as RepeatButton;
    DownButtonElement = GetTemplateChild(PartDownButton) as RepeatButton;

    // Set initial state (without transition)
    UpdateStates(useTransitions: false);
}

Core Principles:

  • If Part is missing or type differs, it's null → don't cause errors
  • Control must work even with incomplete ControlTemplate

5. UpdateStates Helper Method

Centralize state transition logic in a single method:

private void UpdateStates(bool useTransitions)
{
    // ValueStates group
    VisualStateManager.GoToState(this,
        Value >= 0 ? StatePositive : StateNegative,
        useTransitions);

    // FocusStates group
    VisualStateManager.GoToState(this,
        IsFocused ? StateFocused : StateUnfocused,
        useTransitions);
}

When to call UpdateStates:

  • OnApplyTemplate - Initial state (useTransitions: false)
  • Property changed callback - Reflect value change (useTransitions: true)
  • OnGotFocus/OnLostFocus - Focus state (useTransitions: true)

6. Property Changed Callback

private static void OnValueChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown)d;
    control.UpdateStates(useTransitions: true);
    control.OnValueChanged(new ValueChangedEventArgs((int)e.NewValue));
}

7. ControlTemplate Structure (Generic.xaml)

<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Background="{TemplateBinding Background}">

          <!-- Place VisualStateGroups on root element -->
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ValueStates">
              <VisualState x:Name="Positive"/>
              <VisualState x:Name="Negative">
                <Storyboard>
                  <ColorAnimation To="Red"
                    Storyboard.TargetName="ValueText"
                    Storyboard.TargetProperty="(Foreground).(SolidColorBrush.Color)"/>
                </Storyboard>
              </VisualState>
            </VisualStateGroup>

            <VisualStateGroup x:Name="FocusStates">
              <VisualState x:Name="Focused">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames
                    Storyboard.TargetName="FocusVisual"
                    Storyboard.TargetProperty="Visibility">
                    <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Unfocused"/>
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>

          <!-- Define Part elements with x:Name -->
          <RepeatButton x:Name="PART_UpButton" Content="▲"/>
          <TextBlock x:Name="ValueText" Text="{TemplateBinding Value}"/>
          <RepeatButton x:Name="PART_DownButton" Content="▼"/>

        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Checklist

  • Inherit from Control class (not UserControl)
  • Declare required Parts with TemplatePart attribute
  • Declare states with TemplateVisualState attribute
  • Define Part/State names as const strings
  • Subscribe/unsubscribe events in Part property setter
  • Use GetTemplateChild + allow null in OnApplyTemplate
  • Centralize state transitions with UpdateStates helper
  • Place VisualStateManager.VisualStateGroups on ControlTemplate root
  • Place default style in Themes/Generic.xaml
  • Call DefaultStyleKeyProperty.OverrideMetadata in static constructor

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon