컨트롤 제작 개요

WPF(Windows Presentation Foundation) 컨트롤 모델의 우수한 확장성 덕분에 새 컨트롤을 만들 필요성이 상당히 줄어들었습니다. 그러나 어떤 경우에는 여전히 사용자 지정 컨트롤을 만들어야 할 수 있습니다. 이 토픽에서는 WPF(Windows Presentation Foundation)에서 사용자 지정 컨트롤과 다양한 컨트롤 제작 모델을 만들 필요성을 최소화하는 기능에 대해 설명합니다. 또한 새 컨트롤을 만드는 방법을 설명합니다.

새 컨트롤 작성에 대한 대안

지금까지 기존 컨트롤에서 사용자 지정 환경을 구현하려고 하면 배경색, 테두리 너비 및 글꼴 크기와 같은 컨트롤의 표준 속성을 변경하는 것으로 제한되어 있었습니다. 미리 정의된 이러한 매개 변수 이상으로 컨트롤의 모양이나 동작을 확장하려면 일반적으로 기존 컨트롤에서 상속받게 하고 컨트롤 그리기를 담당하는 메서드를 재정의하여 새 컨트롤을 만들어야 했습니다. 여전히 옵션이기는 하지만 WPF를 사용하면 풍부한 콘텐츠 모델, 스타일, 템플릿 및 트리거를 사용하여 기존 컨트롤을 사용자 지정할 수 있습니다. 다음 목록에는 새 컨트롤을 만들지 않고 이러한 기능을 사용하여 사용자 지정 및 일관된 환경을 만드는 예제가 나와 있습니다.

  • 풍부한 콘텐츠. 많은 표준 WPF 컨트롤이 풍부한 콘텐츠를 지원합니다. 예를 들어 Button의 콘텐츠 속성은 Object 형식이므로 이론적으로는 Button에 무엇이든 표시할 수 있습니다. 단추에 이미지와 텍스트를 표시하려면 이미지와 TextBlockStackPanel에 추가하고 StackPanelContent 속성에 할당합니다. 이러한 컨트롤은 WPF 시각적 요소와 임의의 데이터를 표시할 수 있기 때문에 복잡한 시각화를 지원하기 위해 새 컨트롤을 만들거나 기존 컨트롤을 수정할 필요성이 적습니다. Button의 콘텐츠 모델 및 WPF의 다른 콘텐츠 모델에 대한 자세한 내용은 WPF 콘텐츠 모델을 참조하세요.

  • 스타일. Style은 컨트롤의 속성을 나타내는 값의 컬렉션입니다. 스타일을 사용하면 새 컨트롤을 작성하지 않고도 원하는 컨트롤 모양과 동작을 재사용 가능한 표현으로 만들 수 있습니다. 예를 들어 모든 TextBlock 컨트롤에 Arial, 빨간색, 14 크기의 글꼴이 필요하다고 가정해 보겠습니다. 스타일을 리소스로 만들고 이에 따라 적절한 속성을 설정할 수 있습니다. 그러면 애플리케이션에 추가하는 모든 TextBlock의 모양이 같게 됩니다.

  • 데이터 템플릿. DataTemplate을 사용하면 데이터가 컨트롤에 표시되는 방식을 사용자 지정할 수 있습니다. 예를 들어 DataTemplate을 사용하여 데이터가 ListBox에 표시되는 방법을 지정할 수 있습니다. 이에 대한 예제는 데이터 템플릿 개요를 참조하세요. 데이터 모양을 사용자 지정하는 것 외에도 DataTemplate에는 사용자 지정 UI에서 많은 유연성을 제공하는 UI 요소가 포함될 수 있습니다. 예를 들어 DataTemplate을 사용하면 각 항목에 확인란이 있는 ComboBox를 만들 수 있습니다.

  • 컨트롤 템플릿. WPF의 많은 컨트롤은 ControlTemplate을 사용하여 컨트롤의 구조와 모양을 정의합니다. 이를 통해 컨트롤의 모양과 기능을 구분합니다. ControlTemplate을 다시 정의하면 컨트롤 모양을 대폭 변경할 수 있습니다. 예를 들어 신호등 모양의 컨트롤이 필요하다고 가정해 보겠습니다. 이 컨트롤에는 간단한 사용자 인터페이스 및 기능이 있습니다. 컨트롤은 세 개의 원으로, 한 번에 하나씩만 불을 켤 수 있습니다. 조금만 살펴보면 RadioButton에서는 한 번에 하나의 기능만 선택하게 되어 있으며 RadioButton의 기본 모양은 신호등에 있는 조명과 다르다는 것을 알 수 있습니다. RadioButton은 컨트롤 템플릿을 사용하여 모양을 정의하므로 컨트롤의 요구 사항에 맞도록 ControlTemplate을 재정의한 다음 라디오 단추를 사용하여 신호등을 만드는 것이 쉽습니다.

    참고

    RadioButtonDataTemplate을 사용할 수 있지만 이 예제에서는 DataTemplate이 충분하지 않습니다. DataTemplate은 컨트롤의 콘텐츠 모양을 정의합니다. RadioButton의 경우 콘텐츠는 RadioButton이 선택되었는지 여부를 나타내는 원의 오른쪽에 표시되는 항목입니다. 신호등의 예제에서 라디오 단추는 "불을 켤 수 있는" 원이어야 합니다. 신호등의 모양 요구 사항은 RadioButton의 기본 모양과 매우 다르기 때문에 ControlTemplate을 다시 정의해야 합니다. 일반적으로 DataTemplate은 컨트롤의 콘텐츠(또는 데이터)를 정의하는 데 사용되고 ControlTemplate은 컨트롤이 구성되는 방식을 정의하는 데 사용됩니다.

  • 트리거 Trigger를 사용하면 새 컨트롤을 만들지 않고도 컨트롤의 모양과 동작을 동적으로 변경할 수 있습니다. 예를 들어 애플리케이션에 여러 ListBox 컨트롤이 있고 이들을 선택하면 각 ListBox의 항목을 굵고 빨간색으로 표시하려고 한다고 가정해 보겠습니다. 기본적으로 처음 수행하는 작업은 아마도 ListBox에서 상속받는 클래스를 만들고 OnSelectionChanged 메서드를 재정의하여 선택한 항목의 모양을 변경하는 것이겠지만, 더 좋은 방법은 선택한 항목의 모양을 변경하는 ListBoxItem 스타일에 트리거를 추가하는 것입니다. 트리거를 사용하면 속성 값을 변경하거나 속성 값을 기반으로 작업을 수행할 수 있습니다. EventTrigger를 사용하면 이벤트가 발생할 때 작업을 수행할 수 있습니다.

