Datagrids with multiple detail grids

Recently a client asked me to demonstrate how to display two detail grids embedded in a parent grid whenever the user clicks on a record.

I couldn’t find a clear example on the web, so here is a solution.

In this example, I have created a simple data structure where we have a list of Companies. Each company has a list of addresses (the locations of the company and a list of staff members). It’s not the best data structure in the world, but it does serve as a good example of what we are trying to achieve.

It is very easy to display a detail grid embedded in a master grid. I shall go through a standard implementation of how to do this first, and then move to implementing multiple detail grids.

Create a single detail grid

Here is a xaml snippet that implements a master grid and a detail grid, with some extra formatting to make it a bit more tidy and user friendly.

 <DataGrid AutoGenerateColumns="False"  ItemsSource="{Binding companies}"
   x:Name="exampleGrid" Margin="10,10,10,10" VerticalAlignment="Stretch"
   HorizontalAlignment="Stretch"
   RowDetailsVisibilityChanged="ExampleGrid_OnRowDetailsVisibilityChanged">
     <DataGrid.Columns>
         <DataGridTextColumn IsReadOnly="true" Binding="{Binding CompanyName}" Header="Company Name"/>
     </DataGrid.Columns>
     <DataGrid.RowDetailsTemplate>
         <DataTemplate>
             <DataGrid Name="innerGrid" Margin="10,10,10,10" BorderThickness="5" />
         </DataTemplate>
     </DataGrid.RowDetailsTemplate>
 </DataGrid>

There is quite a lot of stuff going in this small snippet, so let’s break it down…


<DataGrid AutoGenerateColumns="False"  ItemsSource="{Binding companies}"
  x:Name="exampleGrid" Margin="10,10,10,10" VerticalAlignment="Stretch"
  HorizontalAlignment="Stretch"
  RowDetailsVisibilityChanged="ExampleGrid_OnRowDetailsVisibilityChanged">

    <!-- snip irrelevant stuff here -->

</DataGrid>

In this bit, we are declaring a new DataGrid component:

  • We don’t want it to automatically generate columns (AutoGenerateColumns="False") because if we did, then the grid would display a column for each public property of the object that it is bound to. We only want the grid to display the name of the company at this level which we shall implement later.
  • We bind the DataGrid to a source (ItemsSource="{Binding companies}").
  • We give the DataGrid a name (x:Name="exampleGrid")
  • We give the DataGrid a bit of a margin when displaying data (Margin="10,10,10,10"). This just makes it look a bit more pretty.
  • We make the DataGrid resize when its parent component is resized (VerticalAlignment="Stretch" HorizontalAlignment="Stretch").
  • We assign an event handler to be fired when the RowDetailsVisibilityChanged event is fired (RowDetailsVisibilityChanged="ExampleGrid_OnRowDetailsVisibilityChanged"). This event is fired when the user clicks on a record in the DataGrid.

Inside the definition of the DataGrid, we have some other stuff. Remember that we only wanted to display the name of the company at the top level? We do that with this code…

<DataGrid.Columns>
    <DataGridTextColumn IsReadOnly="true" Binding="{Binding CompanyName}" Header="Company Name"/>
