diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 12db5a0634..d408defd52 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -580,6 +580,14 @@ { "source_path": "dotnet-desktop-guide/framework/wpf/properties/dependency-property-security.md", "redirect_url": "/dotnet/desktop/wpf/advanced/dependency-property-security?view=netframeworkdesktop-4.8" + }, + { + "source_path": "dotnet-desktop-guide/net/wpf/advanced/safe-constructor-patterns-for-dependencyobjects.md", + "redirect_url": "/dotnet/desktop/wpf/properties/safe-constructor-patterns-for-dependencyobjects?view=netdesktop-6.0" + }, + { + "source_path": "dotnet-desktop-guide/framework/wpf/properties/safe-constructor-patterns-for-dependencyobjects.md", + "redirect_url": "/dotnet/desktop/wpf/advanced/safe-constructor-patterns-for-dependencyobjects?view=netframeworkdesktop-4.8" } ] } diff --git a/dotnet-desktop-guide/net/wpf/properties/safe-constructor-patterns-for-dependencyobjects.md b/dotnet-desktop-guide/net/wpf/properties/safe-constructor-patterns-for-dependencyobjects.md new file mode 100644 index 0000000000..788012b515 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/safe-constructor-patterns-for-dependencyobjects.md @@ -0,0 +1,91 @@ +--- +title: "Safe Constructor Patterns for DependencyObjects" +description: "Learn about safe constructor patterns for DependencyObjects in Windows Presentation Foundation (WPF)." +ms.date: "12/15/2021" +dev_langs: + - "csharp" + - "vb" +helpviewer_keywords: + - "constructor patterns for dependency objects [WPF]" + - "dependency objects [WPF], constructor patterns" + - ".NET analyzers [WPF]" +--- + + +# Safe constructor patterns for DependencyObjects (WPF .NET) + +There's a general principle in managed code programming, often enforced by code analysis tools, that class constructors shouldn't call overridable methods. If an overridable method is called by a base class constructor, and a derived class overrides that method, then the override method in the derived class can run before the derived class constructor. If the derived class constructor performs class initialization, then the derived class method might access uninitialized class members. Dependency property classes should avoid setting dependency property values in a class constructor to avoid runtime initialization problems. This article describes how to implement constructors in a way that avoids those problems. + +[!INCLUDE [desktop guide under construction](../../includes/desktop-guide-preview-note.md)] + +## Property system virtual methods and callbacks + +Dependency property virtual methods and callbacks are part of the Windows Presentation Foundation (WPF) property system and expand the versatility of dependency properties. + +A basic operation like setting a dependency property value using will invoke the event and potentially several WPF property system callbacks. + +`OnPropertyChanged` is an example of a WPF property system virtual method that can be overridden by classes that have in their inheritance hierarchy. If you set a dependency property value in a constructor that's called during instantiation of your custom dependency property class, and a class derived from it overrides the `OnPropertyChanged` virtual method, then the derived class `OnPropertyChanged` method will run prior to any derived class constructor. + + and are examples of WPF property system callbacks that can be registered by dependency property classes, and overridden by classes that derive from them. If you set a dependency property value in the constructor of your custom dependency property class, and a class that derives from it overrides one of those callbacks in property metadata, then the derived class callback will run before any derived class constructor. This issue isn't relevant to since it isn't part of property metadata and can only be specified by the registering class. + +For more information on dependency property callbacks, see [Dependency property callbacks and validation](dependency-property-callbacks-and-validation.md). + +### .NET analyzers + +.NET compiler platform analyzers inspect your C# or Visual Basic code for code quality and style issues. If you call overridable methods in a constructor when analyzer rule [CA2214](/dotnet/fundamentals/code-analysis/quality-rules/ca2214) is active, you'll get the warning `CA2214: Don't call overridable methods in constructors`. But, the rule won't flag virtual methods and callbacks that are invoked by the underlying WPF property system when a dependency property value is set in a constructor. + +### Issues caused by derived classes + +If you [seal](/dotnet/csharp/language-reference/keywords/sealed) your custom dependency property class, or otherwise know that your class won't be derived from, then derived class runtime initialization issues don't apply to that class. But, if you create a dependency property class that's inheritable, for instance if you're creating templates or an expandable control library set, avoid calling overridable methods or setting dependency property values from a constructor. + +The following test code demonstrates an unsafe constructor pattern, where a base class constructor sets a dependency property value thus triggering calls to virtual methods and callbacks. + +:::code language="csharp" source="./snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml.cs" id="TestUnsafeConstructorPattern"::: +:::code language="vb" source="./snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml.vb" id="TestUnsafeConstructorPattern"::: + +The order in which methods are called in the unsafe constructor pattern test is: + +1. Derived class static constructor, which overrides the dependency property metadata of `Aquarium` to register and . + +1. Base class constructor, which sets a new dependency property value resulting in a call to the method. The `SetValue` call triggers callbacks and events in the following order: + + 1. , which is implemented in the base class. This callback isn't part of dependency property metadata and can't be implemented in the derived class by overriding metadata. + + 1. `PropertyChangedCallback`, which is implemented in the derived class by overriding dependency property metadata. This callback causes a null reference exception when it calls a method on the uninitialized class field `s_temperatureLog`. + + 1. `CoerceValueCallback`, which is implemented in the derived class by overriding dependency property metadata. This callback causes a null reference exception when it calls a method on the uninitialized class field `s_temperatureLog`. + + 1. event, which is implemented in the derived class by overriding the virtual method. This event causes a null reference exception when it calls a method on the uninitialized class field `s_temperatureLog`. + +1. Derived class parameterless constructor, which initializes `s_temperatureLog`. + +1. Derived class parameter constructor, which sets a new dependency property value resulting in another call to the `SetValue` method. Since `s_temperatureLog` is now initialized, callbacks and events will run without causing null reference exceptions. + +These initialization issues are avoidable through use of safe constructor patterns. + +## Safe constructor patterns + +The derived class initialization issues demonstrated in the test code can be resolved in different ways, including: + +- Avoid setting a dependency property value in a constructor of your custom dependency property class if your class might be used as a base class. If you need to initialize a dependency property value, consider setting the required value as the default value in property metadata during dependency property registration or when overriding metadata. + +- Initialize derived class fields before their use. For example, using any of these approaches: + + - Instantiate and assign instance fields in a single statement. In the previous example, the statement `List s_temperatureLog = new();` would avoid late assignment. + + - Perform assignment in the derived class static constructor, which runs ahead of any base class constructor. In the previous example, putting the assignment statement `s_temperatureLog = new List();` in the derived class static constructor would avoid late assignment. + + - Use lazy initialization and instantiation, which initializes objects as and when they're needed. In the previous example, instantiating and assigning `s_temperatureLog` by using lazy initialization and instantiation would avoid late assignment. For more information, see [Lazy initialization](/dotnet/framework/performance/lazy-initialization). + +- Avoid using uninitialized class variables in WPF property system callbacks and events. + +## See also + +- +- +- +- +- [Dependency property callbacks and validation](dependency-property-callbacks-and-validation.md) +- [Custom Dependency Properties](custom-dependency-properties.md) +- [Dependency Properties Overview](dependency-properties-overview.md) +- [Dependency Property Security](dependency-property-security.md) diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/App.xaml.cs b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/App.xaml.cs new file mode 100644 index 0000000000..31285520b3 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace CodeSampleCsharp +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/AssemblyInfo.cs b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/AssemblyInfo.cs new file mode 100644 index 0000000000..8b5504ecfb --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/CodeSampleCsharp.csproj b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/CodeSampleCsharp.csproj new file mode 100644 index 0000000000..22d78ee6d6 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/CodeSampleCsharp.csproj @@ -0,0 +1,24 @@ + + + + WinExe + net6.0-windows + true + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml new file mode 100644 index 0000000000..a38bcb0615 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml.cs b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml.cs new file mode 100644 index 0000000000..3d9f1d1b30 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/MainWindow.xaml.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows; + +namespace CodeSampleCsharp +{ + /// + /// Interaction logic for MainWindow.xaml. + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + TestUnsafeConstructorPattern(); + } + + // + 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 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(); + } + + // 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); + } + } + // +} diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.Designer.cs b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..64552f96fd --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CodeSampleCsharp.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CodeSampleCsharp.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.resx b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/Properties/Resources.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/app.xaml b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/app.xaml new file mode 100644 index 0000000000..867d2f015f --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/csharp/app.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml new file mode 100644 index 0000000000..f225972592 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml.vb b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml.vb new file mode 100644 index 0000000000..084cbe917e --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/Application.xaml.vb @@ -0,0 +1,6 @@ +Class Application + + ' Application-level events, such as Startup, Exit, and DispatcherUnhandledException + ' can be handled in this file. + +End Class diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/AssemblyInfo.vb b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/AssemblyInfo.vb new file mode 100644 index 0000000000..025ee7271e --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/AssemblyInfo.vb @@ -0,0 +1,11 @@ +Imports System.Windows + +'The ThemeInfo attribute describes where any theme specific and generic resource dictionaries can be found. +'1st parameter: where theme specific resource dictionaries are located +'(used if a resource is not found in the page, +' or application resource dictionaries) + +'2nd parameter: where the generic resource dictionary is located +'(used if a resource is not found in the page, +'app, and any theme specific resource dictionaries) + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/CodeSampleVb.vbproj b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/CodeSampleVb.vbproj new file mode 100644 index 0000000000..34db9b0460 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/CodeSampleVb.vbproj @@ -0,0 +1,22 @@ + + + + WinExe + net6.0-windows + CodeSampleVb + true + + + + + + + + + + + + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml new file mode 100644 index 0000000000..ffa82590aa --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml.vb b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml.vb new file mode 100644 index 0000000000..3abe77f5b0 --- /dev/null +++ b/dotnet-desktop-guide/net/wpf/properties/snippets/safe-constructor-patterns-for-dependencyobjects/vb/MainWindow.xaml.vb @@ -0,0 +1,173 @@ +Namespace CodeSampleVb + + ' + ' Interaction logic for MainWindow.xaml. + ' + Partial Public Class MainWindow + Inherits Window + + Public Sub New() + InitializeComponent() + TestUnsafeConstructorPattern() + End Sub + + ' + 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 + ' + +End Namespace diff --git a/dotnet-desktop-guide/net/wpf/toc.yml b/dotnet-desktop-guide/net/wpf/toc.yml index 8e1cec192b..e697dea2cc 100644 --- a/dotnet-desktop-guide/net/wpf/toc.yml +++ b/dotnet-desktop-guide/net/wpf/toc.yml @@ -100,6 +100,8 @@ items: href: properties/framework-property-metadata.md - name: Dependency property security href: properties/dependency-property-security.md + - name: Safe constructor patterns for DependencyObjects + href: properties/safe-constructor-patterns-for-dependencyobjects.md - name: Common tasks items: - name: Implement a dependency property diff --git a/redirects_generator/definitions.json b/redirects_generator/definitions.json index 31c5ee7f39..d3f02b2625 100644 --- a/redirects_generator/definitions.json +++ b/redirects_generator/definitions.json @@ -391,6 +391,10 @@ "SourceUrl": "/dotnet/desktop/wpf/advanced/dependency-property-security?view=netframeworkdesktop-4.8", "TargetUrl": "/dotnet/desktop/wpf/properties/dependency-property-security?view=netdesktop-6.0" }, + { + "SourceUrl": "/dotnet/desktop/wpf/advanced/safe-constructor-patterns-for-dependencyobjects?view=netframeworkdesktop-4.8", + "TargetUrl": "/dotnet/desktop/wpf/properties/safe-constructor-patterns-for-dependencyobjects?view=netdesktop-6.0" + }, // Systems - XAML {