스타일, 템플릿 및 트리거에 대한 자세한 내용은 스타일 지정 및 템플릿을 참조하세요.

일반적으로 컨트롤이 기존 컨트롤의 기능을 반영하지만 컨트롤이 다르게 보이게 하려면 이 섹션에서 설명하는 메서드 중 하나를 사용하여 기존 컨트롤의 모양을 변경할 수 있는지 여부를 먼저 고려해야 합니다.

컨트롤 제작 모델

풍부한 콘텐츠 모델, 스타일, 템플릿 및 트리거를 사용하면 새 컨트롤을 만들어야 하는 필요성이 최소화됩니다. 그러나 새 컨트롤을 만들어야 한다면 WPF의 다양한 컨트롤 제작 모델을 이해하는 것이 중요합니다. WPF는 컨트롤을 만들기 위해 세 가지 일반적인 모델을 제공하며 각 모델은 서로 다른 일련의 기능과 유연성 수준을 제공합니다. 세 가지 모델의 기본 클래스는 UserControl, ControlFrameworkElement입니다.

UserControl에서 파생

WPF에서 컨트롤을 만드는 가장 간단한 방법은 UserControl에서 파생하는 것입니다. UserControl에서 상속받는 컨트롤을 빌드할 때 UserControl에 기존 구성 요소를 추가하고, 구성 요소의 이름을 지정하고, XAML의 이벤트 처리기를 참조합니다. 그런 다음 코드에서 명명된 요소를 참조하고 이벤트 처리기를 정의할 수 있습니다. 이 개발 모델은 WPF의 애플리케이션 개발에 사용된 모델과 매우 유사합니다.

