사용자 지정 가능한 모양이 있는 컨트롤 만들기

WPF(Windows Presentation Foundation)를 사용하면 모양을 사용자 지정할 수 있는 컨트롤을 만들 수 있습니다. 예를 들어 새 ControlTemplate을 만들어 설정 속성보다 더 많이 CheckBox의 모양을 변경할 수 있습니다. 다음 그림에서는 기본 ControlTemplate을 사용하는 CheckBox와 사용자 지정 ControlTemplate을 사용하는 CheckBox를 보여줍니다.

기본 컨트롤 템플릿을 사용하는 확인란. 기본 컨트롤 템플릿을 사용하는 확인란

사용자 지정 컨트롤 템플릿을 사용하는 확인란. 사용자 지정 컨트롤 템플릿을 사용하는 확인란

컨트롤을 만들 때 파트 및 상태 모델을 따르는 경우 컨트롤의 모양을 사용자 지정할 수 있습니다. Blend for Visual Studio 같은 디자이너 도구는 파트 및 상태 모델을 지원하므로, 이 모델을 따르면 이러한 유형의 애플리케이션에서 컨트롤을 사용자 지정할 수 있습니다. 이 토픽에서는 파트 및 상태 모델과 사용자 고유의 컨트롤을 만들 때 모델을 따르는 방법에 대해 설명합니다. 이 토픽에서는 사용자 지정 컨트롤 NumericUpDown 예제를 사용하여 이 모델의 철학을 설명합니다. NumericUpDown 컨트롤은 사용자가 컨트롤의 단추를 클릭하여 늘리거나 줄일 수 있는 숫자 값을 표시합니다. 다음 그림에서는 이 토픽에서 설명하는 NumericUpDown 컨트롤을 보여줍니다.

NumericUpDown 사용자 지정 컨트롤. 사용자 지정 NumericUpDown 컨트롤

이 항목에는 다음과 같은 섹션이 포함되어 있습니다.

사전 요구 사항

이 토픽에서는 독자들이 기존 컨트롤에 대한 새 ControlTemplate 컨트롤을 만드는 방법을 알고 있고 컨트롤 계약의 요소가 무엇인지 잘 알고 있으며, 컨트롤에 대한 템플릿 만들기에서 설명하는 개념을 이해하고 있다고 가정합니다.

참고

모양을 사용자 지정할 수 있는 컨트롤을 만들려면 UserControl 클래스가 아닌 Control 클래스 또는 이 클래스의 서브클래스 중 하나에서 상속하는 컨트롤을 만들어야 합니다. UserControl에서 상속하는 컨트롤은 빠르게 만들 수 있지만 ControlTemplate을 사용하지 않으며 모양을 사용자 지정할 수 없습니다.

파트 및 상태 모델

파트 및 상태 모델은 컨트롤의 시각적 구조 및 시각적 동작을 정의하는 방법을 지정합니다. 파트 및 상태 모델을 따르려면 다음을 수행해야 합니다.

  • 컨트롤의 ControlTemplate에서 시각적 구조 및 시각적 동작을 정의합니다.

  • 컨트롤의 논리가 컨트롤 템플릿의 파트와 상호 작용하는 경우 특정 모범 사례를 따릅니다.

  • ControlTemplate에 포함할 내용을 지정하는 컨트롤 계약을 제공합니다.

컨트롤의 ControlTemplate에서 시각적 구조 및 시각적 동작을 정의할 때, 애플리케이션 작성자는 코드를 작성하는 대신 새 ControlTemplate을 만들어 컨트롤의 시각적 구조와 시각적 동작을 변경할 수 있습니다. ControlTemplate에서 정의해야 하는 FrameworkElement 개체와 상태를 애플리케이션 작성자에게 알려주는 컨트롤 계약을 제공해야 합니다. 컨트롤이 불완전한 ControlTemplate을 제대로 처리할 수 있도록 ControlTemplate의 파트와 상호 작용할 때 몇 가지 모범 사례를 따라야 합니다. 이러한 세 가지 원칙을 지키면 애플리케이션 작성자는 WPF와 함께 제공되는 컨트롤과 마찬가지로 매우 쉽게 컨트롤에 대한 ControlTemplate을 만들 수 있습니다. 다음 섹션에서는 이러한 권장 사항을 자세히 설명합니다.

