Build Your Own DataGrid for Silverlight: Step 3

1. Introduction

Tutorial overview

This article is the third part of a tutorial explaining how to build a full featured DataGrid using Silverlight and the GOA Toolkit.

In the first part of this tutorial, we saw way of creating a read-only grid's body. In the second part, we focused on the implementation of editing and validation features. In this third part, we will turn our attention to the headers of the grid.

All along this article, we will assume that you have completed reading the first and second parts of the tutorial. If it is not the case, we strongly advise to do so. You can access them here:

Get Started

This tutorial was written using the GOA Toolkit 2009 Vol1 build 289. If you have already implemented the first and the second steps of the tutorial before this article was released, you may need to upgrade. Check that you are working with GOA Toolkit 2009 Vol1 build 289 or after.

Goals

During the two previous tutorials, we built the body part of the grid. The grid body that we have built has all the basic features of a data grid (cell, navigation, cell editing ...). Some advanced features have been implemented (virtual mode, DataTemplate ...). Nevertheless, our data grid will not be useful until we are able to link headers to the cells it displays.

This is the purpose of this third tutorial.

As our grid can display hierarchical data (nodes and children), we must be able to link the headers to any level of the grid. We will implement that in two different ways:

By allowing to display the headers inside the grid at the top of each children collection,

and by allowing displaying the headers at the top of the grid.

In the first case, the grid will have the following look:

In the second case, the headers will be displayed at the top of the grid.

We will also allow implement all the alternatives in-between:

Display some of the headers inside the grid body and others outside.

Display top headers only according to some predefined conditions or rules

...

When hierarchical data is displayed, the user may sometimes find the display confusing. He may have trouble understanding the different levels of the hierarchy. To make hierarchical data more readable, we will allow our grid to display a title at the top of each level. If you watch the two pictures above, you can see the "Countries", "Regions", and "Persons" titles displayed at the top of the headers. Headers and titles are not mandatory. We will be able to choose to display them or not. We will also be able to display the titles but not the headers, or the opposite.

We will also implement the features needed to allow the user to resize the headers.

Out of scoop

We will not discuss the possibility of sorting the data of the grid by clicking on a row header. This feature is more data related than headers related. It is not difficult to add a visual state that displays an up arrow or a down arrow in a header when the user clicks on it. What is more difficult is to apply the corresponding sorting rule to the underlying data. This could be the subject of another tutorial.

2. Preparing the grid

Refactoring the display of the grid

Our data grid is able to display a large amount of complex hierarchical data. We must ensure that the data is presented in the most readable way to the user. Adding headers inside the grid body will add a level of complexity. Therefore, before starting the implementation of the headers, let's make some changes in the way the rows are displayed in order to make the gird more attractive.

Alternate rows color

When setting the AlternateType property of our grid body to the "Items" value, the background of one row out of two is displayed using an alternate color.

The purpose of this feature is to make the grid more readable. Nevertheless, the alternate colors are too bright and it makes it difficult to read the grid.

In the latest releases of the GOA Toolkit, the brushes and the colors definitions defined at the top of the generic.xaml file of the GOAOpen project have been modified to make alternate colors more sober. Let's modify our generic.xaml file in the same way.

At the top of the generic.xaml file of the GOAOpen project, select the colors definition section. It is the section of the file between the color "tag":

Extended parent node backcolor

When we built the Container_RowNodeStyle style (the style used by our rows when the grid displays hierarchical data), we have used the existing Container_NodeStyle style of the GoaOpen project and we have enhanced it a little bit in order that it fits our needs. The result style displays the background of the nodes in a different way when they are extended and collapsed.

We will remove this behavior because it adds confusion when it is used in a complex control like a gird.

Let's also increase the Indentation default value to 30 and change the margins of ValidElement and the FocusElement to make them more readable.

Locate the Container_RowNodeStyle style at the end of the generic.xaml file and replace the style with this one:

Separator between parent's nodes and children

Parent and children nodes are displayed using different indentations in order that the hierarchy of the data is visually displayed inside the grid. Nevertheless, we would like to make the distinction between a parent and its children more accentuated by adding a space between them.

