Tuesday, January 28, 2020

XamDataGrid with UpdateSourceTrigger=PropertyChanged

One of the most annoying bugs in the Infragistics XamDataGrid is lack of support for UpdateSourceTrigger=PropertyChanged. This is an essential feature of binding that causes the bound property to get updated whenever the value in the control is updated. By default, the bound property is only updated when the control loses focus. It is particularly important in data grids because the control is not considered to have lost focus until the user clicks off the row that contains it.

Let's suppose you have a data grid that contains a checkbox and the combobox next to it that should only be enabled if the checkbox is checked. Using the MVVM model the IsChecked property of the checkbox and the IsEnabled property of the combobox are bound to the same property. The user checks the checkbox which sets the bound property which enables the combobox.


By default, the user would have to click off the row to update the bound property. This is not acceptable. Setting UpdateSourceTrigger=PropertyChanged in the IsChecked binding will fix this. 

XamDataGrid does not honor this. There are no errors or warnings, but specifying this option in AlternativeBinding actually breaks the binding. The only solutions (other than not using the XamDataGrid) is to replace Infragistic's CellValuePresenter with your own custom one for every column you need this behavior for, or to accept some compromise behaviors.

This project demonstrates the problem and the DataGrid and XamDataGrid solutions.

Start a new C# WPF project using any Framework after 3.0 and call it InfragisticsUpdateSource. add references to InfragisticsWPF4 like this.


Let's start with the code behind which simply creates some classes and properties we can bind to. The combo box will be bound to the GL1099Line list and the data grids will be bound to the DetailLines list. Replace MainWindow.xaml.cs with this code.


using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace InfragisticsUpateSource
{
    public class cGL1099Line
    {
        public int ID { get; set; }
        public string Code { get; set; }
        public string Description { get; set; }
    }

    public class cDetailLine : INotifyPropertyChanged
    {
        private bool _Is1099;
        private cGL1099Line _GL1099Line;

        public int ID { get; set; }
        public bool Is1099
        {
            get { return _Is1099; }
            set
            {
                _Is1099 = value;
                PropChanged("Is1099");
            }
        }

        public cGL1099Line GL1099Line
        {
            get { return _GL1099Line; }
            set
            {
                _GL1099Line = value;
                PropChanged("GL1099Line");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public List<cGL1099Line> GL1099Lines { get; set; }
        public List<cDetailLine> DetailLines { get; set; }

        private cDetailLine _SelectedDetailLine;
        public cDetailLine SelectedDetailLine
        {
            get { return _SelectedDetailLine; }
            set
            {
                _SelectedDetailLine = value;
                PropChanged("SelectedDetailLine");
            }
        }

        public MainWindow()
        {
            GL1099Lines = new List<cGL1099Line>()
            {
                new cGL1099Line() {ID=1, Code="1", Description="Line 1"},
                new cGL1099Line() {ID=2, Code="2", Description="Line 2"}
            };

            DetailLines = new List<cDetailLine>()
            {
                new cDetailLine() {ID=1, Is1099=false, GL1099Line=null},
                new cDetailLine() {ID=2, Is1099=false, GL1099Line=null}
            };

            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

    }
}

Option 1 - compromise.

We will start with a solution that uses some compromise behaviors. Although the requirement is to enable the combo box that is next to checked boxes, we can use the fact that only one row can be active at a time and enable/disable the whole column depending on the state of the check box in the currently active row.

This technique makes use of the ActiveRow's checkbox to determine the enabled state of the entire combo box column via the ActiveItem property.

Replace MainWindow.xaml with this XAML.

<Window x:Class="InfragisticsUpateSource.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:InfragisticsUpateSource"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        xmlns:igE="http://infragistics.com/Editors"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <igDP:XamDataGrid Grid.Column="0" DataSource="{Binding DetailLines}" GroupByAreaLocation="None"
                          ActiveDataItem="{Binding SelectedDetailLine, UpdateSourceTrigger=PropertyChanged}">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" FixedFieldUIType="None"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Description="">
                    <igDP:FieldLayout.Fields>
                        <igDP:CheckBoxField Label="Is 1099" Name="Is1099"/>
                        <igDP:ComboBoxField Label="1099 Line" Name="GL1099Line"
                                            IsEnabled="{Binding Path=DataContext.SelectedDetailLine.Is1099, Source={StaticResource ProxyElement}}"
                                            ItemsSource="{Binding DataContext.GL1099Lines, Source={StaticResource ProxyElement}}" DisplayMemberPath="Description">
                            <igDP:ComboBoxField.EditorStyle>
                                <Style TargetType="igE:XamComboEditor">
                                    <Setter Property="SelectedItem" Value="{Binding Path=DataItem.GL1099Line}"/>
                                </Style>
                            </igDP:ComboBoxField.EditorStyle>
                        </igDP:ComboBoxField>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>
    </Grid>
</Window>


Click on the check box in row 1 and you can edit the combo box.


then move focus to row 2 and you can't edit any combo boxes.


The frustrating thing about this approach is you still have to define a style for the editor in order to bind the selected item from the combo box back to the active DetailLine. Why isn't there a SelectedItem binding for the ComboBoxField? Microsoft figured it out, why can't Infragistics? This stinks of Hack.

Option 2 - No compromise

My users aren't very good at compromise (although they're improving!) so let's look at what it takes to do exactly what the specifications say - Only enable/disable the combo box (why isn't that called a combox?) on the active row. 

In order to do almost anything with the XamDataGrid you have to re-template the CellValuePresenters  and effectively write your own data grid column.

Add another column to the grid and put this in it. It's a honking great piece of non-intuitive XAML to do something that should take no more than a dozen lines.

        <igDP:XamDataGrid Grid.Column="1" DataSource="{Binding DetailLines}" GroupByAreaLocation="None" SelectedDataItem="{Binding SelectedDetailLine, UpdateSourceTrigger=PropertyChanged}">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" FixedFieldUIType="None"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" SummaryUIType="SingleSelect" LabelTextAlignment="Center"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Description="">
                    <igDP:FieldLayout.Fields>
                        <igDP:CheckBoxField Label="Is 1099" Name="Is1099">
                            <igDP:CheckBoxField.CellValuePresenterStyle>
                                <Style TargetType="igDP:CellValuePresenter">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <CheckBox HorizontalAlignment="Center" VerticalAlignment="Center"
                                                          IsChecked="{Binding RelativeSource={RelativeSource AncestorType=igDP:CellValuePresenter}, Path=Record.DataItem.Is1099, UpdateSourceTrigger=PropertyChanged, IsAsync=True}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </igDP:CheckBoxField.CellValuePresenterStyle>
                        </igDP:CheckBoxField>
                        <igDP:ComboBoxField Label="1099 Line" Name="GL1099Line">
                            <igDP:ComboBoxField.CellValuePresenterStyle>
                                <Style TargetType="igDP:CellValuePresenter">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <ComboBox IsEditable="False" DisplayMemberPath="Description"
                                                          IsEnabled="{Binding RelativeSource={RelativeSource AncestorType=igDP:CellValuePresenter}, Path=Record.DataItem.Is1099}"
                                                          ItemsSource="{Binding DataContext.GL1099Lines, Source={StaticResource ProxyElement}}"
                                                          SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=igDP:CellValuePresenter}, Path=Record.DataItem.GL1099Line, UpdateSourceTrigger=PropertyChanged, IsAsync=True}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </igDP:ComboBoxField.CellValuePresenterStyle>
                        </igDP:ComboBoxField>
                    </igDP:FieldLayout.Fields>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>

