Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / XAML

Build Your Own DataGrid for Silverlight: Step 1

Rate me:
Please Sign up or sign in to vote.
5.00/5 (26 votes)
23 Apr 2009CPOL52 min read 161.3K   1.6K   84   11
Learn how to build the body part of your DataGrid using Silverlight and the GOA Toolkit. Implement Virtual Mode, work with hierarchical data, and build cells and cells navigation.

This tutorial is part of a set. You can read step 2 here: Build Your Own DataGrid for Silverlight: Step 2.

1. Introduction

Why would I create a grid myself?

Before diving into this tutorial, let's have a look at some of the benefits of writing our own data grid control:

  • Out-of-the-box grids never have all the features we need. Either they are really poor and do not fit our needs, or they are enclosed in huge assemblies that make the size of our application just too big. On top of that, they require a long time to learn.
  • On the opposite, by following the steps of this tutorial, we will build a grid that we will fully domesticate. At any time, we will be able to add features that we really need and only the ones we need, keeping the project at reasonable size. Furthermore, we will learn the way some of the key components of Silverlight and the GOA Toolkit work, and we will be able to apply our knowledge to build other high level controls.

However, pay attention to the fact that in order to complete this tutorial, we will need the free edition of the GOA Toolkit for Silverlight (http://www.netikatech.com/products/toolkit). This library will allow us to take shortcuts, and without them, this tutorial would have the size of an entire book. It will allow us to create up to five instances of our GridBody.

Grid's Body

Just to be sure that we all use the same words to designate the same things, here is a picture describing the elements of the data grid:

DataGrid Elements

In this first part of the tutorial, we will focus on the implementation of a read-only body. In the second part, we will discuss on how to add editing features to our grid, and in the third part, we will turn our attention to the headers.

2. Getting started

Download and install the GOA Toolkit

This tutorial was written using GOA Toolkit 2009 Vol. 1 Build 212. Be sure to have installed this release or a more recent one on your computer. You can download a trial setup from the NETiKA TECH web site: www.netikatech.com/downloads.

If you do not know the GOA Toolkit at all, we suggest that you spend some time to quickly scan the tutorial that is provided with the GOA Toolkit. This tutorial is installed during the setup of the GOA Toolkit. You can access it through the Start menu:

GOA Tutorial

Create a new solution in Visual Studio

  • In Visual Studio, create a new Silverlight project and name it "GridBody".
  • Add references to the GoaEssentials assembly and to the GoaOpen project.

The easiest way to achieve this is to follow the steps described in the HowTos documentation:

HowTos

  • Open the HowTos documentation
  • Select the How To Start node on the left of the application screen
  • Follow the instructions on the right

At the end of this process, we should have a solution having a hierarchy like this one:

GridBody Solution

3. Basic grid body

GOA Toolkit architecture quick overview

If you have not read the GOA Toolkit tutorial, here is a summary that you should read.

The GOA Toolkit focuses on controls that are able to display several items. Lists, menus, tabs, toolbars and data grids are controls of this type. In the GOA Toolkit, this kind of control is called a List control.

The GOA Toolkit is subdivided into two libraries: GOA Essentials and GOA Open.

Base components requiring care in their development and maintenance are grouped in GOA Essentials. These components are at the heart of the GOA Toolkit and they should not be modified without watching out.

GOA Open is built on top of GOA Essentials. GOA Open is provided with its source code. It is made of high level controls such as menus or toolbars. They are all built the same way. If you learn how a GOA Open control is built, you may apply your knowledge on other controls. Most of the time, GOA open controls are made of one or several GOA Essentials components on which styles have been applied.

GOA Open provides three sets of List controls.

Commands

These controls are mainly used to perform actions inside the application. Menus and toolbars are part of this set.

Containers

Container controls are used to display data in various ways. Lists, trees, or combos are part of this set.

Navigators

Navigator controls allow navigating between sets of data or parts of the application. TabStrips, TabTrees, TabLists, or NavigationBars are part of this set.

Implementing a very basic grid's body

Our grid's body will be implemented using a HandyContainer control. This is the control that best fits our needs.

Let's first see what it is possible to do with this control without changing anything.

Let's add a HandyContainer to the Page.xaml of our GridBody application. At the same time, we will add the necessary XMLNS references at the top of the XAML, and remove the Width and Height settings.

XML
<UserControl x:Class="GridBody.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
    xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
    <Grid x:Name="LayoutRoot" Background="White">
        <o:HandyContainer
            x:Name="MyGridBody">
            
        </o:HandyContainer>
    </Grid>
</UserControl>

Of course, if we start our application now, our page will not display anything. We need to attach data to it.

Preparing the data

We need data to be able to test our grid. We are going to fill it with a collection of persons.

Person class

Here is the code of the Person class. We have to add this class to the GridBody project.

C#
using Open.Windows.Controls;

namespace GridBody
{
    public class Person : ContainerDataItem
    {
        public Person(string firstName, string lastName, string address, 
            string city, string zipCode, bool isCustomer, string comment)
        {
            this.firstName = firstName;
            this.lastName = lastName;
            this.address = address;
            this.city = city;
            this.zipCode = zipCode;
            this.isCustomer = isCustomer;
            this.comment = comment;
        }

        private string firstName;
        public string FirstName
        {
            get { return firstName; }
            set
            {
                if (firstName != value)
                {
                    firstName = value;
                    OnPropertyChanged("FirstName");
                }
            }
        }

        private string lastName;
        public string LastName
        {
            get { return lastName; }
            set
            {
                if (lastName != value)
                {
                    lastName = value;
                    OnPropertyChanged("LastName");
                }
            }
        }

        private string address;
        public string Address
        {
            get { return address; }
            set
            {
                if (address != value)
                {
                    address = value;
                    OnPropertyChanged("Address");
                }
            }
        }

        private string city;
        public string City
        {
            get { return city; }
            set
            {
                if (city != value)
                {
                    city = value;
                    OnPropertyChanged("City");
                }
            }
        }

        private string zipCode;
        public string ZipCode
        {
            get { return zipCode; }
            set
            {
                if (zipCode != value)
                {
                    zipCode = value;
                    OnPropertyChanged("ZipCode");
                }
            }
        }

        private bool isCustomer;
        public bool IsCustomer
        {
            get { return isCustomer; }
            set
            {
                if (isCustomer != value)
                {
                    isCustomer = value;
                    OnPropertyChanged("IsCustomer");
                }
            }
        }

        private string comment;
        public string Comment
        {
            get { return comment; }
            set
            {
                if (comment != value)
                {
                    comment = value;
                    OnPropertyChanged("Comment");
                }
            }
        }        
    }
}

Note that we have made the Person class inherit from the ContainerDataItem class. This is not mandatory, but it is recommended. It is the easiest way to create a data class that can work with a HandyContainer control. If you choose not to inherit from the ContainerDataItem class, you should at least implement the INotifyPropertyChanged interface in your class. This interface holds a PropertyChanged event the purpose of which is to notify the grid when the value of a property has been modified. This interface is already implemented in the ContainerDataItem. In order to make it work properly, you need to call the OnPropertyChanged method in the setter of each property.

Fill the HandyContainer

In order to fill the grid body, we are going to fill the ItemsSource property of the HandyContainer control with a collection of persons. We could use any kind of collection that implements the IList interface. However, if we would like the GridBody to be able to handle changes in the collection (such as when we add or remove a person), the collection should implement INotifyCollectionChange. This is the case of the ObservableCollection.

Nevertheless, the ObservableCollection provided with Silverlight is limited. Using this collection, you can only add or remove elements one at a time.

On the opposite, the HandyContainer is able to manage the manipulation of several items at a time. Therefore, we will use a GObservableCollection (provided with the Goa Toolkit). This collection implements all the interfaces needed, and provides methods to manipulate several items at a time.

So, let's create our collection of persons and fill the ItemsSource of the GridBody in the constructor of the Page of our application:

C#
public partial class Page : UserControl
{
    private GObservableCollection<Person> personCollection;

    public Page()
    {
        InitializeComponent();

        personCollection = new GObservableCollection<Person>();
        for (int personIndex = 0; personIndex < 1000; personIndex++)
            personCollection.Add(new Person("FirstName" + personIndex,
                                            "LastName" + personIndex,
                                            "Address" + personIndex,
                                            "City" + personIndex,
                                            "ZipCode" + personIndex,
                                            personIndex % 2 == 0,
                                            "Comment" + personIndex));

        MyGridBody.ItemsSource = personCollection;
    }
}

As the purpose of this tutorial is not to explain how to connect and retrieve data from a database or an application server, the data is generated from code.

If we start our application now, we will face two problems:

  • The application is slow to start.
  • The grid does not display the persons' data, but it displays the name of the type of the Person class.

Before going further, let's start by correcting these two problems.

VirtualMode

When displaying and manipulating UIElements, Silverlight is not as fast as a desktop application. This is the reason why our application is slow when it starts. The HandyContainer control creates a UIElement for each person of the collection. As our collection holds 1000 persons, 1000 UIElements must be created. Creating 1000 UIElements is a "long" process, and it slows our application down.

Fortunately, the HandyContainer control implements a VirtualMode. When it is in Virtual Mode, only the items that fit inside the displayed area of the control are created. This way, the number of UIElements that must be created and manipulated is cut down to a more acceptable value.

Applying this change is fast, and can be made directly in the XAML of the page of our application.

XML
<UserControl x:Class="GridBody.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
    xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
    <Grid x:Name="LayoutRoot" Background="White">
        <o:HandyContainer
            x:Name="MyGridBody"
            VirtualMode="On">
            
        </o:HandyContainer>
    </Grid>
</UserControl>

If we start the application now, we notice that it is a lot faster to start. Furthermore, now that the Virtual Mode is on, the performance of the grid will depend a lot less on the number of elements in our data collection.

ItemsTemplate

We have not told the GridBody how it must display the person's data.

This can be done by using the ItemTemplate property of the control. The ItemTemplate is the data template that must be applied to each item in order to display the data (the person) it is linked to.

We are going to create a DataTemplate that uses TextBlocks and Borders in order to mimic grid cells:

XML
<UserControl x:Class="GridBody.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
    xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
    <Grid x:Name="LayoutRoot" Background="White">
        <o:HandyContainer
            x:Name="MyGridBody"
            VirtualMode="On">
            <o:HandyContainer.ItemTemplate>
                <g:ItemDataTemplate>
                    <g:GDockPanel>
                        <g:GStackPanel Orientation="Horizontal" 
                                  g:GDockPanel.Dock="Top">
                            <Border BorderBrush="Black" 
                                      BorderThickness="1" 
                                      Width="100" Padding="2">
                                <TextBlock Text="{Binding FirstName}"/>
                            </Border>
                            <Border BorderBrush="Black" 
                                     BorderThickness="1" 
                                     Width="100" Padding="2">
                                <TextBlock Text="{Binding LastName}"/>
                            </Border>
                            <Border BorderBrush="Black" 
                                    BorderThickness="1" Width="100" 
                                    Padding="2">
                                <TextBlock Text="{Binding Address}"/>
                            </Border>
                            <Border BorderBrush="Black" 
                                      BorderThickness="1" 
                                      Width="100" Padding="2">
                                <TextBlock Text="{Binding City}"/>
                            </Border>
                            <Border BorderBrush="Black" 
                                   BorderThickness="1" 
                                   Width="100" Padding="2">
                                <TextBlock Text="{Binding ZipCode}"/>
                            </Border>
                        </g:GStackPanel>
                        <Border BorderBrush="Black" 
                                    BorderThickness="1" 
                                    g:GDockPanel.Dock="Fill" Padding="2">
                            <TextBlock Text="{Binding Comment}" />
                        </Border>
                    </g:GDockPanel>
                </g:ItemDataTemplate>
            </o:HandyContainer.ItemTemplate>
        </o:HandyContainer>
    </Grid>
</UserControl>

Note that, to fill the ItemTemplate property, we have used an ItemDataTemplate which is a special kind of a DataTemplate. This is not mandatory, but working with ItemDataTemplate rather than DataTemplate allows better customizing the way items are displayed. If we start the application, the data of the person is correctly displayed although the result is far from perfect.

DefaultItemModel

We would like to remove the space that is displayed between the items (i.e., the rows). This can be done by removing the padding on each item.

Space Between the Items

The items should not stretch from one border to the other. This can be resolved by applying a Left value to the HorizontalAlignement property of each item.

Item Stretch

A way to apply these changes is to modify the style of the item, but we do not want to make such a change for the moment. It is easier to use the DefaultItemModel property of the HandyContainer.

The DefaultItemModel property allows defining special property values to apply on each item of a HandyContainer. In order to do this, we have to fill the DefaultItemModel property of the control with a ContainerItem. Each property value (except for the style) that we apply to the ContainerItem of the DefaultItemModel will also be applied to each item of the HandyContainer:

XML
<o:HandyContainer.DefaultItemModel>
    <o:ContainerItem HandyStyle="StandardItem" 
               Padding="0" HorizontalAlignment="Left"/>
</o:HandyContainer.DefaultItemModel>

If we start our application now, the data is better displayed.

AlternateType

It is not easy to see where an item starts and where it finishes. It would be a lot easier if one item out of two has another background. The AlternateType property of the HandyContainer allows us to accomplish this:

XML
<o:HandyContainer 
    x:Name="MyGridBody"
    VirtualMode="On"
    AlternateType="Items">

Cells

Let's start the application and watch the result of our work. We are still far from a data grid, but it is an interesting start.

What is missing? First, we would like that the cells inside the items are real cells and not TextBlocks with a border. The cells should be able to display different kind of data - not just text. The user should be able to navigate from one cell to another. At the same time, we would like to keep the flexibility of the ItemTemplate. Using panels inside an item template allows us to easily set the location of each cell. We are not limited to display the cells on a single row like in a standard grid.

But before implementing those features, let's explore another possibility of the HandyContainer: the nodes.

Nodes

We would like that our grid is also able to display hierarchical data. The items of the HandyContainer are able to manage this. Each item can be a node.

As a sample, we are going to make our persons members of countries, and display them grouped by the countries they belong to. Let's create a very simple Country class and add it to the GridBody project:

C#
using Open.Windows.Controls;

namespace GridBody
{
    public class Country : ContainerDataItem
    {
        public Country(string name)
        {
            this.name = name;

        }

        private string name;
        public string Name
        {
            get { return name; }
            set
            {
                if (name != value)
                {
                    name = value;
                    OnPropertyChanged("Name");
                }
            }
        }
    }
}

Next, let's change the ItemsSource of our GridBody:

C#
public partial class Page : UserControl
{
    //private GObservableCollection<Person> personCollection;
    private GObservableCollection<Country> countryCollection;

    public Page()
    {
        InitializeComponent();

        //personCollection = new GObservableCollection<Person>();
        //for (int personIndex = 0; personIndex < 1000; personIndex++)
        //    personCollection.Add(new Person("FirstName" + personIndex, 
        //                                    "LastName" + personIndex, 
        //                                    "Address" + personIndex, 
        //                                    "City" + personIndex, 
        //                                    "ZipCode" + personIndex, 
        //                                    personIndex % 2 == 0, 
        //                                    "Comment" + personIndex));

        //MyGridBody.ItemsSource = personCollection;

        countryCollection = new GObservableCollection<Country>();
        for (int countryIndex = 0; countryIndex < 100; countryIndex++)
        {
            Country country = new Country("CountryName" + countryIndex);
            for (int personIndex = 0; personIndex < 10; personIndex++)
                country.Children.Add(new Person("FirstName" + personIndex, 
                                                "LastName" + personIndex, 
                                                "Address" + personIndex, 
                                                "City" + personIndex, 
                                                "ZipCode" + personIndex, 
                                                personIndex % 2 == 0, 
                                                "Comment" + personIndex));


            country.IsExpanded = true;
            countryCollection.Add(country);
        }

        MyGridBody.ItemsSource = countryCollection;
    }
}

As the Country class inherits from the ContainerDataItem class, we were automatically able to use two very interesting properties in the code above: Children and IsExpanded. The Children property allows defining the children of an element. Once its children property is filled, the HandyContainer manages the item as a node. The IsExpanded property allows defining whether the node (i.e., the item) is expanded (opened) or not.

DataPresenter

However, if we start the application, the countries nodes are not displayed. This is because we still need to change the ItemTemplate of the GridBody and describe the way the countries will be displayed.

In the ItemTemplate, we must be able to describe at the same time how the persons and the countries are displayed. This is done by the use of the HandyDataPresenter.

XML
<o:HandyContainer.ItemTemplate>
    <g:ItemDataTemplate>
        <Grid>
            <o:HandyDataPresenter DataType="GridBody.Person">
                <g:GDockPanel>
                    <g:GStackPanel Orientation="Horizontal" 
                             g:GDockPanel.Dock="Top">
                        <Border BorderBrush="Black" 
                                   BorderThickness="1" 
                                   Width="100" Padding="2">
                            <TextBlock Text="{Binding FirstName}"/>
                        </Border>
                        <Border BorderBrush="Black" 
                                 BorderThickness="1" 
                                 Width="100" Padding="2">
                            <TextBlock Text="{Binding LastName}"/>
                        </Border>
                        <Border BorderBrush="Black" 
                               BorderThickness="1" 
                               Width="100" Padding="2">
                            <TextBlock Text="{Binding Address}"/>
                        </Border>
                        <Border BorderBrush="Black" 
                                  BorderThickness="1" 
                                  Width="100" Padding="2">
                            <TextBlock Text="{Binding City}"/>
                        </Border>
                        <Border BorderBrush="Black" 
                                 BorderThickness="1" 
                                 Width="100" Padding="2">
                            <TextBlock Text="{Binding ZipCode}"/>
                        </Border>
                    </g:GStackPanel>
                    <Border BorderBrush="Black" 
                            BorderThickness="1" 
                            g:GDockPanel.Dock="Fill" Padding="2">
                        <TextBlock Text="{Binding Comment}" />
                    </Border>
                </g:GDockPanel>
            </o:HandyDataPresenter>
            <o:HandyDataPresenter DataType="GridBody.Country">
                <g:GStackPanel Orientation="Horizontal">
                    <Border BorderBrush="Black" 
                              BorderThickness="1" 
                              g:GDockPanel.Dock="Fill"  Padding="2">
                        <TextBlock Text="{Binding Name}" />
                    </Border>
                    <Border BorderBrush="Black" 
                             BorderThickness="1" 
                             g:GDockPanel.Dock="Fill" Padding="2">
                        <TextBlock Text="{Binding Children.Count}" />
                    </Border>
                </g:GStackPanel>
            </o:HandyDataPresenter>
        </Grid>
    </g:ItemDataTemplate>
</o:HandyContainer.ItemTemplate>

The HandyDataPresenter displays its content only if the data linked to the item is of a predefined type.

In our sample, we have defined the DataType properties of the HandyDataPresenters in order that the first HandyDataPresenter is displayed only when the item is linked to a person and the second HandyDataPresenter is displayed only when the item is linked to a country.

If we start our application now, countries and persons are displayed, but they are all aligned to the left, and there is no indentation between the levels of the hierarchy.

This is because the default style of the items of the HandyContainer control does not implement indentation. In order to use a style that visually implements the standard features of nodes, we must tell the HandyContainer to do so:

XML
<o:HandyContainer
    x:Name="MyGridBody"
    VirtualMode="On"
    AlternateType="Items"
    HandyDefaultItemStyle="Node">

This way, the nodes will be indented, and an arrow will be displayed in front of each node to allow the user to expand or collapse it.

As nodes items have a margin, we must suppress it by updating the DefaultItemModel property of the HandyContainer:

XML
<o:HandyContainer.DefaultItemModel>
    <o:ContainerItem HandyStyle="Node" 
         Padding="0" 
         HorizontalAlignment="Left" Margin="0"/>
</o:HandyContainer.DefaultItemModel>

4. Cells

Introduction

It is now time to start implementing our cells. The first things we need are cells that can display different kinds of data. In this tutorial, we will implement the TextCell class and the CheckBoxCell class. You will be able to easily implement the other kinds of cells yourself.

We will add all our new features directly in the GoaOpen project. This project is the open part of the GOA Toolkit.

Let's create a new Extensions folder inside the GoaOpen project. We will put all our GOA improvements in that folder. Let's also add a Grid subfolder to the Extensions folder. This folder will hold all the improvements related to our Grid.

Extension Folder

Preparations

TreeHelper class

Let's first implement a helper class that we will use at several places in our code.

The TreeHelper class implements a IsChildOf method which allows to know if an element of the tree is a child of another element of the tree. For instance, if the button "button1" is a child of the canvas canvas1, the following call will return true:

C#
TreeHelper.IsChildOf(canvas1, button1)

Add this class inside the Extensions\Grid folder of the GoaOpen project.

C#
using System.Windows;
using System.Windows.Media;

namespace Open.Windows.Controls
{
    public static class TreeHelper
    {
        public static bool IsChildOf(DependencyObject parent, 
                           DependencyObject child)
        {
            DependencyObject parentElement = child;
            while (parentElement != null)
            {
                if (parentElement == parent)
                    return true;

                parentElement = VisualTreeHelper.GetParent(parentElement);
            }

            return false;
        }
    }
}

Preparing the HandyContainer

Before implementing the cells, we need to add a few methods and properties to the HandyContainer.

The HandyContainer class is located in the GoaControls\HandyList\HandyList\HandyContainer folder of the GoaOpen project.

We will not add our methods directly to the HandyContainer file. In order to keep our changes apart from the code provided in GoaOpen, we will add a new HandyContainer file in the Extensions\Grid folder we have just created.

HandyContainer Partial Class

Let's modify the existing HandyContainer.cs file (the one that is located in the GoaControls\HandyList\HandyList\HandyContainer folder) in order that it contains a partial class:

C#
namespace Open.Windows.Controls
{
    /// <summary>
    /// Containers controls are used to display data in various ways. 
     ///Lists, Trees or Combos are part of this set. 
    /// </summary>
    public partial class HandyContainer : HandyListControl
    {
        public static readonly DependencyProperty HandyStyleProperty;
        public static readonly DependencyProperty HandyDefaultItemStyleProperty;

Let's create a new HandyContainer partial class in the new HandyContainer file (the one that we just created in the the Extensions\Grid folder).

C#
using System;
using System.Windows;
using Netika.Windows.Controls;
using System.Windows.Media;
using System.Windows.Input;

namespace Open.Windows.Controls
{
    public partial class HandyContainer : HandyListControl
    {
    
    }
}
GetParentContainer method

The GetParentContainer static method will allow finding the parent HandyContainer of a Framework element. For instance, if the "cell1" cell is a cell of the "GridBody1" HandyContainer, the following call will return a reference to GridBody1 HandyContainer:

C#
HandyContainer.GetParentContainer(cell);

Let's add this method to our new partial class:

C#
using System;
using System.Windows;
using Netika.Windows.Controls;
using System.Windows.Media;
using System.Windows.Input;

namespace Open.Windows.Controls
{
    public partial class HandyContainer : HandyListControl
    {
        public static HandyContainer GetParentContainer(FrameworkElement element)
        {
            DependencyObject parentElement = element;
            while (parentElement != null)
            {
                HandyContainer parentContainer = parentElement as HandyContainer;
                if (parentContainer != null)
                    return parentContainer;

                parentElement = VisualTreeHelper.GetParent(parentElement);
            }

            return null;
        }
    }
}
CurrentCellName property

Remember that one of our requirements was that we would like that the location of the cells could be set by the use of panels inside the ItemTemplate of the GridBody. This means that the cells will not necessarily be displayed side by side in a single line. Therefore, we cannot designate a cell by using an index as it is usually done in a standard Grid. In the case of elaborated layouts, it will not be clear which cell is designated by which index.

Therefore, we will force the use of a name for each cell, and will provide ways to manipulate the cells from their name.

At this time, we will add a CurrentCellName property to the HandyContainer. The CurrentCell is the cell of the grid that holds the focus. The CurrentCellName property will be filled with the name of the current cell.

We will also add a CurrentCellNameChanged event. This event is raised when the current cell is changed.

C#
public event EventHandler CurrentCellNameChanged;
private string currentCellName;
public string CurrentCellName
{
    get { return currentCellName; }
    internal set
    {
        if (currentCellName != value)
        {
            currentCellName = value;
            OnCurrentCellNameChanged(EventArgs.Empty);

        }
    }
}

protected virtual void OnCurrentCellNameChanged(EventArgs e)
{
    if (CurrentCellNameChanged != null)
        CurrentCellNameChanged(this, e);
}

Cells

In the GoaOpen project, let's first create a Cell abstract class that implements features shared by all the cells, whatever the data type they display is. The TextCell class and the CheckBoxCell class will inherit from the Cell class.

C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Open.Windows.Controls
{
    public abstract class Cell : Control
    {
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (string.IsNullOrEmpty(this.Name))
                throw new InvalidCastException("A cell must have a name");
        }

        private bool isFocused;
        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);

            if (!isFocused)
            {
                VisualStateManager.GoToState(this, "Focused", true);

                isFocused = true;
                HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
                if (parentContainer != null)
                {
                    parentContainer.CurrentCellName = this.Name;
                }
            }
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);

            object currentFocusedElement = FocusManager.GetFocusedElement();
            if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
            {
                isFocused = false;
                VisualStateManager.GoToState(this, "Standard", true);
            }
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            object currentFocusedElement = FocusManager.GetFocusedElement();
            if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
            {
                this.Focus();
            }
        }
    }
}

