Monday, March 19, 2018

Binding XamDataGrid column headers

XamDataGrid has the same problem binding DataGridColumns that the Microsoft DataGrid has. The column is not part of the visual tree so it does not inherit the DataContext of the DataGrid. I explained my preferred solution to this problem some time ago. The solution for the XamDataGrid is very similar.

Start with the result of yesterday's blog. We are going to create a property and bind the first column's header to it. The property will only be crudely implemented because I want to focus on the XAML.

Add a property called BoundLabel to the MainWindow class, in real life you would implement INotifyProperty changed etc. Initialize it before InitializeComponent.


public partial class MainWindow : Window
    {
        public string BoundLabel { get; set; }

        ObservableCollection<Order> orders;
        public MainWindow()
        {
            BoundLabel = "I AM BOUND!";
            InitializeComponent();
            orders = new ObservableCollection<Order>()
            {
               new Order{ OrderID = 1, OrderName = "Order 1"},
               new Order{ OrderID = 2, OrderName = "Order 2"}
            };
            igDataGrid.DataSource = orders;
        }
    }


Now we need to define the DataContext for the window, create a resource that can inherit that DataContext, create an invisible element to 'realize' that resource, and finally change the DataGridColumn to reference the resource. Note we cannot use RelativeSource to bind the DataGridColumn because it is not in the visual tree and so it has no ancestors.

This sounds like a lot of work, but even if you have multiple DataGridColumns that need bindings, you only have to do all this once. As a side note, you might ask "How does the data binding work (ie the Name property)?". The answer is that Microsoft did some hidden plumbing that we cannot get to.

This is what the XAML looks like now.


<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"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement"/>
    </Window.Resources>
    <Grid>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <igDp:XamDataGrid x:Name="igDataGrid" GroupByAreaLocation="None" Theme="LunaSilver">
            <igDp:XamDataGrid.FieldLayoutSettings>
                <igDp:FieldLayoutSettings AutoGenerateFields="false"/>
            </igDp:XamDataGrid.FieldLayoutSettings>
            <igDp:XamDataGrid.FieldSettings>
                <igDp:FieldSettings ExpandableFieldRecordHeaderDisplayMode="AlwaysDisplayHeader"/>
            </igDp:XamDataGrid.FieldSettings>
            <igDp:XamDataGrid.FieldLayouts>
                <igDp:FieldLayout Description="Orders" Key="Orders">
                <igDp:FieldLayout.Fields>
                        <igDp:Field Label="{Binding Path=DataContext.BoundLabel, Source={StaticResource ProxyElement}}" Name="OrderID"/>
                    <igDp:Field Label="Order Name" Name="OrderName"/>
                    <igDp:Field Label="Orders" 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>

The window looks like this.


This is by no means the only way to solve the problem, but it's a reasonable one. In an MVVM solution, you would create a StaticResource that got a reference to local:vm and work with that.

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