Friday, March 16, 2018

Implementing XamDataGrid hierarchies

One of the things that frustrates me about working with Infragistics is the lack of examples. For example, I was able to find a demo of a load on demand hierarchical grid that used automatic column generation, but that feature is pretty useless in real world applications. What I really needed was an example of explicitly creating the hierarchical links in XAML or code.

The demo I started with is here. I put a break point in the task load routine and then looked at the hundreds of properties the datagrid had. I was able to reverse engineer some XAML that demonstrates how to set up the properties required to make the demo work with explicit band and field definitions.

Extract the sample code from the demo and replace the Infragistics references with your Infragistics version.


The XAML and code in MainWindow that comes with the demo looks like this.


<Window x:Class="DataGrid_LazyLoadChildren.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:igDp="http://infragistics.com/DataPresenter"
        xmlns:local="clr-namespace:DataGrid_LazyLoadChildren"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <igDp:XamDataGrid x:Name="igDataGrid" >
        </igDp:XamDataGrid>
    </Grid>
</Window>


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

namespace DataGrid_LazyLoadChildren
{
    public partial class MainWindow : Window
    {
        ObservableCollection<Order> orders;
        public MainWindow()
        {
            InitializeComponent();
            orders = new ObservableCollection<Order>()
            {
               new Order{ OrderID = 1, OrderName = "Order 1"},
               new Order{ OrderID = 2, OrderName = "Order 2"}
            };
            igDataGrid.DataSource = orders;
        }
    }

    public class Order : INotifyPropertyChanged
    {
        private int _orderID;
        public int OrderID
        {
            get { return _orderID; }
            set { _orderID = value; PropChanged("OrderID"); }
        }

        private string _orderName;
        public string OrderName
        {
            get { return _orderName; }
            set { _orderName = value; PropChanged("OrderName"); }
        }

        private List<Task> _tasks = null;
        public List<Task> Tasks
        {
            get
            {
                if (_tasks == null)
                {
                    BackgroundWorker worker = new BackgroundWorker();
                    worker.DoWork += (ss, ee) =>
                    {
                         Thread.Sleep(500);
                        _tasks = GetData(OrderID);
                        PropChanged("Tasks");
                    };
                    worker.RunWorkerAsync();
                }
                return _tasks;
            }
        }

        public static List<Task> GetData(int orderID)
        {
            List<Task> data = new List<Task>();
            if (orderID == 1)
            {
                data.Add(new Task { TaskID = 1, TaskName = "Task 1" });
                data.Add(new Task { TaskID = 2, TaskName = "Task 2" });
                data.Add(new Task { TaskID = 3, TaskName = "Task 3" });
                data.Add(new Task { TaskID = 4, TaskName = "Task 4" });
            }
            else
            {
                data.Add(new Task { TaskID = 21, TaskName = "Task 21" });
                data.Add(new Task { TaskID = 22, TaskName = "Task 22" });
                data.Add(new Task { TaskID = 23, TaskName = "Task 23" });
                data.Add(new Task { TaskID = 24, TaskName = "Task 24" });
            }
            return data;
        }

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

    public class Task : INotifyPropertyChanged
    {
        private int _taskID;
        public int TaskID
        {
            get { return _taskID; }
            set { _taskID = value; PropChanged("TaskID"); }
        }

        private string taskName;
        public string TaskName
        {
            get { return taskName; }
            set { taskName = value; PropChanged("TaskName"); }
        }

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

The resulting application looks like this.


As you see, the XamDataGrid is capable of figuring out how to display the data by looking at the enumerable property (tasks) of the order object and creating a child grid to display the tasks. This is great if you have compliant users, but mine want all sorts of extra stuff that requires explicit band (FieldLayout) and column (Field) definitions.

I figured out how the columns need to be defined to produce the same effect.

<Window x:Class="DataGrid_LazyLoadChildren.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:igDp="http://infragistics.com/DataPresenter"
        xmlns:local="clr-namespace:DataGrid_LazyLoadChildren"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <igDp:XamDataGrid x:Name="igDataGrid" GroupByAreaLocation="None" Theme="LunaSilver">
            <igDp:XamDataGrid.FieldLayoutSettings>
                <igDp:FieldLayoutSettings AutoGenerateFields="false"/>
            </igDp:XamDataGrid.FieldLayoutSettings>
            <igDp:XamDataGrid.FieldLayouts>
                <igDp:FieldLayout Description="Orders" Key="Orders">
                <igDp:FieldLayout.Fields>
                    <igDp:Field Label="Order ID" Name="OrderID"/>
                    <igDp:Field Label="Order Name" Name="OrderName"/>
                    <igDp:Field Name="Tasks"/>
                </igDp:FieldLayout.Fields>
            </igDp:FieldLayout>
            <igDp:FieldLayout Description="Tasks" Key="Tasks" ParentFieldLayoutKey="Orders" ParentFieldName="Tasks">
                <igDp:Field Label="Task ID" Name="TaskID"/>
                <igDp:Field Label="Task Name" Name="TaskName"/>
            </igDp:FieldLayout>
            </igDp:XamDataGrid.FieldLayouts>
        </igDp:XamDataGrid>
    </Grid>
</Window>

There are several things to note.
  • The invisible field in the first fieldlayout linked to the Tasks property of the Order. Because Tasks is enumerable, XamDataGrid understands it cannot be displayed. Without this field, we will not see an expander.
  • In the second fieldlayout we reference the first fieldlayout by setting our ParentFieldLayoutKey to the first fieldlayout's key (Orders). This tells XamDataGrid which of our ancestors contains the data that will populate this fieldlayout.
  • In the second fieldlayout we specify the field that contains the data we will display by setting ParentFieldName to the Name of the ancestor's field (Tasks).
  • These two properties tell XamDataGrid that the Tasks property of our Orders ancestor will be our "DataSource".
To me this is pretty hokey. For one thing the existence of an expander on the row is a row property, not a field property. Second, Microsoft has carefully designed their data grid to guide the code to look at the underlying data, not the properties of the grid - this is good MVVM design. The XamDataGrid should not need the third column at all, the child grid should be looking at the underlying data source of the parent band, not properties of the parent band.

This XAML produces pretty much the same application as the one that came with the demo except the column headers are better, but we are now in a position to provide some customization to our users. For example, let's add an child grid description.

Add a label to the Tasks field so it looks like this
    <igDp:Field Label="Order's Tasks" Name="Tasks"/>

and add FieldSettings section below the FieldLayoutSettings like this.
    <igDp:XamDataGrid.FieldSettings>
        <igDp:FieldSettings ExpandableFieldRecordHeaderDisplayMode="AlwaysDisplayHeader"/>
    </igDp:XamDataGrid.FieldSettings>

Run the application again and expand one of the rows.


Take a look at the diagram below to see how the critical bits link together


No comments:

Post a Comment