If you look at the screen shot below you can see that an editable combo box still looks editable even when the active row is not editable. That's what we get for enabling/disabling individual combo boxes instead of all them. It's functionally the same, but provides useful visual cues to the user.



Option 3 - Microsoft's DataGrid

Now for the Microsoft solution which literally takes 11 lines of XAML. Add another column to the grid and add this XAML.

        <DataGrid Grid.Column="2" ItemsSource="{Binding DetailLines}" AutoGenerateColumns="False" CanUserAddRows="False" SelectedItem="{Binding SelectedDetailLine, UpdateSourceTrigger=PropertyChanged}">
            <DataGrid.Columns>
                <DataGridCheckBoxColumn Header="Is 1099" Width="*"
                                        Binding="{Binding Is1099, UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridComboBoxColumn Header="1099 Line" Width="*"  DisplayMemberPath="Description"
                                        ItemsSource="{Binding Path=DataContext.GL1099Lines, Source={StaticResource ProxyElement}}"
                                        SelectedItemBinding="{Binding Path=GL1099Line, UpdateSourceTrigger=PropertyChanged}"
                                        IsReadOnly="{Binding Path=DataContext.SelectedDetailLine.Is1099, Source={StaticResource ProxyElement}, Converter={StaticResource NotConverter}}">
                </DataGridComboBoxColumn>
            </DataGrid.Columns>
        </DataGrid>

The DataGridComboColumn has an IsReadOnly property instead of a IsEnabled property so I need a converter to invert the Is1099 property in the DetailLine.

Add a class file to the project and call it Converters. Replace the code with this.

using System;
using System.Globalization;
using System.Windows.Data;

namespace InfragisticsUpateSource
{
    class NotConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !bool.Parse(value.ToString());
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


Add a reference to the converter in the Resources section.
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement"/>
        <local:NotConverter x:Key="NotConverter"/>
    </Window.Resources>

The only thing missing is edit-on-focus which I covered in this post.


I will agree that the XamDataGrid looks better out the box, but there are serious issues with functionality. The developer shouldn't have to work so hard or write so much non-intuitive XAML to get basic features to work.

No comments:

Post a Comment