DependencyObject의 안전한 생성자 패턴

코드 분석 도구에 의해 적용되는 관리 코드 프로그래밍에는 클래스 생성자가 재정의 가능한 메서드를 호출해서는 안 된다는 일반적인 원칙이 있습니다. 재정의 가능한 메서드를 기본 클래스 생성자에서 호출했고 파생 클래스가 이 메서드를 재정의한다면, 파생 클래스 생성자 보다 먼저 파생 클래스의 재정의 메서드를 실행할 수 있습니다. 파생 클래스 생성자가 클래스 초기화를 수행하는 경우, 파생 클래스 메서드는 초기화되지 않은 클래스 멤버에 액세스할 수 있습니다. 런타임 초기화 문제를 방지 하기 위해, 종속성 속성 클래스는 클래스 생성자에서 종속성 속성 값을 설정하지 않아야 합니다. 이 문서에서는 이러한 문제를 방지하는 방식으로 DependencyObject 생성자를 구현하는 방법을 설명합니다.

중요

.NET 7 및 .NET 6에 관한 데스크톱 가이드 설명서는 제작 중입니다.

속성 시스템 가상 메서드 및 콜백

종속성 속성 가상 메서드 및 콜백은 WPF(Windows Presentation Foundation) 속성 시스템의 일부이며 종속성 속성의 다양성을 확장합니다.

SetValue를 이용한 종속성 속성 값 설정 같은 기본 작업은 OnPropertyChanged 이벤트를 호출하며 여러 WPF 속성 시스템 콜백을 호출할 수도 있습니다.

OnPropertyChanged는 상속 계층 구조에 DependencyObject가 있는 클래스에 의해 재정의될 수 있는 대표적인 WPF 속성 시스템 가상 메서드입니다. 사용자 지정 종속성 속성 클래스를 인스턴스화하는 동안 호출되는 생성자에서 종속성 속성 값을 설정했고, 여기서 파생된 클래스가 OnPropertyChanged 가상 메서드를 재정의한다면 파생 클래스 OnPropertyChanged 메서드는 어떤 파생 클래스 생성자보다 먼저 실행됩니다.

PropertyChangedCallbackCoerceValueCallback은 종속성 속성 클래스에서 등록할 수 있고 이 클래스에서 파생되는 클래스에서 재정의할 수 있는 대표적인 WPF 속성 시스템 콜백입니다. 사용자 지정 종속성 속성 클래스의 생성자에서 종속성 속성 값을 설정했고 이 클래스에서 파생된 클래스가 속성 메타데이터에 있는 이러한 콜백 중 하나를 재정의한다면, 파생 클래스 콜백은 어떤 파생 클래스 생성자 보다 먼저 실행됩니다. 이 문제는 ValidateValueCallback과 관련이 없습니다. 속성 메타데이터의 일부가 아니며 등록 클래스에서만 지정할 수 있기 때문입니다.

종속성 속성 콜백에 대한 자세한 내용은 종속성 속성 콜백 및 유효성 검사를 참조하세요.

.NET 분석기

.NET 컴파일러 플랫폼 분석기는 C# 또는 Visual Basic 코드를 검사하여 코드 품질 및 스타일 문제를 확인합니다. 분석기 규칙 CA2214가 활성 상태일 때 생성자에서 재정의 가능한 메서드를 호출하면, CA2214: Don't call overridable methods in constructors라는 경고가 표시됩니다. 그러나 종속성 속성 값이 생성자에서 설정되었다면 이 규칙은 기본 WPF 속성 시스템에서 호출하는 가상 메서드 및 콜백에 플래그를 지정하지 않습니다.

파생 클래스가 유발하는 문제

사용자 지정 종속성 속성 클래스를 봉인하거나 클래스가 파생되지 않는다는 사실을 안다면, 파생 클래스 런타임 초기화 문제는 이 클래스에 적용되지 않습니다. 그러나 템플릿이나 확장 가능한 컨트롤 라이브러리 집합을 만들 때처럼 상속 가능한 종속성 속성 클래스를 만들 때는, 재정의 가능한 메서드를 호출하거나 생성자에서 종속성 속성 값을 설정해선 안 됩니다.