In the OnApplyTemplate, we make sure that the cell has a name. Cells are referenced by their names, and defining a name on each cell is mandatory.

The cell that holds the focus (this means that either the cell or one of the controls it contains has the focus) is the current cell. Therefore, when the cell gets the focus (watch the OnGotFocus method), we notify its parent HandyContainer by setting the value of the CurrentCellName property.

Furthermore, we call the VisualStateManager.GoToState method to switch the state of the cell to "Focused". This way, we will be able to modify the look of the cell (we will do this in the style applied to the cell) when it becomes the current cell.

When the focus leaves the cell (watch the OnLostFocus event), we switch the state of the cell back the "Standard" value.

The OnMouseLeftButtonDown method puts the focus on the cell when the user clicks on it.

TextCell

Code

The code of the TextCell is very simple. A Text property allows defining the text that the cell must display.

In the constructor, we define the default style that must be used by the TextCell. We will add this style to the generic.xaml file in the next step.

C#
using System.Windows;

namespace Open.Windows.Controls
{
    public class TextCell : Cell
    {
        public static readonly DependencyProperty TextProperty;

        static TextCell()
        {
            TextProperty = DependencyProperty.Register("Text", 
                                typeof(string), typeof(TextCell), null);
        }

        public TextCell()
        {
            DefaultStyleKey = typeof(TextCell);
        }

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }
    }
}
Style

We also need to implement the Style of the TextCell.

GOA Open is delivered with two generic files: generic.xaml and genericSL.xaml. The first file is the one used by default. It contains the default styles that are applied to the GOA open controls.

The genericSL.xaml file contains alternative styles for the GOA Open controls. When these styles are applied to the GOA controls, they have a look that is close to the look of the standard Silverlight controls. If you do not know how to use the styles provided in the genericSL.xaml file instead of the ones provided in the default generic.xaml file, read the instructions in the ReadMe.Txt file of the GoaOpen project.

In this tutorial, we will assume that you use the styles provided in the default generic.xaml file of the GoaOpen project. If this is not the case, we recommend reactivating them.

If you download the code of this tutorial, you will see that we also provide a genericSL file containing the Standard Sliverlight style for the grid.

Let's add at the end of the generic.xaml file a separator that clearly separates our styles from the other provided GoaOpen styles:

XML
        . . .

        </Setter.Value>
        </Setter>
    </Style>

    <!--================================================================================-->
    <!--================================================================================-->
    <!--=============================  GRID ============================================-->
    <!--================================================================================-->
    <!--=================================================================================-->

</ResourceDictionary>

Then, let's add the style of our TextCell after the separator.

