📜 ⬆️ ⬇️

WPF: What to do when a property does not support binding

Introduction


WPF is a great technology that, in spite of all its flaws, I love very much. However, it is often necessary to write not markup, but code that helps the first one to work as it should. I would like to avoid this and write pure XAML, but so far none of my applications are more complicated than simple ones without various helper (helpers) classes written in C #. Fortunately, there are common cases where one helper can immediately solve a group of problems.

The discussion below will deal with the binding in the usual properties of visual elements that are not dependency properties. Regular means of WPF will not do this. Everything else, we can not learn about the changes of this property, except by subscribing to a special event, which contradicts the MVVM pattern. Such events for each property can be their own. The most common example is PasswordBox and its Password property. So we can not do it:

<PasswordBox Password={Binding OtherProperty} /> 

We will not go into details why the PasswordBox developers did not allow to bind to the password property. We will think what can be done here.

If you do not want to read my reasoning, then at the end of the article published the final code and a link to Github.
')

We are determined with the method of implementation


So! I would like such a decision to make it look like a usual binding, but with some additional parameters. It is also a good idea to be able to do two-way binding. To implement the above, the helper will need three input parameters:
  1. Visual property
  2. Event reporting changes
  3. Data source for binding

For example, for PasswordBox, these will be respectively: the Password property, the PasswordChanged event, and the source OtherProperty .

There are different ways to achieve what you want. Let us dwell on the standard for such cases mechanism - behaviors.

Behavior (behavior) is a class that adds additional functionality to the visual element. There are two types of behavior: static and inherited from the class Behavior <T> . A description of their differences is beyond the scope of the article. I chose the second option, as having great potential.

Write the code


Add a link to the System.Windows.Interactivity.dll assembly. This is part of the Expression Blend's SDK editor and is located in the Extensions section of the Visual Studio assembly selection window.

Create a class that inherits from Behavior <T> :

 public class DependecyPropertyBehavior : Behavior<DependencyObject> { } 

The generic type DependencyObjec t is selected as the most common. After all, we are writing a universal class suitable for any element, and not just PasswordBox .

Algorithm work will be brief. To bind from property to source:
  1. Subscribe to the event of changing the property of the visual element.
  2. In the handler, write the updated value to the source.

For backlinking:
  1. Through binding, we determine the moment of change of the source value.
  2. Write the updated value to the property of the visual element.

For the above input parameters, we will create three properties:

 public string Property { get; set; } public string UpdateEvent { get; set; } public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } } 


In this case, the Property and UpdateEvent properties are normal, that's enough. The Binding property, on the contrary, must be a dependency property, because it is here that the data source is connected.

Now that we have all the input data, let's proceed to processing it in the overridden OnAttached () method. It is called when a behavior is attached to a visual element. The latter can be accessed through the property of the AssociatedObject class. In contrast, OnDetaching () is called when detached.

We need objects to work with the property of the visual element and the event through reflection. The following shows how to receive and subscribe to an event, notifying you of changes in the property of a visual element.

Further in the examples, I lowered various checks to null so as not to clog the attention. In the final version of the class they are present.

 private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); //     _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); //  ,      _eventInfo = elementType.GetEvent(UpdateEvent); //       _handler = CreateDelegateForEvent(_eventInfo, EventFired); //  _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { //  _eventInfo.RemoveEventHandler(AssociatedObject, _handler); } 

In the code above there is a method CreateDelegateForEvent () . It compiles the delegate object for the specified event at run time. After all, we do not know in advance the signature of the event handler. When compiled, the delegate places a call to the action method, which in our case is EventFired () . In it, we will perform the actions we need to update the value of the data source.

 private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); } 

This operation is quite resource-intensive, but it is performed only once, with the connection behavior. It can be optimized by sacrificing flexibility by assuming that events can only be RoutedEvent . Then, instead of an expensive compilation, it is enough to subscribe to the event with the indication of the EventFired () handler, having previously changed its signature to be compatible with RoutedEventHandler . But we leave here the original version. Premature optimization is evil.