ControlTemplate에서 컨트롤의 시각적 구조 및 시각적 동작 정의

파트 및 상태 모델을 사용하여 사용자 지정 컨트롤을 만들 때, 논리 대신 ControlTemplate의 시각적 구조와 시각적 동작을 정의합니다. 컨트롤의 시각적 구조는 컨트롤을 구성하는 FrameworkElement 개체의 복합 구조입니다. 시각적 동작은 컨트롤이 특정 상태일 때 표시되는 방식입니다. 컨트롤의 시각적 구조 및 시각적 동작을 지정하는 ControlTemplate 만들기에 대한 자세한 내용은 컨트롤의 템플릿 만들기를 참조하세요.

NumericUpDown 컨트롤의 예제에서 시각적 구조에는 2개의 RepeatButton 컨트롤과 1개의 TextBlock이 있습니다. NumericUpDown 컨트롤의 코드에서 이러한 컨트롤을 추가하면(예: 생성자에서 추가) 해당 컨트롤의 위치를 변경할 수 없습니다. 코드에서 컨트롤의 시각적 구조와 시각적 동작을 정의하는 대신 ControlTemplate에서 정의해야 합니다. 그러면 ControlTemplate을 바꿀 수 있으므로 애플리케이션 개발자가 단추와 TextBlock의 위치를 사용자 지정하고, Value가 음수일 때 발생하는 동작을 지정할 수 있습니다.

다음 예제에서는 Value를 늘리는 RepeatButton, Value를 줄이는 RepeatButton, Value를 표시하는 TextBlock이 포함된 NumericUpDown 컨트롤의 시각적 구조를 보여줍니다.

<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">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     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>

NumericUpDown 컨트롤의 시각적 동작은 값이 음수이면 빨간색 글꼴로 표시하는 것입니다. Value가 음수일 때 코드에서 TextBlockForeground를 변경하면 NumericUpDown이 항상 빨간색 음수 값을 표시합니다. ControlTemplateVisualState 개체를 추가하여 ControlTemplate에서 컨트롤의 시각적 동작을 지정합니다. 다음 예제에서는 PositiveNegative 상태에 대한 VisualState 개체를 보여줍니다. PositiveNegative는 상호 배타적이므로(컨트롤이 항상 둘 중 하나) 이 예제에서는 VisualState 개체를 단일 VisualStateGroup에 넣습니다. 컨트롤이 Negative 상태가 되면 TextBlockForeground가 빨간색으로 바뀝니다. 컨트롤이 Positive 상태이면 Foreground가 원래 값으로 돌아갑니다. ControlTemplate에서 VisualState 개체를 정의하는 내용은 컨트롤의 템플릿 만들기에서 자세히 설명합니다.

참고

ControlTemplate의 루트 FrameworkElement에서 VisualStateManager.VisualStateGroups 연결된 속성을 설정해야 합니다.

<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" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

코드에서 ControlTemplate의 파트 사용

ControlTemplate 작성자는 FrameworkElement 또는 VisualState 개체를 의도적으로 또는 실수로 생략할 수 있지만, 컨트롤의 논리가 제대로 작동하려면 해당 파트가 필요할 수 있습니다. 파트 및 상태 모델은 FrameworkElement 또는 VisualState 개체가 없는 ControlTemplate에 대한 복원력이 있어야 한다고 지정합니다. FrameworkElement, VisualState 또는 VisualStateGroupControlTemplate에 없을 때 컨트롤에서 예외를 throw하거나 오류를 보고하면 안 됩니다. 이 섹션에서는 FrameworkElement 개체와 상호 작용하고 상태를 관리하기 위한 권장 모범 사례를 설명합니다.

누락된 FrameworkElement 개체 예상

ControlTemplate에서 FrameworkElement 개체를 정의할 때, 컨트롤의 논리가 이러한 개체 중 일부와 상호 작용해야 할 수도 있습니다. 예를 들어 NumericUpDown 컨트롤은 단추의 Click 이벤트를 구독하여 Value를 늘리거나 줄이고 TextBlockText 속성을 Value로 설정합니다. 사용자 지정 ControlTemplate에서 TextBlock 또는 단추를 생략하는 경우 컨트롤의 일부 기능이 손실되는 것은 감내할 수 있지만 컨트롤에서 오류가 발생하지 않도록 해야 합니다. 예를 들어 ControlTemplateValue를 변경하는 단추가 없으면 NumericUpDown에서 이 기능이 사라지지만 ControlTemplate을 사용하는 애플리케이션은 계속 실행됩니다.

