Dependency Properties & Attached Properties

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Almost every property you set in WPF XAML (Background, Width, Text, IsEnabled, Grid.Row) is a dependency property. A dependency property is a normal-looking CLR property whose value is not stored in a field of the object but is computed by the WPF property system from several possible inputs: a local value, a style, a template, an animation, a binding, a value inherited from a parent, or a default. That computation is why styles, triggers, data binding, animation and theming all work on the same properties. This page was written against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2 (11.0.0-rc.2) with general availability expected at .NET Conf, 10 to 13 November 2026; the property system is unchanged by it as far as the release notes published at the time of writing show. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table further down.

The running scenario is Bookshelf Desktop. Here it supplies a StarRating control (a custom FrameworkElement with Rating, Maximum and a read-only IsRated property), a Bookshelf.Desktop.Theming.UiDensity attached property that flows down the element tree, and two attached behaviours: select-all-on-focus for the search box and a double-click command for the book list. The BooksViewModel, BookDetailsViewModel and BookViewModel are plain CommunityToolkit.Mvvm objects, and that matters for a rule stated early: view models are not dependency objects. They implement INotifyPropertyChanged (through ObservableObject), and the dependency properties live on the view side (controls, behaviours), where the binding engine connects the two. The binding syntax itself is on the Data Binding page; the XAML syntax for setting properties, including attached property syntax, is on the XAML Language Fundamentals page (XAML Language Fundamentals), and styles and triggers (which are the main consumers of precedence) have their own later page.

Why dependency properties exist

A plain CLR property backed by a field is cheap and simple, but it cannot take part in the WPF features that the user interface depends on. Microsoft Learn lists what a dependency property adds. The table maps each capability to the part of Bookshelf Desktop that needs it.

Capability What it means Bookshelf Desktop example

Styles and triggers

The property can be the target of a Setter, Trigger or DataTrigger.

A style sets StarRating.Maximum for the whole app; a trigger changes Opacity when a book is unavailable.

Data binding

The property can be the target of a Binding (only a dependency property can be a binding target).

{Binding Rating} on StarRating.Rating shows and edits BookViewModel.Rating.

Dynamic resources

The property can take a {DynamicResource Key}.

Foreground switches when the app swaps the light and dark dictionaries.

Inheritance

An unset value comes from the parent element.

FontSize set once on the main window reaches every control; so does the custom UiDensity.

Animation

Storyboards can animate the property.

Opacity of a toast message, Width of the side panel.

Change notification

The property system calls a callback whenever the effective value changes, whatever the reason.

StarRating redraws when Rating changes from XAML, a binding, a style or code.

Metadata

Default value, "this affects layout", "binds two-way by default", "inherits" are declared once for the property.

Rating is declared AffectsRender and BindsTwoWayByDefault, so users do not repeat Mode=TwoWay.

Designer support

The Visual Studio designer reads the registered properties and edits them.

Maximum appears in the Properties window.

The cost model is also part of the design. The documentation states that dependency properties use sparse storage: the property system keeps only the values that were actually set on an instance, not one field per property for every instance of every control, and a control such as a Button has a very large number of properties. Reading a dependency property is a lookup through the precedence rules, so it is slower than reading a field. That is acceptable for UI properties, and wrong for hot data such as the contents of a 100,000-element collection.

Anatomy of a dependency property

A dependency property needs four pieces, and the naming is mandatory, not stylistic:

  1. a class that derives from DependencyObject (everything in the WPF element hierarchy does, and so do Freezable objects such as brushes);

  2. a public static readonly DependencyProperty identifier field named PropertyNameProperty;

  3. a call to DependencyProperty.Register (or RegisterReadOnly, or RegisterAttached) that assigns that field, giving the name, the value type, the owner type and optionally metadata and a validation callback;

  4. a CLR wrapper property with the same name, whose accessors do nothing but GetValue and SetValue.

flowchart LR A["Register call: name, type, owner, metadata, validate callback"] --> B["Static identifier field RatingProperty"] B --> C["CLR wrapper Rating: GetValue / SetValue only"] D["XAML attribute Rating=4"] --> E["XAML loader calls SetValue with RatingProperty"] C --> F["Property system stores the local value on the instance"] E --> F F --> G["Validate, coerce, then property-changed callback"]

Notice that the XAML loader does not call the wrapper: it looks up the identifier by the name and calls SetValue directly. A binding does the same. So any logic placed in the wrapper’s setter runs only for C# callers, and a value that arrives from XAML, a style or a binding skips it. This is why the documentation insists that the wrapper contain only GetValue and SetValue; all logic goes into the metadata callbacks.

A complete example: the StarRating control

The control below draws a row of stars as a filled bar (to keep the sample free of image assets). It shows registration, metadata, validation, coercion, a property-changed callback, a read-only property and layout flags in a single class.

using System.Globalization;
using System.Windows;
using System.Windows.Media;

namespace Bookshelf.Desktop.Controls;

/// <summary>Shows a book rating as filled and empty stars.</summary>
public class StarRating : FrameworkElement
{
    private const double StarSize = 16;

    // 1. A read-only property: RegisterReadOnly returns a *key*, kept private.
    private static readonly DependencyPropertyKey IsRatedPropertyKey =
        DependencyProperty.RegisterReadOnly(
            name: nameof(IsRated),
            propertyType: typeof(bool),
            ownerType: typeof(StarRating),
            typeMetadata: new FrameworkPropertyMetadata(defaultValue: false));

    // The public identifier is derived from the key: it can read, not write.
    public static readonly DependencyProperty IsRatedProperty = IsRatedPropertyKey.DependencyProperty;