다음 테스트 코드에서는 기본 클래스 생성자에서 종속성 속성 값을 설정하여 가상 메서드 및 콜백에 대한 호출을 트리거하는 안전하지 않은 생성자 패턴을 보여 줍니다.

    private static void TestUnsafeConstructorPattern()
    {
        //Aquarium aquarium = new();
        //Debug.WriteLine($"Aquarium temperature (C): {aquarium.TempCelcius}");

        // Instantiate and set tropical aquarium temperature.
        TropicalAquarium tropicalAquarium = new(tempCelcius: 25);
        Debug.WriteLine($"Tropical aquarium temperature (C): " +
            $"{tropicalAquarium.TempCelcius}");

        /* Test output:
        Derived class static constructor running.
        Base class ValidateValueCallback running.
        Base class ValidateValueCallback running.
        Base class ValidateValueCallback running.
        Base class parameterless constructor running.
        Base class ValidateValueCallback running.
        Derived class CoerceValueCallback running.
        Derived class CoerceValueCallback: null reference exception.
        Derived class OnPropertyChanged event running.
        Derived class OnPropertyChanged event: null reference exception.
        Derived class PropertyChangedCallback running.
        Derived class PropertyChangedCallback: null reference exception.
        Aquarium temperature (C): 20
        Derived class parameterless constructor running.
        Derived class parameter constructor running.
        Base class ValidateValueCallback running.
        Derived class CoerceValueCallback running.
        Derived class OnPropertyChanged event running.
        Derived class PropertyChangedCallback running.
        Tropical aquarium temperature (C): 25
        */
    }
}

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, owner type, property metadata with default value,
    // and validate-value callback.
    public static readonly DependencyProperty TempCelciusProperty =
        DependencyProperty.Register(
            name: "TempCelcius",
            propertyType: typeof(int),
            ownerType: typeof(Aquarium),
            typeMetadata: new PropertyMetadata(defaultValue: 0),
            validateValueCallback: 
                new ValidateValueCallback(ValidateValueCallback));

    // Parameterless constructor.
    public Aquarium()
    {
        Debug.WriteLine("Base class parameterless constructor running.");

        // Set typical aquarium temperature.
        TempCelcius = 20;

        Debug.WriteLine($"Aquarium temperature (C): {TempCelcius}");
    }

    // Declare public read-write accessors.
    public int TempCelcius
    {
        get => (int)GetValue(TempCelciusProperty);
        set => SetValue(TempCelciusProperty, value);
    }

    // Validate-value callback.
    public static bool ValidateValueCallback(object value)
    {
        Debug.WriteLine("Base class ValidateValueCallback running.");
        double val = (int)value;
        return val >= 0;
    }
}

public class TropicalAquarium : Aquarium
{
    // Class field.
    private static List<int> s_temperatureLog;

    // Static constructor.
    static TropicalAquarium()
    {
        Debug.WriteLine("Derived class static constructor running.");

        // Create a new metadata instance with callbacks specified.
        PropertyMetadata newPropertyMetadata = new(
            defaultValue: 0,
            propertyChangedCallback: new PropertyChangedCallback(PropertyChangedCallback),
            coerceValueCallback: new CoerceValueCallback(CoerceValueCallback));

        // Call OverrideMetadata on the dependency property identifier.
        TempCelciusProperty.OverrideMetadata(
            forType: typeof(TropicalAquarium),
            typeMetadata: newPropertyMetadata);
    }

    // Parameterless constructor.
    public TropicalAquarium()
    {
        Debug.WriteLine("Derived class parameterless constructor running.");
        s_temperatureLog = new List<int>();
    }

    // Parameter constructor.
    public TropicalAquarium(int tempCelcius) : this()
    {
        Debug.WriteLine("Derived class parameter constructor running.");
        TempCelcius = tempCelcius;
        s_temperatureLog.Add(tempCelcius);
    }

    // Property-changed callback.
    private static void PropertyChangedCallback(DependencyObject depObj, 
        DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("Derived class PropertyChangedCallback running.");
        try
        {
            s_temperatureLog.Add((int)e.NewValue);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class PropertyChangedCallback: null reference exception.");
        }
    }

    // Coerce-value callback.
    private static object CoerceValueCallback(DependencyObject depObj, object value)
    {
        Debug.WriteLine("Derived class CoerceValueCallback running.");
        try
        {
            s_temperatureLog.Add((int)value);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class CoerceValueCallback: null reference exception.");
        }
        return value;
    }

    // OnPropertyChanged event.
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("Derived class OnPropertyChanged event running.");
        try
        {
            s_temperatureLog.Add((int)e.NewValue);
        }
        catch (NullReferenceException)
        {
            Debug.WriteLine("Derived class OnPropertyChanged event: null reference exception.");
        }