UserControl을 올바르게 빌드하면 풍부한 콘텐츠, 스타일 및 트리거의 이점을 활용할 수 있습니다. 그러나 컨트롤이 UserControl에서 상속받는 경우 이 컨트롤을 사용하는 사용자는 그 모양을 사용자 지정하기 위해 DataTemplate 또는 ControlTemplate을 사용자 지정할 수 없습니다. 템플릿을 지원하는 사용자 지정 컨트롤을 만들려면 Control 클래스 또는 파생 클래스 중 하나(UserControl 제외)에서 파생해야 합니다.

UserControl에서 파생하는 이점

다음 사항이 모두 적용되면 UserControl에서 파생하는 것을 고려합니다.

  • 애플리케이션을 빌드하는 방법과 유사하게 컨트롤을 빌드하려고 합니다.

  • 컨트롤이 기존 구성 요소로만 구성됩니다.

  • 복잡한 사용자 지정을 지원하지 않아도 됩니다.

Control에서 파생

Control 클래스에서 파생은 대부분의 기존 WPF 컨트롤에서 사용되는 모델입니다. Control 클래스에서 상속받는 컨트롤을 만들 때 템플릿을 사용하여 모양을 정의합니다. 그렇게 함으로써 작동 논리를 시각적 표현과 분리합니다. 가능한 경우 ControlTemplate에서 이벤트 대신 명령 및 바인딩을 사용하고 요소를 참조하지 않아도 UI 및 논리를 분리할 수 ​​있습니다. 컨트롤의 UI와 논리가 적절히 분리되면 컨트롤의 사용자가 컨트롤의 ControlTemplate을 재정의하여 모양을 사용자 지정할 수 있습니다. 사용자 지정 Control을 빌드하는 것은 UserControl을 빌드하는 것처럼 간단하지 않지만 사용자 지정 Control은 최상의 유연성을 제공합니다.

Control에서 파생하는 이점

다음 사항 중 하나가 적용되면 UserControl 클래스를 사용하는 대신 Control에서 파생하는 것을 고려합니다.

  • ControlTemplate을 통해 컨트롤 모양을 사용자 지정이 가능하게 하려고 합니다.

  • 컨트롤이 다른 테마를 지원하게 하려고 합니다.

FrameworkElement에서 파생

UserControl 또는 Control에서 파생되는 컨트롤은 기존 요소를 작성하는 데 의존합니다. FrameworkElement에서 상속받는 모든 개체는 ControlTemplate에 있을 수 있기 때문에 여러 시나리오에서 이 솔루션을 사용할 수 있습니다. 그러나 컨트롤의 모양이 단순한 요소 컴퍼지션 이상의 기능을 필요로 하는 경우가 있습니다. 이러한 시나리오의 경우 구성 요소를 FrameworkElement 기반으로 하는 것이 올바른 선택입니다.

FrameworkElement 기반 구성 요소를 빌드하는 두 가지 표준 방법(직접 렌더링과 사용자 지정 요소 컴퍼지션)이 있습니다. 직접 렌더링은 FrameworkElementOnRender 메서드를 재정의하고 구성 요소 시각적 개체를 명시적으로 정의하는 DrawingContext 작업을 제공하는 작업을 포함합니다. 이는 ImageBorder에서 사용하는 메서드입니다. 사용자 지정 요소 컴퍼지션은 Visual 형식의 개체를 사용하여 구성 요소의 모양을 구성하는 작업을 포함합니다. 예제는 DrawingVisual 개체 사용을 참조하세요. Track은 사용자 지정 요소 컴퍼지션을 사용하는 WPF의 컨트롤 예입니다. 직접 렌더링과 사용자 지정 요소 컴퍼지션을 같은 컨트롤에서 혼합하여 사용할 수도 있습니다.

FrameworkElement에서 파생하는 이점

다음 사항 중 하나라도 적용되면 FrameworkElement에서 파생하는 것을 고려합니다.

  • 단순한 요소 컴퍼지션에서 제공하는 기능 이상으로 컨트롤의 모양을 정확하게 제어하려고 합니다.

  • 자체 렌더링 논리를 정의하여 컨트롤의 모양을 정의하려고 합니다.

  • UserControlControl에서 가능한 방법 이상의 새로운 방법으로 기존 요소를 구성하려고 합니다.