    // 2. Maximum: layout changes when it changes; its callback re-coerces Rating.
    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register(
            name: nameof(Maximum),
            propertyType: typeof(int),
            ownerType: typeof(StarRating),
            typeMetadata: new FrameworkPropertyMetadata(
                defaultValue: 5,
                flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
                propertyChangedCallback: OnMaximumChanged),
            validateValueCallback: IsValidMaximum);

    // 3. Rating: redraws, binds two-way by default, is coerced into 0..Maximum.
    public static readonly DependencyProperty RatingProperty =
        DependencyProperty.Register(
            name: nameof(Rating),
            propertyType: typeof(double),
            ownerType: typeof(StarRating),
            typeMetadata: new FrameworkPropertyMetadata(
                defaultValue: 0.0,
                flags: FrameworkPropertyMetadataOptions.AffectsRender
                     | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                propertyChangedCallback: OnRatingChanged,
                coerceValueCallback: CoerceRating),
            validateValueCallback: IsValidRating);

    // CLR wrappers: GetValue / SetValue and nothing else.
    public int Maximum
    {
        get => (int)GetValue(MaximumProperty);
        set => SetValue(MaximumProperty, value);
    }

    public double Rating
    {
        get => (double)GetValue(RatingProperty);
        set => SetValue(RatingProperty, value);
    }

    public bool IsRated
    {
        get => (bool)GetValue(IsRatedProperty);
        private set => SetValue(IsRatedPropertyKey, value); // key, not identifier
    }

    // Validation: static, no instance, runs for every value including defaults.
    private static bool IsValidMaximum(object value) => value is int max && max is >= 1 and <= 20;

    private static bool IsValidRating(object value) =>
        value is double rating && !double.IsNaN(rating) && !double.IsInfinity(rating);

    // Coercion: has the instance, may depend on other properties.
    private static object CoerceRating(DependencyObject d, object baseValue)
    {
        var control = (StarRating)d;
        return Math.Clamp((double)baseValue, 0, control.Maximum);
    }

    private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
        d.CoerceValue(RatingProperty); // a smaller Maximum must clip Rating; a larger one restores it

    private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
        ((StarRating)d).IsRated = (double)e.NewValue > 0;

    protected override Size MeasureOverride(Size availableSize) => new(Maximum * StarSize + 32, StarSize);

    protected override void OnRender(DrawingContext drawingContext)
    {
        var empty = Brushes.LightGray;
        var filled = Brushes.Goldenrod;
        for (var i = 0; i < Maximum; i++)
        {
            var fill = Math.Clamp(Rating - i, 0, 1);
            var cell = new Rect(i * StarSize, 0, StarSize - 2, StarSize - 2);
            drawingContext.DrawRectangle(empty, null, cell);
            if (fill > 0)
            {
                drawingContext.DrawRectangle(filled, null, new Rect(cell.X, cell.Y, cell.Width * fill, cell.Height));
            }
        }

        var label = new FormattedText(
            Rating.ToString("0.#", CultureInfo.CurrentCulture),
            CultureInfo.CurrentCulture,
            FlowDirection,
            new Typeface("Segoe UI"),
            11,
            Brushes.Black,
            VisualTreeHelper.GetDpi(this).PixelsPerDip);
        drawingContext.DrawText(label, new Point(Maximum * StarSize + 4, 0));
    }
}

Points to read in that listing:

  • The default value 0.0 is a double. A default whose type does not match the registered propertyType (for instance 0 for a double property) fails when the type’s static initializer runs, which surfaces as a TypeInitializationException the first time the class is used. The documentation says to make sure the default matches the registered type.

  • IsRated is written inside the class through SetValue(IsRatedPropertyKey, ...). Outside code can read the property and bind to it, but SetValue(IsRatedProperty, ...) throws, and TwoWay bindings to it are rejected.

  • The OnMaximumChanged callback calls CoerceValue(RatingProperty). Coercion is not re-evaluated automatically when an unrelated property changes; the dependent property has to be told to re-run it.

  • Math.Clamp in the coercion keeps the user-set (desired) value intact. See the coercion section below.

Using it from XAML needs no code-behind and no wrapper call; the loader calls SetValue with RatingProperty:

<Window x:Class="Bookshelf.Desktop.Views.BookDetailsWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:Bookshelf.Desktop.Controls"
        Title="Book details" Width="420" Height="240">
    <Window.Resources>
        <!-- Every StarRating in this window shows 10 stars unless it overrides Maximum locally. -->
        <Style TargetType="controls:StarRating">
            <Setter Property="Maximum" Value="10"/>
        </Style>
    </Window.Resources>

    <StackPanel Margin="16">
        <TextBlock Text="{Binding Title}" FontWeight="SemiBold"/>

        <!-- BindsTwoWayByDefault: no Mode=TwoWay needed. -->
        <controls:StarRating Rating="{Binding Rating}" Margin="0,8,0,0"/>

        <!-- IsRated is read-only: it can be bound from, never to. -->
        <TextBlock Text="Rated" Margin="0,8,0,0"
                   Visibility="{Binding IsRated, ElementName=ratingControl,
                                Converter={StaticResource BooleanToVisibilityConverter}}"/>
        <controls:StarRating x:Name="ratingControl" Rating="{Binding Rating}" Maximum="5"/>
    </StackPanel>
</Window>

The BooleanToVisibilityConverter key is assumed to be declared in App.xaml (the converter ships with WPF, so a <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> entry is enough). Converters are covered on the Converters, Collection Views & Data Templates page.

