Wednesday, January 12, 2022

Making the Bindable attribute useful

I recently spent several hours trying to understand a subtle binding problem and finally realized that classes at different levels of inheritance had identically named properties. One raised PropertyChanged and the other didn't. The XAML was binding to the wrong one and I had to add a RelativeSource clause.

Surely, I thought, there's a way to mark a property as unsuitable for binding so that an error, or at least a warning, is raised if a binding is created with that property as the target. There is a Binding(true|false) attribute but that does almost nothing. All it does is allow/prevent the property being listed by intellisense in certain circumstances. Surely, I thought, I can do better. Here it is.

The intent is to write a routine that finds all the bindings and makes sure none of them use a property marked Binding(false) as the target. ie, something like this.

        [Bindable(false)]
        public string NonBindableProperty { getset; }

For clarity, I broke the problem into three parts.
  1. Find all properties with an explicit Bindable(false) attribute
  2. Find all bindings
  3. Find all bindings whose targets are in the list from 1.
Start a new WPF Framework, C#, Visual Studio project and call it FindNonBindableBindings. Here's the XAML. It binds one textblock to a bindable property, and another to a non-bindable property.

<Window x:Class="FindNonBindableBindings.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FindNonBindableBindings"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="Find NonBindable Bindings" Height="450" Width="800">
    <StackPanel>
        <TextBlock Text="{Binding BindableProperty}"/>
        <TextBlock Text="{Binding NonBindableProperty}"/>
    </StackPanel>
</Window>

Let's start the code-behind with just the properties and the constructor.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup.Primitives;
 
namespace FindNonBindableBindings
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _BindableProperty;
        public string BindableProperty
        {
            get { return _BindableProperty; }
            set { SetProperty(ref _BindableProperty, value); }
        }
 
        [Bindable(false)]
        public string NonBindableProperty { get; set; }
 
        [Bindable(false)]
        public string UnusedNonBindableProperty get; set; }
       
        public MainWindow()
        {
            InitializeComponent();
            BindableProperty = "Hello";
            NonBindableProperty = "Goodbye";
        } 

        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

If you run this you can see that modifying NonBindableProperty after InitializeComponent does not update the textblock. Even though we marked the property as Binding(False), WPF did nothing to let us know we had bound to it.


Let's start by creating a simple class to hold source and target details about a binding so we can generate more useful error messages. Add this class directly in the FindNonBindableBindings namespace. As the original problem was caused by multiple identically named properties, we have to track the DataContext of the target properties.

    public class BindingInfo
    {
        public String TargetName { get; set; }
        public String ElementType { get; set; }
        public String DependencyPropertyName { get; set; }
        public String DataContextName { get; set; }
    }

Now we can add some local variables to the constructor.

    List<PropertyInfo> NonBindableProperties;

    List<BindingInfo> Bindings;
    List<String> Errors;                                                                                                                                                                       
And write a method to return a list of all properties with explicit Binding(false) attributes. This is step 1 from above.

    private List<PropertyInfo> FindNonBindableProperties(FrameworkElement fe)
    {
        List<PropertyInfo> NonBindableProperties = new List<PropertyInfo>();
        foreach (PropertyInfo PI in fe.GetType().GetProperties())
        {
            BindableAttribute BA = PI.GetCustomAttribute<BindableAttribute>(true);
            if (BA != null && !BA.Bindable)
            {
                NonBindableProperties.Add(PI);
            }
        }
        return NonBindableProperties;
    }

Step 2 is to find all bindings recursively, like this.

    private List<BindingInfo> FindAllBindings(DependencyObject fe)
    {
        List<BindingInfo> Bindings = new List<BindingInfo>();
        MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(fe);
        if (markupObject != null)
        {
            foreach (MarkupProperty mp in markupObject.Properties)
            {
                if (mp.DependencyProperty != null)
                {
                    BindingBase b = BindingOperations.GetBindingBase(fe, mp.DependencyProperty);
                    if (b != null)
                        Bindings.Add(new BindingInfo() 
                        
                            TargetName = (b as Binding).Path.Path, 
                            ElementType = fe.GetType().Name, 
                            DependencyPropertyName = mp.DependencyProperty.Name,
                            DataContextName = (fe as FrameworkElement).DataContext.GetType().FullName 
                        });
                }
            }
        }
 
        foreach (object child in LogicalTreeHelper.GetChildren(fe))
            if (child is DependencyObject)
                Bindings.AddRange(FindAllBindings(child as DependencyObject));
 
        return Bindings;
    }

Step 3 compares the results of steps 1 and 2, returning a list of errors. 

    private List<String> CheckForNonBindableBindings(List<PropertyInfo> NonBindableProperties, List<BindingInfo> Bindings)

    {
        List<String> Errors = new List<string>();
        foreach (PropertyInfo PI in NonBindableProperties)
        {
            BindingInfo binding = Bindings.FirstOrDefault(b => b.TargetName == PI.Name && b.DataContextName == PI.DeclaringType.FullName);
            if (binding != null)
                Errors.Add(String.Format("Non-Bindable property '{3}.{0}' is the target of binding from the {1} property of a {2} element",
                           binding.TargetName, binding.DependencyPropertyName, binding.ElementType, binding.DataContextName));
        }
        return Errors;
    }

Lastly we tie it all together with a few lines at the end of the constructor.


        NonBindableProperties = FindNonBindableProperties(this);
        Bindings = FindAllBindings(this);
        Errors = CheckForNonBindableBindings(NonBindableProperties, Bindings);

        MessageBox.Show(string.Join(Environment.NewLine, Errors));                                                  

Let's run the application again. Now we see a dialog box telling us we bound to a property we didn't intend to use for binding.







                                                                                                                                                                                                                                                                                                                                                                                         


 


No comments:

Post a Comment