다음 모범 사례를 따르면 컨트롤이 누락된 FrameworkElement 개체에 올바르게 응답합니다.

  1. 코드에서 참조해야 하는 각 FrameworkElementx:Name 특성을 설정합니다.

  2. 상호 작용해야 하는 각 FrameworkElement의 프라이빗 속성을 정의합니다.

  3. 컨트롤이 FrameworkElement 속성의 set 접근자에서 처리하는 모든 이벤트를 구독하고 구독을 취소합니다.

  4. OnApplyTemplate 메서드의 2단계에서 정의한 FrameworkElement 속성을 설정합니다. 이는 ControlTemplateFrameworkElement를 컨트롤에 사용할 수 있는 가장 빠른 시간입니다. FrameworkElementx:Name을 사용하여 ControlTemplate에서 가져옵니다.

  5. 멤버에 액세스하기 전에 FrameworkElementnull이 아닌지 확인합니다. null인 경우 오류를 보고하지 마세요.

다음 예제에서는 NumericUpDown 컨트롤이 이전 목록의 권장 사항에 따라 FrameworkElement 개체와 상호 작용하는 방법을 보여줍니다.

ControlTemplate에서 NumericUpDown 컨트롤의 시각적 구조를 정의하는 예제에서는 Value를 늘리는 RepeatButtonx:Name 특성을 UpButton으로 설정했습니다. 다음 예제에서는 ControlTemplate에 선언된 RepeatButton을 나타내는 UpButtonElement 속성을 선언합니다. set 접근자는 UpDownElementnull이 아니면 단추의 Click 이벤트를 구독 취소한 다음, 속성을 설정하고 Click 이벤트를 구독합니다. 여기에는 나와 있지 않지만 다른 RepeatButton에 대해 정의된 DownButtonElement 속성도 있습니다.

private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}
Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property

다음 예제에서는 NumericUpDown 컨트롤의 OnApplyTemplate을 보여줍니다. 이 예제에서는 GetTemplateChild 메서드를 사용하여 ControlTemplate에서 FrameworkElement 개체를 가져옵니다. 이 예제에서는 GetTemplateChild가 지정된 이름이지만 필요한 형식이 아닌 이름을 사용하는 FrameworkElement를 찾는 것을 막습니다. 또한 지정된 x:Name이지만 형식이 잘못된 이름을 사용하는 요소를 무시하는 것이 좋습니다.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

이전 예제에서 설명한 모범 사례를 따르면 ControlTemplateFrameworkElement가 없어도 컨트롤이 계속 실행됩니다.

VisualStateManager를 사용하여 상태 관리

VisualStateManager는 컨트롤의 상태를 추적하고 상태를 전환하는 데 필요한 논리를 수행합니다. ControlTemplateVisualState 개체를 추가할 때, VisualStateManager가 액세스할 수 있도록 VisualStateGroup에 개체를 추가하고 VisualStateManager.VisualStateGroups 연결된 속성에 VisualStateGroup을 추가합니다.

다음 예제에서는 컨트롤의 PositiveNegative 상태에 해당하는 VisualState 개체를 보여주는 이전 예제를 반복합니다. NegativeVisualStateStoryboardTextBlockForeground를 빨간색으로 바꿉니다. NumericUpDown 컨트롤이 Negative 상태이면 Negative 상태의 스토리보드가 시작됩니다. 컨트롤이 Positive 상태로 돌아가면 Negative 상태인 Storyboard가 중지됩니다. Negative에 대한 Storyboard가 중지되면 Foreground가 원래 색으로 돌아가므로 PositiveVisualStateStoryboard를 포함할 필요는 없습니다.

<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" 
              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을 참조하지 않기 때문에 TextBlockNumericUpDown에 대한 컨트롤 계약에 없습니다. ControlTemplate에서 참조되는 요소는 이름을 갖고 있지만 컨트롤의 새 ControlTemplate이 해당 요소를 참조할 필요가 없으므로 컨트롤 계약에 포함될 필요가 없습니다. 예를 들어 NumericUpDown에 대한 새 ControlTemplate을 만드는 사람은 Foreground를 변경하여 Value가 음수임을 나타내지 않기로 결정할 수 있습니다. 이 경우 코드나 ControlTemplateTextBlock을 이름으로 참조하지 않습니다.