Metadata and FrameworkPropertyMetadata options

Every dependency property carries a metadata object created at registration. For a type that derives from FrameworkElement, use FrameworkPropertyMetadata, which extends PropertyMetadata (default value, property-changed callback, coerce callback) with a set of boolean flags exposed through the FrameworkPropertyMetadataOptions enumeration. The options are combined with |.

Option Meaning Typical use

AffectsMeasure

A change requires a new measure pass for this element (the space it needs changes).

Width, Maximum of StarRating, FontSize.

AffectsArrange

A change requires a new arrange pass (position changes, size does not).

HorizontalAlignment.

AffectsParentMeasure

A change requires the parent to measure again.

A custom attached layout hint read by your own panel. The built-in Grid.Row instead uses a property-changed callback that invalidates the parent Grid.

AffectsParentArrange

A change requires the parent to arrange again.

A custom attached position value read by your own panel. The built-in Canvas.Left instead uses a property-changed callback that invalidates the parent Canvas.

AffectsRender

The element needs to be redrawn, but layout is unaffected.

Background, the Rating of StarRating.

Inherits

Children take the value from the nearest ancestor when they have no value of their own. Register with RegisterAttached (see below).

FontSize, FlowDirection, DataContext, UiDensity.

OverridesInheritanceBehavior

Inheritance continues across a boundary that normally stops it (such as a Frame).

Advanced; framework internals mostly.

NotDataBindable

The property cannot be a binding target (the metadata IsDataBindingAllowed is false).

Large objects where binding is not a real scenario.

BindsTwoWayByDefault

A binding on this property uses TwoWay unless it says otherwise.

TextBox.Text, Selector.SelectedItem, StarRating.Rating.

Journal

Navigation journaling should keep this value when a page is navigated away from.

Selector.SelectedIndex.

SubPropertiesDoNotAffectRender

Changes to sub-properties of the value (for example of a Freezable) need no redraw of this element.

Optimization for Freezable-typed properties.

None

No option set (the default when no flags are passed).

Properties that only need a value store.

FrameworkPropertyMetadata has constructor overloads that take the default value, the flags, the property-changed callback, the coerce callback, an isAnimationProhibited flag and a defaultUpdateSourceTrigger (for properties that should, for example, update their binding source PropertyChanged by default). The flags exist because the property system and the layout engine read them: setting AffectsMeasure is what makes InvalidateMeasure run for you. Forgetting a flag is a visible bug (a control that changes size but is not re-laid out), and setting AffectsMeasure where AffectsRender is enough is a performance bug, because a measure pass walks the subtree.

The documentation recommends the following reading of the layout flags: AffectsMeasure for anything that changes the space an element occupies, AffectsArrange for anything that moves it without resizing it, and AffectsRender for anything purely visual. Inherits carries a performance cost and the documentation says to set it only when needed.

Overriding metadata and adding an owner

A derived class can change the metadata of an inherited dependency property without re-registering it, and a class can reuse a property registered by a different class.

using System.Windows;
using System.Windows.Controls;

namespace Bookshelf.Desktop.Controls;

/// <summary>A TextBox that starts with a larger font and wraps its text.</summary>
public class SearchBox : TextBox
{
    static SearchBox()
    {
        // OverrideMetadata: new default value for an inherited property, per derived type.
        FontSizeProperty.OverrideMetadata(
            typeof(SearchBox),
            new FrameworkPropertyMetadata(16.0));

        // A custom control gets its own default style (Themes/Generic.xaml).
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(SearchBox),
            new FrameworkPropertyMetadata(typeof(SearchBox)));
    }
}

/// <summary>Reuses TextBlock.TextProperty rather than registering a new "Caption" property.</summary>
public class Caption : FrameworkElement
{
    public static readonly DependencyProperty TextProperty =
        System.Windows.Controls.TextBlock.TextProperty.AddOwner(typeof(Caption));

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }
}

OverrideMetadata merges, but not uniformly: a PropertyChangedCallback supplied by the override is chained after the base one, so both run, while a CoerceValueCallback supplied by the override replaces the base one, and a new default value replaces the base default. Whatever you do not pass is kept from the base metadata. The validation callback is not part of the metadata and cannot be overridden (see below).

Value precedence

When several inputs try to set the same property, the property system does not take "the last write". It evaluates a fixed ordered list and the first source that has a value wins. The official page calls it "the definitive order of precedence", highest first, and it has 11 steps.

The 11 steps of WPF dependency property value precedence, from coercion at the top to the metadata default value at the bottom
# Source What it covers

1

Property system coercion

The result of the property’s CoerceValueCallback. Applies even on top of an animation.

2

Active animations, or animations with a Hold behavior

An animation wins over the base value even when the base value is local.

3

Local values

A XAML attribute or property element, the CLR wrapper, SetValue. A value set through a binding or a resource reference has the same precedence as one assigned directly.

4

TemplatedParent template property values

Only for elements created by a ControlTemplate or DataTemplate. Inside that template: triggers first, then property sets (usually XAML attributes).

5

Implicit styles

Applies only to the Style property: a style resource whose TargetType matches the element type exactly, found in the page or application. The lookup does not extend to theme styles.

6

Style triggers

A trigger inside an explicit or implicit style that exists in the page or application. Triggers in default styles are lower.

7

Template triggers

A trigger from a directly applied template or from a template inside a style.

8

Style setter values

A Setter in a style that exists in the page or application.

9

Default styles (theme styles)

The control’s built-in style, selected through DefaultStyleKey. Active triggers first, then setters.

