顧客管理の作成


WPFでは、クライアント制御はUIをより柔軟に変更することができる.
EX)上は基本的なCheckBox,下はControlTemplateを修正したCheckBoxである.

顧客管理の原則の作成

  • によって制御されるControlTemplateでは、視覚構造および視覚動作が定義される.
  • で制御される制御テンプレートでビジュアル構造とビジュアル動作を定義すると、アプリケーション・ライターは、コードを記述して制御されるビジュアル構造とビジュアル動作を変更するのではなく、新しい制御テンプレートを作成できます.
  • が基本的に提供する制御視覚構造と動作に従う.
  • 制御テンプレートに含めるべき制御プロトコルを提供します.
  • 制御プロトコルは、ControlTemplateで定義されたFrameworkElementオブジェクトおよびステータスをアプリケーション作成者に提供する.
  • ControlのControlTemplateでのビジュアル構造とビジュアル動作の定義


    Controlの視覚構造は,Controlを構成するFrameworkElementオブジェクトの合成物である.
    Control TemplateにFrameworkElementを追加し、全体的な制御を実現します.
    制御する視覚動作は、特定の状態にあるときの表現を制御するものである.
    ControlTemplateでステータスを定義し、プロパティステータス時の制御の変更を実現
    EX)NumericUpDown制御のControlTemplate-可視化構造
    <ControlTemplate TargetType="src:NumericUpDown">
      <Grid  Margin="3" 
             Background="{TemplateBinding Background}">
        <Grid>
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>
    
          <Border BorderThickness="1" BorderBrush="Gray" 
                  Margin="7,2,2,2" Grid.RowSpan="2" 
                  Background="#E0FFFFFF"
                  VerticalAlignment="Center" 
                  HorizontalAlignment="Stretch">
    
            <!--값을 나타내기 위한 TextBlock이 포함되어 있다.-->
            <TextBlock Name="TextBlock"
                       Width="60" TextAlignment="Right" Padding="5"
                       Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                         AncestorType={x:Type src:NumericUpDown}}, 
                         Path=Value}"/>
          </Border>
    
           <!--값을 조절하기 위한 2개의 RepeatButton이 포함되어 있다.-->
          <RepeatButton Content="Up" Margin="2,5,5,0"
            Name="UpButton"
            Grid.Column="1" Grid.Row="0"/>
          <RepeatButton Content="Down" Margin="2,0,5,5"
            Name="DownButton"
            Grid.Column="1" Grid.Row="1"/>
    
          <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
            Stroke="Black" StrokeThickness="1"  
            Visibility="Collapsed"/>
        </Grid>
    
      </Grid>
    </ControlTemplate>
    EX)NumericUpDown制御のControlTemplate-可視挙動
    <ControlTemplate TargetType="local:NumericUpDown">
      <Grid  Margin="3" 
             Background="{TemplateBinding Background}">
    
        <VisualStateManager.VisualStateGroups>
          <VisualStateGroup Name="ValueStates">
    
            <!--상태가 "Negative"일 경우 색상을 Red로 변경-->        
            <VisualState Name="Negative">
              <Storyboard>
                <ColorAnimation To="Red"
                  Storyboard.TargetName="TextBlock" 
                  Storyboard.TargetProperty="(Foreground).(Color)"/>
              </Storyboard>
    
            </VisualState>
    
            <!--상태가 ="Positive"일 경우 색상을 원래 색으로 되돌림-->        
            <VisualState Name="Positive"/>
          </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
      </Grid>
    </ControlTemplate>

    コードの一部でControlTemplateを使用する


    FrameworkElementオブジェクトの欠落が予想されます


    Control TemplateでFrameworkElementオブジェクトを定義する場合、制御ロジック(コードの背後に存在する)は、特定のオブジェクトと対話する必要がある場合があります.
    EX)
    NumericUpDown 컨트롤은 Button의 Click 이벤트를 구독하여 Value를 늘리거나 줄이도록 만들기 위해 
    코드 비하인드에서 Button 객체를 가져와 Button이 눌렀을때 로직 처리를 한다.
    
    TextBlock의 Text 속성을 Value로 설정할 수 있도록 코드 비하인드에서 Text 속성을 처리한다.
    カスタムControlTemplateがTextBlockまたはButtonを省略している場合は、制御できる機能の一部が失われますが、制御にエラーが発生しないことを確認してください.
    EX)
    ControlTemplate에 값을 변경하는 Button이 없는 경우 NumericUpDown은 해당 기능을 
    상실하지만 ControlTemplate을 사용하는 어플리케이션은 계속 실행될 수 있도록 처리해야 한다.
    以下の方法は、制御されていないFrameworkElementオブジェクトを正しく処理するための推奨事項です.
    1.コードで参照するFrameworkElementごとにx:Nameプロパティを設定します.
    2.相互作用が必要なFrameworkElementごとにprivateプロパティを定義します.
    3.FrameworkElementプロパティのsetアクセス者でControl処理のすべてのイベントを購読してキャンセルします.
    4.OnApplyTemplateメソッドで、手順2で定義したFrameworkElementプロパティを設定します.
    FrameworkElementのx:Nameで設定した名前を使用して、ControlTemplateからElementをインポートできます.
    OnApplyTemplateはFrameworkElementを最初にインポートできます.
    5.メンバーにアクセスする前に、FrameworkElementがnullでないことを確認する必要があります.
    nullであれば、エラー処理はできません.これは作成を制御する際に考慮すべき全体的な事項です.
    EX)
    /*
    1. x:Name 특성 설정 : 
    ControlTemplate에서 NumericUpDown 컨트롤의 시각적 구조를 정의하는 예제에서 
    Value를 증가시키는 RepeatButton에는 x:Name이 UpButton으로 설정되어 있다
    */
    
    
    private RepeatButton upButtonElement;
    private RepeatButton UpButtonElement
    {
        // 2. 상호 작용해야 하는 FrameworkElement에 대한 private 속성 정의
        
        get
        {
            return upButtonElement;
        }
        set
        {
            // 3. FrameworkElement 속성의 set 접근자에서 Control이 처리하는 모든 이벤트를
            // 구독 및 구독 취소
            if (upButtonElement != null)
            {
                // upButtonElement가 null이 아닐 경우 구독을 먼저 취소
                upButtonElement.Click -=
                    new RoutedEventHandler(upButtonElement_Click);
            }
            
            // upButtonElement에 값을 설정
            upButtonElement = value;
    
            if (upButtonElement != null)
            {
                // upButtonElement에 이벤트를 구독
                upButtonElement.Click +=
                    new RoutedEventHandler(upButtonElement_Click);
            }
        }
    }
    public override void OnApplyTemplate()
    {
        // 4. OnApplyTemplate 메서드에서 2단계에서 정의한 FrameworkElement 속성을 설정
            
        // ControlTemplate에서 FrameworkElement 개체를 가져오는 GetTemplateChild 메서드
        // GetTemplateChild를 통해서 "UpButton", "DownButton" FrameworkElement를 가져온 후 
        // private으로 선언된 속성에 설정한다.
        // 여기서는 "UpButton"이라는 FrameworkElement가 없거나 타입이 다를 경우에 대한 처리를 해주어야
        // 모범 사례이다
        
        // Bind
        UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
        DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
        //TextElement = GetTemplateChild("TextBlock") as TextBlock;
        UpdateStates(false);
    }

    VisualStateManagerによるステータス管理


    VisualStateManagerは、制御のステータスを追跡し、ステータス間の変換に必要な論理を実行します.
    VisualStateオブジェクトをControlTemplateに追加する場合は、VisualStateGroupに追加し、VisualStateGroupをVisualStateManagerに追加します.VisualStateGroupに追加し、VisualStateManagerがコンポーネントにアクセスできるようにします.
    EX)
    <ControlTemplate TargetType="local:NumericUpDown">
      <Grid  Margin="3" 
             Background="{TemplateBinding Background}">
    
        <VisualStateManager.VisualStateGroups>
          <VisualStateGroup Name="ValueStates">
    
            <!--Make the Value property red when it is negative.-->
            <VisualState Name="Negative">
              <Storyboard>
                <ColorAnimation To="Red"
                  Storyboard.TargetName="TextBlock"  <-- 이전 예제에서 x:name="TextBlock" 
                                                         으로 설정된 TextBlock Control
                  Storyboard.TargetProperty="(Foreground).(Color)"/>
              </Storyboard>
    
            </VisualState>
    
            <!--Return the TextBlock's Foreground to its 
                original color.-->
            <VisualState Name="Positive"/>
          </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
      </Grid>
    </ControlTemplate>
    TextBlockは名前が付けられていますが、コードの背後にある制御ロジックはTextBlockを参照しません.したがって、TextBlockはNumericUpDownの制御プロトコルには含まれません.
    Control Templateが参照する要素には名前がありますが、新しいControl Templateはこれらの要素を参照する必要がないため、制御プロトコルの一部になる必要はありません.
    EX)
    NumericUpDown Control에 대한 새 ControlTemplate을 만드는 사람은 
    Value가 음수일때 Foreground를 변경하여 나타내지 않기로 결정할 수 있다.
    이 경우 코드 비하인드와 ControlTemplate 모두 x:Name으로 TextBlock을 참조하지 않는다.
    制御ロジックは制御状態を変える役割を果たす.
    EX)制御ロジック
    /*
    NumericUpDown Control이 GoToState 메서드를 호출하여 Value가 0 이상일 때 
    Positive 상태로, Value가 0 미만일 때 Negative 상태로 전환하는 것을 보여준다.
    */
    if (Value >= 0)
    {
        // GoToState 메서드는 스토리보드를 적절하게 시작 및 중지하는 데 필요한 로직을 수행한다.
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }
    ステータスを変更するためにControlがGoToStateを呼び出すと、VisualStateManagerは次の操作を行います.
  • 制御用のVisualStateにシーケンスイメージボードがある場合は、シーケンスイメージボードを起動します.そして、制御対象のVisualStateにシーケンスイメージボードがある場合、シーケンスイメージボードは閉じます.
  • 制御が指定された状態にある場合、GoToStateはtrueを返し、何も実行しません.
  • がステータスが制御テンプレート制御に含まれていないことを指定した場合、GoToStateはfalseを返し、何も実行しません.
  • VisualStateManagerを使用するためのベストプラクティス

  • 属性管理状態
  • を使用する.
  • NumericUpDownコントロールは、Value属性を使用して正または負の状態を追跡します.
  • NumericUpDownコントロールは、IsFocusプロパティを追跡するFocusedおよびUnFocusedステータスを定義します.
  • Isフォーカス=trueはフォーカス状態
  • を示す.
  • ステータス間で変換するヘルプメソッドを使用します.
  • ステータスを更新する単一のメソッドセットでVisualStateManagerを呼び出し、コード管理性を維持
  • 制御は、任意の状態で更新局を呼び出して変更することができる.
  • EX)
    private void UpdateStates(bool useTransitions)
    {
        if (Value >= 0)
        {
            // Control이 이미 해당 상태에 있을 때 GoToState에 상태 이름을 전달하면 
            // GoToState는 아무 작업도 수행하지 않으므로 Control의 현재 상태를 확인할 필요가 없다.        
            // 예를 들어 값이 한 음수에서 다른 음수로 변경되면 음수 상태에 대한 스토리보드가
            // 중단되지 않고 사용자는 Control의 변경 사항을 볼 수 없다.
            VisualStateManager.GoToState(this, "Positive", useTransitions);
        }
        else
        {
            VisualStateManager.GoToState(this, "Negative", useTransitions);
        }
    
        if (IsFocused)
        {
            VisualStateManager.GoToState(this, "Focused", useTransitions);
        }
        else
        {
            VisualStateManager.GoToState(this, "Unfocused", useTransitions);
        }
    }
    制御ステータスが変更される可能性のある3つの一般的な場所
    1.ControlTemplateがControlに適用された場合
    2.属性変更時
    3.イベント発生時
    EX) 1. ControlTemplateがControlに適用される場合
    public override void OnApplyTemplate()
    {
        
        UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
        DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
        //TextElement = GetTemplateChild("TextBlock") as TextBlock;
    	
        // ControlTemplate이 적용될 때 Control이 올바른 상태로 나타나도록 
        // OnApplyTemplate 메서드에서 Control의 상태를 업데이트해야 한다.
        // UpdateStates 호출하여 Negative/Postive 상태를 업데이트 한다.
        UpdateStates(false);
    }
    EX) 2. 属性変更時(値変更時呼び出し値変更callback)
    private static void ValueChangedCallback(DependencyObject obj,
        DependencyPropertyChangedEventArgs args)
    {
        NumericUpDown ctl = (NumericUpDown)obj;
        int newValue = (int)args.NewValue;
    
        // Value가 바뀌었으므로 Negative/Postive 상태가 바뀐것을 갱신해야 할 필요 있음
        ctl.UpdateStates(true);
    
        // ValueChanged event를 발생 시키기 위해 OnValueChanged 호출
        ctl.OnValueChanged(
            new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
                newValue));
    }
    EX) 3. イベント発生時
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);
        
        // Focus Event가 발생해서 Focus 상태를 갱신할 필요 있음
        UpdateStates(true);
    }

    制御プロトコルの提供


    ControlTemplate作成者がTemplateに何を追加するかを知るために、制御プロトコルを提供する必要があります.
    制御プロトコル3要素
  • 制御ロジックで使用されるビジュアル要素
  • RepeatButton(FrameworkElement)
  • 制御状態と各状態が属するグループ
  • FocusStates(Focused, Unfocused - VisualStateGroup)
  • ValueStates(Postive, Negative - VisualStateGroup)
  • 共通属性
  • 視覚的影響制御
    NumericUpDown制御を作成するにはRepeatButton FrameworkElementが必要です
    制御に必要なFrameworkElementオブジェクトを指定するには、TemplatePartAttributeを使用して、希望するElementの名前とフォーマットを指定します.
    数値UpDown制御を作成するにはFocus State、Value Stateが必要です
    コントロールの可能な状態を指定するには、TemplateVisualStateAttributeを使用して、ステータス名と所属するVisualStateGroupを指定します.
    制御外観に影響を与えるすべての共通属性も制御プロトコルの一部です.
    EX)
    
    /*
    닷넷 4.6.1 기준 테스트를 한 결과 Control 계약이 없어도 동작하는 데는 문제가 없었다.
    */
    
    // TemplatePart, TemplateVisualState 모두 뒤에 Attribute를가 생략됨
    
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public static readonly DependencyProperty BackgroundProperty;
        public static readonly DependencyProperty BorderBrushProperty;
        public static readonly DependencyProperty BorderThicknessProperty;
        public static readonly DependencyProperty FontFamilyProperty;
        public static readonly DependencyProperty FontSizeProperty;
        public static readonly DependencyProperty FontStretchProperty;
        public static readonly DependencyProperty FontStyleProperty;
        public static readonly DependencyProperty FontWeightProperty;
        public static readonly DependencyProperty ForegroundProperty;
        public static readonly DependencyProperty HorizontalContentAlignmentProperty;
        public static readonly DependencyProperty PaddingProperty;
        public static readonly DependencyProperty TextAlignmentProperty;
        public static readonly DependencyProperty TextDecorationsProperty;
        public static readonly DependencyProperty TextWrappingProperty;
        public static readonly DependencyProperty VerticalContentAlignmentProperty;
    
        public Brush Background { get; set; }
        public Brush BorderBrush { get; set; }
        public Thickness BorderThickness { get; set; }
        public FontFamily FontFamily { get; set; }
        public double FontSize { get; set; }
        public FontStretch FontStretch { get; set; }
        public FontStyle FontStyle { get; set; }
        public FontWeight FontWeight { get; set; }
        public Brush Foreground { get; set; }
        public HorizontalAlignment HorizontalContentAlignment { get; set; }
        public Thickness Padding { get; set; }
        public TextAlignment TextAlignment { get; set; }
        public TextDecorationCollection TextDecorations { get; set; }
        public TextWrapping TextWrapping { get; set; }
        public VerticalAlignment VerticalContentAlignment { get; set; }
    }

    完了したNumericUpDown

    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:NumericUpDown">
        
        <!-- NumericUpDown Contrl에 대한 Style 정의 -->
        <Style TargetType="{x:Type local:NumericUpDownCtl}">
            <!-- NumericUpDown Contrl의 Template 속성 설정 -->
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:NumericUpDownCtl}">
                        <Grid Margin="3" Background="{TemplateBinding Background}">
                            <!-- VisualStateManager 사용하여 Control의 상태 관리 -->
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup Name="ValueStates">
                                    <VisualState Name="Negative">
                                        <!-- Negative 상태일떄 실행하는 Storyboard -->
                                        <!--Value 속성(x:Name이 TextBlock인 TextBlock Control)의 
                                        Foreground를 Red로 변경-->
                                        <Storyboard>
                                            <ColorAnimation To="Red"
                                                            Storyboard.TargetName="TextBlock" 
                                                            Storyboard.TargetProperty="(Foreground).(Color)"/>
                                        </Storyboard>
                                    </VisualState>
                                    <!-- Positive 상태일때 TextBlock의 Foreground를 원래 색상으로 되돌려 Control을 
                                    초기 상태로 될돌림 -->
                                    <VisualState Name="Positive"/>
                                </VisualStateGroup>
                                <VisualStateGroup Name="FocusStates">
                                    <!--Add a focus rectangle to highlight the entire control
                                    when it has focus.-->
                                    <VisualState Name="Focused">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                                           Storyboard.TargetProperty="Visibility" 
                                                                           Duration="0">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <!--Return the control to its initial state by
                                    hiding the focus rectangle.-->
                                    <VisualState Name="Unfocused"/>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <!-- Control을 FrameworkElement로 구성 -->
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
    
                                <!-- Element에 이름에 Name 속성을 설정해서 Control에서 참고 할 수 있도록 함 -->
                                <Border BorderThickness="1" Margin="7,2,2,2" Grid.RowSpan="2" 
                                        BorderBrush="Gray" Background="#E0FFFFFF"
                                        VerticalAlignment="Center" HorizontalAlignment="Stretch">
                                    <!--Bind the TextBlock to the Value property-->
                                    <TextBlock Name="TextBlock"
                                               Width="60" TextAlignment="Right" Padding="5"
                                               Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                                                AncestorType={x:Type local:NumericUpDownCtl}}, 
                                                              Path=Value}"/>
                                </Border>
    
                                <RepeatButton Content="Up" Margin="2,5,5,0" Name="UpButton"
                                              Grid.Column="1" Grid.Row="0"/>
                                <RepeatButton Content="Down" Margin="2,0,5,5" Name="DownButton"
                                              Grid.Column="1" Grid.Row="1"/>
                                <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                                           Stroke="Black" StrokeThickness="1" Visibility="Collapsed"/>
                            </Grid>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
    
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Input;
    
    namespace NumericUpDown
    {
        public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
    
        //ValueChanged Event 인자
        public class ValueChangedEventArgs : RoutedEventArgs
        {
            private int _Value;
            public int Value => _Value;
            public ValueChangedEventArgs(RoutedEvent id, int num)
            {
                _Value = num;
                RoutedEvent = id;//발생되는 Event 정보 설정
            }
        }
    
        // Control 계약
        // ControlTemplate 작성자가 Template에 무엇을 넣을지 알 수 있도록 Control 계약을 제공
        // dotnet 4.6.1에서 테스트 해본 결과 없어도 동작에는 문제가 없었다.
        //[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
        //[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
        //[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
        //[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
        //[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
        //[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
        public class NumericUpDownCtl : Control
        {
            public static readonly DependencyProperty ValueProperty =
                DependencyProperty.Register(
                    nameof(Value), typeof(int), typeof(NumericUpDownCtl),
                    new PropertyMetadata(new PropertyChangedCallback(ValueChangedCallback)));
            public int Value
            {
                get => (int)GetValue(ValueProperty);
                set => SetValue(ValueProperty, value);
            }
            private static void ValueChangedCallback(DependencyObject obj,
                DependencyPropertyChangedEventArgs args)
            {
                NumericUpDownCtl ctl = (NumericUpDownCtl)obj;
                int newValue = (int)args.NewValue;
    
                // Call UpdateStates because the Value might have caused the
                // control to change ValueStates.
                ctl.UpdateStates(true);
    
                // NumericUpDown의 ValueChanged event 발생
                ctl.OnValueChanged(
                    new ValueChangedEventArgs(ValueChangedEvent, newValue));
            }
            protected virtual void OnValueChanged(ValueChangedEventArgs e)
            {
                // ValueChanged event를 구족한 어플리케이션이 알람을 받아서
                // Value가 바뀌었다는 것을 알 수 있도록 이벤트 발생
                RaiseEvent(e);
            }
    
            // ValueChanged event 선언
            // 어플리케이션에서 ValueChanged event 를 구독할 수 있다.
            public static readonly RoutedEvent ValueChangedEvent =
                EventManager.RegisterRoutedEvent(
                    nameof(ValueChanged),// event 이름
                    RoutingStrategy.Direct,// event routing 전략
                    typeof(ValueChangedEventHandler),//handler type
                    typeof(NumericUpDownCtl)); 
    
            public event ValueChangedEventHandler ValueChanged
            {
                add => AddHandler(ValueChangedEvent, value);
                remove => RemoveHandler(ValueChangedEvent, value);
            }
    
            private RepeatButton _DownButtonElement;
            private RepeatButton DownButtonElement 
            {
                //private으로 선언해야 외부에서 함부로 접근할 수 없다. 
                //속성은 OnApplyTemplate에서 설정될 것이다.
                get => _DownButtonElement;
                set
                {
                    if (_DownButtonElement != null)
                    {
                        _DownButtonElement.Click -=
                            new RoutedEventHandler(DownButtonElement_Click);
                    }
                    _DownButtonElement = value;
                    if (_DownButtonElement != null)
                    {
                        _DownButtonElement.Click +=
                            new RoutedEventHandler(DownButtonElement_Click);
                    }
                }
            }
            void DownButtonElement_Click(object sender, RoutedEventArgs e)
            {
                Value--;
            }
    
    
            private RepeatButton _UpButtonElement;
            private RepeatButton UpButtonElement
            {
                get => _UpButtonElement;
                set
                {
                    if (_UpButtonElement != null)
                    {
                        _UpButtonElement.Click -=
                            new RoutedEventHandler(UpButtonElement_Click);
                    }
                    _UpButtonElement = value;
                    if (_UpButtonElement != null)
                    {
                        _UpButtonElement.Click +=
                            new RoutedEventHandler(UpButtonElement_Click);
                    }
                }
            }
            void UpButtonElement_Click(object sender, RoutedEventArgs e)
            {
                Value++;
            }
            public NumericUpDownCtl()
            {
                DefaultStyleKey = typeof(NumericUpDownCtl);
                this.IsTabStop = true;
            }
            public override void OnApplyTemplate()
            {
                UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
                DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
                //TextElement = GetTemplateChild("TextBlock") as TextBlock;
    
                UpdateStates(false);
            }
            protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
            {
                base.OnMouseLeftButtonDown(e);
                Focus();
            }
            protected override void OnGotFocus(RoutedEventArgs e)
            {
                base.OnGotFocus(e);
                UpdateStates(true);
            }
            protected override void OnLostFocus(RoutedEventArgs e)
            {
                base.OnLostFocus(e);
                UpdateStates(true);
            }
            private void UpdateStates(bool useTransitions)
            {
                if (Value >= 0)
                { VisualStateManager.GoToState(this, "Positive", useTransitions); }
                else
                { VisualStateManager.GoToState(this, "Negative", useTransitions); }
    
                if (IsFocused)
                { VisualStateManager.GoToState(this, "Focused", useTransitions); }
                else
                { VisualStateManager.GoToState(this, "Unfocused", useTransitions); }
            }
        }
    }
    

    ソース

  • https://docs.microsoft.com/en-us/dotnet/desktop/wpf/controls/creating-a-control-that-has-a-customizable-appearance?view=netframeworkdesktop-4.8