XML
<Style TargetType="o:TextCell">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" 
               Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" 
               Value="{StaticResource DefaultForeground}"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Stretch" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="2,2,1,1" />
    <Setter Property="Width" Value="100"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:TextCell">
                <Grid>
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="FocusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <TextBlock 
                        x:Name="TextElement" 
                        Text="{TemplateBinding Text}"
                        Margin="{TemplateBinding Padding}"
                        HorizontalAlignment=
                          "{TemplateBinding HorizontalContentAlignment}"
                        VerticalAlignment=
                          "{TemplateBinding VerticalContentAlignment}"/>
                    <Rectangle Name="FocusElement" 
                        Stroke="{StaticResource DefaultFocus}" 
                        StrokeThickness="1" 
                        IsHitTestVisible="false" 
                        StrokeDashCap="Round" 
                        Margin="0,1,1,0" 
                        StrokeDashArray=".2 2" 
                        Visibility="Collapsed" />
                    <Rectangle Name="CellRightBorder" 
                        Stroke="{TemplateBinding BorderBrush}" 
                        StrokeThickness="0.5" 
                        Width="1" 
                        HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Note that in the TextCell style:

  • We have set a default Width value. This way if the Width of the cell is not defined in the ItemTemplate, the default width will be applied.
  • The TextCell holds a TextBlock that will display the value of the Text property of the TextCell.
  • The FocusElement is a dotted rectangle. It is collapsed by default, and becomes visible when the CommonStates value becomes "Focused".
  • A vertical line, built using a Rectangle that has a width of 1 pixel, is displayed at the right of the cell. This line is used to draw the right border of the cell.

After all these modifications, let's try to start our application again. But before that, we need to change the ItemTemplate of the GridBody control that is on the Page of the GridBody Tutorial project.

Let's replace all the Border/TextBlock pairs with TextCell. Do not forget to give a name to each cell:

XML
<g:ItemDataTemplate>
    <Grid>
        <o:HandyDataPresenter DataType="GridBody.Person">
            <g:GDockPanel>
                <g:GStackPanel Orientation="Horizontal" 
                             g:GDockPanel.Dock="Top">
                    <o:TextCell Text="{Binding FirstName}" 
                      x:Name="FirstName"/>
                    <o:TextCell Text="{Binding LastName}" 
                      x:Name="LastName"/>
                    <o:TextCell Text="{Binding Address}" 
                      x:Name="Address"/>
                    <o:TextCell Text="{Binding City}" 
                      x:Name="City"/>
                    <o:TextCell Text="{Binding ZipCode}" 
                      x:Name="ZipCode"/>
                </g:GStackPanel>
                <o:TextCell Text="{Binding Comment}" 
                   g:GDockPanel.Dock="Fill" 
                   x:Name="Comment" Width="Auto"/>
            </g:GDockPanel>
        </o:HandyDataPresenter>
        <o:HandyDataPresenter DataType="GridBody.Country">
            <g:GStackPanel Orientation="Horizontal">
                <o:TextCell Text="{Binding Name}"  
                          x:Name="CountryName"/>
                <o:TextCell Text="{Binding Children.Count}"  
                          x:Name="ChildrenCount"/>
            </g:GStackPanel>
        </o:HandyDataPresenter>
    </Grid>
</g:ItemDataTemplate>

If we start the application now, we will notice that the cells are correctly displayed, and that when we click on a cell, it gets the focus. Nevertheless, the display is not perfect. We would like to have the ability to add horizontal lines between the rows.

But before doing this, let's write the code of the other kind of cells we would like to implement: the CheckBoxCell.

CheckBoxCell

Code

Let's add a CheckBoxCell class to the GoaOpen project.

C#
using System;
using System.Windows;

namespace Open.Windows.Controls
{
    public class CheckBoxCell : Cell
    {
        public static readonly DependencyProperty IsCheckedProperty;
        public static readonly DependencyProperty CheckMarkVisibilityProperty;

        private bool isOnReadOnlyChange;

        static CheckBoxCell()
        {
            IsCheckedProperty = DependencyProperty.Register("IsChecked",
                typeof(bool),
                typeof(CheckBoxCell),
                new PropertyMetadata(new PropertyChangedCallback(OnIsCheckedChanged)));
            CheckMarkVisibilityProperty = 
              DependencyProperty.Register("CheckMarkVisibility",
                typeof(Visibility),
                typeof(CheckBoxCell),
                new PropertyMetadata(Visibility.Collapsed,
                    new PropertyChangedCallback(OnCheckMarkVisibilityChanged)));
        }

        public CheckBoxCell()
        {
            DefaultStyleKey = typeof(CheckBoxCell);
        }

        public bool IsChecked
        {
            get { return (bool)GetValue(IsCheckedProperty); }
            set { SetValue(IsCheckedProperty, value); }
        }

        private static void OnIsCheckedChanged(DependencyObject d, 
                            DependencyPropertyChangedEventArgs e)
        {
            CheckBoxCell cell = (CheckBoxCell)d;
            cell.OnIsCheckedChanged((bool)e.NewValue);
        }

        protected virtual void OnIsCheckedChanged(bool isChecked)
        {
            isOnReadOnlyChange = true;
            if (isChecked)
                CheckMarkVisibility = Visibility.Visible;
            else
                CheckMarkVisibility = Visibility.Collapsed;
            isOnReadOnlyChange = false;
        }

        public Visibility CheckMarkVisibility
        {
            get { return (Visibility)GetValue(CheckMarkVisibilityProperty); }
            private set { SetValue(CheckMarkVisibilityProperty, value); }
        }

        private static void OnCheckMarkVisibilityChanged(DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            CheckBoxCell cell = (CheckBoxCell)d;
            if (!cell.isOnReadOnlyChange)
                throw new InvalidOperationException("Property is read only");
        }
    }
}

The code of this cell is quite simple. We have implemented two properties: IsChecked and CheckMarkVisibility. The IsChecked property will be bound to the data. The CheckMarkVisibility property allows defining if the CheckMark that the cell will display is displayed or not. The CheckMarkVisibility property value will depend on the IsChecked property value.

Alternatively, we could have used two states: IsChecked and IsNotChecked, and use the same kind of process as with the focus element.

Style
XML
<Style TargetType="o:CheckBoxCell">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderBrush" 
          Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" 
          Value="{StaticResource DefaultForeground}"/>
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Width" Value="20"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:CheckBoxCell">
                <Grid Background="Transparent">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="focusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Rectangle 
                            x:Name="ShadowVisual" 
                            Fill="{StaticResource DefaultShadow}" 
                            Height="12" 
                            Width="12" 
                            RadiusX="2" 
                            RadiusY="2" 
                            Margin="1,1,-1,-1"/>
                    <Border 
                            x:Name="BackgroundVisual" 
                            Background="{TemplateBinding Background}" 
                            Height="12" 
                            Width="12" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            CornerRadius="2" 
                            BorderThickness="{TemplateBinding BorderThickness}"/>
                    <Grid 
                            x:Name="CheckMark" 
                            Width="8" 
                            Height="8" 
                            Visibility="{TemplateBinding CheckMarkVisibility}" >
                        <Path 
                                Stretch="Fill" 
                                Stroke="{TemplateBinding Foreground}" 
                                StrokeThickness="2" 
                                Data="M129.13295,140.87834 L132.875,145 L139.0639,137" />
                    </Grid>
                    <Rectangle 
                            x:Name="ReflectVisual" 
                            Fill="{StaticResource DefaultReflectVertical}" 
                            Height="5" 
                            Width="10" 
                            Margin="1,1,1,6" 
                            RadiusX="2" 
                            RadiusY="2"/>
                    <Rectangle 
                            Name="focusElement" 
                            Stroke="{StaticResource DefaultFocus}" 
                            StrokeThickness="1" 
                            Fill="{TemplateBinding Background}" 
                            IsHitTestVisible="false" 
                            StrokeDashCap="Round" 
                            Margin="0,1,1,0" 
                            StrokeDashArray=".2 2" 
                            Visibility="Collapsed" />
                    <Rectangle 
                            Name="CellRightBorder" 
                            Stroke="{TemplateBinding BorderBrush}" 
                            StrokeThickness="0.5" 
                            Width="1" 
                            HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now that we have already made the style of the TextBoxCell, the CheckBoxCell style seems quite simple.

  • We have set a default Width value.
  • The visibility of the CheckMark element is bound to the CheckMarkVisibility property.
  • The FocusElement and the vertical right border are managed exactly the same way in the TextCell and in the CheckBoxCell.

In order to see what a CheckBoxCell looks like, let's add one in the ItemTemplate of the GridBody control of the GridBody project, and let's bind it to the IsCustomer property of the Persons:

XML
<g:ItemDataTemplate>
    <Grid>
        <o:HandyDataPresenter DataType="GridBody.Person">
            <g:GDockPanel>
                <g:GStackPanel Orientation="Horizontal" 
                             g:GDockPanel.Dock="Top">
                    <o:TextCell Text="{Binding FirstName}" 
                      x:Name="FirstName"/>
                    <o:TextCell Text="{Binding LastName}" 
                      x:Name="LastName"/>
                    <o:TextCell Text="{Binding Address}" 
                      x:Name="Address"/>
                    <o:TextCell Text="{Binding City}" 
                      x:Name="City"/>
                    <o:TextCell Text="{Binding ZipCode}" 
                      x:Name="ZipCode"/>
                    <o:CheckBoxCell IsChecked="{Binding IsCustomer}" 
                       x:Name="IsCustomer"/>
                </g:GStackPanel>
                <o:TextCell Text="{Binding Comment}" 
                  g:GDockPanel.Dock="Fill" 
                  x:Name="Comment" Width="Auto"/>
            </g:GDockPanel>
        </o:HandyDataPresenter>
        <o:HandyDataPresenter DataType="GridBody.Country">
            <g:GStackPanel Orientation="Horizontal">
                <o:TextCell Text="{Binding Name}" 
                  x:Name="CountryName"/>
                <o:TextCell Text="{Binding Children.Count}" 
                  x:Name="ChildrenCount"/>
            </g:GStackPanel>
        </o:HandyDataPresenter>
    </Grid>
</g:ItemDataTemplate>

5. HandyContainer Grid style

Introduction

We have not created a style for the HandyContainer used to build our GridBody yet.

GoaOpen already provides several styles for the HandyContainer. The HandyStyle property allows choosing between the provided styles. This property is an enumerator. A style is associated to each enumerator of the enumeration. When you choose a value for the HandyStyle property, the HandyContainer will look for the corresponding style in the generic.xaml file and apply it.

Until now, the ListStyle was applied to the HandyContainer that we have used to create our GridBody. Nevertheless, we would like not to use the ListStyle but a style of our own that we can change when we need to.

GridBody style

At this time, we will just make a copy of the ListStyle that is provided in the generic.xaml file of GoaOpen.

  • Find the ListStyle in the generic.xaml file
  • Copy it at the end of the file (just after the CheckBoxCell style)
  • Rename it to GridBodyStyle.
XML
<Style x:Key="GridBodyStyle" TargetType="o:HandyContainer">
    <Setter Property="Orientation" Value="Vertical" />
    <Setter Property="Background" 
      Value="{StaticResource DefaultControlBackground}" />
    <Setter Property="BorderBrush" 
      Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="RestoreFocusMode" Value="LastFocusedItem" />
    <Setter Property="AutoClipContent" Value="True" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
    <Setter Property="HandyStyle" Value="ListStyle"/>
    <!-- needed for combo-->
    <Setter Property="HandyScrollerStyle" Value="StandardScrollerStyle"/>
    <Setter Property="HandyItemsPanelModel" Value="StandardPanel" />
    <Setter Property="HandyStatersModel" Value="StandardStaters"/>
    <Setter Property="HandyDefaultItemStyle" Value="Calculated"/>
    <Setter Property="HandyItemContainerStyle" Value="StandardItem"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="SelectionMode" Value="Single"/>
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="ShowColSeparators" Value="False"/>
    <Setter Property="ShowRowSeparators" Value="False"/>
    <Setter Property="ColSpace" Value="5"/>
    <Setter Property="RowSpace" Value="5"/>
    <Setter Property="ItemContainerDefinedStyle" 
      Value="{StaticResource EmptyStyle}"/>
    <Setter Property="SeparatorStyle" 
      Value="{StaticResource Container_SeparatorStyle}"/>
    <Setter Property="StandardItemStyle" 
      Value="{StaticResource Container_ItemStyle}"/>
    <Setter Property="ListItemStyle" 
      Value="{StaticResource Container_ListItemStyle}"/>
    <Setter Property="DetailsItemStyle" 
      Value="{StaticResource Container_ItemDetailStyle}"/>
    <Setter Property="CheckBoxStyle" 
      Value="{StaticResource Container_CheckBoxStyle}"/>
    <Setter Property="RadioButtonStyle" 
      Value="{StaticResource Container_RadioButtonStyle}"/>
    <Setter Property="ToggleButtonStyle" 
      Value="{StaticResource Container_ToggleButtonStyle}"/>
    <Setter Property="NodeStyle" 
      Value="{StaticResource Container_NodeStyle}"/>

    <Setter Property="DropDownListStyle" 
      Value="{StaticResource Container_DropDownListStyle}"/>
    <Setter Property="DropDownButtonStyle" 
      Value="{StaticResource Container_DropDownButtonStyle}"/>

    <Setter Property="ColSeparatorsStyle" 
      Value="{StaticResource StandardColSeparatorStyle}"/>
    <Setter Property="RowSeparatorsStyle" 
      Value="{StaticResource StandardRowSeparatorStyle}"/>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyContainer">
                <Border 
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        Padding="{TemplateBinding Padding}">
                    <Grid x:Name="ELEMENT_Root">
                        <g:Scroller 
                                x:Name="ElementScroller"
                                Style="{TemplateBinding ScrollerStyle}" 
                                Background="Transparent"
                                BorderThickness="0"
                                Margin="{TemplateBinding Padding}">
                            <g:GItemsPresenter
                                x:Name="ELEMENT_ItemsPresenter"
                                Opacity="{TemplateBinding Opacity}"
                                Cursor="{TemplateBinding Cursor}"
                                HorizontalAlignment =
                                  "{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment =
                                  "{TemplateBinding VerticalContentAlignment}"/>
                        </g:Scroller>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

HandyContainerStyle enum

We need that the new predefined style "GridBodyStyle" can be applied to the HandyContainer by choosing a new value for the HandyStyle property of the HandyContainer. To do this, we must add the GridBodyStyle enumerator to the HandyContainerStyle enum.

  • In the GoaOpen project, locate the HandyContainerStyle file and open it (it is located in the GoaControls\HandyList\HandyList\HandyContainer folder).
  • Add the new GridBodyStyle enumerator at the end of the list:
C#
namespace Open.Windows.Controls
{
    public enum HandyContainerStyle
    {
        None = 0,
        ListStyle,
        ShelfStyle,
        VerticalShelfStyle,
        ComboListStyle,
        GridBodyStyle
    }
}

Apply the style

Let's apply this new style to the MyGridBody control contained in the Page of the GridBody project:

XML
<o:HandyContainer
            x:Name="MyGridBody"
            VirtualMode="On"
            AlternateType="Items"
            HandyDefaultItemStyle="Node"
            HandyStyle="GridBodyStyle">

Now, the GridBodyStyle will be applied to the HandyContainer. As we have not modified the style yet, at this time, we will not see any difference if we start our application.

6. ContainerItem styles

Introduction

The HandyContainer control has a HandyDefaultItemStyle property which allows choosing which style is applied to the items it contains. We have already used this property in our introduction when we applied the node style to the items. This property is an enumerator. It can have the following values: None, Calculated, ItemContainer, Separator, StandardItem, ListItem, DetailsItem, CheckBox, RadioButton, ToggleButton, Node, DropDownList, DropDownButton.

Each one of these values is associated to a property of the HandyContainer containing a style. The following properties are defined:

  • ItemContainerDefinedStyle
  • SeparatorStyle
  • StandardItemStyle
  • ListItemStyle
  • DetailsItemStyle
  • CheckBoxStyle
  • RadioButtonStyle
  • ToggleButtonStyle
  • NodeStyle
  • DropDownListStyle
  • DropDownButtonStyle

When we select the StandardItem value for the HandyDefaultItemStyle property of the HandyContainer, the style that is defined in the StandardItemStyle property is applied to each item of the HandyContainer. When we select the Node value for the HandyDefaultItemStyle property of the HandyContainer, the style that is defined in the NodeStyle property is applied to each item of the HandyContainer, and so on.

Until now, we have worked with the StandardItemStyle (default style) and the NodeStyle.

The same way we have defined our own GridBodyStyle to apply to the HandyContainer, we would like to define our own StandardItemStyle and NodeStyles that we can change when we need to.

Creating the styles

If you look at the GridBodyStyle that we have created in the generic.xaml file, you will see these two properties defined:

XML
<Setter Property="StandardItemStyle" Value="{StaticResource Container_ItemStyle}"/>
<Setter Property="NodeStyle" Value="{StaticResource Container_NodeStyle}"/>

This means that when you choose the StandardItem value for the HandyDefaultItemStyle property, the "Container_ItemStyle" style is applied to each item of the HandyContainer, and when you choose the "Node" value for the HandyDefaultItemStyle property, the "Container_NodeStyle" style is applied to each item.

Let's replace these two values by new ones:

XML
<Setter Property="StandardItemStyle" Value="{StaticResource Container_RowItemStyle}"/>
<Setter Property="NodeStyle" Value="{StaticResource Container_RowNodeStyle}"/>

This way, when we choose the StandardItem value for the HandyDefaultItemStyle, the new "Container_RowItemStyle" style will be applied to each item of the HandyContainer, and when we choose the "Node" value for the HandyDefaultItemStyle, the "Container_RowNodeStyle" style will be applied to each item.

We still need to create both styles.

Here is the code for them. You can copy and paste them in the generic.xaml file.

Be careful to paste them just before the GridBodyStyle style. As the GridBodyStyle makes references to Container_RowItemStyle and Container_RowNodeStyle, it is better to define the two item styles before the HandyContainer style.

XML
<Style x:Key="Container_RowItemStyle" TargetType="o:HandyListItem">
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Background" 
      Value="{StaticResource DefaultControlBackground}" />
    <Setter Property="Foreground" 
      Value="{StaticResource DefaultForeground}"/>
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Indentation" Value="10" />
    <Setter Property="IsTabStop" Value="True" />
    <Setter Property="IsKeyActivable" Value="True"/>
    <Setter Property="ItemUnpressDropDownBehavior" 
      Value="CloseAll" />
    <Setter Property="BorderBrush" 
      Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyListItem">
                <Grid Background="Transparent" x:Name="LayoutRoot">

                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Normal"/>
                            <vsm:VisualState x:Name="Disabled">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" 
                                       Storyboard.TargetName="ELEMENT_ContentPresenter" 
                                       Storyboard.TargetProperty="Opacity" 
                                       To="0.6"/>
                                    <DoubleAnimation Duration="0" 
                                       Storyboard.TargetName="SelectedVisual" 
                                       Storyboard.TargetProperty="Opacity" 
                                       To="0.6"/>
                                    <DoubleAnimation Duration="0" 
                                      Storyboard.TargetName="ReflectVisual" 
                                      Storyboard.TargetProperty="Opacity" 
                                      To="0"/>

                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="FocusStates">
                            <vsm:VisualState x:Name="NotFocused"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="FocusVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                      <DiscreteObjectKeyFrame KeyTime="0">
                                        <DiscreteObjectKeyFrame.Value>
                                          <Visibility>Visible</Visibility>
                                        </DiscreteObjectKeyFrame.Value>
                                      </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="MouseOverStates">
                            <vsm:VisualState x:Name="NotMouseOver"/>
                            <vsm:VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                          Storyboard.TargetName="MouseOverVisual" 
                                          Storyboard.TargetProperty="Visibility" 
                                          Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="PressedStates">
                            <vsm:VisualState x:Name="NotPressed"/>
                            <vsm:VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="PressedVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="SelectedStates">
                            <vsm:VisualState x:Name="NotSelected"/>
                            <vsm:VisualState x:Name="Selected">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="SelectedVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ReflectVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="AlternateStates">
                            <vsm:VisualState x:Name="NotIsAlternate"/>
                            <vsm:VisualState x:Name="IsAlternate">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName=
                                           "AlternateBackgroundVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="BackgroundVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="OrientationStates">
                            <vsm:VisualState x:Name="Horizontal"/>
                            <vsm:VisualState x:Name="Vertical"/>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                        </Grid.RowDefinitions>
                        <Border x:Name="BackgroundVisual" 
                           Background="{TemplateBinding Background}" 
                           Grid.RowSpan="2" />
                        <Border x:Name="AlternateBackgroundVisual" 
                           Background=
                             "{StaticResource DefaultAlternativeBackground}" 
                           Grid.RowSpan="2" 
                           Visibility="Collapsed"/>
                        <Rectangle x:Name="SelectedVisual" 
                           Fill="{StaticResource DefaultDownColor}" 
                           Grid.RowSpan="2" Visibility="Collapsed"/>
                        <Rectangle x:Name="MouseOverVisual" 
                           Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                           Grid.RowSpan="2" 
                           Margin="0,0,1,0" 
                           Visibility="Collapsed"/>
                        <Grid x:Name="PressedVisual" 
                                Visibility="Collapsed" 
                                Grid.RowSpan="2" >
                            <Grid.RowDefinitions>
                                <RowDefinition Height="1*"/>
                                <RowDefinition Height="1*"/>
                            </Grid.RowDefinitions>
                            <Rectangle 
                              Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                              Grid.Row="1" Margin="0,0,1,0" />
                        </Grid>
                        <Rectangle x:Name="ReflectVisual" 
                           Fill="{StaticResource DefaultReflectVertical}" 
                           Margin="1,1,1,0" Visibility="Collapsed"/>
                        <Rectangle x:Name="FocusVisual" Grid.RowSpan="2" 
                           Stroke="{StaticResource DefaultFocus}" 
                           StrokeDashCap="Round" 
                           Margin="0,1,1,0" StrokeDashArray=".2 2" 
                           Visibility="Collapsed"/>
                        <!-- Item content -->
                        <g:GContentPresenter
                                Grid.RowSpan="2" 
                                x:Name="ELEMENT_ContentPresenter"
                                Content="{TemplateBinding Content}"
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                OrientatedHorizontalAlignment=
                                  "{TemplateBinding HorizontalContentAlignment}"
                                OrientatedMargin="{TemplateBinding Padding}"
                                OrientatedVerticalAlignment=
                                  "{TemplateBinding VerticalContentAlignment}"  
                                PresenterOrientation=
                                  "{TemplateBinding PresenterOrientation}"/>
                        <Rectangle x:Name="BorderElement" Grid.RowSpan="2"
                                   Stroke="{TemplateBinding BorderBrush}" 
                                   StrokeThickness="{TemplateBinding BorderThickness}" 
                                   Margin="-1,0,0,-1"/>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="Container_RowNodeStyle" TargetType="o:HandyListItem">
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Foreground" 
       Value="{StaticResource DefaultForeground}"/>
    <Setter Property="Background" Value="White" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Indentation" Value="10" />
    <Setter Property="IsTabStop" Value="True" />
    <Setter Property="IsKeyActivable" Value="True"/>
    <Setter Property="ItemUnpressDropDownBehavior" Value="CloseAll" />
    <Setter Property="BorderBrush" 
      Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyListItem">
                <Grid x:Name="LayoutRoot" Background="Transparent">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Normal"/>
                            <vsm:VisualState x:Name="Disabled">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" 
                                       Storyboard.TargetName=
                                         "ELEMENT_ContentPresenter" 
                                       Storyboard.TargetProperty="Opacity" 
                                       To="0.6"/>
                                    <DoubleAnimation Duration="0" 
                                      Storyboard.TargetName="ExpandedVisual" 
                                      Storyboard.TargetProperty=
                                        "Opacity" To="0.6"/>
                                    <DoubleAnimation Duration="0" 
                                         Storyboard.TargetName="SelectedVisual" 
                                         Storyboard.TargetProperty="Opacity"
                                         To="0.6"/>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ExpandedReflectVisual"
                                         Storyboard.TargetProperty=
                                           "Visibility" Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="SelectedReflectVisual"
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <DoubleAnimation Duration="0" 
                                      Storyboard.TargetName="HasItem" 
                                      Storyboard.TargetProperty="Opacity" 
                                      To="0.6"/>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="FocusStates">
                            <vsm:VisualState x:Name="NotFocused"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="FocusVisual"
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="MouseOverStates">
                            <vsm:VisualState x:Name="NotMouseOver"/>
                            <vsm:VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="MouseOverVisual" 
                                         Storyboard.TargetProperty=
                                           "Visibility" Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ExpandedOverVisual" 
                                         Storyboard.TargetProperty="Visibility" 
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="PressedStates">
                            <vsm:VisualState x:Name="NotPressed"/>
                            <vsm:VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="PressedVisual" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="SelectedStates">
                            <vsm:VisualState x:Name="NotSelected"/>
                            <vsm:VisualState x:Name="Selected">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="SelectedVisual" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="HasItemsStates">
                            <vsm:VisualState x:Name="NotHasItems">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" 
                                         Storyboard.TargetName="ExpandedVisual" 
                                         Storyboard.TargetProperty="Opacity"
                                         To="0"/>

                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="HasItems">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="HasItem" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>

                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="IsExpandedStates">
                            <vsm:VisualState x:Name="NotIsExpanded"/>
                            <vsm:VisualState x:Name="IsExpanded">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="CheckedArrow" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ArrowUnchecked" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ExpandedVisual"
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="AlternateStates">
                            <vsm:VisualState x:Name="NotIsAlternate"/>
                            <vsm:VisualState x:Name="IsAlternate">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="AlternateBackgroundVisual"
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="BackgroundVisual"
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="InvertedStates">
                            <vsm:VisualState x:Name="InvertedItemsFlowDirection">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ArrowCheckedToTop" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                         Storyboard.TargetName="ArrowCheckedToBottom" 
                                         Storyboard.TargetProperty="Visibility"
                                         Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                          <DiscreteObjectKeyFrame.Value>
                                             <Visibility>Collapsed</Visibility>
                                          </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="NormalItemsFlowDirection"/>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Rectangle Width="{TemplateBinding FullIndentation}" />
                        <Grid MinWidth="16" Margin="0,0,1,0">
                            <Grid x:Name="HasItem" 
                                   Visibility="Collapsed" 
                                   Height="16" Width="16" 
                                   Margin="0,0,0,0">
                                <Path x:Name="ArrowUnchecked" 
                                   HorizontalAlignment="Right" Height="8" 
                                   Width="8" 
                                   Fill="{StaticResource DefaultForeground}" 
                                   Stretch="Fill" 
                                   Data="M 4 0 L 8 4 L 4 8 Z" />
                                <Grid x:Name="CheckedArrow" 
                                        Visibility="Collapsed">
                                    <Path x:Name="ArrowCheckedToTop" 
                                       HorizontalAlignment="Right" 
                                       Height="8" Width="8" 
                                       Fill="{StaticResource DefaultForeground}" 
                                       Stretch="Fill" 
                                       Data="M 8 4 L 0 4 L 4 0 z" 
                                       Visibility="Collapsed"/>
                                    <Path x:Name="ArrowCheckedToBottom" 
                                       HorizontalAlignment="Right" 
                                       Height="8" Width="8" 
                                       Fill="{StaticResource DefaultForeground}" 
                                       Stretch="Fill" 
                                       Data="M 0 4 L 8 4 L 4 8 Z" />
                                </Grid>
                                <ToggleButton x:Name="ELEMENT_ExpandButton" 
                                   Height="16" Width="16"  
                                   Style="{StaticResource EmptyToggleButtonStyle}" 
                                   IsChecked="{TemplateBinding IsExpanded}" 
                                   IsThreeState="False" IsTabStop="False"/>
                            </Grid>
                        </Grid>
                        <Grid>
                            <Border x:Name="BackgroundVisual" 
                                    Background="{TemplateBinding Background}" />
                            <Rectangle 
                              Fill="{StaticResource DefaultAlternativeBackground}" 
                              x:Name="AlternateBackgroundVisual" 
                              Visibility="Collapsed"/>
                            <Grid x:Name="ExpandedVisual" 
                                     Visibility="Collapsed">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle Fill="{StaticResource DefaultBackground}"
                                    Grid.RowSpan="2"/>
                                <Rectangle x:Name="ExpandedOverVisual" 
                                    Fill=
                                     "{StaticResource DefaultDarkGradientBottomVertical}" 
                                    Grid.RowSpan="2" 
                                    Visibility="Collapsed" 
                                    Margin="0,0,0,1"/>
                                <Rectangle x:Name="ExpandedReflectVisual" 
                                   Fill="{StaticResource DefaultReflectVertical}" 
                                   Margin="0,1,0,0"/>
                            </Grid>
                            <Grid x:Name="SelectedVisual" 
                                     Visibility="Collapsed" >
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle 
                                   Fill="{StaticResource DefaultDownColor}" 
                                   Grid.RowSpan="2"/>
                                <Rectangle x:Name="SelectedReflectVisual" 
                                           Fill="{StaticResource DefaultReflectVertical}" 
                                           Margin="0,1,1,0" 
                                           RadiusX="1" RadiusY="1"/>
                            </Grid>
                            <Rectangle x:Name="MouseOverVisual" 
                               Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                               Visibility="Collapsed" Margin="0,0,1,0"/>
                            <Grid x:Name="PressedVisual" 
                                     Visibility="Collapsed">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle Fill="{StaticResource DefaultDownColor}"
                                   Grid.RowSpan="2"/>
                                <Rectangle 
                                  Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                                  Grid.Row="1" Margin="0,0,1,0"/>
                                <Rectangle Fill="{StaticResource DefaultReflectVertical}" 
                                    Margin="0,1,1,0" 
                                    RadiusX="1" RadiusY="1"/>
                            </Grid>
                            <Rectangle HorizontalAlignment="Stretch" 
                               VerticalAlignment="Top" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="0.5" 
                               Height="1"/>
                            <Rectangle x:Name="FocusVisual" 
                              Stroke="{StaticResource DefaultFocus}" 
                              StrokeDashCap="Round" Margin="0,1,1,0" 
                              StrokeDashArray=".2 2" 
                              Visibility="Collapsed"/>
                            <g:GContentPresenter
                              x:Name="ELEMENT_ContentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              Cursor="{TemplateBinding Cursor}"
                              OrientatedHorizontalAlignment=
                                "{TemplateBinding HorizontalContentAlignment}"
                              OrientatedMargin="{TemplateBinding Padding}"
                              OrientatedVerticalAlignment=
                                "{TemplateBinding VerticalContentAlignment}" 
                              PresenterOrientation=
                                "{TemplateBinding PresenterOrientation}"/>
                            <Rectangle x:Name="BorderElement" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="{TemplateBinding BorderThickness}" 
                               Margin="-1,0,0,-1"/>
                        </Grid>
                    </StackPanel>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In order to avoid a boring editing process during this tutorial, we provide you the final Container_RowItemStyle and the Container_RowNodeStyle styles. This way, you can just make a copy/paste of them in the generic.xaml.