10

Inheritance

A value inherited from the parent element, for properties registered with Inherits.

11

Default value from metadata

The default registered with the property, possibly overridden per type. For an inheritable property, the default of the parent takes precedence over the default of the child.

Three consequences matter every day:

  • A local value beats style, trigger and template values (except coercion and animation). That is why setting Background="Red" on a button in XAML makes a Style trigger that sets Background on mouse-over have no effect, and the reason that a hand-set Foreground stops a theme style from greying out a disabled button. Put presentation in styles, not in attributes on each element, if it is meant to be overridable.

  • Inside a style, triggers beat setters (step 6 against step 8), and a style beats the theme (steps 5 to 8 against 9). So an app-level style overrides the Fluent or Aero2 look, and a trigger overrides the style’s setter.

  • Coercion is above everything, including running animations, so a coerced range cannot be escaped by an animation or a local value.

The Style property is special. It cannot itself be styled, and only three things set it: an explicit style (acts as a local value, step 3), an implicit style (step 5) and the default style (step 9, null until the theme is evaluated).

Setting a value without destroying its source

Writing a local value (step 3) has a side effect: it replaces whatever local source was there, including a binding or a DynamicResource. Two APIs fine-tune this:

API Effect

SetValue(property, value)

Sets a local value. If the property had a local binding or dynamic resource reference, it is replaced and ClearValue will not bring it back.

SetCurrentValue(property, value)

Changes the current effective value without touching the source of the previous value and without giving the new value local precedence. A binding stays alive, and the next trigger or style action can set the property again. Use it in control code that reflects user interaction while the property may be bound.

ClearValue(property)

Removes the local value only. The new effective value is whatever the rest of the list gives (a theme style value, for instance), which is not necessarily the metadata default.

ReadLocalValue(property)

Returns the local value, or DependencyProperty.UnsetValue if there is none.

DependencyPropertyHelper.GetValueSource(obj, property)

Returns a ValueSource whose BaseValueSource says which of the steps supplied the value, and whether it is animated, coerced or an expression (a binding).

The following snippet, from Bookshelf Desktop’s debugging toolbox, answers the question "why does this property have that value?":

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;

namespace Bookshelf.Desktop.Diagnostics;

public static class PropertyDiagnostics
{
    /// <summary>Writes which precedence step supplied the value of a dependency property.</summary>
    public static void Describe(DependencyObject target, DependencyProperty property)
    {
        ValueSource source = DependencyPropertyHelper.GetValueSource(target, property);

        Debug.WriteLine(
            $"{property.OwnerType.Name}.{property.Name} = {target.GetValue(property)} " +
            $"from {source.BaseValueSource} " +
            $"(animated: {source.IsAnimated}, coerced: {source.IsCoerced}, expression: {source.IsExpression})");
    }

    public static void Example(Button saveButton)
    {
        Describe(saveButton, Control.BackgroundProperty);
        // For a button styled in App.xaml this prints a BaseValueSource such as Style or DefaultStyle,
        // and Local once the code or the XAML sets Background directly.
    }
}

The BaseValueSource enumeration names the origins (for example Default, Inherited, DefaultStyle, DefaultStyleTrigger, Style, TemplateTrigger, StyleTrigger, ImplicitStyleReference, ParentTemplate, ParentTemplateTrigger, Local). The names were listed from knowledge of the API and are not reproduced in the precedence page itself, which links to the reference documentation for them.

Change callbacks, coercion and validation

Three callbacks exist, and they differ in where they are registered and what they receive:

Callback Delegate Registered in Receives the instance? Purpose

Validate

ValidateValueCallback: bool (object value)

The Register call (not metadata)

No

Reject values that are never valid. Cannot be overridden by derived classes.

Coerce

CoerceValueCallback: object (DependencyObject, object baseValue)

Metadata

Yes

Adjust a candidate value, possibly depending on other properties.

Property changed

PropertyChangedCallback: void (DependencyObject, DependencyPropertyChangedEventArgs)

Metadata

Yes

React after the effective value changed. The arguments carry Property, OldValue and NewValue.

Validation

A validate-value callback returns true when a value is acceptable. When it returns false, the property system throws an ArgumentException (the documented example message is "'∞' is not a valid value for property 'CurrentReading'") and the new value is not applied, so the code that sets the property must be ready to handle it. Validation runs for:

  • object initialization, when the default value is applied;

  • every SetValue (and so every wrapper set, binding and XAML attribute);

  • metadata overrides that specify a new default value.

Because the callback has no instance parameter, all instances share it: it can check "is this a legal Maximum?" but not "is this legal given the other properties of this control?". That second job belongs to coercion. The Validation page covers how user input errors from bindings are surfaced to the user.

Coercion

A coerce callback returns the value the property should actually hold. It runs when the property is set and whenever CoerceValue is called for it. Two properties of the mechanism are easy to miss:

  • The desired value is kept. The user-set value is retained alongside the coerced result. In the official Gauge2 sample, CurrentReading = 5 with MaxReading = 10; lowering MaxReading to 3 coerces the reading to 3, raising it to 4 gives 4, and raising it back to 10 brings the reading back to 5 without the code storing anything. The same happens in StarRating: shrink Maximum from 10 to 3 and a Rating of 7 shows as 3; set Maximum back to 10 and it shows 7 again.

  • Default values are not coerced. A property starts with its metadata default, or returns to it after ClearValue, without passing through the coerce callback.