컨트롤 제작 기본 사항

앞에서 설명한 것처럼 WPF의 가장 강력한 기능 중 하나는 컨트롤의 기본 속성 설정 이상으로 모양 및 동작을 변경하면서 사용자 지정 컨트롤을 만들지 않아도 되는 것입니다. 스타일 지정, 데이터 바인딩 및 트리거 기능은 WPF 속성 시스템 및 WPF 이벤트 시스템에 의해 가능합니다. 다음 섹션에서는 사용자 지정 컨트롤을 만드는 데 사용하는 모델에 관계없이 따라야 하는 몇 가지 방법을 설명합니다. 이에 따라 사용자 지정 컨트롤의 사용자는 WPF에 포함된 컨트롤의 경우처럼 이러한 기능을 사용할 수 있습니다.

종속성 속성 사용

속성이 종속성 속성인 경우 다음을 수행할 수 있습니다.

  • 스타일에서 속성을 설정합니다.

  • 속성을 데이터 소스에 바인딩합니다.

  • 속성의 값으로 동적 리소스를 사용합니다.

  • 속성에 애니메이션 효과를 줍니다.

컨트롤의 속성이 이 기능을 지원하도록 하려면 종속성 속성으로 구현해야 합니다. 다음 예제에서는 다음을 수행하여 Value라는 종속성 속성을 정의합니다.

  • ValueProperty라는 DependencyProperty 식별자를 publicstaticreadonly 필드로 정의합니다.

  • DependencyProperty.Register를 호출하여 속성 시스템에 속성 이름을 등록하고 다음을 지정합니다.

    • 속성의 이름입니다.

    • 속성의 형식입니다.

    • 속성을 소유하는 형식입니다.

    • 속성의 메타데이터입니다. 메타데이터에는 속성의 기본값인 CoerceValueCallbackPropertyChangedCallback이 포함되어 있습니다.

  • 속성의 getset 접근자를 구현하여 종속성 속성을 등록하는 데 사용된 이름과 동일한 이름인 Value라는 CLR 래퍼 속성을 정의합니다. getset 접근자는 각각 GetValueSetValue만 호출합니다. 클라이언트와 WPF가 접근자를 바이패스하고 GetValueSetValue를 직접 호출할 수 있기 때문에 종속성 속성의 접근자에 추가 논리가 포함되지 않는 것이 좋습니다. 예를 들어 속성이 데이터 소스에 바인딩되면 해당 속성의 set 접근자가 호출되지 않습니다. get 및 set 접근자에 추가 논리를 추가하는 대신 ValidateValueCallback, CoerceValueCallbackPropertyChangedCallback 대리자를 사용하여 값이 변경될 때 값에 응답하거나 값을 확인합니다. 이 콜백에 대한 자세한 내용은 종속성 속성 콜백 및 유효성 검사를 참조하세요.

  • CoerceValue라는 CoerceValueCallback 메서드를 정의합니다. CoerceValueValueMinValue보다 크거나 같고 MaxValue보다 작거나 같도록 합니다.

  • OnValueChanged라는 PropertyChangedCallback 메서드를 정의합니다. OnValueChangedRoutedPropertyChangedEventArgs<T> 개체를 만들고 ValueChanged 라우트된 이벤트를 발생시키도록 준비합니다. 라우트된 이벤트는 다음 섹션에서 설명합니다.

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;			

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}
''' <summary>
''' Identifies the Value dependency property.
''' </summary>
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(Decimal), GetType(NumericUpDown), New FrameworkPropertyMetadata(MinValue, New PropertyChangedCallback(AddressOf OnValueChanged), New CoerceValueCallback(AddressOf CoerceValue)))

''' <summary>
''' Gets or sets the value assigned to the control.
''' </summary>
Public Property Value() As Decimal
    Get
        Return CDec(GetValue(ValueProperty))
    End Get
    Set(ByVal value As Decimal)
        SetValue(ValueProperty, value)
    End Set
End Property

