Friday, October 26, 2018

Overcoming XamDataGrid scrolling performance problems

The Infragistics XamDataGrid has poor performance while scrolling in certain scenarios.

A very common design when displaying data grids with child data is to defer fetching the child data until the user expands the parent. The parent needs to know if it has children so the row expander can be initially displayed, but can fetch the actual children on demand.

Let's consider a grid that displays employees and, for employees that manage other employees, displays a child grid of those employees. The grid might look like this.


When we fetch the parent list of employees, we know Ian manages someone but we don't know who. Because it takes a few milliseconds to get the list of Ian's reports we only want to do it if the user clicks the row expander.

Infragistic's XamDataGrid does not support this common scenario. The ExpansionIndicatorDisplayMode attribute can take the following values.

  • Default - used to make the developer think they're getting an extra option when they aren't
  • Never - Never show a row expander
  • Always - Always show a row expander
  • CheckOnExpand - Show a row expander until the user clicks on it, try to fetch the children, then hide it if there are no children
  • CheckOnDisplay - Try to fetch the children as soon as the row moves into view and hide the row expander if there are no children.
For 99% of scenarios only CheckOnDisplay is even remotely useful when a parent may or may not have children. Deferred scrolling is unacceptable. Unfortunately there is no way to bind the visibility of the row expander to data in the row.

When you use the CheckOnDisplay option the grid looks for children as rows are scrolled into view. If it takes even 100 ms to check for children the scrolling performance will be unacceptable.

Let's create a poorly scrolling page and see how we can fix it. Create a new WPF project in C# and call it ScrollingXamDataGrid.


Add project references to Infragistics like this.


The XAML is trivial.


<Window x:Class="ScrollingXamDataGrid.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:ScrollingXamDataGrid"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d" DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="200" Width="300">
    <Grid>
        <igDP:XamDataGrid GroupByAreaLocation="None" DataSource="{Binding Employees}" ScrollingMode="Immediate">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False"
                                          ExpansionIndicatorDisplayMode="CheckOnDisplay" AllowFieldMoving="No"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowEdit="False"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout Key="Employee">
                    <igDP:TextField Label="Name" Name="Name"/>
                    <igDP:Field Name="Reports"/>
                </igDP:FieldLayout>
                <igDP:FieldLayout ParentFieldLayoutKey="Employee" ParentFieldName="Reports">
                    <igDP:TextField Label="Manages" Name="Name" HorizontalContentAlignment="Right"/>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>       
    </Grid>
</Window>

The code behind is fairly simple to. The cEmployee.Reports getter simulates a trip to the middle tier and database.

using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Windows;

namespace ScrollingXamDataGrid
{
    public partial class MainWindow : Window
    {
        public class cEmployee
        {
            public String Name { get; set; }
            public ObservableCollection<cEmployee> Reports
            {
                get
                {
                    Thread.Sleep(100);
                    switch (this.Name)
                    {
                        case "Ian": return new ObservableCollection<cEmployee>() { new cEmployee("Xavier") };
                        case "Marge": return new ObservableCollection<cEmployee>() { new cEmployee("Yvette") };
                        case "Nancy": return  new ObservableCollection<cEmployee>() { new cEmployee("Zooks") };
                        default: return null;
                    }
                }
            }
         

            public cEmployee(String Name)
            {
                this.Name = Name;
            }
        }

        private ObservableCollection<cEmployee> _Employees;
        public ObservableCollection<cEmployee> Employees
        {
            get { return _Employees; }
            set { _Employees = value; }
        }

        public MainWindow()
        {
            Employees = new ObservableCollection<cEmployee>();
            Employees.Add(new cEmployee("Abby"));
            Employees.Add(new cEmployee("Brian"));
            Employees.Add(new cEmployee("Chris"));
            Employees.Add(new cEmployee("Dan"));
            Employees.Add(new cEmployee("Englebert"));
            Employees.Add(new cEmployee("Frank"));
            Employees.Add(new cEmployee("Gabby"));
            Employees.Add(new cEmployee("Hortense"));
            Employees.Add(new cEmployee("Ian"));
            Employees.Add(new cEmployee("Jack"));
            Employees.Add(new cEmployee("Kim"));
            Employees.Add(new cEmployee("Lionel"));
            Employees.Add(new cEmployee("Marge"));
            Employees.Add(new cEmployee("Nancy"));
            Employees.Add(new cEmployee("Ollie"));
            Employees.Add(new cEmployee("Peter"));
            Employees.Add(new cEmployee("Quincy"));
            Employees.Add(new cEmployee("Rachael"));
            Employees.Add(new cEmployee("Stephen"));
            Employees.Add(new cEmployee("Tarquin"));
            Employees.Add(new cEmployee("Ulla"));
            Employees.Add(new cEmployee("Vince"));
            Employees.Add(new cEmployee("Walter"));
            Employees.Add(new cEmployee("Xavier"));
            Employees.Add(new cEmployee("Yvette"));
            Employees.Add(new cEmployee("Zolla"));
            InitializeComponent();
        }
    }
}

If you run this application you will note that scrolling is initially sluggish until the cEmployee.Reports getter has been called for each employee.

If I was using the Microsoft DataGrid I would simply add a boolean property to cEmployee called HasReports and write a trigger to hide the row expander if HasReports = False. A little research tells us the row expander can be styled but if we try the same approach on the XamDataGrid we see we cannot hide the row expander - the trigger is overridden by the grid. But we can change the color - effectively hiding it.

Add a new property to the cEmployee class called HasReports.
            public bool HasReports { get; set; }

Modify the existing constructor and add a new one.
            public cEmployee(String Name)
            {
                this.Name = Name;
                this.HasReports = false;
            }

            public cEmployee(String Name, bool HasReports)
            {
                this.Name = Name;
                this.HasReports = HasReports;
            }

Alter the initiators for Ian, Marge, and Nancy.
            Employees.Add(new cEmployee("Ian", true));
            Employees.Add(new cEmployee("Marge", true));
            Employees.Add(new cEmployee("Nancy", true));

Normally this functionality would be implemented by adding CASE WHEN EXISTS (SELECT ...) THEN 1 ELSE 0 END AS HasReports to the SQL that populated the employee list.

Lastly we need to tweak the XAML to always show the expander but hide it if HasReports is false.
  • Add the xmlns:igWindows="http://infragistics.com/Windows" namespace so we can create the style.
  • Change the display mode to ExpansionIndicatorDisplayMode="Always".
  • Create a row expander style with a trigger that sets the row expander white (transparent might work too). 

                <igDP:XamDataGrid.Resources>
                    <Style TargetType="igWindows:ExpansionIndicator">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding DataItem.HasReports}" Value="False">
                                <Setter Property="Foreground" Value="White"/>
                                <Setter Property="Background" Value="White"/>
                                <Setter Property="BorderBrush" Value="White"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </igDP:XamDataGrid.Resources>

We now have the same functionality as ExpansionIndicatorDisplayMode="CheckOnDisplay" but the employee's reports are not fetched from the database until the user expands the parent. The 100 ms delay that slowed scrolling is not detectable when expanding the parent.