Returning DependencyProperty.UnsetValue from a coerce callback rejects the change: the previous value stays. The official sample uses it together with GetValueSource to refuse locally set values. Circular dependencies between coerced properties are legal but each change then triggers several re-evaluations, so the documentation advises keeping the web of dependencies small and unambiguous.

The order of the three callbacks, as observed in practice, is validate the incoming value, coerce it, then raise the property-changed callback if the effective value differs from the old one. The Learn pages above describe each callback but do not give a single numbered sequence, so treat that order as common knowledge, not a documented guarantee.

The property-changed callback

The callback is where logic belongs: invalidate a cache, update a read-only property (as OnRatingChanged does), or hook and unhook event handlers on the old and new value. It is static, so the instance arrives as the first argument. For a property of a reference type that raises events, the OldValue and NewValue pair is exactly what the next section needs.

Read-only dependency properties

A read-only dependency property is for state the control computes and the outside should only observe: IsMouseOver, IsKeyboardFocused, ActualWidth, and StarRating.IsRated here.

Step Detail

Register

DependencyProperty.RegisterReadOnly(...) returns a DependencyPropertyKey, not a DependencyProperty.

Expose the identifier

public static readonly DependencyProperty IsRatedProperty = IsRatedPropertyKey.DependencyProperty; so XAML, triggers and bindings can read it.

Keep the key private

The key is the capability to write. SetValue(DependencyPropertyKey, value) is the only way to set the value, so a private or internal key limits writes to your assembly. Learn notes that this gives a read-only property better set security than a read-write one.

Wrapper

A getter, and a private (or no) setter that uses the key.

Attached variant

RegisterAttachedReadOnly for attached properties.

Triggers can test a read-only property, so the pattern "control computes state, style reacts" works without any code:

<Style TargetType="controls:StarRating">
    <Style.Triggers>
        <!-- IsRated is read-only; the trigger only reads it. -->
        <Trigger Property="IsRated" Value="False">
            <Setter Property="Opacity" Value="0.5"/>
        </Trigger>
    </Style.Triggers>
</Style>

A read-only dependency property can be the source of a binding, but Mode=OneWayToSource or TwoWay toward it does not work (the property cannot be written through a binding). When the value must flow into a view model, a common workaround is an attached behaviour that observes the read-only property and forwards it).

Collection-type dependency properties

A mutable reference type default is a trap. If a Books dependency property of type ObservableCollection<BookViewModel> is registered with new ObservableCollection<BookViewModel>() as its default value, every instance of the control shares that one collection, because metadata is per property, not per instance. The documented fix is to register with a null (or empty) default and to create the collection in the instance constructor:

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using Bookshelf.Core;

namespace Bookshelf.Desktop.Controls;

public class ShelfView : Control
{
    private static readonly DependencyPropertyKey BooksPropertyKey =
        DependencyProperty.RegisterReadOnly(
            name: nameof(Books),
            propertyType: typeof(ObservableCollection<Book>),
            ownerType: typeof(ShelfView),
            typeMetadata: new FrameworkPropertyMetadata(defaultValue: null)); // not new ObservableCollection<Book>()

    public static readonly DependencyProperty BooksProperty = BooksPropertyKey.DependencyProperty;

    public ShelfView()
    {
        // One collection per instance.
        SetValue(BooksPropertyKey, new ObservableCollection<Book>());
    }

    public ObservableCollection<Book> Books => (ObservableCollection<Book>)GetValue(BooksProperty);
}

Calling SetValue on a property from a constructor is subject to the safe-constructor rules in the documentation ("Safe constructor patterns for DependencyObjects"): do it in the constructor of a sealed class or after the base initialization, and do not call virtual members. A read-only collection property whose items are managed by the control, as above, is the simplest safe shape.

Property value inheritance

Some properties flow from parent to child: FontSize, FontFamily, Foreground, FlowDirection, AllowDrop and DataContext are the common ones. This is not class inheritance; it is the property system walking up the element tree (mostly the logical tree) to find the nearest ancestor that has a value, and it sits at step 10 of the precedence list, so anything above it (a local value, a style, a template) wins.

To make your own property inheritable:

  1. register it with RegisterAttached, even if you also expose a CLR wrapper (the documentation says inheritance for non-attached properties is undefined across some element boundaries);

  2. pass FrameworkPropertyMetadataOptions.Inherits;

  3. give it a default value so there is something to inherit.

Bookshelf Desktop uses this for a density setting that the user chooses once (on the main window) and every control in the window reads:

using System.Windows;

namespace Bookshelf.Desktop.Theming;

public enum UiDensity
{
    Comfortable,
    Compact,
}