The Container_RowItemStyle and the Container_RowNodeStyle styles have been created by copying Container_ItemStyle and Container_NodeStyle and by adjusting a few elements in order that they look more like a grid row:

  • We have set the value of the padding and the margin properties to 0 in order that there is no space displayed between the items (i.e., the rows) of the grid or between the border of the items and the cells they contain.
  • We have also set the HorizontalAlignement value to Left.

Thanks to these changes, we can now remove the DefaultItemModel that was associated to the GridBody of the GridBody project at the beginning of this tutorial.

XML
<o:HandyContainer
            x:Name="MyGridBody"
            VirtualMode="On"
            AlternateType="Items"
            HandyDefaultItemStyle="Node"
            HandyStyle="GridBodyStyle">
    <o:HandyContainer.ItemTemplate>
        <g:ItemDataTemplate>
            <Grid>
                <o:HandyDataPresenter DataType="GridBody.Person">
                    <g:GDockPanel>
                        <g:GStackPanel Orientation="Horizontal" 
                                g:GDockPanel.Dock="Top">
                            <o:TextCell Text="{Binding FirstName}" 
                                x:Name="FirstName"/>
                            <o:TextCell Text="{Binding LastName}" 
                                x:Name="LastName"/>
                            <o:TextCell Text="{Binding Address}" 
                                x:Name="Address"/>
                            <o:TextCell Text="{Binding City}" 
                                x:Name="City"/>
                            <o:TextCell Text="{Binding ZipCode}" 
                                x:Name="ZipCode"/>
                            <o:CheckBoxCell 
                              IsChecked="{Binding IsCustomer}" 
                              x:Name="IsCustomer"/>
                        </g:GStackPanel>
                        <o:TextCell Text="{Binding Comment}" 
                          g:GDockPanel.Dock="Fill" x:Name="Comment" 
                          Width="Auto"/>
                    </g:GDockPanel>
                </o:HandyDataPresenter>
                <o:HandyDataPresenter DataType="GridBody.Country">
                    <g:GStackPanel Orientation="Horizontal">
                        <o:TextCell Text="{Binding Name}" 
                          x:Name="CountryName"/>
                        <o:TextCell Text="{Binding Children.Count}"  
                          x:Name="ChildrenCount"/>
                    </g:GStackPanel>
                </o:HandyDataPresenter>
            </Grid>
        </g:ItemDataTemplate>
    </o:HandyContainer.ItemTemplate>
</o:HandyContainer>

Furthermore, we have added to the styles a Rectangle named BorderElement, and that will display the border of the items. We have also applied some small changes in order that the elements are well positioned inside the items.

We will not spend our time to explain each element and property of these styles. These styles are large, but there is nothing extraordinary with them. Nevertheless, if you have time, you can read them carefully, or better, try to modify them in order to deeply understand how they work.

Both styles are built the same way. The main difference between the two is that the Container_RowNodeStyle style can handle nodes:

  • The content of a node child is indented.
  • If a node has children, an arrow is displayed in front of it, allowing expanding or collapsing the node.

If we start our application again, we can see that borders are displayed around the items of the grid (i.e., the rows) and that our grid looks nicer.

Missing line

However, there is a line missing between the two rows of the cells displayed inside the person items:

Missing Line

This is logical. In our styles, we have built the right border for the cells and the borders for the items (i.e., the rows), but we have to add the border between the rows of the cells ourselves.

If we look at the ItemDataTemplate below, we will see that we have added a rectangle just before the TextCell displaying the comment. This rectangle is used to display the separator line.

Do not forget to apply the same change to your ItemDataTemplate.

XML
<g:ItemDataTemplate>
    <Grid>
        <o:HandyDataPresenter DataType="GridBody.Person">
            <g:GDockPanel>
                <g:GStackPanel Orientation="Horizontal" 
                            g:GDockPanel.Dock="Top">
                    <o:TextCell Text="{Binding FirstName}" 
                            x:Name="FirstName"/>
                    <o:TextCell Text="{Binding LastName}" 
                            x:Name="LastName"/>
                    <o:TextCell Text="{Binding Address}" 
                            x:Name="Address"/>
                    <o:TextCell Text="{Binding City}" 
                            x:Name="City"/>
                    <o:TextCell Text="{Binding ZipCode}" 
                            x:Name="ZipCode"/>
                    <o:CheckBoxCell IsChecked="{Binding IsCustomer}" 
                            x:Name="IsCustomer"/>
                </g:GStackPanel>
                <Rectangle Height="1" 
                  Stroke="{StaticResource DefaultListControlStroke}" 
                  StrokeThickness="0.5" Margin="-1,0,0,-1" 
                  g:GDockPanel.Dock="Top"/>
                <o:TextCell Text="{Binding Comment}" 
                  g:GDockPanel.Dock="Fill" x:Name="Comment" 
                  Width="Auto"/>
            </g:GDockPanel>
        </o:HandyDataPresenter>
        <o:HandyDataPresenter DataType="GridBody.Country">
            <g:GStackPanel Orientation="Horizontal">
                <o:TextCell Text="{Binding Name}" 
                   x:Name="CountryName"/>
                <o:TextCell Text="{Binding Children.Count}" 
                   x:Name="ChildrenCount"/>
            </g:GStackPanel>
        </o:HandyDataPresenter>
    </Grid>
</g:ItemDataTemplate>

If we try to start our application now, it will not work. The stroke value of the rectangle we have just added is linked to a DefaultListControlStroke static resource that we have not defined yet.

The styles defined in the generic.xaml files of GoaOpen reference brushes and colors that are defined at the top of the file. This way, changing the default colors of the styles is easy: we just have to change the brushes and colors defined at the top of the file.

If we look at the top of the file, we will see that there are a lot of other predefined brushes and colors: such as "Background:Beige, StandardColor: Brown, ActionColor: Green" or "All Grey". In order to use these predefined brushes and colors instead of the default ones, you have to comment the default predefined brushes and colors and uncomment the ones you would like to use.

In order that our separator rectangle looks nice, we have applied the DefaultListControlStroke resource value to its stroke. Nevertheless, as the DefaultListControlStroke is defined in the generic.xaml file of the GoaOpen project, it is not accessible from our GridBody project. We have to make a copy of it.

Let's add it to the App.xaml file of our GridBody project.

XML
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="GridBody.App"
             >
    <Application.Resources>
        <SolidColorBrush x:Key="DefaultListControlStroke" 
               Color="#FF99B0BB" />
    </Application.Resources>
</Application>

7. Cell navigation

Introduction

We now have a grid body that looks well.

We have cells inside the items (i.e., the rows) of the grid body. We can navigate using the keyboard between the items of the grid, and we can navigate between the cells of an item by clicking on them.

The main missing feature that we must implement before finishing the first part of this tutorial is the ability to navigate from cell to cell using standard navigation keys such as the right and left arrows and the Home or End keys.

Keyboard navigation between cells inside an item

SpatialNavigator

In order to be able to navigate between the cells inside an item, we can use the SpatialNavigator. The SpatialNavigator is a class that can manage the key navigation between the children of a panel. By "connecting" the SpatialNavigator to a panel, we automatically allow the user to navigate between the children of the panel using its keyboard (arrow keys, Home and End keys).

When moving the focus from one child to another, the SpatialNavigator takes into account the location of the children, and moves the focus to the nearest element in the direction represented by the key pressed by the user. For instance, if the user presses the "down arrow" key, the SpatialNavigator will find the closest element below the currently focused element, and will move the focus to it.

Let's add SpatialNavigators to the panels that are used inside the ItemTemplate of the GridBody of the GridBody project:

XML
<g:ItemDataTemplate>
    <Grid>
        <o:HandyDataPresenter DataType="GridBody.Person">
            <g:GDockPanel>
                <g:GDockPanel.KeyNavigator>
                    <g:SpatialNavigator/>
                </g:GDockPanel.KeyNavigator>
                <g:GStackPanel Orientation="Horizontal" 
                           g:GDockPanel.Dock="Top">
                    <g:GStackPanel.KeyNavigator>
                        <g:SpatialNavigator/>
                    </g:GStackPanel.KeyNavigator>
                    <o:TextCell Text="{Binding FirstName}" 
                       x:Name="FirstName"/>
                    <o:TextCell Text="{Binding LastName}" 
                       x:Name="LastName"/>
                    <o:TextCell Text="{Binding Address}" 
                       x:Name="Address"/>
                    <o:TextCell Text="{Binding City}" 
                       x:Name="City"/>
                    <o:TextCell Text="{Binding ZipCode}" 
                       x:Name="ZipCode"/>
                    <o:CheckBoxCell IsChecked="{Binding IsCustomer}" 
                       x:Name="IsCustomer"/>
                </g:GStackPanel>
                <Rectangle Height="1" 
                   Stroke="{StaticResource DefaultListControlStroke}" 
                   StrokeThickness="0.5" Margin="-1,0,0,-1" 
                   g:GDockPanel.Dock="Top"/>
                <o:TextCell Text="{Binding Comment}" 
                   g:GDockPanel.Dock="Fill" x:Name="Comment" 
                   Width="Auto"/>
            </g:GDockPanel>
        </o:HandyDataPresenter>
        <o:HandyDataPresenter DataType="GridBody.Country">
            <g:GStackPanel Orientation="Horizontal">
                <g:GStackPanel.KeyNavigator>
                    <g:SpatialNavigator/>
                </g:GStackPanel.KeyNavigator>
                <o:TextCell Text="{Binding Name}" 
                   x:Name="CountryName"/>
                <o:TextCell Text="{Binding Children.Count}" 
                   x:Name="ChildrenCount"/>
            </g:GStackPanel>
        </o:HandyDataPresenter>
    </Grid>
</g:ItemDataTemplate>

If we start our application now, we are able to navigate between the cells of an item using the keyboard. We can verify this by performing the following actions:

  • Click on a cell in order that it becomes the current cell (it gets the focus).
  • Use the right arrow, the left arrow, the Home key, or the End key to navigate between the cells.

Nevertheless, there is nothing to ensure that when we move the current cell from cell to cell, it keeps visible. We can verify this by performing the following actions:

  • Start the application.
  • Resize the grid in order that the last cells (of the rows) are not visible.
  • Click on the first cell of a row in order that it becomes the current cell (it gets the focus).
  • Use the end key to navigate to the last cell.

The last cell becomes the current cell, but the HandyContainer does not scroll on the right in order to make it visible. In order to correct this problem, we are going to add the EnsureCellIsVisible method to the HandyContainer, and the GetPosition method to the Cell class.

GetPosition

The GetPosition static method is used to know the position of the cell in comparison to another UIElement.

Let's add this method to the code of the Cell class:

C#
public static Point GetPosition(Cell cell, UIElement element)
{
    Point result = new Point();

    MatrixTransform transform = null;
    try
    {
        transform = cell.TransformToVisual(element) as MatrixTransform;
    }
    catch
    {
    }

    result.X = transform.Matrix.OffsetX;
    result.Y = transform.Matrix.OffsetY;

    return result;
}

EnsureCellIsVisible

The purpose of the EnsureCellsVisible method is to modify the HorizontalOffset value of the HandyContainer in order to make a cell visible.

In the EnsureCellIsVisible method, we first call the Cell.GetPosition method in order to have the position of the cell inside the ItemsHost (the ItemsHost is the panel that is inside the HandyContainer and that contains the items of the HandyContainer).

If the position of the Left of the cell is to the left of the "left border" of the ItemsHost, we change the HorizontalOffset in order that the left of the cell is exactly at the "left border" of the ItemsHost. If the position of the Right of the cell is to the right of the "right border" of the ItemsHost, we change the HorizontalOffset in order that the right of the cell is exactly at the "right border" of the ItemsHost.

EnsureCellIsVisible and HorizontalOffset

Let's add this method to our HandyContainer partial class:

C#
public void EnsureCellIsVisible(Cell cell)
{
    GStackPanel itemsHost = (GStackPanel)this.ItemsHost;
    Point cellPosition = Cell.GetPosition(cell, itemsHost);
    if (cellPosition.X < 0)
        this.HorizontalOffset += cellPosition.X;
    else if ((cellPosition.X + cell.ActualWidth > itemsHost.ViewportWidth) && 
             (cell.ActualWidth <= this.ViewportWidth))
        this.HorizontalOffset += cellPosition.X + cell.ActualWidth - this.ViewportWidth;
}

Call EnsureCellIsVisible

We need now to call EnsureCellIsVisible when a cell becomes the current cell, i.e., when it gets the focus. Let's modify the code of the OnGotFocus method of the Cell class, as follows:

C#
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);

    if (!isFocused)
    {
        VisualStateManager.GoToState(this, "Focused", true);

        isFocused = true;
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            parentContainer.CurrentCellName = this.Name;
            parentContainer.EnsureCellIsVisible(this);
        }
    }
}

We can now restart our application and ensure that the current cell keeps visible.

  • Start the application.
  • Resize the grid in order that the last cells (of the rows) are not visible.
  • Click on the first cell of a row in order that it becomes the current cell (it gets the focus).
  • Use the End key to navigate to the last cell.

This time. the horizontal offset of the HandyContainer is automatically modified in order that the current cell keeps visible.

Keyboard navigation between the cells of two items

Keeping cells in the same "column"

When moving from one item to another item using the up or down arrow key. or using the PageUp or PageDown key, we would like that the current cell of the source row becomes the current cell of the target row.

For instance, at this time, if the current cell is Address6 and I press the up arrow, the focus is moved to the item that is displayed above the current item. but no cell is focused anymore. In this case, we would like that Address5 becomes the current cell.

Keep Current Cell in Same Column

The first thing to do to be able to manage this case is to be able to tell an item (i.e., a row) which one of its cells must become the current cell. Let's extend the ContainerItem class in order to be able to manage the cells it contains.

Extend the ContainerItem class

The ContainerItem is the type that is used when creating the items of the HandyContainer. We will add a feature to this class the same way we have added features to the HandyContainer class.

First, let's create a ContainerItem partial class in the Extensions\Grid folder of the GoaOpen project.

ContainerItem Partial Class

C#
using System;
using System.Windows;
using System.Windows.Input;
using System.Collections.Generic;
using System.Windows.Media;
using System.Windows.Controls;

namespace Open.Windows.Controls
{
    public partial class ContainerItem : HandyListItem
    {
    }
}

Let's add the partial keyword to the ContainerItem class that already exists in GoaOpen (it is located in the GoaControls\HandyList\HandyList\HandyContainer folder):

C#
using System;
using System.Windows;

namespace Open.Windows.Controls
{
    /// <summary>
    /// Item to use inside a HandyContainer control. 
    /// </summary>
    public partial class ContainerItem : HandyListItem
    {
        public static readonly DependencyProperty HandyStyleProperty;
        public static readonly DependencyProperty HandyOverflowedStyleProperty;

        . . .

Let's add a FocusCell method to the ContainerItem partial class that we have just added. This method will accept the name of a cell as parameter. It will find the cell that has the name of the parameter (if any), and will set the focus on it.

C#
public bool FocusCell(string cellName)
{
    object focusedElement = FocusManager.GetFocusedElement();
    FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
    if (firstChild != null)
    {
        Cell cell = firstChild.FindName(cellName) as Cell;
        if (cell != null)
        {
            cell.Focus();
        }
    }

    return false;
}

private DependencyObject GetFirstTreeChild()
{
    ContentPresenter presenter = this.ContentPresenter;
    if (presenter != null)
    {
        if (VisualTreeHelper.GetChildrenCount(presenter) > 0)
            return VisualTreeHelper.GetChild(presenter, 0);
    }

    return null;

}

GridSpatialNavigator

When the user presses the Up, Down arrow key, or the Page Up or the Page Down key, the focus is moved from item to item. This is possible because a SpatialNavigator is linked to the ItemsHost of the HandyContainer. The ItemsHost is the panel that is inside the HandyContainer and that contains the items of the HandyContainer.

We are going to enhance the SpatialNavigator that is linked to the ItemHost in order that it works as we would like.

Let's first create a GridSpatialNavigator that inherits from the SpatialNavigator in the Extensions\Grid folder.

C#
namespace Open.Windows.Controls
{
    public class GridSpatialNavigator : SpatialNavigator
    {
    }
}

Let's modify the GridBodyStyle of the HandyContainer in order that our GridSpatialNavigator is used instead of the standard SpatialNavigator. The description of the ItemsHost that the HandyContainer must use is defined in the ItemsPanelModel property of the HandyContainer. By default, this property contains the following value:

XML
<g:GStackPanelModel>
    <g:GStackPanelModel.ChildrenAnimator>
        <g:TweenChildrenAnimator Duration="00:00:0.1" 
                      TransitionType="Linear" />
    </g:GStackPanelModel.ChildrenAnimator>
    <g:GStackPanelModel.KeyNavigator>
        <o:SpatialNavigator/>
    </g:GStackPanelModel.KeyNavigator>
</g:GStackPanelModel>

This means that, by default, the ItemsHost is a GStackPanel, and that:

  • A TweenChildrenAnimator is used to manage the animation of the items.
  • A SpatialNavigator is used to manage the key navigation between the items.

We would like that our GridSpatialNavigator is used instead of the standard SpatialNavigator. We do not want to change anything else at this time.

Let's modify the GridBodyStyle that we have created at the end of the generic.xaml file.

Just before the line defining the Template property of the GridBody (<Setter Property="Template">), let's add a new setter that describes the ItemsHost to use:

XML
<Setter Property="ItemsPanelModel">
    <Setter.Value>
        <g:GStackPanelModel>
            <g:GStackPanelModel.ChildrenAnimator>
                <g:TweenChildrenAnimator Duration="00:00:0.1" 
                             TransitionType="Linear" />
            </g:GStackPanelModel.ChildrenAnimator>
            <g:GStackPanelModel.KeyNavigator>
                <o:GridSpatialNavigator/>
            </g:GStackPanelModel.KeyNavigator>
        </g:GStackPanelModel>
    </Setter.Value>
</Setter>

If we look at the GridBodyStyle style, we will also see this property:

XML
<Setter Property="HandyItemsPanelModel" Value="StandardPanel" />

The HandyItemsPanelModel is an enum property. It allows to choose the panel that must be used as the ItemsHost. It works the same way as the HandyStyle property.

If we do not change the value of the HandyItemsPanelModel property, the value we have set in the ItemsPanelProperty will not be taken into account, and the value of the HandyItemPanelModel property will be used instead. Of course, we could have enhanced the HandyItemsPanelModel property in order that it allows us to select our new ItemsPanelModel, but this is not the purpose of this tutorial.

Let's just set the HandyItemsPanelModel property to the "None" value. This way, it will not interfere with the ItemsPanelModel property:

XML
<Setter Property="HandyItemsPanelModel" Value="None" />

Let's come back to our GridSpatialNavigator and add some code to it:

C#
using System.Windows.Input;
using Netika.Windows.Controls;

namespace Open.Windows.Controls
{
    public class GridSpatialNavigator : SpatialNavigator
    {
        public Key LastKeyProcessed
        {
            get;
            internal set;
        }

        public ModifierKeys LastModifier
        {
            get;
            internal set;
        }

        public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
        {
            LastKeyProcessed = e.Key;
            LastModifier = Keyboard.Modifiers;

            base.ActiveKeyDown(container, e);

        }

        public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
        {
            LastKeyProcessed = e.Key;
            LastModifier = Keyboard.Modifiers;
            base.KeyDown(container, e);
        }


        protected override Model GetNakedClone()
        {
            return new GridSpatialNavigator();
        }
    }
}

The ActiveKeyDown and the KeyDown methods are the methods that are called on the SpatialNavigator when the user presses a keyboard key.

We have modified these methods and put the Key value in the LastKeyProcessed property and the Modifiers value in the LastModifier property. This way, we will know which key was processes by the GridSpatialNavigator and will be able to take actions to modify its default behavior.

The GetNakedClone method is used by the GOA Toolkit to be able to create a clone of the SpatialNavigator when needed.

We will know that the GridSpatialNavigator has processed a key and moved the focus to another item when the OnNavigatorSetKeyboardFocus method of our HandyContainer will be called. This is where we will take actions in order that the right cell has the focus.

But before doing this, to be sure we fully understand what we are doing, let's recall the different steps of the process when a user presses a key:

KeyDown process

In our HandyContainer partial class, let's modify the OnNavigatorSetKeyboardFocus method:

C#
protected override void OnNavigatorSetKeyboardFocus(UIElement item)
{
    base.OnNavigatorSetKeyboardFocus(item);

    GridSpatialNavigator gridSpatialNavigator = GetGridSpatialNavigator();
    if (gridSpatialNavigator != null)
    {
        if ((gridSpatialNavigator.LastKeyProcessed == Key.Down) ||
             (gridSpatialNavigator.LastKeyProcessed == Key.Up) ||
             (gridSpatialNavigator.LastKeyProcessed == Key.PageDown) ||
             (gridSpatialNavigator.LastKeyProcessed == Key.PageUp))
        {
            if (item != null)
            {
                if (!String.IsNullOrEmpty(CurrentCellName))
                {
                    ContainerItem newItem = (ContainerItem)item;
                    newItem.FocusCell(CurrentCellName);
                }
            }
        }
    }
}

private GridSpatialNavigator GetGridSpatialNavigator()
{
    GPanel gPanel = this.ItemsHost as GPanel;
    if (gPanel != null)
        return gPanel.KeyNavigator as GridSpatialNavigator;

    return null;
}

If the GridSpatialNavigator has processed the Down, Up, Page Down, or the Page Up keys, we force the cell having the CurrentCellName name to have the focus.

Let's test our changes:

  • Start the application.
  • Click on the Address8 cell.
  • Press the up arrow key.

The Address7 cell becomes the current cell (i.e., the cell that has the focus). This is the behavior we expected.

Ctrl-Home and Ctrl-End keys

Introduction

Let's start our application and analyze what happens when we press the Ctrl-Home and the Ctrl-End keys.

  • Start the application.
  • Click on the Address8 cell.
  • Press the Ctrl-Home key.

FirstName8 becomes the current cell. If we had pressed the Ctrl-End key, the cell at the end of the current row would have become the current cell. This behavior is easily understandable: the SpatialNavigators that we have defined in our ItemTemplate process the key pressed by the user and change the focus of the cells. Nevertheless, we would like that when the user presses the Ctrl-Home key, the first cell of the first row of the grid becomes the current cell - not the first cell of the current row. In the same way, we would like that when the user presses the Ctrl-End key, the last cell of the last row of the grid becomes the current cell - not the last cell of the current row.

Extend the ContainerItem

Our requirements are "when the user press the Ctrl-Home key, the first cell of the first row of the grid becomes the current cell" and "when the user presses the Ctrl-End key, the last cell of the last row of the grid becomes the current cell".

But, what are the first cell and the last cell? Remember that the location of the cells is set using panels inside the ItemTemplate of the HandyContainer. It means that cells are not necessarily located on one line, side by side.

We will postulate that the first cell of a row is the cell that is the closest to the top left corner of the row (i.e., the item), and that the last cell is the one that is close to the bottom right corner of the row.

Let's enhance our ContainerItem partial class and write methods to find the first cell and the last cell of an item (i.e., a row).

C#
private class CellPosition
{
    public CellPosition(Cell cell, Point position)
    {
        Cell = cell;
        Position = position;
    }

    public Cell Cell
    {
        get;
        private set;
    }

    public Point Position
    {
        get;
        private set;
    }
}

private class CellPositionComparer : IComparer<CellPosition>
{

