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>
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
PushBindingandPushBindingManager- 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
FishCountproperty in Microsoft's documentation: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)
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:
OneWaybinding between the read-only target property (e.g.,Validation.HasErrors) and an implementation-specific property (let's call itListenerProperty)ListenerProperty.ListenerPropertychanges, update a second implementation-specific property, say...MirrorPropertyto the same value.OneWayToSourcebinding betweenMirrorPropertyand your view model to push the value back to your view modelI've made an illustration of how
PushBindingworks (the example from the blog post I linked)