public static class DensityOptions
{
    public static readonly DependencyProperty DensityProperty =
        DependencyProperty.RegisterAttached(
            name: "Density",
            propertyType: typeof(UiDensity),
            ownerType: typeof(DensityOptions),
            defaultMetadata: new FrameworkPropertyMetadata(
                defaultValue: UiDensity.Comfortable,
                flags: FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static UiDensity GetDensity(DependencyObject element)
    {
        ArgumentNullException.ThrowIfNull(element);
        return (UiDensity)element.GetValue(DensityProperty);
    }

    public static void SetDensity(DependencyObject element, UiDensity value)
    {
        ArgumentNullException.ThrowIfNull(element);
        element.SetValue(DensityProperty, value);
    }
}
<Window x:Class="Bookshelf.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:theming="clr-namespace:Bookshelf.Desktop.Theming"
        theming:DensityOptions.Density="Compact"
        Title="Bookshelf Desktop" Width="900" Height="600">
    <Window.Resources>
        <!-- Style trigger on an inherited attached property: Compact shrinks every Button below. -->
        <Style TargetType="Button">
            <Style.Triggers>
                <Trigger Property="theming:DensityOptions.Density" Value="Compact">
                    <Setter Property="Padding" Value="4,1"/>
                    <Setter Property="MinHeight" Value="22"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <StackPanel>
        <!-- Both buttons inherit Compact from the Window: no per-button setting. -->
        <Button Content="Add book" Command="{Binding AddBookCommand}"/>
        <Button Content="Delete" Command="{Binding DeleteBookCommand}"/>
        <!-- A local value stops the inheritance for this subtree only. -->
        <Button Content="Large action" theming:DensityOptions.Density="Comfortable"/>
    </StackPanel>
</Window>

Inheritance has costs and limits. It makes every property change in the subtree more expensive, so the documentation tells authors to set Inherits only when it is needed. It follows the tree the property system sees: it passes through a discontinuous logical tree only for attached properties, and it stops at deliberate boundaries such as a Frame (the documentation names Frame as an example of an inheritance-blocking boundary). Content in a Popup, ContextMenu or ToolTip is not a visual child of its owner; WPF connects the owner for the common cases, but a resource or inherited value that works in a window and not in a popup is the first symptom to look for.

Attached properties

An attached property is a dependency property defined by one class and set on instances of other classes. The defining class does not own the instance data; any DependencyObject can carry the value. Grid.Row, Canvas.Left, DockPanel.Dock, ToolTipService.ShowDuration, Validation.ErrorTemplate and ScrollViewer.HorizontalScrollBarVisibility are attached properties. They exist because a parent often needs information about each child that the child class cannot know about (which row of the grid it sits in).

Aspect Regular dependency property Attached property

Registration

Register / RegisterReadOnly

RegisterAttached / RegisterAttachedReadOnly

CLR access

An instance wrapper Rating

Static accessors GetX(DependencyObject) and SetX(DependencyObject, value) (naming is mandatory)

XAML syntax

<StarRating Rating="3"/>

<TextBlock Grid.Row="1"/> (attached property syntax: Owner.Property)

Value lives on

An instance of the owner class (or its subclasses)

Any DependencyObject, so it does not need a wrapper on the target

Typical use

Control state

Layout hints, shared services and behaviours, inherited ambient values

The accessor parameters should be as general as the intended targets: DependencyObject allows any target, while a narrower type (UIElement, Selector) makes XAML reject targets that cannot be supported. The XAML processor and the designer find the property through the names GetPropertyName and SetPropertyName.

An attached property can also carry a change callback. That is the hook that turns a bare value into a behaviour.

Attached behaviours

An attached behaviour is an attached property whose change callback wires up the element it was set on: it subscribes to events, calls into a command or changes the element’s state. The target element knows nothing about the behaviour, the XAML says what should happen, and the code that implements it lives in one reusable class instead of in the code-behind of every window. It is the idiomatic way, without extra libraries, to add things WPF does not offer out of the box.

The first behaviour selects the whole text of the search box when it gets keyboard focus:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Bookshelf.Desktop.Behaviors;

public static class TextBoxBehavior
{
    public static readonly DependencyProperty SelectAllOnFocusProperty =
        DependencyProperty.RegisterAttached(
            name: "SelectAllOnFocus",
            propertyType: typeof(bool),
            ownerType: typeof(TextBoxBehavior),
            defaultMetadata: new PropertyMetadata(false, OnSelectAllOnFocusChanged));

    public static bool GetSelectAllOnFocus(DependencyObject element) =>
        (bool)element.GetValue(SelectAllOnFocusProperty);

    public static void SetSelectAllOnFocus(DependencyObject element, bool value) =>
        element.SetValue(SelectAllOnFocusProperty, value);

    private static void OnSelectAllOnFocusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not TextBox textBox)
        {
            return;
        }

        // Always unhook first: callbacks run for every change, and the same box can be set twice.
        textBox.GotKeyboardFocus -= OnGotKeyboardFocus;
        if ((bool)e.NewValue)
        {
            textBox.GotKeyboardFocus += OnGotKeyboardFocus;
        }
    }

    private static void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) =>
        ((TextBox)sender).SelectAll();
}

The second binds a view-model command to an event the element has no command for. ListBox has a MouseDoubleClick event but no Command property, so a behaviour provides one, and BooksViewModel.OpenBookCommand stays free of UI types.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Bookshelf.Desktop.Behaviors;

public static class ListBoxBehavior
{
    public static readonly DependencyProperty DoubleClickCommandProperty =
        DependencyProperty.RegisterAttached(
            name: "DoubleClickCommand",
            propertyType: typeof(ICommand),
            ownerType: typeof(ListBoxBehavior),
            defaultMetadata: new PropertyMetadata(null, OnDoubleClickCommandChanged));

    public static ICommand? GetDoubleClickCommand(DependencyObject element) =>
        (ICommand?)element.GetValue(DoubleClickCommandProperty);

    public static void SetDoubleClickCommand(DependencyObject element, ICommand? value) =>
        element.SetValue(DoubleClickCommandProperty, value);

    private static void OnDoubleClickCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not Control control)
        {
            return;
        }

        // OldValue / NewValue tell us whether to hook or unhook; avoids leaks and double subscription.
        control.MouseDoubleClick -= OnMouseDoubleClick;
        if (e.NewValue is ICommand)
        {
            control.MouseDoubleClick += OnMouseDoubleClick;
        }
    }

    private static void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (sender is not ListBox listBox || GetDoubleClickCommand(listBox) is not { } command)
        {
            return;
        }

        var parameter = listBox.SelectedItem;
        if (parameter is not null && command.CanExecute(parameter))
        {
            command.Execute(parameter);
        }
    }
}

