0

I have tried to bind my view model property to Validation.HasErrors attached property of a text box (which is read-only). I found a good working solution in this answer by Johan Larsson: https://stackoverflow.com/a/39392158 But I am not an expert on WPF, so I have a hard time understanding how and why it works. I am really puzzled because I don't know all the implicit rules of WPF and XAML engines. I understand the basics of attached properties, binding, and XAML markup, but I don't understand how it comes together in the end. Can someone clarify what is happening here? Here is the code from the solution:

public static class OneWayToSource
{
    public static readonly DependencyProperty BindingsProperty = DependencyProperty.RegisterAttached(
        "Bindings",
        typeof(OneWayToSourceBindings),
        typeof(OneWayToSource),
        new PropertyMetadata(default(OneWayToSourceBindings), OnBinidngsChanged));

    public static void SetBindings(this FrameworkElement element, OneWayToSourceBindings value)
    {
        element.SetValue(BindingsProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
    public static OneWayToSourceBindings GetBindings(this FrameworkElement element)
    {
        return (OneWayToSourceBindings)element.GetValue(BindingsProperty);
    }

    private static void OnBinidngsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((OneWayToSourceBindings)e.OldValue)?.ClearValue(OneWayToSourceBindings.ElementProperty);
        ((OneWayToSourceBindings)e.NewValue)?.SetValue(OneWayToSourceBindings.ElementProperty, d);
    }
}

public class OneWayToSourceBindings : FrameworkElement
{
    private static readonly PropertyPath DataContextPath = new PropertyPath(nameof(DataContext));
    private static readonly PropertyPath HasErrorPath = new PropertyPath($"({typeof(Validation).Name}.{Validation.HasErrorProperty.Name})");
    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.Register(
        nameof(HasError),
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    internal static readonly DependencyProperty ElementProperty = DependencyProperty.Register(
        "Element",
        typeof(UIElement),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(UIElement), OnElementChanged));

    private static readonly DependencyProperty HasErrorProxyProperty = DependencyProperty.RegisterAttached(
        "HasErrorProxy",
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(bool), OnHasErrorProxyChanged));

    public bool HasError
    {
        get { return (bool)this.GetValue(HasErrorProperty); }
        set { this.SetValue(HasErrorProperty, value); }
    }

    private static void OnHasErrorProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.SetCurrentValue(HasErrorProperty, e.NewValue);
    }

    private static void OnElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null)
        {
            BindingOperations.ClearBinding(d, DataContextProperty);
            BindingOperations.ClearBinding(d, HasErrorProxyProperty);
        }
        else
        {
            var dataContextBinding = new Binding
                                         {
                                             Path = DataContextPath,
                                             Mode = BindingMode.OneWay,
                                             Source = e.NewValue
                                         };
            BindingOperations.SetBinding(d, DataContextProperty, dataContextBinding);

            var hasErrorBinding = new Binding
                                      {
                                          Path = HasErrorPath,
                                          Mode = BindingMode.OneWay,
                                          Source = e.NewValue
                                      };
            BindingOperations.SetBinding(d, HasErrorProxyProperty, hasErrorBinding);
        }
    }
}

Xaml part:

<StackPanel>
    <TextBox Text="{Binding ValueInVM, UpdateSourceTrigger=PropertyChanged}">
        <local:OneWayToSource.Bindings>
            <local:OneWayToSourceBindings HasError="{Binding ValueInVM}" />
        </local:OneWayToSource.Bindings>
    </TextBox>
    <CheckBox IsChecked="{Binding ValueInVM, Mode=OneWay}" />
</StackPanel>
Yasin Br
  • 1,675
  • 1
  • 6
  • 16
  • Does this answer your question? [OneWayToSource binding from readonly property in XAML](https://stackoverflow.com/questions/658170/onewaytosource-binding-from-readonly-property-in-xaml) – Mike Christiansen Jul 06 '22 at 16:07

1 Answers1

0

You may find this question/answer more helpful. The most upvoted answer (but not the accepted answer...) has an associated blog post that goes over the details a bit more.

That blog post includes PushBinding and PushBindingManager - the two main pieces you need to get all this working well (plus a couple other little classes, but they're no biggie).

These are a bit more well done (in my opinion) than the code you referenced in your post. It's what I use in my own code (or, at least, what I derived mine from).


First, the most crucial point to consider:

Read-only dependency properties do not have public property setters. Looking at the example FishCount property in Microsoft's documentation:

    internal static readonly DependencyPropertyKey FishCountPropertyKey =
        DependencyProperty.RegisterReadOnly(
          name: "FishCount",
          propertyType: typeof(int),
          ownerType: typeof(Aquarium),
          typeMetadata: new FrameworkPropertyMetadata());

    // Declare a public get accessor.
    public int FishCount =>
        (int)GetValue(FishCountPropertyKey.DependencyProperty);

You can see that there's only a public getter - no setter. Now, let's add to this, the documentation for read-only attached properties states (emphasis mine)

Read-only attached properties are a rare scenario, because the primary scenario for an attached property is its use in XAML. Without a public setter, an attached property cannot be set in XAML syntax.


So - if we can't set it in XAML - maybe we can set it in C#.

That's what the code that you're referencing does.

The general approach to this problem is to:

  • Provide an approach (usually an attached property) that supports binding via XAML (i.e., read-write properties)
  • Hook into the above so that when a change is made, some logic is performed in C# that will:
    • Create a OneWay binding between the read-only target property (e.g., Validation.HasErrors) and an implementation-specific property (let's call it ListenerProperty)
    • Subscribe to the property changed event for ListenerProperty.
    • When ListenerProperty changes, update a second implementation-specific property, say... MirrorProperty to the same value.
    • Create a OneWayToSource binding between MirrorProperty and your view model to push the value back to your view model

I've made an illustration of how PushBinding works (the example from the blog post I linked)

Illustration of how PushBinding works

Mike Christiansen
  • 1,104
  • 2
  • 13
  • 30