In WPF, attached properties are a type of dependency property that allows properties to be defined in one class but used in another. They are widely used in scenarios like behaviors, layout configurations, and interactions where a property needs to be applied to multiple elements without modifying their class definitions. Traditionally, defining attached properties requires boilerplate code, but source generators can automate this process, reducing errors and improving maintainability.
Let's define an attached property using source generators.
[AttachedProperty<bool>("IsDraggable")]
public static partial class DragBehavior
{
}
- The
AttachedPropertyAttribute
automatically generates theIsDraggable
property withGet
andSet
methods.
<UserControl xmlns:local="clr-namespace:MyApp.MyUserControl"
x:Name="UcMyUserControl">
<Grid local:DragBehavior.IsDraggable="True">
<TextBlock Text="Drag Me!" />
</Grid>
</UserControl>
This allows the IsDraggable
property to be applied to any UI element dynamically.
This example showcases advanced metadata
for attached properties, allowing:
- ✔️ Automatic property registration
- ✔️ Flexible application to various UI elements
- ✔️ Custom property value coercion and validation
- ✔️ Efficient UI updates
- ✔️ Simplified code structure
- ✅ Eliminates boilerplate – Just declare the property, and the generator handles the rest.
- ✅ Ensures consistency – Less room for human error.
[AttachedProperty<bool>("IsDraggable"]
public static partial class DragBehavior
{
}
In this example:
- The
[AttachedProperty<bool>("IsDraggable")]
attribute declares an attached property namedIsDraggable
of typebool
for theDragBehavior
class. - The source generator will automatically create the necessary methods and property registration.
The source generator will produce code equivalent to:
public static partial class DragBehavior
{
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached(
"IsDraggable",
typeof(bool),
typeof(DragBehavior),
new PropertyMetadata(defaultValue: BooleanBoxes.FalseBox));
public static bool GetIsDraggable(UIElement element)
=> (bool)element.GetValue(IsDraggableProperty);
public static void SetIsDraggable(UIElement element, bool value)
=> element?.SetValue(IsDraggableProperty, value);
}
[AttachedProperty<bool>(
"IsDraggable",
DefaultValue = false,
PropertyChangedCallback = nameof(PropertyChangedCallback),
CoerceValueCallback = nameof(CoerceValueCallback),
Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default,
IsAnimationProhibited = true,
ValidateValueCallback = nameof(ValidateValueCallback))]
public partial class DragBehavior
{
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
private static object CoerceValueCallback(DependencyObject d, object baseValue)
{
throw new NotImplementedException();
}
private static bool ValidateValueCallback(object value)
{
throw new NotImplementedException();
}
}
In this example:
-
[AttachedProperty<bool>("IsDraggable")]
- Declares a dependency property named
IsDraggable
of typebool
for theDragBehavior
class. - Unlike a regular dependency property, an attached property is not tied to a single class but can be applied to any UI element.
- The source generator will automatically create:
- A
DependencyProperty
field forIsDraggableProperty
. - Static
GetIsDraggable
andSetIsDraggable
methods, allowing other controls to use this property dynamically.
- A
- Declares a dependency property named
-
DefaultValue = false
- Specifies the default value of
IsDraggable
asfalse
, meaning that elements are not draggable unless explicitly enabled.
- Specifies the default value of
-
PropertyChangedCallback = nameof(PropertyChangedCallback)
- Assigns a property changed callback method, that is triggered whenever the
IsDraggable
value changes. - This allows dynamic behavior updates—e.g., adding or removing event handlers for drag operations.
- Assigns a property changed callback method, that is triggered whenever the
-
CoerceValueCallback = nameof(CoerceValueCallback)
- Called before the property value is assigned.
- This method can modify the value before applying it.
- Example: If an element must not be draggable under certain conditions, the
CoerceValueCallback
can force the value back tofalse
.
-
Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender
- Specifies that changes to
IsDraggable
affect the UI layout and rendering. AffectsMeasure
: Triggers a re-measure of the UI element if this property changes.AffectsRender
: Causes a re-render of the control when this property is modified.- Example: If
IsDraggable
changes, drag indicators or visual cues might need updating.
- Specifies that changes to
-
FrameworkPropertyMetadataOptions
- A flag-based enumeration used to specify additional property behavior in WPF.
- Other possible values include:
AffectsParentMeasure
: Causes a layout pass on the parent when the property changes.AffectsArrange
: Forces an arrange pass when the property changes.Inherits
: Allows the property value to propagate down the visual tree.BindsTwoWayByDefault
: Sets the default binding mode to TwoWay.
- These flags optimize performance by ensuring layout changes only occur when necessary.
-
DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default
- Specifies how data binding updates the property's source.
- The
Default
value means that the default behavior of the property type is used. - Other possible values:
PropertyChanged
: Updates the source immediately when the property changes.LostFocus
: Updates the source when the control loses focus (e.g., leaving a text box).Explicit
: Requires manual invocation ofBindingExpression.UpdateSource()
.
-
IsAnimationProhibited = true
- Prevents animations from affecting this property.
- Some dependency properties allow animations to change their values smoothly over time.
- By setting
IsAnimationProhibited = true
, you ensure that no animations can modifyIsDraggable
. - This is useful for properties where instant updates are required, such as boolean state changes.
-
ValidateValueCallback = nameof(ValidateValueCallback)
- Assigns a validation callback method, which ensures that only valid values are assigned to the dependency property.
- This function executes before the property value is set, allowing you to reject invalid values before they are applied.
- The
ValidateValueCallback
method should return abool
:true
: The value is accepted and applied to the property.false
: The value is considered invalid, and an exception is thrown.
The source generator will produce equivalent code:
public partial class DragBehavior
{
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.Register(
nameof(IsDraggable),
typeof(bool),
typeof(DragBehavior),
new FrameworkPropertyMetadata(
defaultValue: BooleanBoxes.FalseBox,
propertyChangedCallback: PropertyChangedCallback,
coerceValueCallback: CoerceValueCallback,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
defaultUpdateSourceTrigger: UpdateSourceTrigger.Default,
isAnimationProhibited: true,
validateValueCallback = ValidateValueCallback));
public bool IsDraggable
{
get => (bool)GetValue(IsDraggableProperty);
set => SetValue(IsDraggableProperty, value);
}
}