Custom Control 제작

48031 단어 wpfwpf

WPF에서 Custom Control은 좀더 유연한 UI 수정을 할 수 있다.

EX) 위에는 기본적인 CheckBox 모습이고 아래는 ControlTemplate을 수정한 CheckBox 모습이다.

Custom Control을 만들기 위한 원칙

  • Control의 ControlTemplate에서 시각적 구조와 시각적 동작을 정의한다.
    • Control의 ControlTemplate에서 시각적 구조와 시각적 동작을 정의할 때 응용 프로그램 작성자는 코드를 작성하는 대신 새 ControlTemplate을 만들어 Control의 시각적 구조와 시각적 동작을 변경할 수 있다.
  • 기본적으로 제공하는 Control 시각적 구조와 동작을 따르도록 지향한다.
  • ControlTemplate에 포함되어야 할 Control 계약을 제공한다.
    • Control 계약은 어플리케이션 작성자에게 ControlTemplate에서 정의해야 하는 FrameworkElement 객체 와 상태를 알려준다.

Control의 ControlTemplate에서 시각적 구조와 시각적 동작을 정의

Control의 시각적 구조는 Control을 구성하는 FrameworkElement 개체의 합성물이다.
ControlTemplate에 FrameworkElement를 추가하여 전체적인 Control의 모습을 구현한다.

Control의 시각적 동작은 Control이 특정 상태에 있을 때 나타나는 방식이다.
ControlTemplate에 상태를 정의하여 특성 상태가 되었을때 Control의 변경된 모습을 구현한다

EX) NumericUpDown Control의 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 Control의 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 객체 예상

ControlTemplate에서 FrameworkElement 객체를 정의할 때 Control의 로직(코드 비하인드에 존재)이 일부 객체와 상호 작용해야 할 수 있다.
EX)

NumericUpDown 컨트롤은 Button의 Click 이벤트를 구독하여 Value를 늘리거나 줄이도록 만들기 위해 
코드 비하인드에서 Button 객체를 가져와 Button이 눌렀을때 로직 처리를 한다.

TextBlock의 Text 속성을 Value로 설정할 수 있도록 코드 비하인드에서 Text 속성을 처리한다.

사용자 지정 ControlTemplate이 TextBlock 또는 Button을 생략하는 경우 Control의 일부 기능이 손실되는 것은 허용되지만 Control이 오류를 일으키지 않도록 해야 한다.

EX)

ControlTemplate에 값을 변경하는 Button이 없는 경우 NumericUpDown은 해당 기능을 
상실하지만 ControlTemplate을 사용하는 어플리케이션은 계속 실행될 수 있도록 처리해야 한다.

다음 방법은 Control이 누락된 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인 경우 오류 처리를 하면 안되며 이는 Control 제작시 전체적으로 고려해야할 사항이다.

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는 Control의 상태를 추적하고 상태 간 전환에 필요한 로직을 수행한다.

ControlTemplate에 VisualState 객체를 추가할 때 VisualStateGroup에 추가하고 VisualStateGroup을 VisualStateManager.VisualStateGroups에 추가하여 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에 이름이 지정되었지만 코드 비하인드에 있는 Control의 로직이 TextBlock을 참조하지 않기 때문에 TextBlock은 NumericUpDown에 대한 Control 계약에 없다.

ControlTemplate에서 참조하는 Element에는 이름이 있지만 Control에 대한 새 ControlTemplate이 해당 Element를 참조할 필요가 없을 수 있으므로 Control 계약의 일부가 될 필요가 없다.

EX)

NumericUpDown Control에 대한 새 ControlTemplate을 만드는 사람은 
Value가 음수일때 Foreground를 변경하여 나타내지 않기로 결정할 수 있다.
이 경우 코드 비하인드와 ControlTemplate 모두 x:Name으로 TextBlock을 참조하지 않는다.

Control의 로직은 Control의 상태를 변경하는 역할을 한다.

EX) Control 로직

/*
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는 다음을 수행한다.

  • Control에 사용할 VisualState에 스토리보드가 있으면 스토리보드가 시작. 그런 다음 Control이 오는 VisualState에 스토리보드가 있으면 스토리보드가 종료된다.
  • Control이 이미 지정된 상태에 있는 경우 GoToState는 아무 작업도 수행하지 않고 true를 반환한다.
  • 지정된 상태가 ControlTemplate 제어에 없으면 GoToState는 아무 작업도 수행하지 않고 false를 반환한다.

VisualStateManager 작업을 위한 모범 사례

  1. 속성을 사용하여 상태 관리
  • NumericUpDown 컨트롤은 Value 속성을 사용하여 Positive 또는 Negative 상태인지 추적한다.
  • NumericUpDown 컨트롤은 IsFocused 속성을 추적하는 Focused 및 UnFocused 상태를 정의한다.
    • IsFocused = true 이면 Focused 상태
  1. 상태 간 전환을 위한 헬퍼 메서드 사용한다.
  • 상태를 업데이트하는 단일 메서드는 VisualStateManager에 대한 호출을 중앙 집중화하고 코드를 관리 가능하게 유지
  • Control은 어떤 상태로 변경하던 UpdateStates를 호출하여 변경할 수 있다.

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);
    }
}

Control의 상태가 변경될 수 있는 세 가지 일반적인 위치
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. 속성이 변경될 때(Value 가 변경될때 ValueChangedCallback 호출됨)

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);
}

Control 계약 제공

ControlTemplate 작성자가 Template에 무엇을 넣을지 알 수 있도록 Control 계약을 제공해야 한다.
Control 계약의 3 요소

  • Control의 로직에서 사용하는 시각적 Element
    • RepeatButton(FrameworkElement)
  • Control의 상태와 각 상태가 속한 그룹
    • FocusStates(Focused, Unfocused - VisualStateGroup)
    • ValueStates(Postive, Negative - VisualStateGroup)
  • Control에 시각적으로 영향을 주는 public 속성

NumericUpDown Control을 만들기 위해서는 RepeatButton FrameworkElement가 필요하다
Control에 필요한 FrameworkElement 객체를 지정하려면 예상되는 Element의 이름과 형식을 지정하는 TemplatePartAttribute를 사용한다.

NumericUpDown Control을 만들기 위해서는 FocusStates, ValueStates가 필요하다
Control의 가능한 상태를 지정하려면 상태의 이름과 속해 있는 VisualStateGroup을 지정하는 TemplateVisualStateAttribute를 사용한다.

Control의 모양에 영향을 주는 모든 public 속성도 Control 계약의 일부이다.

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); }
        }
    }
}

출처

좋은 웹페이지 즐겨찾기