The view model and the XAML that consume both behaviours. This BooksViewModel is a reduced stand-in for the full one (which takes IBookRepository and IDialogService): it keeps only the members the behaviours need and takes the MVVM INavigationService from Bookshelf.Core, whose NavigateTo<TViewModel>(object parameter) overload passes the book id.

using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

public partial class BooksViewModel : ObservableObject
{
    private readonly IBookRepository _repository;
    private readonly INavigationService _navigation;

    public BooksViewModel(IBookRepository repository, INavigationService navigation)
    {
        _repository = repository;
        _navigation = navigation;
    }

    public ObservableCollection<BookViewModel> Books { get; } = [];

    [ObservableProperty]
    public partial string SearchText { get; set; } = string.Empty;

    // The behaviour passes the selected BookViewModel as the command parameter.
    [RelayCommand]
    private void OpenBook(BookViewModel book) => _navigation.NavigateTo<BookDetailsViewModel>(book.Id);

    [RelayCommand]
    private async Task LoadAsync(CancellationToken cancellationToken)
    {
        Books.Clear();
        foreach (var book in await _repository.GetAllAsync(cancellationToken))
        {
            Books.Add(new BookViewModel(book));
        }
    }
}
<UserControl x:Class="Bookshelf.Desktop.Views.BooksView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:b="clr-namespace:Bookshelf.Desktop.Behaviors">
    <DockPanel>
        <!-- Attached behaviour: select all text when the box gets focus. -->
        <TextBox DockPanel.Dock="Top"
                 Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
                 b:TextBoxBehavior.SelectAllOnFocus="True"/>

        <!-- Attached behaviour: double-click opens the selected book. -->
        <ListBox ItemsSource="{Binding Books}" DisplayMemberPath="Title"
                 b:ListBoxBehavior.DoubleClickCommand="{Binding OpenBookCommand}"/>
    </DockPanel>
</UserControl>

The BooksViewModel uses the [ObservableProperty] partial-property form introduced for C# 13 and later (the partial string SearchText { get; set; } shape; MVVM Toolkit 8.4 supports it on partial classes) and a generated OpenBookCommand. Setting the attached property to {Binding OpenBookCommand} works because the target property is a dependency property: the attached property is the binding target, which is the whole point of registering it as one.

Two things to know when writing behaviours. First, the PropertyChangedCallback of an attached property also runs during XAML load and again whenever a style or binding changes the value, so every behaviour must be idempotent: unhook before hooking, as above. Second, a static event handler on the attached class holds no reference to the element beyond the event subscription, which is the right shape; a lambda that captures state would instead keep the element alive for as long as the event source lives.

Behaviours with the XAML Behaviors library

For more than one or two behaviours, the maintained library is Microsoft.Xaml.Behaviors.Wpf (the successor to the old Blend System.Windows.Interactivity assembly), which provides Interaction.Behaviors, Interaction.Triggers, EventTrigger, InvokeCommandAction, Behavior<T> and TriggerAction<T> as reusable building blocks. It is not part of WPF itself and not documented under the WPF docs; the project is at https://github.com/microsoft/XamlBehaviorsWpf. Hand-written attached behaviours remain a good choice for small, specific needs (fewer dependencies, no extra namespace).

Using dependency properties from code

Task API

Bind in code

BindingOperations.SetBinding(target, StarRating.RatingProperty, new Binding(nameof(BookViewModel.Rating))), or the SetBinding method every FrameworkElement has.

Set a dynamic resource in code

element.SetResourceReference(Control.BackgroundProperty, "Bookshelf.Brush.Surface").

Observe a property you do not own

DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox)).AddValueChanged(box, handler). Remember to call RemoveValueChanged; the descriptor keeps a strong reference to the target and the handler, a classic leak. Prefer a Binding to a helper dependency property, or a PropertyChangedCallback.

Re-run coercion

CoerceValue(property).

Force re-evaluation from other state

InvalidateProperty(property).

Learn the metadata

property.GetMetadata(typeof(StarRating)); the DefaultMetadata property returns the base metadata.

Enumerate local values

LocalValueEnumerator from GetLocalValueEnumerator().

WPF compared with WinUI 3 and Windows Forms

The idea of a dependency property exists in WPF and in WinUI 3 (and UWP, whose XAML stack it descends from), but the two implementations are not the same, and Windows Forms has no equivalent.

Aspect WPF (.NET 10) WinUI 3 (Windows App SDK 2.5.1) Windows Forms (.NET 10)

Concept

DependencyObject, DependencyProperty, full property system

DependencyObject, DependencyProperty, a smaller property system (Windows.UI.Xaml heritage)

None. Plain CLR properties and INotifyPropertyChanged; Control raises XxxChanged events by convention.

Register

DependencyProperty.Register(name, type, owner, FrameworkPropertyMetadata) with flags, coercion, validation

DependencyProperty.Register(name, type, owner, PropertyMetadata); metadata has a default value and a changed callback

Not applicable. [DefaultValue], [Bindable] attributes and Binding objects for designers and data binding.

Metadata flags (AffectsMeasure, Inherits, BindsTwoWayByDefault)

Yes (FrameworkPropertyMetadataOptions)

No flags; layout and render invalidation are explicit calls (InvalidateMeasure)

Not applicable

Coercion and validation callbacks

Yes

No built-in coerce or validate callback; implement in the CLR wrapper or changed callback

