Dependency properties and routed events

Why dependency properties exist, how to register one correctly, attached properties, and how bubbling and tunnelling events actually reach your handler.

Dependency properties

A plain CLR property stores a value per object. A dependency property stores nothing until a value is set, then resolves through an ordered list of sources: animation, local value, style, template, inherited value, default. That resolution order is what makes styles, triggers and bindings work at all.

public class RatingBox : Control
{
    public static readonly DependencyProperty RatingProperty =
        DependencyProperty.Register(
            nameof(Rating),                 // must match the CLR property name
            typeof(int),
            typeof(RatingBox),
            new FrameworkPropertyMetadata(
                defaultValue: 0,
                flags: FrameworkPropertyMetadataOptions.AffectsRender |
                       FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                propertyChangedCallback: OnRatingChanged,
                coerceValueCallback: CoerceRating));

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

    private static object CoerceRating(DependencyObject d, object baseValue) =>
        Math.Clamp((int)baseValue, 0, 5);

    private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var box = (RatingBox)d;
        box.InvalidateVisual();
    }
}
  • The wrapper property never does work; every side effect belongs in the metadata callbacks, or bindings and styles bypass it.
  • AffectsMeasure, AffectsArrange and AffectsRender tell the layout system what to invalidate. Without them a changed value silently does not repaint.
  • CoerceValueCallback is for clamping and is re-run when the source changes - do not implement validation there.
  • Register with RegisterAttached for properties that belong on someone else's element, such as Grid.Row.

Routed events

StrategyDirectionTypical events
BubblingElement upwards to the rootMost: Click, KeyDown, SelectionChanged
TunnellingRoot downwards to the elementPreview-prefixed: PreviewKeyDown, PreviewMouseDown
DirectOnly the element itselfMouseEnter on a specific element
// the tunnel event arrives first; the bubble event afterwards
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Escape)
    {
        ClosePanel();
        e.Handled = true;      // stops the bubble event too
    }
}

// handle an event raised by any descendant
private void OnListClick(object sender, RoutedEventArgs e)
{
    if (e.OriginalSource is FrameworkElement fe && fe.DataContext is Order order)
        _viewModel.Selected = order;
}
// a custom routed event on a reusable control
public class DropZone : Border
{
    public static readonly RoutedEvent FileDroppedEvent = EventManager.RegisterRoutedEvent(
        nameof(FileDropped), RoutingStrategy.Bubble,
        typeof(EventHandler<FileDroppedEventArgs>), typeof(DropZone));

    public event EventHandler<FileDroppedEventArgs> FileDropped
    {
        add => AddHandler(FileDroppedEvent, value);
        remove => RemoveHandler(FileDroppedEvent, value);
    }

    protected override void OnDrop(DragEventArgs e)
    {
        base.OnDrop(e);
        RaiseEvent(new FileDroppedEventArgs(FileDroppedEvent, this) { Paths = (string[])e.Data.GetData(DataFormats.FileDrop) });
    }
}
⚠️
In a routed handler you are usually looking at e.OriginalSource, not sender. In a button with a templated visual, sender is the Button while OriginalSource is the inner Border inside its template - casting OriginalSource to the control type is the classic InvalidCastException in WPF.

FAQ

Do I need a dependency property for a binding target?
Yes. Only a DependencyProperty can be bound - that is the primary reason custom controls expose them rather than plain properties.
Why does my handler mark the event handled and nothing happens?
Something upstream already handled it, or the element is not in the route. Add a handler with handledEventsToo: true on the ancestor to see what is arriving, then remove the diagnostic.

What WPF is and how a project is structured Custom controls and user controls

Last refreshed 2026-09-18.