A dependency property in WPF is a property you can set from the WPF markup language and bind to a property of another object (like a view model) using a Binding element. Unlike C# properties, you must programmatically register dependency properties using DependencyProperty.Register. To expose a dependency property as a C# property, you typically write boilerplate code as demonstrated in the following example:
class MyClass
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.Register( nameof(IsEnabled), typeof(bool), typeof(MyClass));
public bool IsEnabled
{
get { return (bool)GetValue(IsEnabledProperty); }
set { SetValue(IsEnabledProperty, value); }
}
}
Instead of writing this boilerplate, add the [DependencyProperty] aspect to a C# automatic property to convert it into a WPF dependency property:
class MyClass
{
[DependencyProperty]
public bool IsEnabled { get; set; }
}
The [DependencyProperty] aspect provides:
- Zero boilerplate.
- Integration with Metalama.Patterns.Contracts to validate dependency properties using aspects like [NotNull] or [Url] (for details, see Metalama.Patterns.Contracts).
- Support for custom pre- and post-assignment callbacks.
- Detection of mutable or read-only dependency properties based on property accessor accessibility.
- Handling of default values.
Creating a dependency property
To create a dependency property using the [DependencyProperty] aspect:
- Add the Metalama.Patterns.Wpf package to your project.
- Open a class derived from DependencyObject, such as a window or user control.
- Add an automatic property to this class.
- Add the [DependencyProperty] custom attribute to this automatic property.
- Optionally, add any contract from the Metalama.Patterns.Contracts package to the automatic property. For details about contracts, see Metalama.Patterns.Contracts.
Example: a simple dependency property
The following example demonstrates the code generation pattern for a standard property.
1using Metalama.Patterns.Wpf;
2using System.Windows.Controls;
3
4namespace Doc.DependencyProperties.Simple;
5
6internal class MyControl : UserControl
7{
8 [DependencyProperty]
9 public double BorderWidth { get; set; }
10}
1using Metalama.Patterns.Wpf;
2using System.Windows;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.Simple;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 public double BorderWidth
11 {
12 get
13 {
14 return (double)GetValue(BorderWidthProperty);
15 }
16
17 set
18 {
19 this.SetValue(BorderWidthProperty, value);
20 }
21 }
22
23 public static readonly DependencyProperty BorderWidthProperty;
24
25 static MyControl()
26 {
27 BorderWidthProperty = DependencyProperty.Register("BorderWidth", typeof(double), typeof(MyControl));
28 }
29}
Example: a read-only dependency property
In the following example, the automatic property has a private setter. The [DependencyProperty] aspect generates a read-only dependency property.
1using Metalama.Patterns.Wpf;
2using System.Windows.Controls;
3
4namespace Doc.DependencyProperties.ReadOnly;
5
6internal class MyControl : UserControl
7{
8 [DependencyProperty]
9 public double BorderWidth { get; private set; }
10}
1using Metalama.Patterns.Wpf;
2using System.Windows;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.ReadOnly;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 public double BorderWidth
11 {
12 get
13 {
14 return (double)GetValue(BorderWidthProperty);
15 }
16
17 private set
18 {
19 this.SetValue(BorderWidthPropertyKey, value);
20 }
21 }
22
23 public static readonly DependencyProperty BorderWidthProperty;
24 private static readonly DependencyPropertyKey BorderWidthPropertyKey;
25
26 static MyControl()
27 {
28 BorderWidthPropertyKey = DependencyProperty.RegisterReadOnly("BorderWidth", typeof(double), typeof(MyControl), null);
29 BorderWidthProperty = BorderWidthPropertyKey.DependencyProperty;
30 }
31}
Adding validation through a contract
The most straightforward way to add validation to a dependency property is to add an aspect from the Metalama.Patterns.Contracts package.
Example: a dependency property with contracts
In the following example, a [Positive] contract is added to the automatic property. The [DependencyProperty] aspect generates code to enforce this precondition.
1using Metalama.Patterns.Contracts;
2using Metalama.Patterns.Wpf;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.Contract;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 [NonNegative]
11 public double BorderWidth { get; set; }
12}
1using Metalama.Patterns.Contracts;
2using Metalama.Patterns.Wpf;
3using System;
4using System.Windows;
5using System.Windows.Controls;
6
7namespace Doc.DependencyProperties.Contract;
8
9internal class MyControl : UserControl
10{
11 [DependencyProperty]
12 [NonNegative]
13 public double BorderWidth
14 {
15 get
16 {
17 return (double)GetValue(BorderWidthProperty);
18 }
19
20 set
21 {
22 this.SetValue(BorderWidthProperty, value);
23 }
24 }
25
26 public static readonly DependencyProperty BorderWidthProperty;
27
28 static MyControl()
29 {
30 BorderWidthProperty = DependencyProperty.Register("BorderWidth", typeof(double), typeof(MyControl), new PropertyMetadata()
31 {
32 CoerceValueCallback = (d, value) =>
33 {
34 value = ApplyBorderWidthContracts((double)value);
35 return value;
36 }
37 });
38 }
39
40 private static double ApplyBorderWidthContracts(double value)
41 {
42 if (value < 0)
43 {
44 throw new ArgumentOutOfRangeException("value", value, "The 'value' parameter must be greater than or equal to 0.");
45 }
46
47 return value;
48 }
49}
Adding validation through a callback method
You can also add validation to a dependency property by adding a callback method to your code. For a property named Foo, name the validation method ValidateFoo with one of the following signatures:
static void ValidateFoo(TPropertyType value)static void ValidateFoo(DependencyProperty property, TPropertyType value)static void ValidateFoo(TDeclaringType instance, TPropertyType value)static void ValidateFoo(DependencyProperty property, TDeclaringType instance, TPropertyType value)void ValidateFoo(TPropertyType value)void ValidateFoo(DependencyProperty property, TPropertyType value)
where TDeclaringType is the declaring type of the target property, DependencyObject, or object, and TPropertyType is any type assignable from the actual type of the target property. TPropertyType can also be a generic type parameter, in which case the method must have exactly one generic parameter.
To specify the validation method explicitly instead of relying on a naming convention, use the DependencyPropertyAttribute.ValidateMethod property.
These methods must throw an exception when the value is invalid.
Example: validation callback
The following example implements a profanity filter on a dependency property. If the value contains the word foo, it throws an exception.
1using Metalama.Patterns.Wpf;
2using System;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.Validate;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 public string? Title { get; set; }
11
12 private void ValidateTitle( string value )
13 {
14 if ( value.Contains( "foo", StringComparison.OrdinalIgnoreCase ) )
15 {
16 throw new ArgumentOutOfRangeException( nameof(value) );
17 }
18 }
19}
1using Metalama.Patterns.Wpf;
2using System;
3using System.Windows;
4using System.Windows.Controls;
5
6namespace Doc.DependencyProperties.Validate;
7
8internal class MyControl : UserControl
9{
10 [DependencyProperty]
11 public string? Title
12 {
13 get
14 {
15 return (string?)GetValue(TitleProperty);
16 }
17
18 set
19 {
20 this.SetValue(TitleProperty, value);
21 }
22 }
23
24 private void ValidateTitle(string value)
25 {
26 if (value.Contains("foo", StringComparison.OrdinalIgnoreCase))
27 {
28 throw new ArgumentOutOfRangeException(nameof(value));
29 }
30 }
31
32 public static readonly DependencyProperty TitleProperty;
33
34 static MyControl()
35 {
36 TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(MyControl), new PropertyMetadata()
37 {
38 CoerceValueCallback = (d, value_1) =>
39 {
40 ((MyControl)d).ValidateTitle((string?)value_1);
41 return value_1;
42 }
43 });
44 }
45}
Handling default values
When you initialize an automatic property with a value in the property declaration (not from the constructor), this expression is used as the default value of the dependency property. In WPF, if you attempt to set a property to its default value in a XAML file, the assignment is grayed out as redundant.
Note
When you initialize an automatic property to a value, this value is also assigned to the property from the instance constructor to mimic the behavior of a C# automatic property. There's a slight difference: in standard automatic properties, the initial value is assigned before the base constructor executes. However, with a dependency property, the value is assigned after the base constructor is invoked.
The following example demonstrates the code generation pattern when an automatic property is initialized to a value.
1using Metalama.Patterns.Wpf;
2using System.Windows.Controls;
3
4namespace Doc.DependencyProperties.DefaultValue;
5
6internal class MyControl : UserControl
7{
8 [DependencyProperty]
9 public double BorderWidth { get; set; } = 5;
10}
1using Metalama.Patterns.Wpf;
2using System.Windows;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.DefaultValue;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 public double BorderWidth
11 {
12 get
13 {
14 return (double)GetValue(BorderWidthProperty);
15 }
16
17 set
18 {
19 this.SetValue(BorderWidthProperty, value);
20 }
21 }
22
23 public static readonly DependencyProperty BorderWidthProperty;
24
25 static MyControl()
26 {
27 BorderWidthProperty = DependencyProperty.Register("BorderWidth", typeof(double), typeof(MyControl), new PropertyMetadata((double)5));
28 }
29
30 public MyControl()
31 {
32 BorderWidth = 5;
33 }
34}
If the property initial value shouldn't be interpreted as the default value of the dependency property, disable this behavior by setting InitializerProvidesDefaultValue to false. This property is available both as an attribute property on the DependencyPropertyAttribute class and through the ConfigureDependencyProperty fabric extension method.
Adding a PropertyChanged callback
While the validate method executes before the assignment, you can also execute code after assigning a dependency property to its new value. For a property named Foo, add a method named OnFooChanged with one of these signatures:
static void OnFooChanged()static void OnFooChanged(DependencyProperty property)static void OnFooChanged(TDeclaringType instance)static void OnFooChanged(DependencyProperty property, TDeclaringType instance)void OnFooChanged()void OnFooChanged(DependencyProperty property)void OnFooChanged(TPropertyType value)void OnFooChanged(DependencyProperty oldValue, DependencyProperty newValue)void OnFooChanged<T>(T value)void OnFooChanged<T>(T oldValue, T newValue)
As with the validate method, explicitly identify the property-changed method instead of relying on a naming convention by using the DependencyPropertyAttribute.PropertyChangedMethod property.
Example: post-assignment callback
In the following example, the OnBorderWidthChanged method is executed after the value of the BorderWidth property has changed.
1using Metalama.Patterns.Wpf;
2using System.Windows.Controls;
3
4namespace Doc.DependencyProperties.OnPropertyChanged;
5
6internal class MyControl : UserControl
7{
8 [DependencyProperty]
9 public double BorderWidth { get; set; }
10
11 [DependencyProperty]
12 public double AvailableWidth { get; private set; }
13
14 private void OnBorderWidthChanged()
15 {
16 this.AvailableWidth = this.Width - this.BorderWidth * 2;
17 }
18}
1using Metalama.Patterns.Wpf;
2using System.Windows;
3using System.Windows.Controls;
4
5namespace Doc.DependencyProperties.OnPropertyChanged;
6
7internal class MyControl : UserControl
8{
9 [DependencyProperty]
10 public double BorderWidth
11 {
12 get
13 {
14 return (double)GetValue(BorderWidthProperty);
15 }
16
17 set
18 {
19 this.SetValue(BorderWidthProperty, value);
20 }
21 }
22
23 [DependencyProperty]
24 public double AvailableWidth
25 {
26 get
27 {
28 return (double)GetValue(AvailableWidthProperty);
29 }
30
31 private set
32 {
33 this.SetValue(AvailableWidthPropertyKey, value);
34 }
35 }
36
37 private void OnBorderWidthChanged()
38 {
39 this.AvailableWidth = this.Width - this.BorderWidth * 2;
40 }
41
42 public static readonly DependencyProperty AvailableWidthProperty;
43 private static readonly DependencyPropertyKey AvailableWidthPropertyKey;
44 public static readonly DependencyProperty BorderWidthProperty;
45
46 static MyControl()
47 {
48 BorderWidthProperty = DependencyProperty.Register("BorderWidth", typeof(double), typeof(MyControl), new PropertyMetadata((d, e) => ((MyControl)d).OnBorderWidthChanged()));
49 AvailableWidthPropertyKey = DependencyProperty.RegisterReadOnly("AvailableWidth", typeof(double), typeof(MyControl), null);
50 AvailableWidthProperty = AvailableWidthPropertyKey.DependencyProperty;
51 }
52}
Customizing naming conventions
The preceding examples rely on the default naming convention, which is based on the following assumptions:
- Given a property named
Foo:- The name of the field containing the DependencyProperty object is
FooProperty. - The name of the validation method is
ValidateFoo. - The name of the post-assignment callback is
OnFooChanged.
- The name of the field containing the DependencyProperty object is
Modify this naming convention by calling the ConfigureDependencyProperty fabric extension method, then builder.AddNamingConvention, and supplying an instance of the DependencyPropertyNamingConvention class.
If specified, the DependencyPropertyNamingConvention.PropertyNamePattern is a regular expression that matches the name of the WPF dependency property from the name of the C# property. If unspecified, the default matching algorithm is used (the dependency property name equals the C# property name). The OnPropertyChangedPattern and ValidatePattern properties are regular expressions that match the validate and property-changed methods. The RegistrationFieldName property represents the name of the field containing the DependencyProperty object. In these expressions, the {PropertyName} substring is replaced by the dependency property name returned by PropertyNamePattern.
Naming conventions are evaluated by priority order. The default priority is the order in which you added the convention. Override it by supplying a value to the priority parameter.
The default naming convention is evaluated last and can't be modified.
Example: Czech naming convention
Here is an illustration of a coding convention for the Czech language.
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Wpf.Configuration;
3
4namespace Doc.DependencyProperties.NamingConvention;
5
6internal class Fabric : ProjectFabric
7{
8 public override void AmendProject( IProjectAmender amender )
9 {
10 amender.ConfigureDependencyProperty( builder =>
11 builder.AddNamingConvention(
12 new DependencyPropertyNamingConvention(
13 "czech" )
14 {
15 ValidatePattern =
16 "Kontrolovat{PropertyName}"
17 } ) );
18 }
19}
1using System;
2using System.Windows;
3using Metalama.Patterns.Wpf;
4
5namespace Doc.DependencyProperties.NamingConvention;
6
7public class MojeOkno : Window
8{
9 [DependencyProperty]
10 public double ŠířkaRámečku { get; set; }
11
12 // No, víme, skloňovat neumíme.
13 private void KontrolovatŠířkaRámečku( double value )
14 {
15 if ( value < 0 )
16 {
17 throw new ArgumentOutOfRangeException();
18 }
19 }
20}
1using System;
2using System.Windows;
3using Metalama.Patterns.Wpf;
4
5namespace Doc.DependencyProperties.NamingConvention;
6
7public class MojeOkno : Window
8{
9 [DependencyProperty]
10 public double ŠířkaRámečku
11 {
12 get
13 {
14 return (double)GetValue(ŠířkaRámečkuProperty);
15 }
16
17 set
18 {
19 this.SetValue(ŠířkaRámečkuProperty, value);
20 }
21 }
22
23 // No, víme, skloňovat neumíme.
24 private void KontrolovatŠířkaRámečku(double value)
25 {
26 if (value < 0)
27 {
28 throw new ArgumentOutOfRangeException();
29 }
30 }
31
32 public static readonly DependencyProperty ŠířkaRámečkuProperty;
33
34 static MojeOkno()
35 {
36 ŠířkaRámečkuProperty = DependencyProperty.Register("ŠířkaRámečku", typeof(double), typeof(MojeOkno), new PropertyMetadata()
37 {
38 CoerceValueCallback = (d, value_1) =>
39 {
40 ((MojeOkno)d).KontrolovatŠířkaRámečku((double)value_1);
41 return value_1;
42 }
43 });
44 }
45}