Not applicable

Read-only dependency property

Yes (RegisterReadOnly and a key)

No equivalent registration; expose a read-only CLR property over a private DP, or a plain property

Not applicable

Value precedence

11 documented steps

A simpler order (local, animation, style, template, default); not the same 11-step list

Not applicable

Property value inheritance for custom properties

Yes (Inherits with RegisterAttached)

Built-in properties inherit (FontSize, DataContext and so on); custom inheritance is not available

Ambient properties (Font, ForeColor, BackColor) copy from the parent when unset

Attached properties

RegisterAttached, with GetX and SetX accessors

RegisterAttached, with the same accessor pattern

Not applicable (extender providers such as ToolTip are the closest idea)

Observe a change from outside

DependencyPropertyDescriptor.AddValueChanged or a binding

DependencyObject.RegisterPropertyChangedCallback

The XxxChanged event on the control

The WinUI and WinForms rows were written from the API reference knowledge and from the shape of the WinUI documentation (Dependency properties overview under Windows App SDK: https://learn.microsoft.com/en-us/windows/apps/develop/platform/xaml/dependency-properties-overview), not re-read line by line against the version 2.5.1 documentation; the WinUI precedence order is simplified here.

A WinUI 3 version of the Rating property shows the difference in shape: no flags, no coercion, so clamping moves into the changed callback:

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace Bookshelf.WinUI.Controls;

public sealed class StarRating : Control
{
    public static readonly DependencyProperty RatingProperty =
        DependencyProperty.Register(
            nameof(Rating),
            typeof(double),
            typeof(StarRating),
            PropertyMetadata.Create(0.0, OnRatingChanged));

    public double Rating
    {
        get => (double)GetValue(RatingProperty);
        set => SetValue(RatingProperty, value);
    }

    private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (StarRating)d;
        var clamped = Math.Clamp((double)e.NewValue, 0, 5);
        if (clamped != (double)e.NewValue)
        {
            control.Rating = clamped; // re-enters the callback once with the clamped value
        }
    }
}

What changed from .NET Framework to .NET 8, 9, 10 and 11

Topic Change

The property system

Unchanged. DependencyObject, DependencyProperty, the precedence list and the metadata options are the same in .NET Framework 4.8 and in .NET 10. The code in this page compiles unchanged for both except for the C# language features (collection expressions, file-scoped namespaces, nullable annotations, partial properties).

Assemblies

WindowsBase, PresentationCore and PresentationFramework ship as part of the Windows Desktop runtime; project files use UseWPF instead of references to the framework assemblies.

Nullable reference types

Wrapper properties return the declared type; a dependency property whose default is null should use a nullable wrapper type (ICommand?), as in GetDoubleClickCommand above. GetValue returns object?-like values, so casts need care.

Behaviours library

Microsoft.Xaml.Behaviors.Wpf replaces System.Windows.Interactivity; it supports .NET (Core) and .NET Framework.

Source generators for dependency properties

None in WPF itself. The MVVM Toolkit’s [ObservableProperty] generates INotifyPropertyChanged properties on ObservableObject view models, not dependency properties. The toolkit’s attribute does not apply to DependencyObject classes.

.NET 9 and .NET 10 WPF

New in these releases are Fluent theme support (ThemeMode), MessageBox and performance work; the dependency property APIs are not listed among the changes in the what’s-new pages.

.NET 11 (RC2)

The WPF release notes list bug fixes only at the time of writing; nothing to change on this page.

What Microsoft Learn does not provide

  • The WPF documentation gives the precedence list and the callback behaviour but no single ordered sequence for validate, coerce and changed callbacks; the order stated above is how WPF behaves, not a documented contract.

  • The BaseValueSource members are described in the API reference, not in the conceptual articles.

  • There is no official guidance on unit-testing dependency properties (they need an STA thread for Freezable and FrameworkElement-derived types, and the tests should not require a display).

  • There is no official source generator for WPF dependency properties; the community offers some, and their quality varies. They are not covered here.

  • The WinUI 3 documentation covers its own dependency property system but does not publish a precedence list equal in detail to WPF’s 11 steps.

Pitfalls

  • Logic in the wrapper. It does not run for XAML, styles or bindings. Put it in a callback.

  • A default value of the wrong type (0 for a double) throws when the type is first used.

  • A shared mutable default, such as a collection: create it per instance.

  • Registering twice on the same type, or with a mismatching name for the identifier field: styles, templates and designers depend on the Property suffix.

  • Forgetting a metadata flag. No AffectsMeasure means the control does not re-layout; AffectsMeasure where AffectsRender suffices makes the UI slower.

  • Local values defeat styles. A hard-coded Background in XAML is the usual reason a trigger or a theme "does not work". Use DependencyPropertyHelper.GetValueSource to find the source.

  • SetValue instead of SetCurrentValue in a control. Setting SelectedItem from control code with SetValue removes the binding the app put on it.

  • Event handler leaks in attached behaviours that subscribe to events of a long-lived object, or in DependencyPropertyDescriptor.AddValueChanged.

  • Thread affinity. A DependencyObject is a DispatcherObject: reading or writing its properties from another thread throws InvalidOperationException. Freezable objects that are frozen can be shared across threads, and a view model is not a DependencyObject precisely so that it can be updated off the UI thread (collections excepted; see the threading page).

  • Dependency properties on view models. Do not derive a view model from DependencyObject: it ties the model to WPF and to the UI thread. Use ObservableObject.

  • Overusing attached behaviours for logic that belongs in a command or a converter: a hidden event subscription is harder to test than a command.