Private Shared Overloads Function CoerceValue(ByVal element As DependencyObject, ByVal value As Object) As Object
    Dim newValue As Decimal = CDec(value)
    Dim control As NumericUpDown = CType(element, NumericUpDown)

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue))

    Return newValue
End Function

Private Shared Sub OnValueChanged(ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
    Dim control As NumericUpDown = CType(obj, NumericUpDown)

    Dim e As New RoutedPropertyChangedEventArgs(Of Decimal)(CDec(args.OldValue), CDec(args.NewValue), ValueChangedEvent)
    control.OnValueChanged(e)
End Sub

자세한 내용은 사용자 지정 종속성 속성을 참조하세요.

라우트된 이벤트 사용

종속성 속성이 추가 기능으로 CLR 속성의 개념을 확장하는 것처럼 라우트된 이벤트가 표준 CLR 이벤트의 개념을 확장합니다. 라우트된 이벤트는 다음 동작을 지원하기 때문에 새로운 WPF 컨트롤을 만들 때 이벤트를 라우트된 이벤트로 구현하는 것도 좋습니다.

  • 이벤트는 여러 컨트롤의 부모에서 처리될 수 ​​있습니다. 이벤트가 버블링 이벤트인 경우 요소 트리의 단일 부모가 이벤트를 구독할 수 있습니다. 그런 다음 애플리케이션 작성자는 하나의 처리기를 사용하여 여러 컨트롤의 이벤트에 응답할 수 있습니다. 예를 들어 ListBox의 각 항목에 컨트롤이 포함되어 있으면(컨트롤이 DataTemplate에 포함되어 있으므로) 애플리케이션 개발자가 ListBox에서 컨트롤의 이벤트에 대한 이벤트 처리기를 정의할 수 있습니다. 이벤트가 컨트롤 중 하나에서 발생할 때마다 이벤트 처리기가 호출됩니다.

  • 라우트된 이벤트는 애플리케이션 개발자가 스타일 내에서 이벤트 처리기를 지정할 수 있게 해주는 EventSetter에서 사용할 수 있습니다.

  • 라우트된 이벤트는 XAML을 사용하여 속성에 애니메이션 효과를 주는 데 유용한 EventTrigger에서 사용할 수 있습니다. 자세한 내용은 애니메이션 개요를 참조하세요.

다음 예제는 다음을 수행하여 라우트된 이벤트를 정의합니다.

  • ValueChangedEvent라는 RoutedEvent 식별자를 publicstaticreadonly 필드로 정의합니다.

  • EventManager.RegisterRoutedEvent 메서드를 호출하여 라우트된 이벤트를 등록합니다. 이 예제에서는 RegisterRoutedEvent를 호출할 때 다음 정보를 지정합니다.

    • 이벤트의 이름은 ValueChanged입니다.

    • 라우팅 전략은 Bubble로, 이는 소스(이벤트를 발생시키는 개체)의 이벤트 처리기가 먼저 호출된 다음, 가장 가까운 부모 요소의 이벤트 처리기부터 시작하여 소스의 부모 요소에 대한 이벤트 처리기가 연속적으로 호출됨을 의미합니다.

    • 이벤트 처리기 형식은 RoutedPropertyChangedEventHandler<T>이며 Decimal 형식으로 구성되어 있습니다.

    • 이벤트의 소유 형식은 NumericUpDown입니다.

  • ValueChanged라는 공용 이벤트를 선언하고 이벤트 접근자 선언을 포함합니다. 이 예제에서는 add 접근자 선언의 AddHandlerremove 접근자 선언의 RemoveHandler를 호출하여 WPF 이벤트 서비스를 사용합니다.

  • ValueChanged 이벤트를 발생시키는 OnValueChanged라는 보호된 가상 메서드를 만듭니다.

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble,
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}
''' <summary>
''' Identifies the ValueChanged routed event.
''' </summary>
Public Shared ReadOnly ValueChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Decimal)), GetType(NumericUpDown))

''' <summary>
''' Occurs when the Value property changes.
''' </summary>
Public Custom Event ValueChanged As RoutedPropertyChangedEventHandler(Of Decimal)
    AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
        MyBase.AddHandler(ValueChangedEvent, value)
    End AddHandler
    RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
        MyBase.RemoveHandler(ValueChangedEvent, value)
    End RemoveHandler
    RaiseEvent(ByVal sender As System.Object, ByVal e As RoutedPropertyChangedEventArgs(Of Decimal))
    End RaiseEvent