컨트롤의 논리는 컨트롤의 상태를 변경하는 역할을 담당합니다. 다음 예제에서는 NumericUpDown 컨트롤이 GoToState 메서드를 호출하여 Value가 0 이상이면 Positive 상태로 전환하고, Value가 0보다 작으면 Negative 상태로 전환하는 방법을 보여줍니다.

if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If

GoToState 메서드는 스토리보드를 적절하게 시작하고 중지하는 데 필요한 논리를 수행합니다. 컨트롤이 상태를 변경하기 위해 GoToState를 호출하면 VisualStateManager는 다음을 수행합니다.

  • 컨트롤이 VisualState로 전환되고 여기에 Storyboard가 있으면 스토리보드가 시작됩니다. 그 후 컨트롤이 VisualState에서 전환되고 여기에 Storyboard가 있으면 스토리보드가 종료됩니다.

  • 컨트롤이 이미 지정된 상태이면 GoToState는 아무 작업도 수행하지 않고 true를 반환합니다.

  • 지정된 상태가 controlControlTemplate에 없으면 GoToState는 아무 작업도 수행하지 않고 false를 반환합니다.

VisualStateManager 사용 모범 사례

컨트롤의 상태를 유지하려면 다음을 수행하는 것이 좋습니다.

  • 속성을 사용하여 상태를 추적합니다.

  • 상태 간에 전환할 도우미 메서드를 만듭니다.

NumericUpDown 컨트롤은 Value 속성을 사용하여 Positive 상태인지 아니면 Negative 상태인지 추적합니다. 또한 NumericUpDown 컨트롤은 IsFocused 속성을 추적하는 FocusedUnFocused 상태를 정의합니다. 컨트롤의 속성과 자연적으로 일치하지 않는 상태를 사용하는 경우 상태를 추적하는 프라이빗 속성을 정의할 수 있습니다.

모든 상태를 업데이트하는 단일 메서드는 VisualStateManager 호출을 중앙 집중화하고 코드를 관리하기 쉽게 유지합니다. 다음 예제에서는 NumericUpDown 컨트롤의 도우미 메서드 UpdateStates를 보여줍니다. Value가 0보다 크거나 같으면 ControlPositive 상태입니다. Value가 0보다 작으면 컨트롤이 Negative 상태입니다. IsFocusedtrue이면 컨트롤이 Focused 상태이고, 그렇지 않으면 Unfocused 상태입니다. 컨트롤은 어떤 상태가 변경되든 상태를 변경해야 할 때마다 UpdateStates를 호출할 수 있습니다.

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);
    }
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub

컨트롤이 이미 GoToState 상태일 때 이름을 전달하면 GoToState가 아무 작업도 수행하지 않으므로 컨트롤의 현재 상태를 확인할 필요가 없습니다. 예를 들어 Value가 음수에서 다른 음수로 바뀌면 Negative 상태에 대한 스토리보드가 중단되지 않고 사용자에게는 컨트롤의 변경 내용이 표시되지 않습니다.

GoToState를 호출하면 VisualStateManagerVisualStateGroup 개체를 사용하여 종료할 상태를 결정합니다. 컨트롤은 항상 ControlTemplate에 정의된 각 VisualStateGroup의 한 가지 상태에 있으며, 동일한 VisualStateGroup 상태에서 다른 상태로 전환되는 경우에만 상태를 떠납니다. 예를 들어 NumericUpDown 컨트롤의 ControlTemplate은 한 VisualStateGroupPositiveNegativeVisualState 개체를 정의하고 다른 그룹에 FocusedUnfocusedVisualState 개체를 정의합니다. (이 토픽의 전체 예제 섹션에서 정의한 FocusedUnfocusedVisualState를 참조할 수 있습니다.) 컨트롤이 Positive 상태에서 Negative 상태로 전환되거나 그 반대로 전환되면 컨트롤이 Focused 상태 또는 Unfocused 상태로 유지됩니다.

컨트롤의 상태가 변경될 수 있는 일반적인 상황은 다음과 같은 세 가지입니다.