In order to implement that feature, we can use the "NodeLevelActionStates" states of the items (i.e., the rows) displayed in our grid. When a node is the first node of a new level in the hierarchy, its NodeLevelActionStates state is "JumpLevelNode". Otherwise, its NodeLevelActionStates state is "NormalLevelNode".

Let's use these states to display a space on the top of an item when the item is the first one of a new level in the hierarchy.

In the Container_RowNodeStyle style, at the end of VisualStateManager.VisualStateGroups, let's add a new VisualStateGroup:

Let's add an invisible rectangle that will be displayed at the top of the item and that will create the space between it and the previous item. In order to do that, we are going to add two rows to the "LayoutRoot" grid of the item. The first row holds the invisible rectangle, and the second row holds the StackPanel of the item.

Remove animations

When displaying simple controls, animations are great. Nevertheless, for advanced controls as our data grid, especially when it is displaying complex hierarchical data with inside headers, the rendering of animations within Silverlight is too slow, and the result is not attractive. Therefore, we will just remove the animations for now.

Locate the GridBodyStyle style at the end of the generic.xaml file.

Modify the ItemsPanelModel setter in order to remove the ChildrenAnimator of the GStackPanelModel:

Data samples

Until now, in our GridBody project, we have generated our data using a loop. This was an easy way to have data to display in the grid. Let's enhance the way we generate the data to have something more close to the real data that can be displayed inside a grid.

Let's suppose that we run an international company that has employees all across the world. Each employee has a rate. We would like the grid to display the following hierarchy:

Countries
Regions
Employees

For each country and region, the grid must display the total rate, which is the sum of the rates of all the employees of the country or the region.

3. Add title

Introduction

When hierarchical data is displayed, the user may sometimes find the display confusing. He may have trouble understanding the different levels of the hierarchy. To make hierarchical data more readable, we will allow our grid to display a title at the top of each level.

ItemTitle control

Control

Let's create an ItemTitle control. This control will be displayed at the top of an item. We will use the NodeLevelActionStates state of the item to make it appear only when the item is the first one after a hierarchical level jump.

Basically, the ItemTitle control has the same style as a standard GContentControl except that we have added a background and a border to it.

Add the ItemTitle in Container_RowNodeStyle

Let's add the ItemTitle in Container_RowNodeStyle in order that it is displayed at the top of the item.

If we draw on a picture the ControlTemplate part of the the Container_RowNodeStyle style, it will look like this:

The ItemTitle must be displayed exactly above the content of the ContainerItem. It must be located at the right of the Node Expansion button. Therefore, after the insertion of the ItemTitle, our picture will look like this:

Add an ItemTitleSource and an ItemTitleStartDepth property to HandyContainer