End Event

''' <summary>
''' Raises the ValueChanged event.
''' </summary>
''' <param name="args">Arguments associated with the ValueChanged event.</param>
Protected Overridable Sub OnValueChanged(ByVal args As RoutedPropertyChangedEventArgs(Of Decimal))
    MyBase.RaiseEvent(args)
End Sub

자세한 내용은 라우트된 이벤트 개요사용자 지정 라우트된 이벤트 만들기를 참조하세요.

바인딩 사용

해당 논리에서 컨트롤의 UI를 분리하려면 데이터 바인딩 사용을 고려합니다. ControlTemplate을 사용하여 컨트롤의 모양을 정의하는 경우 이는 특히 중요합니다. 데이터 바인딩을 사용하면 코드에서 UI의 특정 부분을 참조하지 않아도 될 수 있습니다. ControlTemplate에 있는 요소를 참조하지 않는 것이 좋습니다. 그 이유는 ControlTemplate에 코드 참조 요소가 있고 ControlTemplate이 변경되면 참조된 요소가 새 ControlTemplate에 포함되어야 하기 때문입니다.

다음 예제에서는 NumericUpDown 컨트롤의 TextBlock을 업데이트하며 코드로 컨트롤에 이름을 지정하고 텍스트 상자를 이름으로 참조합니다.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}
Private Sub UpdateTextBlock()
    valueText.Text = Value.ToString()
End Sub

다음 예제에서는 바인딩을 사용하여 동일한 작업을 수행합니다.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

데이터 바인딩에 대한 자세한 내용은 데이터 바인딩 개요를 참조하세요.

디자이너를 위한 디자인

WPF Designer for Visual Studio에서 사용자 지정 WPF 컨트롤에 대한 지원을 받으려면(예:속성 창에서 속성 편집) 다음 지침을 따릅니다. WPF Designer 개발에 대한 자세한 내용은 Visual Studio에서 XAML 디자인을 참조하세요.

종속성 속성

앞에서 "종속성 속성 사용"에서 설명한 대로 CLR getset 접근자를 구현해야 합니다. 디자이너는 래퍼를 사용하여 종속성 속성의 존재를 감지할 수 있지만 WPF 및 컨트롤의 클라이언트와 같이 속성을 가져오거나 설정할 때 접근자를 호출할 필요가 없습니다.

연결된 속성

다음 지침을 사용하여 사용자 지정 컨트롤에서 연결된 속성을 구현해야 합니다.

  • RegisterAttached 메서드를 사용하여 만들었던 PropertyNameProperty 양식의 publicstaticreadonlyDependencyProperty를 갖춥니다. RegisterAttached에 전달되는 속성 이름은 PropertyName과 일치해야 합니다.

  • SetPropertyNameGetPropertyName이라는 publicstatic CLR 메서드 쌍을 구현합니다. 두 메서드 모두 DependencyProperty에서 파생된 클래스를 첫 번째 인수로 수락해야 합니다. SetPropertyName 메서드는 그 형식이 속성의 등록된 데이터 형식과 일치하는 인수도 수락합니다. GetPropertyName 메서드는 동일한 형식의 값을 반환해야 합니다. SetPropertyName 메서드가 누락된 경우 속성이 읽기 전용으로 표시됩니다.

  • SetPropertyNameGetPropertyName은 대상 종속성 개체에서 각각 GetValueSetValue 메서드로 직접 라우트해야 합니다. 디자이너는 메서드 래퍼를 통해 호출하거나 대상 종속성 개체를 직접 호출하여 연결된 속성에 액세스할 수 있습니다.

연결된 속성에 대한 자세한 내용은 연결된 속성 개요를 참조하세요.

공유 리소스 정의 및 사용