다음 예제에서는 이러한 상황에서 NumericUpDown 컨트롤의 상태를 업데이트하는 방법을 보여줍니다.

ControlTemplate이 적용될 때 컨트롤이 올바른 상태로 표시되도록 OnApplyTemplate 메서드에서 컨트롤의 상태를 업데이트해야 합니다. 다음 예제에서는 컨트롤이 적절한 상태가 되도록 OnApplyTemplate에서 UpdateStates를 호출합니다. 예를 들어 NumericUpDown 컨트롤을 만든 다음, Foreground를 녹색으로, Value를 -5로 설정한다고 가정하겠습니다. NumericUpDown 컨트롤에 ControlTemplate이 적용될 때 UpdateStates를 호출하지 않으면 컨트롤이 Negative 상태가 아니고 값은 빨간색이 아닌 녹색입니다. UpdateStates를 호출하여 컨트롤을 Negative 상태로 전환해야 합니다.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

속성이 변경될 때 컨트롤의 상태를 업데이트해야 하는 경우가 많습니다. 다음 예제에서는 전체 ValueChangedCallback 메서드를 보여줍니다. Value가 변하면 ValueChangedCallback이 호출되므로, Value가 양수에서 음수로 또는 그 반대로 바뀌면 이 메서드는 UpdateStates를 호출합니다. 이 컨트롤이 상태를 변경하지 않으므로 Value가 변할 때 UpdateStates를 호출할 수 있지만 양수 또는 음수는 그대로 유지됩니다.

private static void ValueChangedCallback(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
            newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub

이벤트가 발생할 때 상태를 업데이트해야 할 수도 있습니다. 다음 예제에서는 GotFocus 이벤트를 처리하기 위해 NumericUpDownControl에서 UpdateStates를 호출하는 것을 보여줍니다.

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub

VisualStateManager는 컨트롤 상태를 관리하는 데 도움이 됩니다. VisualStateManager를 사용하면 컨트롤의 상태가 올바르게 전환됩니다. 이 섹션에 설명된 권장 사항에 따라 VisualStateManager를 사용하면 컨트롤의 코드를 지속적으로 읽고 유지 관리할 수 있습니다.

컨트롤 계약 제공

ControlTemplate 작성자가 템플릿에 무엇을 넣어야 하는지 알 수 있도록 컨트롤 계약을 제공합니다. 컨트롤 계약에는 세 가지 요소가 있습니다.

  • 컨트롤 논리가 사용하는 시각적 요소

  • 컨트롤의 상태 및 각 상태가 속해 있는 그룹

  • 컨트롤에 시각적으로 영향을 미치는 공용 속성

ControlTemplate을 만드는 사람은 컨트롤의 논리에서 사용하는 FrameworkElement 개체, 각 개체의 형식 및 이름을 알아야 합니다. 또한 ControlTemplate 작성자는 컨트롤의 가능한 상태 이름과 상태가 속한 VisualStateGroup을 알아야 합니다.

NumericUpDown 예제로 돌아가서, 컨트롤은 ControlTemplate에 다음 FrameworkElement 개체가 있기를 기대합니다.

컨트롤은 다음 상태일 수 있습니다.

컨트롤에 필요한 FrameworkElement 개체를 지정하려면 필요한 요소의 이름과 형식을 지정하는 TemplatePartAttribute를 사용합니다. 컨트롤의 가능한 상태를 지정하려면 상태 이름과 상태가 속한 VisualStateGroup을 지정하는 TemplateVisualStateAttribute를 사용합니다. TemplatePartAttributeTemplateVisualStateAttribute를 컨트롤의 클래스 정의에 배치합니다.

컨트롤의 모양에 영향을 주는 공용 속성은 컨트롤 계약에도 포함됩니다.

다음 예제에서는 NumericUpDown 컨트롤의 FrameworkElement 개체 및 상태를 지정합니다.

[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; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

완성된 예제

다음 예제는 NumericUpDown 컨트롤의 전체 ControlTemplate입니다.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <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" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <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>

            <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">
                <!--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:NumericUpDown}}, 
                                 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>

다음 예제에 대 한 논리는 NumericUpDown합니다.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [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 NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);
            }
        }

        private static void ValueChangedCallback(DependencyObject obj,
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }

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

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return 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
            {
                return 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++;
        }

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

    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

참고 항목