의존 속성의 기본 개념
WPF에서 의존 속성(Dependency Property)은 애니메이션, 데이터 바인딩, 스타일 등 다양한 동적 기능을 지원하는 핵심 요소입니다. 대부분의 WPF 요소가 노출하는 속성은 실제로 의존 속성이며, 일반 .NET 속성과 동일한 방식으로 사용할 수 있습니다.
WPF는 이러한 의존 속성 시스템을 통해 고유한 동적特性을 지원하면서도 기존 .NET 코드와의 호환성을 유지합니다.
의존 속성 정의 방법
의존 속성을 생성하는 구문은 일반 .NET 속성과 완전히 다릅니다. 첫 번째 단계는 속성을 나타내는 DependencyProperty 클래스의 정적 인스턴스를 정의하는 것입니다. 이 인스턴스는 관련 클래스의 정적 필드로 선언되며, WPF 요소 간에 공유될 수 있습니다.
public class FrameworkElement : UIElement, ...
{
public static readonly DependencyProperty MarginProperty;
// ...
}
명명 규칙에 따라 의존 속성 필드 이름은 일반 속성 이름 뒤에 "Property" 접미사를 붙입니다. 필드는 readonly 키워드로 선언되어 정적 생성자에서만 설정할 수 있습니다.
의존 속성 등록 과정
의존 속성을 사용하기 전에 반드시 등록해야 하며, 이 작업은 관련 클래스의 정적 생성자에서 수행됩니다. DependencyProperty 클래스는 공개 생성자가 없으므로 DependencyProperty.Register() 정적 메서드를 통해 인스턴스를 생성해야 합니다. 생성 후에는 모든 멤버가 읽기 전용이므로 값을 Register() 메서드의 매개변수로 제공해야 합니다.
static FrameworkElement()
{
var metadata = new FrameworkPropertyMetadata(
new Thickness(),
FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register(
"Margin",
typeof(Thickness),
typeof(FrameworkElement),
metadata,
new ValidateValueCallback(FrameworkElement.IsMarginValid));
}
등록 과정은 두 단계로 이루어집니다: 첫째, FrameworkPropertyMetadata 객체를 생성하여 원하는 서비스(데이터 바인딩, 애니메이션 등)를 지정하고, 둘째, DependencyProperty.Register()를 호출하여 속성을 등록합니다. 이때 다음 정보를 제공해야 합니다:
- 속성 이름
- 속성의 데이터 타입
- 속성을 소유하는 클래스 타입
- (선택 사항)
FrameworkPropertyMetadata객체 - (선택 사항) 유효성 검사 콜백
의존 속성 래핑
마지막 단계는 전통적인 .NET 속성으로 의존 속성을 래핑하는 것입니다. WPF 속성은 DependencyObject 기본 클래스에 정의된 GetValue()와 SetValue() 메서드를 사용합니다.
public Thickness Margin
{
set { SetValue(MarginProperty, value); }
get { return (Thickness)GetValue(MarginProperty); }
}
속성 래퍼에서는 SetValue()와 GetValue()만 호출해야 하며, 추가적인 검증 코드나 이벤트 발생을 포함해서는 안 됩니다. 이는 XAML 파싱과 같은 다른 기능이 속성 래퍼를 우회하여 직접 SetValue()와 GetValue()를 호출할 수 있기 때문입니다.
입력 값 검증은 DependencyProperty.ValidateValueCallback을, 이벤트 발생은 FrameworkPropertyMetadata.PropertyChangedCallback을 사용해야 합니다.
myElement.Margin = new Thickness(5);
로컬 값 설정을 제거하려면 ClearValue() 메서드를 사용합니다:
myElement.ClearValue(FrameworkElement.MarginProperty);
의존 속성의 핵심 동작
변경 알림
속성 변경에 반응하려면 데이터 바인딩을 생성하거나 트리거를 작성할 수 있습니다. 의존 속성 자체는 속성 값 변경에 대한 이벤트를 제공하지 않습니다.
동적 값 해석
의존 속성은 여러 속성 제공자(각각 고유한 우선순위를 가짐)에 의존합니다. WPF 속성 시스템은 다음 우선순위(낮은 순에서 높은 순)로 기본 값을 결정합니다:
- 기본 값 (
FrameworkPropertyMetadata로 설정) - 상속된 값 (
FrameworkPropertyMetadata.Inherits플래그가 설정된 경우) - 테마 스타일 값
- 프로젝트 스타일 값
- 로컬 값 (코드 또는 마크업으로 직접 설정)
기본 값은 최종 속성 값이 아니며, WPF는 다음 4단계를 통해 속성 값을 계산합니다:
- 기본 값
- 표현식 (데이터 바인딩 및 리소스)
- 애니메이션
CoerceValueCallback을 통한 값 수정
의존 속성 공유
일부 클래스는 독립적인 클래스 계층 구조를 가짐에도 의존 속성을 공유합니다. 예를 들어, TextBlock.FontFamily와 Control.FontFamily는 모두 TextElement.FontFamilyProperty를 참조합니다. TextElement 클래스의 정적 생성자가 속성을 등록하면, TextBlock과 Control 클래스는 DependencyProperty.AddOwner() 메서드를 호출하여 재사용합니다:
TextBlock.FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
연결된 의존 속성
연결된 속성(Attached Property)은 의존 속성의 한 형태로, WPF 속성 시스템에 의해 관리됩니다. 차이점은 연결된 속성을 적용하는 클래스가 해당 속성을 정의한 클래스와 다를 수 있다는 점입니다.
연결된 속성을 정의하려면 Register() 대신 RegisterAttached() 메서드를 사용합니다:
var metadata = new FrameworkPropertyMetadata(
0,
new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged));
Grid.RowProperty = DependencyProperty.RegisterAttached(
"Row",
typeof(int),
typeof(Grid),
metadata,
new ValidateValueCallback(Grid.IsIntValueNotNegative));
연결된 속성은 일반 .NET 속성 래퍼를 정의하지 않습니다. 대신, SetPropertyName()과 GetPropertyName() 형태의 정적 메서드 쌍을 제공해야 합니다:
public static int GetRow(UIElement element)
{
if (element == null) throw new ArgumentNullException();
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
if (element == null) throw new ArgumentNullException();
element.SetValue(Grid.RowProperty, value);
}
Grid.SetRow(txtElement, 0);
// 또는 직접 호출
txtElement.SetValue(Grid.RowProperty, 0);
속성 유효성 검사
WPF는 잘못된 값을 방지하기 위해 두 가지 콜백을 제공합니다:
- ValidateValueCallback: 새 값을 수락하거나 거부합니다. 일반적으로 속성 제약 조건을 위반하는 명백한 오류를 포착하는 데 사용됩니다.
- CoerceValueCallback: 새 값을 더 수용 가능한 값으로 수정합니다. 동일 객체에 설정된 의존 속성 값 간의 충돌을 처리하는 데 사용됩니다.
속성 값 설정 시 실행 순서:
CoerceValueCallback이 값을 수정하거나DependencyProperty.UnsetValue를 반환하여 변경을 거부합니다.ValidateValueCallback이 호출되어 유효성을 검사합니다. 이 콜백은 실제 객체에 접근할 수 없으므로 다른 속성 값을 확인할 수 없습니다.- 두 단계가 모두 성공하면
PropertyChangedCallback이 트리거되어 다른 클래스에 알림을 보냅니다.
Validation 콜백 예시
private static bool IsMarginValid(object value)
{
Thickness thickness = (Thickness)value;
return thickness.IsValid(true, false, true, false);
}
Coercion 콜백 예시
private static object CoerceMaximum(DependencyObject d, object value)
{
RangeBase baseObj = (RangeBase)d;
if ((double)value < baseObj.Minimum)
{
return baseObj.Minimum;
}
return value;
}