애플리케이션과 동일한 어셈블리에 컨트롤을 포함하거나 여러 애플리케이션에서 사용할 수 있는 별도의 어셈블리에 컨트롤을 패키지화할 수 있습니다. 대부분, 이 항목에서 설명하는 정보는 사용하는 메서드에 관계없이 적용됩니다. 그러나 주목할 만한 차이점이 하나 있습니다. 애플리케이션과 동일한 어셈블리에 컨트롤을 배치하면 App.xaml 파일에 전역 리소스를 자유롭게 추가할 수 있습니다. 그러나 컨트롤만 포함하는 어셈블리는 Application 개체가 연결되어 있지 않으므로 App.xaml 파일을 사용할 수 없습니다.

애플리케이션이 리소스를 찾을 때 다음 순서로 세 가지 수준을 조사합니다.

  1. 요소 수준

    시스템이 리소스를 참조하는 요소로 시작한 다음 루트 요소에 도달할 때까지 논리 부모 등의 리소스를 검색합니다.

  2. 애플리케이션 수준

    Application 개체에 의해 정의된 리소스입니다.

  3. 테마 수준

    테마 수준 사전은 Themes라는 하위 폴더에 저장됩니다. Themes 폴더의 파일은 테마에 해당합니다. 예를 들어 Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml 등이 있을 수 있습니다. generic.xaml이라는 파일이 있을 수도 있습니다. 시스템이 테마 수준에서 리소스를 찾으면 먼저 테마별 파일에서 찾은 다음 generic.xaml에서 찾습니다.

컨트롤이 애플리케이션과 별도의 어셈블리에 있을 때는 전역 리소스를 요소 수준이나 테마 수준에 배치해야 합니다. 두 가지 방법 모두 장점이 있습니다.

요소 수준에서 리소스 정의

사용자 지정 리소스 사전을 만들어 컨트롤 리소스 사전과 병합하면 요소 수준에서 공유 리소스를 정의할 수 있습니다. 이 메서드를 사용하면 리소스 파일의 이름을 원하는 대로 지정할 수 있으며 컨트롤과 동일한 폴더에 배치할 수 있습니다. 요소 수준의 리소스는 간단한 문자열을 키로 사용할 수도 있습니다. 다음 예제에서는 Dictionary1.xaml이라는 LinearGradientBrush 리소스 파일을 만듭니다.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>
  
</ResourceDictionary>

사전을 정의한 후에는 컨트롤의 리소스 사전과 병합해야 합니다. XAML 또는 코드를 사용하여 이 작업을 수행할 수 있습니다.

다음 예제에서는 XAML을 사용하여 리소스 사전을 병합합니다.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

이 방법의 단점은 참조할 때마다 ResourceDictionary 개체가 만들어진다는 것입니다. 예를 들어 라이브러리에 10개의 사용자 지정 컨트롤이 있고 XAML을 사용하여 각 컨트롤에 대한 공유 리소스 사전을 병합하는 경우 10개의 동일한 ResourceDictionary 개체를 만듭니다. 코드에서 리소스를 병합하고 결과 ResourceDictionary를 반환하는 정적 클래스를 만들면 이 문제를 방지할 수 있습니다.

다음 예제에서는 공유 ResourceDictionary를 반환하는 클래스를 만듭니다.

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml",
                                    System.UriKind.Relative);

                _sharedDictionary =
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

다음 예제에서는 공유 리소스를 InitializeComponent를 호출하기 전에 컨트롤의 생성자에 있는 사용자 지정 컨트롤의 리소스와 병합합니다. SharedDictionaryManager.SharedDictionary는 정적 속성이기 때문에 ResourceDictionary가 한 번만 만들어집니다. InitializeComponent가 호출되기 전에 리소스 사전이 병합되었기 때문에 XAML 파일의 컨트롤에서 리소스를 사용할 수 있습니다.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();
}

테마 수준에서 리소스 정의

WPF를 사용하면 다양한 Windows 테마를 위한 리소스를 만들 수 있습니다. 컨트롤 작성자는 특정 테마의 리소스를 정의하여 사용 중인 테마에 따라 컨트롤의 모양을 변경할 수 있습니다. 예를 들어 Windows 고전 테마(Windows 2000의 기본 테마)에 있는 Button의 모양은 Windows Luna 테마(Windows XP의 기본 테마)의 Button과 다릅니다. 그 이유는 Button이 각 테마에 대해 다른 ControlTemplate을 사용하기 때문입니다.

