Thursday, February 17, 2022

XamDataGrid can't bind same property twice

I came across this bug when I bound the same property twice in a XamDataGrid. I see no reason I shouldn't be able to do this. Not only did the grid throw an exception, but the exception was the wrong one. I wasted a lot of time before I realized what the real problem was. Here's a simple demonstration.

Start Visual Studio and create a new C#, WPF Framework Application. Call it AnotherXamDataGridBug. Set the XAML and Code behind to this.

<Window x:Class="AnotherXamDataGridBug.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:AnotherXamDataGridBug"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <igDP:XamDataGrid DataSource="{Binding Items}" GroupByAreaLocation="None">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="False"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:FieldLayout.Fields>
                        <igDP:TextField Label="Name 1" Name="Name"/>
                        <igDP:TextField Label="Name 2" Name="Name"/>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </Grid>
</Window>
 
--------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Windows;
 
namespace AnotherXamDataGridBug
{
    public partial class MainWindow : Window
    {
        public class cItem
        {
            public string Name { get; set; }
        }
 
        public List<cItem> Items { get; set; }
        public MainWindow()
        {
            Items = new List<cItem>() { new cItem() { Name = "BOB" }};
            try
            {
                InitializeComponent();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.InnerException.Message);
            }
        }
    }
}

Now run it and you will see an exception being thrown by the grid.


We can see perfectly well that there is a public property called Name so this exception is wrong. Firstly, I should be able to bind to the same field twice - I can bind two text boxes to a property, why can't I bind two data grid columns? Secondly, the "field" (should say property) is there so I would expect a better error message.

According to Infragistics you can get around the problem by using AlternateBinding like this.

<igDP:TextField Label="Name 1" AlternateBinding="{Binding Name}"/>
<igDP:TextField Label="Name 2" AlternateBinding="{Binding Name}"/>

Still smells like a bug.

Friday, January 21, 2022

Width=Auto

If you define a UIElement with HorizontalAlignment="Stretch" and a Width, you will find the element does not render the way you want. If you make heavy use of styles, you might need a way to override a style's Width property to allow you to specify a HorizontalAlignment on the element's declaration. It turns out you can do this with Width="Auto". Take a look at the XAML below.

You can see the top text box is ignoring the HorizontalAlignment="Stretch" because of the width defined in the default style. The lower text box uses Width="auto" to override the style's width which allows the HorizontalAlignment="Stretch" to take effect.

<Window x:Class="AutoWidth.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:AutoWidth"
        mc:Ignorable="d"
        Title="AutoWidth" Height="450" Width="800">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Width" Value="100"/>
            <Setter Property="Background" Value="Yellow"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>
       
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Default Width"/>
        <TextBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch"/>
 
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Width=Auto"/>
        <TextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" Width="auto"/>
    </Grid>
</Window>

 



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.