</DataGrid.Columns>
  • The DataGrid.Columns element defines the set of columns that we want to display.
  • For each column that we want to display, we need to define a DataGridTextColumn element.
    • We prevent the user from changing the data in the column (IsReadOnly="true")
    • We bind the column to a property in the data source that the DataGrid is bound to (Binding="{Binding CompanyName}")
    • We give the column a user-friendly name to display in the column header (Header="Company Name)

Also inside the definition of the DataGrid, we add some template information to be used when the user clicks on a row:

<DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <DataGrid name=innerGrid" Margin="10,10,10,10" BorderThickness="5" />
    </DataTemplate>
</DataGrid.RowDetailsTemplate>
  • We declare that we are defining a RowDetailsTemplate for the main DataGrid ()
  • In that template, we define the template itself in a DataTemplate element ()
  • In the DataTemplate, we define a new DataGrid that will display the details of the record that the user has clicked on
    • Define the Grid itself (DataGrid ... )
    • Give it a name (Name="innerGrid")
    • Add some visual stuff to make it a bit more usable (Margin="10,10,10,10" BorderThickness="5")

That’s quite a lot in a small snippet of xaml!

In the code page behind the xaml, we must implement the ExampleGrid_OnRowDetailsVisibilityChanged function:

private void ExampleGrid_OnRowDetailsVisibilityChanged(object sender, DataGridRowDetailsEventArgs e)
{
    // get the object that the user has selected and cast it as a Company object
    var selectedCompany = this.exampleGrid.SelectedItem as Company;

    // if the selected item is a company and the company has some location records
    if ((selectedCompany != null) && (selectedCompany.Locations != null))
    {
        // get the details element of the ExampleGrid object and cast it as a DataGrid
        DataGrid innerGrid = e.DetailsElement as DataGrid;
        // if the details element really is a DataGrid, we can display some data in it
        if (innerGrid != null)
        {
            // Show the locations of the selected company
            innerGrid.ItemsSource = selectedCompany.Locations;
        }
    }
}

All this xaml and C# code comes together to display a pretty little grid like this:
MasterDetailGrids1

This is all well and good, but we also want to display the list of staff at each company as well as the locations of the company.

Create multiple child grids

The obvious solution is to add another DataGrid to the DataTemplate, right?

<DataTemplate>
    <DataGrid Name="innerGrid" Margin="10,10,10,10" BorderThickness="5" />
    staffGrid" Margin="10,10,10,10" BorderThickness="5"/>
</DataTemplate>

Wrong! Do that and you’ll end up with three errors:


Error 1 The property "VisualTree" can only be set once.
Error 2 The property 'VisualTree' is set more than once.
Error 3 The object 'DataTemplate' already has a child and cannot add 'DataGrid'. 'DataTemplate' can accept only one child.

OK, that’s pretty clear. You can only set the "VisualTree" property once and DataTemplate can only have one child.

So, the next guess is to add a second DataTemplate, right?

<DataTemplate>
    <DataGrid Name="innerGrid" Margin="10,10,10,10" BorderThickness="5" />
</DataTemplate>
<DataTemplate>
    <DataGrid Name="staffGrid" Margin="10,10,10,10" BorderThickness="5"/>
</DataTemplate>

Wrong!


Error 1 The property "RowDetailsTemplate" can only be set once.
Error 2 The property 'RowDetailsTemplate' is set more than once.
Error 3 The object 'DataTemplate' already has a child and cannot add 'DataTemplate'. 'DataTemplate' can accept only one child.

OK, so you can only set DataTemplate once.

Let’s try to have multiple RowDetails elements then…

<DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <DataGrid Name="innerGrid" Margin="10,10,10,10" BorderThickness="5" />
    </DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <DataGrid Name="staffGrid" Margin="10,10,10,10" BorderThickness="5"/>
    </DataTemplate>
</DataGrid.RowDetailsTemplate>

That’s got to be it, right? Wrong!


Error 1 The property "RowDetailsTemplate" is set multiple times.
Error 2 The property 'RowDetailsTemplate' is set more than once.
Error 3 'System.Windows.Controls.DataGrid.RowDetailsTemplate' property has already been set and can be set only once.

So at this point, you either resort to Google once more (and that’s probably why you are reading this now) or you give up.

The solution is pretty simple.

You can only have a single child in a DataTemplate. So make that child a StackPanel and stack some DataGrids up on the stack panel:

<DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <StackPanel Name="DetailsPanel">
            <DataGrid Name="innerGrid" Margin="10,10,10,10" BorderThickness="5" />
            <DataGrid Name="staffGrid" Margin="10,10,10,10" BorderThickness="5"/>
        </StackPanel>
    </DataTemplate>
</DataGrid.RowDetailsTemplate>

Huzaah! No errors! Now all we need to do is modify the ExampleGrid_OnRowDetailsVisibilityChanged function to populate both of the grids:

private void ExampleGrid_OnRowDetailsVisibilityChanged(object sender, DataGridRowDetailsEventArgs e)
{
    // get the object that the user has selected and cast it as a Company object
    var selectedCompany = this.exampleGrid.SelectedItem as Company;

    // if the selected item is a company and the company has some location records
    if (selectedCompany != null)
    {
        // get the details element of the main DataGrid and cast it to a StackPanel
        var panel = e.DetailsElement as StackPanel;
        if (panel != null)
        {
            // just be sure that the panel does have two children that are datagrids...
            if (panel.Children.Count >= 2)
            {
                DataGrid innerGrid = panel.Children[0] as DataGrid;

                if ((innerGrid != null) && (selectedCompany.Locations != null))
                {
                    innerGrid.ItemsSource = selectedCompany.Locations;
                }

                DataGrid staffGrid = panel.Children[1] as DataGrid;
                if ((staffGrid != null) && (selectedCompany.Staff != null))
                {
                    staffGrid.ItemsSource = selectedCompany.Staff;
                }
            }
        }
    }
}

Giving us a pretty user interface that looks like this:

MasterDetailGrids2

That’s it! Success.

An example Visual Studio 2012 solution can be found on GitHub at https://github.com/0DegreesKelvin/DataGridWithMultipleDetailGrids

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s