The HandyContainer (i.e., our grid's body) needs to know what to put inside the content of the ItemTitle. This will be the purpose of the ItemTitleSource property that we are going to add to the HandyContainer class.

Most of the time, we probably will not want that the ItemTitle to be displayed for all the levels of the hierarchy (generally, not for the first level). Therefore, we will also add the ItemTitleStartDepth property. This property will tell the grid's body from which level it must start to display the titles of the items. In the picture below, the titles are displayed at the first and second levels of the hierarchy, but they are not displayed at the root level.

Let's add these two dependency properties to our HandyContainer partial class. Edit the HandyContainer.cs file located in the GoaOpen\Extensions\Grid folder. Add the two dependency properties at the beginning of the file:

The ApplyHeadingVisibility method will decide of the visibility of the ItemTitle according to the NodeLevelAction state of the item and the ItemTitleStartDepth property value of the parent HandyContainer. The ApplyHeadingVisibility method will call a method named ApplyTitleContent which will fill the contents of the ItemTitle according to the ItemTitleSource value of the parent HandyContainer.

In order to make these two methods work, we will need to add two "using" clauses at the top of the ContainerItem file:

Let's try our changes by starting the GridBody project. The ItemTitles are displayed at the top of the first person item and at the top of the first region at each level of the hierarchy. Now, let's try to scroll the grid. When we scroll, the ItemTitles are displayed erratically. Sometimes they are displayed at the wrong place. Other times, they are not displayed at all. What has happened?

Override the OnNodeLevelActionChanged and the OnRefreshed methods

Our GridBody is working using a VirtualMode. In order to keep the performance of the grid as fast as possible, only the displayed items (and a little more) are created in the Visual Tree. Furthermore, when the grid is scrolled, rather than destroying existing items and creating new ones, existing items are reused as often as possible (creating a new control and adding it to the Visual Tree is a process that is extremely time consuming).

When an item is reused, the OnApplyTemplate of the item is not called anymore. Therefore, the ApplyHeadingVisibility method is not called on these items. This is why the title of the items are not displayed at the correct place as soon as we scroll the grid.

In order to make the ItemTitle display correctly, we must override the OnNodeLevelActionChanged and the OnRefreshed methods of the ContainerItem.

The OnNodeLevelActionChanged method is called each time the NodeLevelAction state of an item has changed. The OnRefresh method is called each time the ContainerItem has been reused and it is bound to another element of the ItemsSource.

The ApplyHeadingVisibility method must be called each time one of these methods is called.

Furthermore, we must modify ApplyHeadingVisibility in order to take into account that an item can be reused. Let's suppose that an item displays one type of element and its NodeLevelAction state is LevelJump. The user scrolls the grid and the item is reused, but this time, to display another type of element. When it is used again, the item is located at a place where its NodeLevelAction state is LevelJump. As the ItemTitle was already visible before the item was reused, the ApplyTitleContent method is not called inside ApplyHeadingVisiblity and the content of the ItemTitle is not refreshed. Therefore, the ItemTitle displays a wrong title!

To correct this, let's remove the second condition in the if statement of the ApplyHeadingVisibility method:

If we start our application again and scroll the grid, the ItemTitles are displayed at the correct locations.

Dynamic ItemTitleSource or ItemTitleStartDepth

The way ItemTitleSource and ItemTitleStartDepth are implemented, when their values are modified, the titles displayed inside the grid's body are not updated automatically. In order to implement this feature, we should track the ItemTitleSource and ItemTitleStartDepth value changes. This could be implemented in an enhanced version of our grid.

ItemTitle click

If we try to click an item title, we can see that the item containing the item title is selected. This is not the behavior we would like. Clicking an ItemTitle should not have any effect on the item holding it.

Let's modify the ItemTitle class and override the OnMouseLeftButtonDown method to remove this unwanted behavior:

ItemTitle MouseOver

If we move the pointer of the mouse over an item title, we can see that the background of the item containing the item title is displayed using an alternate color. This is because the ItemTitle is part of the item holding it.

Let's trap the MouseEnter and MouseLeave events of the ItemTitle to get rid of this behavior.

We have added two more states to the ContainerItem: the NotMouseOverHeading state and the MouseOverHeading state. We now need to modify the ContainerItem style to take into account these two new states.

Locate Container_RowNodeStyle in the generic.xaml file. Add the following VisualStateGroup at the end of the VisualStateManager.VisualStateGroups of the style:

If we start our application again and move the mouse over an ItemTitle, the background of the item holding the title remains unchanged.

4. Body headers

Introduction

We will call "body headers" the headers that are displayed inside the body of the grid. The headers that are displayed at the top of the grid will be called "top headers".

The process to create a grid can be subdivided in to three steps:

Add a HandyContainer to our XAML page and set its properties appropriately.

Write the ItemTemplate of the HandyContainer in order to choose the way the cells inside the rows (i.e., the items) will be displayed.

Fill the ItemsSource property of the HandyContainer.

Our requirement is the following: we do not want to add a separate step to define the body headers. The headers must be laid out the same way as the cells of the rows.

For instance, if the ItemTemplate of the HandyContainer displays the rows of the grid the following way:

then the header must be displayed the same way:

Therefore, the ItemTemplate that is used to build the rows of the grid will also be used to build the headers.

Update the Cell class

Introduction

The ItemTemplate attached to our GridBody's HandyContainer will be used to build both the rows and the headers. Consequently, it must be possible to define some behaviors of the headers when defining the ItemTemplate of the HandyContainer.

In order to allow this feature, we will add two properties to the Cell class:

Header: This property will allow defining the content of the header attached to the cell. In other words, it will allow to define what will be displayed (text, image...) inside the header.

UserResizeType: The way the user can resize the header and, consequently, the cells. Possible values will be None, Right, and Bottom.

Create the HeaderResizeTypes enum

Let's create the enum that will allow populating the UserResizeType property of the Cell class. In the GoaOpen\Extensions\Grid folder, create the new HeaderResizeType file:

The options represented by the enum can be combined together in order to have a header that can be right resized and bottom resized at the same time. Therefore, we have applied the "Flags" attribute to the enum.

Let's now add the Header and UserResizeType properties of the Cell class:

The HeadersContainer is a ContentControl. We have chosen to inherit from ContentControl because, later on, we will apply the ItemTemplate of the HandyContainer to the ContentTemplate of the HeadersContainer control. This will allow us to apply the same layout to the headers as the layout that is applied to the cell.

For now, let's just keep the HeadersContainer very simple.

HeadersContainer style

We need to define a style for the HeadersContainer.

At this stage, we will make a very simple one. Open the generic.xaml file of the GoaOpen project and navigate to the end of the file. Add the following style:

Display the HeadersContainer inside the GridBody

Introduction

The HeadersContainer will be displayed the same way and just below the ItemTitle. As we have already performed all the necessary steps to be able to display an ItemTitle at the right place inside our grid's body, it is very easy to display the headers. We simply have to repeat the same steps. Right?

Not right!!!!

The ItemTitle is a very simple control that requires only a small amount of time to be laid out and displayed. On the contrary, the HeadersContainer is far more complicated. It requires a DataTemplate to be applied to it in order to be correctly laid out. It will also contain several header children controls. As a header can be resized (not always), it will contain a WindowSizer inside it. Therefore, the headers will be controls that are not simple to lay out and display.

We must take into account the fact that the HeadersContainers will be slow to render, and try to keep the data grid as fast as possible especially when the user scrolls it. Silverlight has a lot of great features, but one of its weaknesses is that it is very slow when laying out and rendering the VisualTree. We must keep this fact in mind when designing new controls.

When we say that the laying out and rendering process is very slow, it does not mean that the process itself takes several seconds to complete. It means that if several operations of this kind are processed at the same time, the whole process can take too much time and make the user interaction with the user interface not smooth anymore.

Until now, we have not really taken care of this aspect because the HandyContainer took it in charge.

Let's take an example. Let's suppose that we have an application displaying the following grid:

Let's suppose that the user press the page-down key and the following rows are displayed:

If we do not take care of what we do, in the case described above, six new headers will be created, laid out, and rendered when the user presses the page-down key. In other cases, it can be worse. This can make the scrolling process a lot slower than it is now.

The only way to avoid this problem is to keep the headers visible in the Visual Tree once they have been created, and reuse them as often as possible.

In order to do that, we will add a Canvas inside our GridBody. We will add our HeadersContainer to this canvas and reuse it as often as possible.

The Canvas is a panel that suits our needs because it allows us to manage exactly the location where the HeadersContainer must be displayed. The fact that it is possible to move a HeadersContainer from one place to another inside the canvas does not imply that the HeadersContainer will be laid out again (as soon as we keep the same height and the same width). This is the most important feature. In our case, the canvas will also be used as a cache. As soon as a HeadersContainer is not needed, it will be moved to a location where it is not visible by the user (for instance, (-1000, -1000)). This way, even if the HeadersContainer is not visible any more, it is still part of the Visual Tree. As soon as we will need a cached HeadersContainer again, we will move it back to the visible area of the canvas. This process will not require the HeadersContainer to be created or laid out again.

Add a Canvas to the HandyContainer

Let's modify the GridBody style and add the canvas that will contain the HeaderContainers inside it.

Locate the GridBodyStyle style inside the generic.xaml file. Locate the Scroller inside the Control template, and replace it with this one:

The Scroller is a control allowing to scroll the content of a Panel. The difference between a Scroller (GOA control) and a ScrollViewer (Silverlight control) is that the scroller allows scrolling the content of panels that implement the IScrollerOperator interface. If we place a panel inside a ScrollViewer, when the user scrolls, the panel is moved accordingly. If we place a panel inside a Scroller, when the user scrolls, the panel is not moved but the panel is told to move its content. We will not go into the details here, let's just note that the Scroller is used instead of the ScrollViewer mainly because it is able to support working in Virtual Mode.

As we have added the HeadersCanvas inside the Scroller, the Scroller now holds several panels. Therefore, we have to tell the Scroller which panel it will scroll when the user will use the scrollbars of the control. We have done this by filling the ScrollOperator property of the Scroller with the name of the GItemPresenter.

Note: The GItemsPresenter is not a panel. Nevertheless, its purpose is to "become" the ItemHost of the HandyContainer. It will be "replaced" by an instance of the panel defined in the ItemsPanelModel property of the HandyContainer. Therefore, the Scroller will see the GItemsPresenter as if it was a panel.

Implement the HeadersContainer caching

We are going to add two methods to the HandyContainer: GetHeadersContainer and CacheHeadersContainer.

The GetHeadersContainer method will allow getting a "free" HeadersContainer. A free HeadersContainer is a HeadersContainer that is not currently used (not displayed). The method will get a free HeadersContainer, either from the cache, or it will create a new one if the cache is empty.

The CacheHeadersContainer method will cache a HeadersContainer that is not used anymore.

All the HeadersContainers will not contain the same headers. For instance, if our grid displays the following hierarchy:

Countries
Regions
Employees

the headers of the countries will not be the same as the headers of the persons.

We will assume that a HeadersContainer can be reused:

If the type of the elements that are linked to the items the HeadersContainer will be linked to is the type of the elements that were linked to the items the HeadersContainer was linked to when it was created. In other words, if a HeadersContainer is linked to items that display persons data, it can be used to become a HeadersContainer linked to other items that display persons data. On the opposite, it cannot be used to become a HeadersContainer linked to items that display countries data (otherwise, the HeadersContainer will need to be rebuilt and this process will take a long time).

If the items linked to the headers are at the same hierarchical level as the items that were linked to the HeadersContainer when it was created.

This not really easy to understand without a picture. Let's look at the two pictures underneath:

The headers that are surrounded by a red rectangle in the first picture can be reused to display the headers that are surrounded by a red rectangle in the second picture.

On the opposite, the headers that are surrounded by a green rectangle in the first picture cannot be reused to display the headers that are surrounded by a red rectangle in the second picture

In the HandyContainer class, let's first add a reference to HeadersCanvas by modifying the OnApplyTemplate method:

The headersContainerCacheCollection contains all the HeadersContainers that have been cached.

When a HeadersContainer is cached (this is done by the CacheHeadersContainer method), it is not removed from the HeadersCanvas. A reference to the HeadersContainer is added to headersContainerCacheCollection, and the HeadersContainer is moved to a location of the canvas where it is not visible (Top = -1000).

When a headerContainer is needed, the GetHeadersContainer method is called. If a suitable HeadersContainer exists in the headersContainerCacheCollection (same data type and same level in the hierarchy), this headersContainer is returned. Otherwise, a new HeadersContainer is created.

Now that we have implemented the headers cache, we can implement the display of the headers by following almost the same steps we followed when we implemented the ItemTitles.

Headers in Container_RowNodeStyle

The HeadersContainer will not be held by the ContainerItem, but it will be held by the HeadersCanvas of the HandyContainer. Nevertheless, we still need to modify the Container_RowNodeStyle to be able to add a hole at the location where the HeadersContainer will be displayed.

In order to be able to build this hole, let's add a grid just below the ItemTitle inside the Container_RowNodeStyle. Locate Container_RowNodeStyle inside the generic.xaml file. Locate the ItemTitle inside the ControlTemplate. Add a grid named Headers below the ItemTitle:

HeadersStartDepth property

The same way we have added an ItemTitleStartDepth property to the HandyContainer, we need to add a HeadersStartDepth property. This property will tell the grid body from which level it must start to display the headers of the items.

Edit the HandyContainer.cs file located in the GoaOpen\Extensions\Grid folder. Add the HeadersStartDepth property:

Note that, in the ApplyHeadingVisibility method, we call the InitializeHeadersContainer method when we need a HeadersContainer, and the RemoveHeadersContainer method when we do not need it anymore.

The InitializeHeadersContainer method calls the GetHeadersContainer method of the parent HandyContainer to get a HeadersContainer from the cache. The RemoveHeadersContainer method calls the CacheHeadersContainer method of the parent HandyContainer to put the HeadersContainer back in the cache.

The HeadersContainer need to be located at the right place, and its width must be set according to the width of the items. This is done at several places:

In the InitializeHeadersContainer, we initialize the location and the size of the HeadersContainer.

In the OnArrange method, we calculate the top location of the HeadersContainer. The OnArrange method is called each time the item is arranged (either moved or resized) by the ItemsHost of the HandyContainer.

In the ArrangeOverride method. The ArrangeOverride method is called by Silverlight each time it must arrange the items.

The RemoveHeadersContainer method is called by ApplyHeadingVisibility when the headings are not visible anymore.

Nevertheless, calling RemoveHeadersContainer from inside the ContainerItem is not enough. If a ContainerItem is removed from the ItemsHost, the ContainerItem is not aware of this action. However, we also need to call the RemoveHeadersContainer method in this case. Luckily, the HandyContainer class has a ClearContainerForItemOverride method that is called each time such an event happens. Let's override the ClearContainerForItemOveride method of the HandyContainer:

The InitializeHeadersContainer method takes into account the fact that the ContainerItem could have been "reused" (see explanation about this above in this tutorial). If the HeadersContainer already exists and is linked to data of the right type and is linked to the correct hierarchical level, it is reused. Otherwise, another one is created.

In order to check which hierarchical level an existing HeadersContainer is linked to, the InitializeHeadersContainer method checks the value of the SourceLevel property of the HeadersContainer.

We have not created this property yet. Let's add it to the HeadersContainer class:

In order to be able to watch the HeadersContainers, we have temporarily filled their content with a "This is the header" string (watch the InitializeHeadersContainer method).

Let's start our application.

The headers are displayed at the correct location. Their indentations are not correct, but this is because we have not created a real style that takes indentation into account for the HeadersContainer yet. We will do that during the next step.

Enhance the HeadersContainer style

Let's add an enhanced style for the HeadersContainer in the generic.xaml file.

This style contains the background that will be displayed underneath the ContentPresenter. It is made of two rectangles which are filled with the Background brush and the DefaultReflectVertical brush.

The style also makes use of a horizontal StackPanel in order to be able to indent the headers the same way the rows are indented (see Container_RowNodeStyle). The RootCanvas will be used to move the headers according to the HorizontalOffset of the grid.

Let's now update the HeadersContainer class in order to take advantages of this new style.

Let's first enhance the HeadersContainer class to load the style dynamically.

The next thing to do is to add the FullIndentation and Indentation properties to the class. The FullIdentation property value will be calculated from the Indentation and SourceLevel property values. It is used inside the style to ensure that the content of the HeadersContainer is indented the same way as the content of the rows (i.e., the items of the HandyContainer).

In some of the methods below, we have referenced a gridBody field. Indeed, in order to be able to modify the location of the headers according to the HorizontalOffset of the grid, we need a reference to the grid.

PrepareGridBody (which calls UpdateHorizontalSettings) must be called when the HeadersContainer has been fully loaded and the template has been applied. Because, when using Siverlight, sometimes the Loaded event occurs before OnApplyTemplate and sometimes after, we have to use an isLoaded and isTemplateApplied flags.

If we start our application now, we can see that if we scroll the grid horizontally, the location of the HeadersContainer is moved accordingly. The HeadersContainers are indented correctly and a background is displayed underneath their contents.

Adding the headers to the HeadersContainer

Now that we have HeadersContainers that are displayed at the correct location inside the grid, we need to fill them with the headers.

Let's modify the GetHeadersContainer method of the HandyContainer to fill the Content and the ContentTemplate of the HeadersContainer with the Content and the ContentTemplate of the item it is linked to:

If we start our application now, we see that the HeadersContainer is filled with the same cells as the items it is linked to.

In order to remove these cells from the HeadersContainer and to replace them with headers, let's add a RegsiterCell method to the HeadersContainer class. The purpose of this method is to replace a cell (the one passed as argument to the method) with a header. It will also format the header, giving it the same size as the cell and filling its content with the value of the Header property of the cell.

But first, let's add a static GetParentHeadersContainer method to the HeadersContainer class. We will need it to be able to find the parent HeadersContainer of a cell:

The RegisterCell method must be called at the right time. If it is called too early, the cell will not have been laid out yet and the size of the header will not be initialized correctly. If it is called too late, the user may have the possibility to interact with the cell before it is replaced by a header.

Let's add a RegisterCell method to the Cell class. This method will call the RegisterCell method of the parent HeadersContainer, if any:

The RegisterCell method will be called as soon as the cell is fully loaded (it is loaded and the template is applied). As it is not possible in Silverlight to predict in which order the Loaded event and OnApplyTemplate method call will occur, we will have to work with an isLoaded and an isTemplateApplied flag to keep track of the calls to the method and event.

If we start our application now, we can see that the headers are displayed at the correct place inside the headersContainerss.

Header style

We need to enhance the style of the headers in order to make the headers more attractive.

At the same time, we will add a WindowsSizer inside the header style. It will allow the user to resize the header. The VerticalSizeMode and the HorizontalSizeMode of the WindowSizer inside the header will be bound to the VerticalSizeMode and the HorizontalSizeMode properties of the header.

If we start our application now, we can see that the headers are almost well rendered.

Header MouseOver

If we look at the headers style, we can see that there is a MouseOver VisualState that is part of the CommonStates VisualStateGroup and that allows to make an element named "MouseOverVisual" visible or collapsed.

In order to make this VisualState work correctly, we need to add the necessary code to the Header class:

The CellName property and the FindHeader method will be used in the next step to be able to keep the size of a header synchronized with the size of the cells it is linked to.

More generally, this property and method can be used to retrieve the header linked to a cell or vice versa.

Resizing the headers

The header's style contains a WindowSizer inside it, but we have not "activated" it yet. Let's modify the Header class in order to take the WindowSizer into account. The cells have a UserResizeType property allowing to define if they can be resized and how (vertically, horizontally, or both). Let's add the same property to the Header class in order to know if it can be resized.

internal HeaderResizeTypes UserResizeType
{
get;
set;
}

As we are working with a WindowSizer inside the header, if we would like to set the size of the header from outside (for instance, to initialize it), we cannot set the size of the header by setting its width and/or height value. If we do this, the size of the header will be fixed and when the WindowSizer will be resized (by the user dragging one of its borders), the header will not be automatically resized. When setting the size of the header from outside, we need to set the size of the WindowSizer and not the size of the header.

Let's modify the OnApplyTemplate method and add SizerHeight and SizerWidth methods to the Header class:

Let's "connect" to the UserResizing and UserResizeComplete events of the header in the RegisterCell method of the HeaderContainer.

Because, when a header is resized, all the cells "linked" to a header must be resized, this action should take place inside the HandyContainer (the gridBody). This is the place where all the cells can be accessed. Therefore, when a header is resized, we will call the _HeaderSizeChanged method of the HandyContainer (this method will be created in the next step).

The ParentCount property of the ContainerItem class allows knowing how many parent nodes the item has. Therefore, in the above method, it allows us to know if the item is at the correct hierarchical level. However, the ParentCount property of the Item class (from which inherits the ContainerItem class) is protected. In order to be able to access its value, we need to add an internal _ParentCount property to the ContainerItem class:

internalint _ParentCount
{
get { returnthis.ParentCount; }
}

If we start our application now, we can see that the cells are resized when the headers are resized. Nevertheless, two problems remain:

When a header is resized, the other headers that are linked to the same cells are not resized.

If we scroll the grid after having resized some cells, after some time, items having cells that do not have the correct size are displayed.

The first problem comes from the fact that nothing warns a header that its linked cells have been resized. The second problem comes from the fact that when a new item is created, its cells get their default size values rather than the new sizes defined by the user.

In order to be able to resolve these problems, we need to be able to save and retrieve the size of the cells when they have been resized.

Let's add a SetCellWidth, a SetCellHeight, a GetCellWidth, and a GetCellHeight method to the HandyContainer class:

We need to call the SetCellWidth and the SetCellHeight methods as soon as a header has been resized (in the _HeaderSizeChanged method of the HandyContainer class). We also need to add an event to this method.

Let's modify the _HeaderSizeChange method to take these new rules into account:

If we try to start our application now, it fails during the call to the InitializeSize method of the cell. If we have a deeper look, we can see that the GetParentContainerItem method of the ContainerItem returns an unexpected null value.

How can it happen that a cell does not have a parent ContainerItem?

In fact, the cell is one of the cells generated by the HeadersContainer when its ContentTemplate and Content properties are filled. As the cell is part of a HeadersContainer, it does not have any parent ContainerItem.

If we start our application and resize a header, we can see that now the size of all the cells and headers keep synchronized.

Note

When a cell width or a cell height has been saved using the SetCellWidth or the SetCellHeight method, this value is kept in memory until the corresponding HandyContainer is destroyed. This means that, if the value of the ItemsSource of the ContainerItem is modified in order to display different kinds of data in the grid, the dirty cell's width and height values are kept in memory. In an enhanced version of the grid, we should clear the contents of the cellWidths and the cellHeights dictionaries when the ItemsSource value is modified.

5. Top headers

Introduction

Our grid is now able to display headers inside the grid body. We still have to implement the top headers. Let's see what happens if we put the HeadersContainer that we have created at the top of the grid. For testing purpose, we will add a top header for the countries rows, a top header for the regions rows, and a top header for the persons rows.

Let's modify the Page.xaml of the GridBody project and add the headers at the top of the GridBody.

Note that, in the code above, we have filled almost all the field values of the instances of the Country, StateProvince, and Person classes with the space value. As the data of these instances is not displayed (the cells are replaced with headers), we could have used any value to fill the fields of these instances. Nevertheless, it is better to avoid using null or an empty value. The size of the headers are calculated from the size of the cells they replace. A cell holding an empty or a null value can have an unexpected size.

Let's start our application and watch our changes.

Headers are displayed at the top, and they work as expected. Nevertheless, we had to set some properties of the HeadersContainer using code in Page.xaml.cs. It would be easier if those properties could be set directly in the XAML code of the page, or if it was not needed to fill those properties at all.

HeadersContainer ContentTemplate and DataContext properties

When our GridBody displays body headersContainers, the ContentTemplate and DataContext properties are set in the GetHeadersContainer method of the HandyContainer. When we use top headersContainers, the GetHeadersContainer method is not called and we have to set the values of these properties ourselves. However, the ContentTemplate value is the ItemTemplate value of the GridBody. Therefore, it can be known as soon as the GridBody property has been set. The DataContext property value can be set from the Content property.

Let's modify the _GridBody property of the HeadersContainer class to fill the ContentTemplate value automatically.

Let's remove the setting of the GridBody property of CountryHeaders, RegionHeaders, and PersonHeaders in the Page.xaml.cs file and set the value of this property in the XAML of the Page.xaml file of our GridBody project:

If we start our application, the top headers keep working as expected.

HeadersContainer SourceLevel property

The SourceLevel property is already a DependencyProperty. We can directly set its value in the XAML of the Page.xaml file.

Let's remove the settings of the SourceLevel property of CountryHeaders, RegionHeaders, and PersonHeaders in the Page.xaml.cs file and set the values of these properties in the XAML of the Page.xaml file of our GridBody project. As the default value of SourceLevel is 1, there is no need to set its value for the CountryHeaders.

HeadersContainer Content property

It is not possible to fill the Content property in the XAML of the page (it should be possible, but would imply making actions a lot more complicated than just setting it in the page.xaml.cs file).

We could make changes in our code in order that the grid looks by itself for a value to set in the Content property. After all, the grid has access to the ItemsSource property! It just has to "scan" the ItemsSource looking for an element that meets the conditions (hierarchy level and type) and then put that element in the Content property.

However, implementing such a feature can have a serious performance impact. When working with data located on a remote server (which is the main purpose of a data grid), most of the time, we would not like to make all the data that populates the grid to be loaded at one time. Instead, we will load only a fraction of the elements of the collection from the remote server, and we will populate the elements as needed when they are displayed in the grid (things are more complicated than that, and this topic should be part of another tutorial, but let's keep it simple here). The grid has no way to know where it can find an element in the collection that meets the necessary criteria to be a good candidate for the Content property value. Therefore, the grid must scan the entire ItemsSource collection in order to find one. If the correct element is at the end of the collection, it would imply that the grid will make all our data travel from the remote server in order to find an acceptable element! This is a not a tolerable drawback.

For this reason, we will not implement such a feature inside our grid even if lots of grids on the market do thinks like that.