        // Mandatory call to base implementation.
        base.OnPropertyChanged(e);
    }
}
    Private Shared Sub TestUnsafeConstructorPattern()
        'Aquarium aquarium = new Aquarium();
        'Debug.WriteLine($"Aquarium temperature (C): {aquarium.TempCelcius}");

        ' Instantiate And set tropical aquarium temperature.
        Dim tropicalAquarium As New TropicalAquarium(tempCelc:=25)
        Debug.WriteLine($"Tropical aquarium temperature (C): 
            {tropicalAquarium.TempCelcius}")

        ' Test output:
        ' Derived class static constructor running.
        ' Base class ValidateValueCallback running.
        ' Base class ValidateValueCallback running.
        ' Base class ValidateValueCallback running.
        ' Base class parameterless constructor running.
        ' Base class ValidateValueCallback running.
        ' Derived class CoerceValueCallback running.
        ' Derived class CoerceValueCallback: null reference exception.
        ' Derived class OnPropertyChanged event running.
        ' Derived class OnPropertyChanged event: null reference exception.
        ' Derived class PropertyChangedCallback running.
        ' Derived class PropertyChangedCallback: null reference exception.
        ' Aquarium temperature(C):  20
        ' Derived class parameterless constructor running.
        ' Derived class parameter constructor running.
        ' Base class ValidateValueCallback running.
        ' Derived class CoerceValueCallback running.
        ' Derived class OnPropertyChanged event running.
        ' Derived class PropertyChangedCallback running.
        ' Tropical Aquarium temperature (C): 25

    End Sub
End Class

Public Class Aquarium
    Inherits DependencyObject

    'Register a dependency property with the specified property name,
    ' property type, owner type, property metadata with default value,
    ' and validate-value callback.
    Public Shared ReadOnly TempCelciusProperty As DependencyProperty =
        DependencyProperty.Register(
        name:="TempCelcius",
        propertyType:=GetType(Integer),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New PropertyMetadata(defaultValue:=0),
        validateValueCallback:=
            New ValidateValueCallback(AddressOf ValidateValueCallback))

    ' Parameterless constructor.
    Public Sub New()
        Debug.WriteLine("Base class parameterless constructor running.")

        ' Set typical aquarium temperature.
        TempCelcius = 20

        Debug.WriteLine($"Aquarium temperature (C): {TempCelcius}")
    End Sub

    ' Declare public read-write accessors.
    Public Property TempCelcius As Integer
        Get
            Return GetValue(TempCelciusProperty)
        End Get
        Set(value As Integer)
            SetValue(TempCelciusProperty, value)
        End Set
    End Property

    ' Validate-value callback.
    Public Shared Function ValidateValueCallback(value As Object) As Boolean
        Debug.WriteLine("Base class ValidateValueCallback running.")
        Dim val As Double = CInt(value)
        Return val >= 0
    End Function

End Class

Public Class TropicalAquarium
    Inherits Aquarium

    ' Class field.
    Private Shared s_temperatureLog As List(Of Integer)

    ' Static constructor.
    Shared Sub New()
        Debug.WriteLine("Derived class static constructor running.")

        ' Create a new metadata instance with callbacks specified.
        Dim newPropertyMetadata As New PropertyMetadata(
                defaultValue:=0,
                propertyChangedCallback:=
                    New PropertyChangedCallback(AddressOf PropertyChangedCallback),
                coerceValueCallback:=
                    New CoerceValueCallback(AddressOf CoerceValueCallback))

        ' Call OverrideMetadata on the dependency property identifier.
        TempCelciusProperty.OverrideMetadata(
                forType:=GetType(TropicalAquarium),
                typeMetadata:=newPropertyMetadata)
    End Sub

    ' Parameterless constructor.
    Public Sub New()
        Debug.WriteLine("Derived class parameterless constructor running.")
        s_temperatureLog = New List(Of Integer)()
    End Sub

    ' Parameter constructor.
    Public Sub New(tempCelc As Integer)
        Me.New()
        Debug.WriteLine("Derived class parameter constructor running.")
        TempCelcius = tempCelc
        s_temperatureLog.Add(TempCelcius)
    End Sub

    ' Property-changed callback.
    Private Shared Sub PropertyChangedCallback(depObj As DependencyObject,
        e As DependencyPropertyChangedEventArgs)
        Debug.WriteLine("Derived class PropertyChangedCallback running.")

        Try
            s_temperatureLog.Add(e.NewValue)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class PropertyChangedCallback: null reference exception.")
        End Try
    End Sub

    ' Coerce-value callback.
    Private Shared Function CoerceValueCallback(depObj As DependencyObject, value As Object) As Object
        Debug.WriteLine("Derived class CoerceValueCallback running.")

        Try
            s_temperatureLog.Add(value)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class CoerceValueCallback: null reference exception.")
        End Try

        Return value
    End Function

    ' OnPropertyChanged event.
    Protected Overrides Sub OnPropertyChanged(e As DependencyPropertyChangedEventArgs)
        Debug.WriteLine("Derived class OnPropertyChanged event running.")

        Try
            s_temperatureLog.Add(e.NewValue)
        Catch ex As NullReferenceException
            Debug.WriteLine("Derived class OnPropertyChanged event: null reference exception.")
        End Try

        ' Mandatory call to base implementation.
        MyBase.OnPropertyChanged(e)
    End Sub

End Class

안전하지 않은 생성자 패턴 테스트에서 메서드가 호출되는 순서는 다음과 같습니다.

  1. Aquarium의 종속성 속성 메타데이터를 재정의하여 PropertyChangedCallbackCoerceValueCallback을 등록하는 파생 클래스 정적 생성자.

  2. SetValue 메서드에 대한 호출의 결과로 새 종속성 속성 값을 설정하는 기본 클래스 생성자. SetValue 호출은 콜백과 이벤트를 다음 순서로 트리거합니다.

    1. 기본 클래스에서 구현되는 ValidateValueCallback. 이 콜백은 종속성 속성 메타데이터의 일부가 아니며, 메타데이터를 재정의하여 파생 클래스에서 구현할 수 없습니다.

    2. 종속성 속성 메타데이터를 재정의하여 파생 클래스에서 구현되는 PropertyChangedCallback. 이 콜백은 초기화되지 않은 클래스 필드인 s_temperatureLog에서 메서드를 호출할 때 null 참조 예외를 유발합니다.

    3. 종속성 속성 메타데이터를 재정의하여 파생 클래스에서 구현되는 CoerceValueCallback. 이 콜백은 초기화되지 않은 클래스 필드인 s_temperatureLog에서 메서드를 호출할 때 null 참조 예외를 유발합니다.

    4. 가상 메서드를 재정의하여 파생 클래스에서 구현되는 OnPropertyChanged 이벤트. 이 이벤트는 초기화되지 않은 클래스 필드인 s_temperatureLog에서 메서드를 호출할 때 null 참조 예외를 유발합니다.

  3. s_temperatureLog를 초기화하는 파생 클래스 매개 변수 없는 생성자.

  4. SetValue 메서드에 대한 다른 호출의 결과로 새 종속성 속성 값을 설정하는 파생 클래스 매개 변수 생성자. 이제 s_temperatureLog가 초기화되었으므로, null 참조 예외가 발생하지 않고 콜백과 이벤트가 실행됩니다.

이러한 초기화 문제는 안전한 생성자 패턴을 사용하여 방지할 수 있습니다.

안전한 생성자 패턴

테스트 코드에 확인한 파생 클래스 초기화 문제는 다음을 비롯한 다양한 방법으로 해결할 수 있습니다.

  • 클래스를 기본 클래스로 사용할 수 있는 경우에는 사용자 지정 종속성 속성 클래스의 생성자에서 종속성 속성 값을 설정하지 마십시오. 종속성 속성 값을 초기화 해야 한다면 종속성 속성을 등록하거나 메타데이터를 재정의할 때 속성 메타데이터의 필수 값을 기본값으로 설정해야 합니다.

  • 사용하기 전에 파생 클래스 필드를 초기화합니다. 예를 들어 다음 방법 중 하나를 사용할 수 있습니다.

    • 단일 문에서 인스턴스 필드를 인스턴스화하고 할당합니다. 이전 예제에서 List<int> s_temperatureLog = new(); 문은 지연 할당을 방지합니다.

    • 기본 클래스 생성자보다 먼저 실행되는 파생 클래스 정적 생성자에서 할당을 수행합니다. 이전 예제에서 파생 클래스 정적 생성자에 할당 문 s_temperatureLog = new List<int>();를 배치하면 지연 할당을 방지할 수 있습니다.

    • 필요한 경우 개체를 초기화하는 지연 초기화 및 인스턴스화를 사용합니다. 이전 예제에서는 지연 초기화 및 인스턴스화를 사용하여 s_temperatureLog를 인스턴스화 및 할당하면 지연 할당을 방지할 수 있습니다. 자세한 내용은 초기화 지연을 참조하세요.

  • WPF 속성 시스템 콜백 및 이벤트에서 초기화되지 않은 클래스 변수를 사용하지 않습니다.

참고 항목