테마와 관련된 리소스는 특정 파일 이름의 리소스 사전에 보관됩니다. 이러한 파일은 컨트롤이 포함된 폴더의 하위 폴더인 Themes라는 폴더에 있어야 합니다. 다음 표에는 각 파일과 관련된 리소스 사전 파일과 테마가 나와 있습니다.

리소스 사전 파일 이름 Windows 테마
Classic.xaml Windows XP의 고전 Windows 9x/2000 모양
Luna.NormalColor.xaml Windows XP의 기본 파란색 테마
Luna.Homestead.xaml Windows XP의 올리브색 테마
Luna.Metallic.xaml Windows XP의 은색 테마
Royale.NormalColor.xaml Windows XP Media Center Edition의 기본 테마
Aero.NormalColor.xaml Windows Vista의 기본 테마

모든 테마에 대해 리소스를 정의할 필요는 없습니다. 특정 테마에 대해 리소스가 정의되지 않은 경우 컨트롤이 리소스에 대해 Classic.xaml을 확인합니다. 현재 테마에 해당하는 파일 또는 Classic.xaml에 리소스가 정의되지 않은 경우 컨트롤이 generic.xaml이라는 리소스 사전 파일에 있는 제네릭 리소스를 사용합니다. generic.xaml 파일은 테마별 리소스 사전 파일과 같은 폴더에 있습니다. generic.xaml은 특정 Windows 테마에 해당하지 않지만 여전히 테마 수준의 사전입니다.

테마 및 UI 자동화 지원 샘플이 있는 C# 또는 Visual Basic NumericUpDown 사용자 지정 컨트롤에는 컨트롤에 대한 NumericUpDown 두 개의 리소스 사전이 포함되어 있습니다. 하나는 generic.xaml이고 다른 하나는 Luna.NormalColor.xaml에 있습니다.

테마별 리소스 사전 파일 중 하나에 ControlTemplate을 배치할 때 다음 예제와 같이 컨트롤에 대한 정적 생성자를 만들고 DefaultStyleKey에서 OverrideMetadata(Type, PropertyMetadata) 메서드를 호출해야 합니다.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
테마 리소스에 대한 키 정의 및 참조

요소 레벨에서 리소스를 정의할 때 문자열을 키로 지정하고 문자열을 통해 리소스에 액세스할 수 있습니다. 테마 수준에서 리소스를 정의할 때는 ComponentResourceKey를 키로 사용해야 합니다. 다음 예제에서는 generic.xaml에서 리소스를 정의합니다.

<LinearGradientBrush 
     x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter}, 
                                  ResourceId=MyEllipseBrush}"  
                                  StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="Blue" Offset="0" />
    <GradientStop Color="Red" Offset="0.5" />
    <GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>

다음 예제에서는 ComponentResourceKey를 키로 지정하여 리소스를 참조합니다.

<RepeatButton 
    Grid.Column="1" Grid.Row="0"
    Background="{StaticResource {ComponentResourceKey 
                        TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                        ResourceId=ButtonBrush}}">
    Up
</RepeatButton>
<RepeatButton 
    Grid.Column="1" Grid.Row="1"
    Background="{StaticResource {ComponentResourceKey 
                    TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                    ResourceId=ButtonBrush}}">
    Down
 </RepeatButton>
테마 리소스의 위치 지정

컨트롤에 대한 리소스를 찾으려면 호스팅 애플리케이션이 어셈블리에 컨트롤 관련 리소스가 있는지 알아야 합니다. ThemeInfoAttribute를 컨트롤이 포함된 어셈블리에 추가하여 이를 수행할 수 있습니다. ThemeInfoAttribute에는 제네릭 리소스의 위치를 ​​지정하는 GenericDictionaryLocation 속성과 테마별 리소스의 위치를 ​​지정하는 ThemeDictionaryLocation 속성이 있습니다.

다음 예제에서는 GenericDictionaryLocationThemeDictionaryLocation 속성을 SourceAssembly로 설정하여 제네릭 및 테마별 리소스가 컨트롤과 동일한 어셈블리에 있다는 것을 지정합니다.

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
           ResourceDictionaryLocation.SourceAssembly)]
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>

참고 항목