The EventFired () method is extremely simple; it writes a new value to the data source:

 private void EventFired() { Binding = _propertyInfo.GetValue(AssociatedObject, null); } 

The only thing left is to change the value of the visual element property when the data source changes. For this, the OnPropertyChanged () override method is suitable , which reports changes to class dependency properties. Since changing the data source also changes the Binding property, we only need to track its new values.

 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } 

Everything seems to be fine. We set a new value for the property of the visual element and ... get a StackOverflowException .

The problem is that when a property changes, the automaton causes a notification event to which we are subscribed. In the event, the source value changes, and when the source changes, the Binding property changes, which brings us back to the OnPropertyChanged () method. Recursion

The simplest solution would be to add a comparison of the old and the new property values:

 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } 

Here we make the assumption that the type Equals () is implemented as it should and will not always return false .

Our helper is ready!

Result


Usage example:

 <StackPanel xmlns:local="clr-namespace:DependecyPropertyBehaviorNamesapce" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" > <PasswordBox> <i:Interaction.Behaviors> <local:DependecyPropertyBehavior UpdateEvent="PasswordChanged" Property="Password" Binding="{Binding Text, ElementName=TestTextBox}" /> </i:Interaction.Behaviors> </PasswordBox> <TextBox x:Name="TestTextBox" /> </StackPanel> 

In this case, the TextBox and PasswordBox will synchronously change values.



Conclusion


We have achieved the work of binding for the property of the visual element, which initially did not support it. Binding works in both directions, and you can use a behavior class for any element without worrying about differences in the names of properties and events.

I apologize in advance for inaccuracies in the text, my first article.
As promised, the final code:

No null checks for quick reference.
 using System; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Windows; using System.Windows.Interactivity; using Expression = System.Linq.Expressions.Expression; namespace DependecyPropertyBehaviorNamesapce { public class DependecyPropertyBehavior : Behavior<DependencyObject> { private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } } public string Property { get; set; } public string UpdateEvent { get; set; } protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); _eventInfo = elementType.GetEvent(UpdateEvent); _handler = CreateDelegateForEvent(_eventInfo, EventFired); _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { _eventInfo.RemoveEventHandler(AssociatedObject, _handler); } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); } private void EventFired() { Binding = _propertyInfo.GetValue(AssociatedObject, null); } } } 


The final version with all checks
 using System; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Windows; using System.Windows.Interactivity; using Expression = System.Linq.Expressions.Expression; namespace DependecyPropertyBehaviorNamesapce { public class DependecyPropertyBehavior : Behavior<DependencyObject> { private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } } public string Property { get; set; } public string UpdateEvent { get; set; } protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); // Getting property. if (Property == null) { PresentationTraceSources.DependencyPropertySource.TraceData( TraceEventType.Error, 1, "Target property not defined." ); return; } _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); if (_propertyInfo == null) { PresentationTraceSources.DependencyPropertySource.TraceData( TraceEventType.Error, 2, string.Format("Property \"{0}\" not found.", Property) ); return; } // Getting event. if (UpdateEvent == null) return; _eventInfo = elementType.GetEvent(UpdateEvent); if (_eventInfo == null) { PresentationTraceSources.MarkupSource.TraceData( TraceEventType.Error, 3, string.Format("Event \"{0}\" not found.", UpdateEvent) ); return; } _handler = CreateDelegateForEvent(_eventInfo, EventFired); _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { if (_eventInfo == null) return; if (_handler == null) return; _eventInfo.RemoveEventHandler(AssociatedObject, _handler); } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; if (AssociatedObject == null) return; if (_propertyInfo == null) return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); } private void EventFired() { if (AssociatedObject == null) return; if (_propertyInfo == null) return; Binding = _propertyInfo.GetValue(AssociatedObject, null); } } } 


Github

Source: https://habr.com/ru/post/215249/


All Articles