    public int Compare(CellPosition x, CellPosition y)
    {
        if (x.Position.Y > y.Position.Y)
            return 1;
        else if (x.Position.Y < y.Position.Y)
            return -1;

        if (x.Position.X > y.Position.X)
            return 1;
        else if (x.Position.X < y.Position.X)
            return -1;

        return 0;
    }

}

public string GetFirstCellName()
{
    this.UpdateLayout();

    List<CellPosition> cellPositions = new List<CellPosition>();
    List<Cell> cells = GetCells();
    UIElement rootVisual = Application.Current.RootVisual;
    foreach (Cell cell in cells)
    {
        cellPositions.Add(new CellPosition(cell, Cell.GetPosition(cell, rootVisual)));
    }

    cellPositions.Sort(new CellPositionComparer());

    foreach (CellPosition cellPosition in cellPositions)
    {
        if (cellPosition.Cell.IsTabStop)
            return cellPosition.Cell.Name;
    }

    return null;

}

public string GetLastCellName()
{
    this.UpdateLayout();

    List<CellPosition> cellPositions = new List<CellPosition>();
    List<Cell> cells = GetCells();
    UIElement rootVisual = Application.Current.RootVisual;
    foreach (Cell cell in cells)
    {
        cellPositions.Add(new CellPosition(cell, Cell.GetPosition(cell, rootVisual)));
    }

    cellPositions.Sort(new CellPositionComparer());

    for (int cellIndex = cellPositions.Count - 1; cellIndex >= 0; cellIndex--)
    {
        CellPosition cellPosition = cellPositions[cellIndex];
        if (cellPosition.Cell.IsTabStop)
            return cellPosition.Cell.Name;
    }

    return null;

}

List<Cell> cellCollection;
private List<Cell> GetCells()
{
    if (cellCollection == null)
    {
        cellCollection = new List<Cell>();
        DependencyObject firstChild = GetFirstTreeChild();
        if (firstChild != null)
            AddChildrenCells(firstChild, cellCollection);
    }

    return cellCollection;
}

private void AddChildrenCells(DependencyObject parent, List<Cell> cellsCollection)
{
    int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
    for (int index = 0; index < childrenCount; index++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(parent, index);
        Cell childCell = child as Cell;
        if (childCell != null)
            cellsCollection.Add(childCell);
        else
            AddChildrenCells(child, cellsCollection);
    }
}

GetFirstCellName first retrieves all the available cells by calling the GetCells method. It then gets the position of all the cells and sorts them using the CellPositionCompare comparer. Then, it returns the first cell that can have the focus (IsTapStop == true). GetLastCellName works the same way. The GetCells method scans the VisualTree to find all the cells that are children of the ContainerItem. In order to avoid scanning the VisualTree each time the GetCells method is called, the result of the scan is cached in the cellCollection collection.

Nevertheless, we must take into account that if a new template is applied to the ContainerItem, cellCollection will not be up-to-date anymore. Therefore, we must override the OnApplyTemplate method and clear the collection cache:

C#
public override void OnApplyTemplate()
{
    cellCollection = null;
    
    base.OnApplyTemplate();
}

If we try to compile the project now, we will face the following error:

Type 'Open.Windows.Controls.ContainerItem' already defines 
  a member called 'OnApplyTemplate' with the same parameter types.

This is because the OnApplyTemplate method is already defined in the "other" ContainerItem partial class of GoaOpen.

Let's resolve this conflict by renaming and rewriting the OnApplyTemplate method of the original ContainerItem class:

C#
private void _OnApplyTemplate()
{
    if ((this.Style == null) && 
               (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
         throw new NotSupportedException("ContainerItem style is null. " + 
               "Please apply a style to the item either using the DefaultItemStyle " + 
               "or the HandyDefaultItemStyle of its container. A frequent mistake " + 
               "is to use a ContainerItem inside a HandyNavigator or a HandyCommand.");
}

Let's call the _OnApplyTemplate method from the OnApplyTemplate method of our own ContainerItem partial class:

C#
public override void OnApplyTemplate()
{
    cellCollection = null;

    _OnApplyTemplate();
    base.OnApplyTemplate();
}

Enhance the GridSpatialNavigator

Now that we have methods that allow us to find the first and the last cell of a ContainerItem, we can enhance our GridSpatialNavigator in order that it takes care of the Ctrl-Home and Ctrl-End keys.

Let's first modify the KeyDown and ActiveKeyDown methods in order that if the user presses the Ctrl-Home or the Ctrl-End key, the default behavior of the navigator is not processed any more.

C#
public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if (((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
    {
        base.ActiveKeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if (((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
    {
        base.KeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

The way we have changed the ActiveKeyDown and KeyDown method, the ProcessKey method is called when the user presses the Ctrl-Home or the Ctrl-End key.

Let's write the ProcessKey method:

C#
private void ProcessKey(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    GStackPanel gStackPanel = (GStackPanel)container;
    HandyContainer parentContainer = HandyContainer.GetParentContainer(gStackPanel);

    if (gStackPanel.Children.Count > 0)
    {
        if ((e.Key == Key.Home) && 
           ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control))
        {
            gStackPanel.MoveToFirstIndex();

            ContainerItem firstItem = (ContainerItem)gStackPanel.Children[0];

            parentContainer.CurrentCellName = firstItem.GetFirstCellName();
            if (firstItem.FocusCell(parentContainer.CurrentCellName))
                e.Handled = true;
            
        }
        else if ((e.Key == Key.End) && 
                ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control))
        {
            gStackPanel.MoveToLastIndex();

            ContainerItem lastContainerItem = 
               (ContainerItem)gStackPanel.Children[gStackPanel.Children.Count - 1];

            parentContainer.CurrentCellName = lastContainerItem.GetLastCellName();
            if (lastContainerItem.FocusCell(parentContainer.CurrentCellName))
                e.Handled = true;
        }                
    }
}

The container parameter of the ProcessKey method contains the panel to which the GridSpatialNavigator is linked to. In our case, this panel is the ItemsHost of our HandyContainer and the panel is a GStackPanel.

In the ProcessKey method, the first thing to do is to move to the first item (or the last item) of the ItemsHost. This is what we do when we call the gStackPanel.MoveToFirstIndex() (or the gStackPanel.MoveToLastIndex()) method.

Then, we have to find the first cell (or the last cell) and put the focus on it to make it the current cell. We must not forget to update the CurrentCellName property value of the HandyContainer at the same time.

RowSpatialNavigator

If we start our application now and try to navigate to the first cell of the first row by pressing the Ctrl-Home key, it does not work. The same problem occurs if we try to navigate to the last cell of the last row by pressing the Ctrl-End key. This is because the SpatialNavigators that we have defined in our ItemTemplate are still processing the key pressed by the use. We have to replace these SpatialNavigators by SpatialNavigators of our own that do not process the Ctrl-Home and the Crl-End keys.

Let's create a new RowSpatialNavigator class in the Extensions\Grid folder of the GoaOpen project, and modify the ActiveKeyDown and KeyDown methods in order that the Ctrl-Home and Ctrl-End keys are not processed anymore.

C#
using System.Windows.Input;
using Netika.Windows.Controls;

namespace Open.Windows.Controls
{
    public class RowSpatialNavigator : SpatialNavigator
    {
        public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
        {
            if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
                ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
                base.ActiveKeyDown(container, e);
        }

        public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
        {
            if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
                ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
                base.KeyDown(container, e);

        }

        protected override Model GetNakedClone()
        {
            return new RowSpatialNavigator();
        }
    }
}

Let's now replace the SpatialNavigators that we have defined in our ItemTemplate of our GridBody with the RowSpatialNavigator:

XML
<o:HandyContainer.ItemTemplate>
    <g:ItemDataTemplate>
        <Grid>
            <o:HandyDataPresenter DataType="GridBody.Person">
                <g:GDockPanel>
                    <g:GDockPanel.KeyNavigator>
                        <o:RowSpatialNavigator/>
                    </g:GDockPanel.KeyNavigator>
                    <g:GStackPanel Orientation="Horizontal" 
                           g:GDockPanel.Dock="Top">
                        <g:GStackPanel.KeyNavigator>
                            <o:RowSpatialNavigator/>
                        </g:GStackPanel.KeyNavigator>
                        <o:TextCell Text="{Binding FirstName}" 
                           x:Name="FirstName"/>
                        <o:TextCell Text="{Binding LastName}" 
                           x:Name="LastName"/>
                        <o:TextCell Text="{Binding Address}" 
                           x:Name="Address"/>
                        <o:TextCell Text="{Binding City}" 
                           x:Name="City"/>
                        <o:TextCell Text="{Binding ZipCode}" 
                           x:Name="ZipCode"/>
                        <o:CheckBoxCell 
                           IsChecked="{Binding IsCustomer}" 
                           x:Name="IsCustomer"/>
                    </g:GStackPanel>
                    <Rectangle Height="1" 
                       Stroke="{StaticResource DefaultListControlStroke}" 
                       StrokeThickness="0.5" Margin="-1,0,0,-1" 
                       g:GDockPanel.Dock="Top"/>
                    <o:TextCell Text="{Binding Comment}" 
                       g:GDockPanel.Dock="Fill" x:Name="Comment" 
                       Width="Auto"/>
                </g:GDockPanel>
            </o:HandyDataPresenter>
            <o:HandyDataPresenter DataType="GridBody.Country">
                <g:GStackPanel Orientation="Horizontal">
                    <g:GStackPanel.KeyNavigator>
                        <o:RowSpatialNavigator/>
                    </g:GStackPanel.KeyNavigator>
                    <o:TextCell Text="{Binding Name}" 
                       x:Name="CountryName"/>
                    <o:TextCell Text="{Binding Children.Count}" 
                       x:Name="ChildrenCount"/>
                </g:GStackPanel>
            </o:HandyDataPresenter>
        </Grid>
    </g:ItemDataTemplate>
</o:HandyContainer.ItemTemplate>

_OnNavigatorSetKeyboardFocus

We are almost done but, not completely yet.

Let's try our changes:

  • Start the application.
  • Set the Address 8 cell as the current cell by clicking on it.
  • Press the Ctrl-Home key.

The first cell of the first item becomes the current cell, as expected. Nevertheless, the selection does not follow our change. The item holding Address 8 is still selected. We can see it because its background remains orange.

Current Cell VS Selected Item

The current value of the SelectionMode property of the HandyContainer is set to "Single". It means that only one item can be selected at a time and that the selection "follows" the focus.

When we press the other navigation keys, the selection "follows" the focus. For instance, if we click on Address8, then on the Address7 cell and then on the Address6 cell, those cells gets the focus, and the items that contain those cells become the selected item: their backgrounds become orange. The same happens if we press the up or the down arrow key.

However, when we press the Ctrl-Home or the Ctrl-End key, the selection does not follow the focused cell. This is because, in the GridSpatialNavigator, we have substituted our own code to the standard SpatialNavigator code. In our code, in the ProcessKey method, we have forgotten to "tell" the HandyNavigator that we have changed the current item. This can be done by calling the "OnNavigatorSetKeyboardFocus" method of the HandyContainer.

Nevertheless, we will not modify the ProcessKey method of the GridSpatialNavigator to call this method, but we will make the change in the FocusCell method of the ContainerItem method.

This way, we will not have to take care of OnNavigatorSetKeyboardFocus anymore. The FocusCell method will call it when necessary.

As the OnNavigatorSetKeyboardFocus method is a protected method, let's first add an internal _OnNavigatorSetKeyboardFocus method to our HandyContainer partial class:

C#
internal void _OnNavigatorSetKeyboardFocus(UIElement item)
{
    this.OnNavigatorSetKeyboardFocus(item);
}

Then, let's modify the FocusCell method of our ContainerItem partial class:

C#
public bool FocusCell(string cellName)
{
    object focusedElement = FocusManager.GetFocusedElement();
    FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
    if (firstChild != null)
    {
        Cell cell = firstChild.FindName(cellName) as Cell;
        if (cell != null)
        {
            if (cell.Focus())
            {
                if (!TreeHelper.IsChildOf(this, focusedElement as DependencyObject))
                {
                    HandyContainer parentContainer = 
                            HandyContainer.GetParentContainer(this);
                    if (parentContainer != null)
                        parentContainer._OnNavigatorSetKeyboardFocus(this);
                }
                return true;
            }
        }
    }
    return false;
}

Let's try our application once more.

Now, everything is fine when we press the Ctrl-Home or the Ctrl-End key.

Tab key

Let's try to use the tab key inside our grid:

  • Start the application.
  • Click the Address8 cell to make it the current cell.
  • Press the Tab key.

The City8 cell becomes the current cell. This is the behavior we expected.

  • Press the Shift-Tab key.

The Address8 cell becomes the current cell again. This is also the behavior we expected.

  • Now, click the Comment8 cell to make it the current cell.
  • Press the Tab key.

The focus is moved to the first item of the grid. This is not at all the behavior we expected.

  • Click the FirstName8 cell to make it the current cell.
  • Press the Shift-Tab key.

The focus is moved to the item holding the FirstName8 cell. This is not the behavior we expected.

When the first cell of an item is the current cell and if we press the Shift-Tab key, we would like that the last cell of the previous item becomes the current cell. When the last cell of an item is the current cell and if we press the Tab key, we would like that the first cell of the next item becomes the current cell.

Let's modify our GridSpatialNavigator in order to implement these two features.

GridSpatialNavigator

First, let's modify the ActiveKeyDown and KeyDown methods to be sure that the ProcessKey method is called when the user presses the Tab key:

C#
public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) &&
        (e.Key != Key.Tab))
    {
        LastKeyProcessed = e.Key;
        base.ActiveKeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) 
         && (e.Key != Key.Tab))
    {
        LastKeyProcessed = e.Key;
        base.KeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

Let's now modify the ProcessKey method to handle the Tab key:

C#
private void ProcessKey(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    GStackPanel gStackPanel = (GStackPanel)container;
    HandyContainer parentContainer = 
       HandyContainer.GetParentContainer(gStackPanel);

    if (gStackPanel.Children.Count > 0)
    {
        if ((e.Key == Key.Home) && 
            ((Keyboard.Modifiers & ModifierKeys.Control) 
                     == ModifierKeys.Control))
        {
         . . .
        }
        else if ((e.Key == Key.End) && 
           ((Keyboard.Modifiers & ModifierKeys.Control) 
                     == ModifierKeys.Control))
        {
         . . .
        }
        else if (e.Key == Key.Tab)
        {
            ContainerItem currentItem = 
              parentContainer.GetElement(parentContainer.HoldFocusItem) 
              as ContainerItem;
            if (currentItem != null)
            {
                if ((Keyboard.Modifiers & ModifierKeys.Shift) == 
                              ModifierKeys.Shift)
                {
                    if (String.IsNullOrEmpty(parentContainer.CurrentCellName) || 
                       (parentContainer.CurrentCellName == 
                                currentItem.GetFirstCellName()))
                    {
                        ContainerItem prevItem = 
                           currentItem.PrevNode as ContainerItem;
                        if (prevItem != null)
                        {
                            parentContainer.CurrentCellName = 
                                          prevItem.GetLastCellName();
                            if (prevItem.FocusCell(parentContainer.CurrentCellName))
                            {
                                gStackPanel.EnsureVisible(
                                     gStackPanel.Children.IndexOf(prevItem));
                                e.Handled = true;
                            }
                        }
                    }
                }
                else
                {
                    if (String.IsNullOrEmpty(parentContainer.CurrentCellName) || 
                       (parentContainer.CurrentCellName == 
                                 currentItem.GetLastCellName()))
                    {
                        ContainerItem nextItem = currentItem.NextNode as ContainerItem;
                        if (nextItem != null)
                        {
                            parentContainer.CurrentCellName = nextItem.GetFirstCellName();
                            if (nextItem.FocusCell(parentContainer.CurrentCellName))
                            {
                                gStackPanel.EnsureVisible(
                                     gStackPanel.Children.IndexOf(nextItem));
                                e.Handled = true;
                            }
                        }
                    }
                }
            }
        }
    }
}

The first thing we do is to find the current item. The current item is the item that has the focus or that contains a control that has the focus: it is the value of the HoldFocusItem property of the HandyContainer.

Then, we check if the current cell is the first cell (or the last cell) of the item by using GetFirstCellName (or GetLastCellName) of the ContainerItem.

Next, we get the previous item (or the next item) by using the PrevNode (or NextNode) property of the current item. After that, we make sure that the last cell (or the first cell) of the previous item (or the next item) is the current cell.

We also call the gStackPanel.EnsureVisible method in order to be sure that the new current item is located in the display area of the ItemHost of the HandyContainer control.

Enter key

When used inside a grid, usually the Enter key has the same behavior as the down arrow key. Let's modify the ActiveKeyDown and KeyDown methods of our GridSpatialNavigator to implement this feature.

C#
public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) 
           != ModifierKeys.Control)) && (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;

        base.ActiveKeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) 
          != ModifierKeys.Control)) && (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;

        base.KeyDown(container, e);
    }
    else
        ProcessKey(container, e);
}

Focus on the item

Introduction

In some cases, the focus can go on the item rather than on the cell.

We can test the two following cases:

  • Click exactly on the line separating the two items
  • Click on the FirstName0 cell and then press the up arrow key.

These two cases are easily explainable.

In the first case, by clicking on the line, we click on an item rather than on a cell. The item gets the focus.

In the second case, the cells contained in the first row are not the same as the cells contained in the second row. Therefore, when moving from one row to another, the GridBody "does not know" on which cell of the item to put the focus.

We could easily modify the two behaviors described above by -for instance- forcing the first cell of an item to become the current cell if no other cell has the focus.

Nevertheless, we will let you implement this feature yourself if you want.

In this tutorial, we will postulate that the fact that an item gets the focus rather than a cell is not an unwanted behavior. Rather than avoiding this to happen, we will provide the user an easy way to move the focus to the first cell or the last cell of the item when it happens.

Modify the ContainerItem class

Let's modify the ContainerItem class to allow the user to use the "Home" and the "End" keys to make the first or the last cell of the item the current cell.

C#
protected override void OnKeyDown(KeyEventArgs e)
{
    base.OnKeyDown(e);

    if (!e.Handled)
    {
        if ((e.Key == Key.Home) && 
            ((Keyboard.Modifiers & ModifierKeys.Control) 
                  != ModifierKeys.Control))
        {
            string firstCellName = GetFirstCellName();
            if (!string.IsNullOrEmpty(firstCellName))
            {
                if (FocusCell(firstCellName))
                    e.Handled = true;

            }
        }

        if ((e.Key == Key.End) && 
            ((Keyboard.Modifiers & ModifierKeys.Control) 
                      != ModifierKeys.Control))
        {
            string lastCellName = GetLastCellName();
            if (!string.IsNullOrEmpty(lastCellName))
            {
                if (FocusCell(lastCellName))
                    e.Handled = true;

            }
        }
    }
}

8. Helper methods

In this section, we will add a few methods to help manipulate the grid from code.

FindCell

Let's add a FindCell method to our ContainerItem class. This method will accept a cell name as parameter, and will return the cell having this name.

C#
public Cell FindCell(string cellName)
{
    FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
    if (firstChild != null)
        return firstChild.FindName(cellName) as Cell;

    return null;
}

CurrentItem

The current item is the ContainerItem that has the focus, or it is the ContainerItem that contains a cell that has the focus.

The HoldFocusItem property of the HandyContainer contains the item that has the focus or the item that contains a control that has the focus. The HoldFocusItem property does not return a ContainerItem, but it returns the element of the ItemsSource collection that is linked to it.

Therefore, we can retrieve the current item from the HoldFocusItem. Nevertheless, we will add a CurrentItem property to our HandyContainer partial class to provide a convenient way to access the CurrentItem.

The GetElement method of the HandyContainer allows finding a ContainerItem from the element of the ItemsSource collection that is linked to it. We are using this method to retrieve the ContainerItem from the HoldFocusItem.

C#
public ContainerItem CurrentItem
{
    get { return this.GetElement(this.HoldFocusItem) as ContainerItem; }
}

CurrentItemIndex

The CurrentItemIndex is the index of the current item. This index is the number of items that are stacked on top of the current item.

This number depends on the fact that some nodes may be collapsed or not. In the first picture below, the index of the current item is 12, whereas in the second picture below, the index of the current item is 2.

CurrentItemIndex When Open Node

CurrentItem When Closed Node

If the HandyContainer is not in virtual mode, an easy way to find the current item index is to use the IndexOf method of the ItemsHost:

C#
ItemsHost.Children.IndexOf(currentItem);

If the HandyContainer is in virtual mode, it is more complicated. In that case, the ItemHost will contain only a subset of the items. The VirtualPageStartIndex property of the HandyContainer contains the index of the first item of the ItemHost children collection.

VirtualPageStartIndex

Therefore, the current item index will be:

C#
VirtualPageStartIndex + ItemsHost.Children.IndexOf(currentItem);

In order to avoid having to make this calculation by ourselves every time we need it, let's add a CurrentItemIndex property to our HandyContainer partial class:

C#
public int CurrentItemIndex
{
    get
    {
        ContainerItem currentItem = this.CurrentItem;
        if (currentItem != null)
        {
            if (this.VirtualMode == VirtualMode.On)
            {
                int currentItemIndex = 
                  this.ItemsHost.Children.IndexOf(currentItem);
                return this.VirtualPageStartIndex + currentItemIndex;
            }
            else
                return this.ItemsHost.Children.IndexOf(currentItem);

        }
        return -1;
    }
}

9. How to use the GridBody

Create a GridBody

As a reminder, here is the list of the steps needed to create a GridBody:

  • Add a HandyContainer to your page and change its HandyStyle to the "GridBodyStyle" value.
    • If you expect to handle a lot of data, set the VirtualMode property of the HandyContainer to "On".
    • If you expect to display hierarchical data in the grid body, set the HandyDefaultItemStyle property value to "Node".
    • If you would like that the items are displayed using an alternate background, set the AlternateType property value to "Items".
  • Prepare the data.
    • Create the data classes that will hold the data displayed in the Grid. These classes should inherit from ContainerDataItem (in our tutorial, we created the Person and Country classes).
    • Fill the ItemsSource of the GridBody with a collection holding your data. Ideally, the collection will be a GObservableCollection, but it is not mandatory.
  • Fill the ItemTemplate of the GridBody.
    • Preferably use an ItemDataTemplate rather than a DataTemplate.
    • If you are displaying items of different kinds (for instance, persons and countries), use HandyContentPresenters to separate their description in the ItemTemplate.
    • Place the Cells inside the ItemDataTemplate using panels, and link RowSpatialNavigators to these panels.
    • Do not forget to name your cells.
    • Do not forget to bind the data to your cells.

GridBody, ContainerItems, and Cells members

You can use all the methods, properties, and events that we have implemented, as well as the ones that are already implemented in the HandyContainer and ContainerItems classes and their ancestors. We will provide here a small description of the most important ones. You can read the Help file provided with GOA Toolkit to have a look at all of them.

GridBody members

VerticalOffset, HorizontalOffset, ViewportHeight, ViewportWidth, ScrollableHeight, and ScrollableWidth

  • VerticalOffset is the index of the item that is displayed at the top of the displayed area.
  • HorizontalOffset is the distance (in pixels) between the left of the items and the left of the displayed area.
  • ViewportHeight is the number of items displayed in the displayed area.
  • ViewportWidth is the width of the display area.
  • ScollableHeight is the total number of items that can be displayed.
  • ScrollableWidth is the width of the largest item.

Offsets

VerticalOffsetChanged, HorizontalOffsetChanged ,VerticalScrollSettingsChanged, HorizontalScrollSettingsChanged

  • The VerticalOffsetChanged and the HorizontalOffsetChanged events occur when the VerticalOffset value or the HorizontalOffset value is changed.
  • VerticalScrollSettingsChanged and HorizontalScrollSettingsChanged occur when the ViewportHeight value or the ViewportWidth value is changed.

EnsureItemVisible

Call the EnsureItemVisible(ItemIndex) method to be sure that the item at the ItemIndex index is displayed in the display area of the control.

GetItemFormIndex

This method will return the ContainerItem from its index.

CurrentItemIndex

This property will return the index of the current item.

CurrentItem

This property will return the ContainerItem that is the current item.

HoldFocusItemChanged

HoldFocusItem is the item that has the focus or that contains a control that has the focus. The HoldFocusItemChanged event occurs when the HoldFocusItem is changed, and therefore it occurs when the CurrentItem is changed.

CurrentCellName

This property contains the name of the current cell.

OnCurrentCellNameChanged

The OnCurrentCellNameChanged event occurs when the current cell name changes.

Items

The Items property contains all the items "linked" to the HandyContainer.

This property can be disturbing at first because when the ItemsSource of the HandyContainer is set, it will not return a collection of all the ContainerItems of the HandyContainer, but it will return a collection of all the elements from which the ContainersItems are generated (these elements come from the ItemsSource).

If you wonder why the Items property does not return a collection of ContainerItems, remind that the HandyContainer can work in VirtualMode. In this case, only a part of the ContainerItems are generated from the ItemsSource.

GetElement

This method allows retrieving a ContainerItem from an element of the ItemsSource.

For instance, you can write:

C#
ContainerItem firstContainerItem = MyGridBody.GetElement(MyGridBody.Items[0]);

GetItemSource (static method)

The GetItemSource method is the opposite of the GetElement method. This method retrieves the source that was used to generated a ContainerItem.

VirtualMode

If set to "On", the HandyContainer will not generate all the ContainerItems from the ItemsSource elements. Only the items displayed are generated.

VirtualPageSize

When working using the VirtualMode, the VirtualPageSize property contains the number of ContainerItems that are generated from the ItemsSource.

VirtualPageStartIndex

When working using the VirtualMode, the VirtualPageStartIndex property contains the index of the first element of the ItemsSource from which the ContainersItems are generated.

VerticalScrollbarVisibility and HorizontalScrollbarVisibility

These properties allow to show or hide the scrollbars.

ItemClick

This event occurs when an item is clicked.

SelectionMode

SelectionMode allows defining the way the items (i.e., the rows) can be selected by the user. In this tutorial, the default value (single selection mode) was used, but you can use another one such as None or Multiple.

SelectedItem

The item that is currently selected (if any). If several items are selected, this property holds the last item that was selected.

SelectedItems

The items that are currently selected (if any).

SelectedItemChanged

This event occurs when the SelectedItems collection has changed.

SelectedItemChanging

This event occurs just before the SelectedItems collection has changed

UIItemIsExpandedChanged

This event occurs when the IsExpanded property of a ContainerItem has changed.

ContainerItems members

FocusCell

This method allows focusing a cell and making it the current cell.

GetFirstCellName

The method returns the name of the first cell of the ContainerItem. The first cell is the cell that is the closest to the top left corner.

GetLastCellName

The method returns the name of the last cell of the ContainerItem. The last cell is the cell that is the closest to the right bottom corner.

FindCell

The method returns a cell from its name.

IsExpanded

If the item has children items (i.e., if the item is a node), this property allows to get or set whether the node is open or not (i.e., whether the children items are displayed or not).

IsExpandedChanged

This event occurs when the IsExpanded property value has changed.

CollapseAll

When the CollapseAll method of an item is called the IsExpanded property value of the item and the IsExpanded property of all its children (and grandchildren) are set to false.

ExpandAll

When the CollapseAll method of an item is called, the IsExpanded property value of the item and the IsExpanded property of all its children (and grandchildren) are set to true.

Items

The Items property contains all the children items "linked" to the Item.

When the ItemsSource of the HandyContainer is set, it will not return a collection of ContainerItems, but it will return a collection of all the elements from which the ContainerItems are generated. Read the description of the Items property of the HandyContainer above to know more.

NextNode

This property will return the item that is just below the current item.

PrevNode

This property will return the item that is just above the current item.

Virtual Mode

Working with a HandyContainer when the VirtualMode is set to "On" can be disturbing at first.

The main concept that must be understood is that all the ContainerItems are not generated from the ItemsSource. Only a subset of them is generated in order to fill the display area of the HandyControl.

If the user scrolls inside the HandyContainer, other ContainerItems are generated in order to keep the display area up-to-date. Therefore, we cannot postulate that there will always be a ContainerItem linked to an element of the ItemsSource of the collection.

Before manipulating a ContainerItem, you must first make sure that this ContainerItem has been generated. The only way to do this is to make sure that the VerticalOffset property has a value that makes the item located in the display area of the control. You can either manually change the VerticalOffset property value, or call the EnsureItemVisible method.

Most of the time, you do not need to manipulate the ContainerItems themselves. It is easier to manipulate the elements of the ItemsSource collection of the HandyContainer.

Pay also attention to the fact that most of the properties of the HandyContainer do not return ContainerItems, but the source element they are linked to. This is the case of the HoldFocusItem, SelectedItem, Items, PressedItem, and MouseOverItem properties, for instance. If you have the reference to an element of the ItemsSource collection and want to find the ContainerItem that is linked to it, use the GetElement method of the HandyContainer. Nevertheless, this method will return a ContainerItem only if it is located in the display area of the control and has been generated.

Exercises

Setting the current cell from the outside

Before setting the current cell, you must make sure the ContainerItem holding the cell is located in the display area of the grid's body. Then, you can use the FocusCell method of the ContainerItem.

Let's suppose we would like that the City cell of the 100th item becomes the current cell. We can write:

C#
MyGridBody.EnsureItemVisible(100);
((ContainerItem) MyGridBody.GetItemFromIndex(100)).FocusCell("City");

Knowing when the current cell has changed

In order to know when the current cell has changed, we have to monitor two events: CurrentCellNameChanged and HoldFocusItemChanged. The HoldFocusItemChanged event will occur each time the current item changes. The CurrentCellNameChanged event will occur each time the current cell name changes.

Examples
  • The CurrentCell is City8, and the user clicks the Address8 cell.
  • The CurrentCellNameChanged event occurs, but the HoldFocusItemChanged event does not.

  • The CurrentCell is City8, and the user clicks the City7 cell.
  • The HoldFocusItemChanged event occurs, but the CurrentCellNameChanged event does not.

  • The CurrentCell is City8, and the user clicks the Address7 cell.

    Both the HoldFocusItemChanged and the CurrentCellNameChanged events occur.

10. Conclusion

Congratulation for having reached the end of this long tutorial. Now that we have laid the grounds of our data grid, the next tutorials will be shorter. Do not miss them.

This tutorial is part of a set. You can read Step 2 here: Build Your Own DataGrid for Silverlight: Step 2.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Christian Amado9-Mar-13 1:58
professionalChristian Amado9-Mar-13 1:58 
QuestionExcelent Artical Pin
Member 77095876-Dec-12 15:09
Member 77095876-Dec-12 15:09 
GeneralGood Job Pin
Watson Jason17-Mar-09 19:52
Watson Jason17-Mar-09 19:52 
GeneralGOA Controls is a commercial product Pin
Pete O'Hanlon12-Mar-09 2:57
subeditorPete O'Hanlon12-Mar-09 2:57 
GeneralRe: GOA Controls is a commercial product Pin
Jeff Karlson12-Mar-09 6:20
Jeff Karlson12-Mar-09 6:20 
GeneralRe: GOA Controls is a commercial product [modified] Pin
User of Users Group12-Mar-09 9:40
User of Users Group12-Mar-09 9:40 
GeneralRe: GOA Controls is a commercial product Pin
Pete O'Hanlon12-Mar-09 10:00
subeditorPete O'Hanlon12-Mar-09 10:00 
GeneralRe: GOA Controls is a commercial product Pin
Braulio Dez2-May-09 3:00
Braulio Dez2-May-09 3:00 
GeneralGreat Article : Thanks Jeff Pin
raam_kimi12-Mar-09 1:34
raam_kimi12-Mar-09 1:34 
General5 Pin
User of Users Group11-Mar-09 13:14
User of Users Group11-Mar-09 13:14 
General10 marks for effort Pin
Sacha Barber11-Mar-09 10:50
Sacha Barber11-Mar-09 10:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.