![]() |
Web Development »
Silverlight »
Controls
Intermediate
License: The Code Project Open License (CPOL)
Build Your Own DataGrid for Silverlight: Step 1By Jeff KarlsonLearn how to build the body part of your DataGrid using Silverlight and GOA Toolkit. Implement Virtual Mode. Work with hierarchical data. Build cells and cells navigation. |
C# (C# 1.0, C# 2.0, C# 3.0), Windows, Silverlight, Dev
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
This tutorial is part of a set. You can read Step 2 here: Build Your Own DataGrid for Silverlight: Step 2
Before diving into this tutorial, let’s have a look at some of the benefits of writing our own data grid control:
However, pay attention to the fact that in order to complete this tutorial, we will need the free edition of GOA Toolkit for Silverlight (http://www.netikatech.com/products/toolkit). This library will allows 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 5 instances of our GridBody.
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.

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.
This tutorial was written using GOA Toolkit 2009 Vol1 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:
If you do not know GOA Toolkit at all, we suggest that you spend a few times to quickly scan the tutorial that is provided with GOA Toolkit. This tutorial is installed during the setup of GOA Toolkit. You can access it through the start menu:

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

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

If you do not have read the GOA Toolkit tutorial, here is a summary that you should read.
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 GOA Toolkit, this kind of controls is called a List Control.
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 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 levels 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 the 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 3 sets of Lists Controls.
These controls are mainly used to perform actions inside the application. Menus and Toolbars are part of this set.
Containers controls are used to display data in various ways. Lists, Trees or Combo are part of this set.
Navigator’s controls allow navigating between sets of data or parts of the application. TabStrips, TabTrees, TabLists or NavigationBars are part of this set.
Our grid’s body will be implemented using a HandyContainer control. This is the controls 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.
<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.
We need data to be able to test our grid. We are going to fill it with a collection of persons.
Here is the code of the Person class. We have to add this class to the GridBody project.
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 on your class. This interface holds a PropertyChanged event which purpose 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.
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 implement 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 the 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 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:
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 are facing two problems:
Before going further, let’s start by correcting these two problems.
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 persons 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 VirtualMode, 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.
<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 from the number of elements in our data collection.
We do not have told the GridBody how it must display the person’s data.
This can be made 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 use TextBlocks and Borders in order to mimic grid cells:
<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.
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.
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.
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 items 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 items of the HandyContainer:
<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.
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 had another background. The AlternateType property of the HandyContainer allows us to accomplish this:
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items">
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 only 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 allow us to easily set the location of each cells. 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.
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 to display them grouped by the countries they belong to.
Let’s create a very simple Country class and add it to the GridBody project:
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:
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.
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.
<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:
<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:
<o:HandyContainer.DefaultItemModel>
<o:ContainerItem HandyStyle="Node" Padding="0" HorizontalAlignment="Left" Margin="0"/>
</o:HandyContainer.DefaultItemModel>
It is now time to start to implement 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 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 Extension folder. This folder will hold all the improvements related to our Grid.
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:
TreeHelper.IsChildOf(canvas1, button1)
Add this class inside the Extension\Grid folder of the GoaOpen project.
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;
}
}
}
Before implementing the cells, we need to add a few method 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.
Let’s modify the existing HandyContainer.cs file (the one that is located in the GoaControls\HandyList\HandyList\HandyContainer folder) in order it contains a partial class:
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 have just created in the the Extensions\Grid folder)
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
{
}
}
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 the GridBody1 HandyContainer:
HandyContainer.GetParentContainer(cell);
Let’s add this method to our new partial class.
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;
}
}
}
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 designate by which index.
Therefore, we will force the use of 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.
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);
}
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.
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 leave 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.
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.
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); }
}
}
}
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 instruction of 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:
. . .
</Setter.Value>
</Setter>
</Style>
<!--=================================================================================================-->
<!--=================================================================================================-->
<!--========================================== GRID ================================================-->
<!--=================================================================================================-->
<!--=================================================================================================-->
</ResourceDictionary>
Then let’s add the style of our TextCell after the separator.
<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:
After all these modifications, let’s try to start our application again.
But before, 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. We have not to forget to give a name to each cells:
<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 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.
Let’s add a CheckBoxCell class to the GoaOpen project.
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 that with the focus element.
<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.
In order to see what a CheckBoxCell look 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:
<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>
We do not have 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.
At this time, we will just make a copy of the ListStyle that is provided in the generic.xaml file of GoaOpen.
<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>
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.
namespace Open.Windows.Controls
{
public enum HandyContainerStyle
{
None = 0,
ListStyle,
ShelfStyle,
VerticalShelfStyle,
ComboListStyle,
GridBodyStyle
}
}
Let’s apply this new style to the MyGridBody control contained in the Page of the GridBody project:
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items"
HandyDefaultItemStyle="Node"
HandyStyle="GridBodyStyle">
Now, the GridBodyStyle will be applied to the HandyContainer. As we do not have modified the style yet, at this time, we will not see any difference if we start our application.
The HandyContainer control has a HandyDefaultItemStyle property which allows choosing which style is applied to the items it contains.
We already have used this property in our introduction when we have 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:
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 theNodeStyle 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.
If you look at the GridBodyStyle that we have created in the generic.xaml file, you will see these two properties defined:
<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:
<Setter Property="StandardItemStyle" Value="{StaticResource Container_RowItemStyle}"/>
<Setter Property="NodeStyle" Value="{StaticResource Container_RowNodeStyle}"/>
This way when we will choose the StandardItem value for the HandyDefaultItemStyle, the new “Container_RowItemStyle” style will be applied to each item of the HandyContainer and when we will 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 of 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 the Container_RowItemStyle and the Container_RowNodeStyle, it is better to define the two items styles before the HandyContainer style.
<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/past of them in the generic.xaml.
The Container_RowItemStyle and the Container_RowNodeStyle styles have been created by copying the Container_ItemStyle and the Container_NodeStyle and by adjusting a few elements in order that they look more like a grid row:
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.
<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 elements and properties 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 it is that the Container_RowNodeStyle style can handle nodes:
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.
However, there is a line missing between the two rows of cells displayed inside the persons’ items:
This is logical. In our styles, we have built right border for the cells and borders for the items (i.e. the rows) but we have to add the border between the rows of 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.
<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 do not have defined yet.
The styles defined in the generic.xaml files of GoaOpen are referencing brushes and colors that are defined at the top of 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.
<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>
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 key such as the right and left arrow and the home or end key.
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:
<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:
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:
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.
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:
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;
}
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
Let’s add this method to our HandyContainer partial class:
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;
}
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 follow:
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.
This time the horizontal offset of the HandyContainer is automatically modified in order that the current cell keeps visible.
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.
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.
The ContainerItem is the type that is used when creating the items of the HandyContainer. We will add 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 Extension\Grid folder of the GoaOpen project.
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):
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.
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;
}
We 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 it work as we would like.
Let’s first create a GridSpatialNavigator that inherits from the SpatialNavigator in the Extensions\Grid folder
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:
<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:
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:
<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 GridBodyStyle style we will also see this property:
<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 that 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 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:
<Setter Property="HandyItemsPanelModel" Value="None" />
Let’s come back to our GridSpatialNavigator and add some code to it:
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 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 step of the process when a user presses a key:
In our HandyContainer partial class, let’s modify the OnNavigatorSetKeyboardFocus method :
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 Key Down, Key Up, PageDown or the Page Up key, we force the cell having the CurrentCellName name to have the focus.
Let’s test our changes.
The Address7 cell becomes the current cell (i.e. the cell that has the focus).
This is the behavior we expected.
Let’s start our application and analyze what happens when we press the Ctrl-Home and the Ctrl-End Key.
The 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 press 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.
Our requirement 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? Remind 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 at the closed 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).
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);
}
}
The GetFirstCellName first retrieve all the available cells by calling the GetCells methods.
It then gets the position of all the cells and sorts them using the CellPositionCompare comparer.
It then returns the first cell that can have the focus (IsTapStop == true).
The 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, the cellCollection will not be up-to-date anymore. Therefore we must override the OnApplyTemplate method and clear the collection cache:
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:
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:
public override void OnApplyTemplate()
{
cellCollection = null;
_OnApplyTemplate();
base.OnApplyTemplate();
}
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 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.
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
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 liked 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 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.
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 Extension\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.
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 by the RowSpatialNavigator
<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>
We are almost done but not completely yet.
Let’s try our changes
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.
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 the 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 the 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:
internal void _OnNavigatorSetKeyboardFocus(UIElement item)
{
this.OnNavigatorSetKeyboardFocus(item);
}
Then let’s modify the FocusCell method of our ContainerItem partial class:
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.
Let’s try to use the tab key inside our grid:
City8 cell becomes the current cell. This is the behavior we expected
Address8 cell becomes the current cell again. This is also the behavior we expected
The focus is moved to the first item of the grid. This is not at all the behavior we expected.
The focus is moved to the item holding 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.
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:
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:
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 the GetFirstCellName (or the GetLastCellName) of the ContainerItem.
Next, we get the previous item (or the next item) by using the PrevNode (or the 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.
When used inside a grid, usually the enter key has the same behavior that the down arrow key.
Let’s modify the ActiveKeyDown and KeyDown method of our GridSpatialNavigator to implement this feature
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);
}
In some case, the focus can go on the item rather than on a cell.
We can test the two following cases:
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 that 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 behavior described above by -for instance- forcing the first cell of an item to become the current cell if no other cell has got the focus.
Nevertheless, we 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.
Let’s modify the ContainerItem class to allow the user to use the “home” and the “end” key to make the first or the last cell of the item the current cell.
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;
}
}
}
}
In this section, we will add a few methods to help to manipulate the grid from code.
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.
public Cell FindCell(string cellName)
{
FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
if (firstChild != null)
return firstChild.FindName(cellName) as Cell;
return null;
}
The current item is the ContainerItem that has the focus or it is the ContainerItem that contains a cell that have 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.
public ContainerItem CurrentItem
{
get { return this.GetElement(this.HoldFocusItem) as ContainerItem; }
}
The CurrentItem index 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 bellow the index of the current item is 12 whereas in the second picture below the index of the current item is 2.
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:
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.
Therefore, the current item index will be:
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:
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;
}
}
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
Prepare the data
Fill the ItemTemplate of the GridBody
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.
The VerticalOffset is the index of the item that is displayed at the top of the displayed area.
The HorizontalOffset is the distance (in pixels) between the left of the items and the left of the displayed area.
The ViewportHeight is the number of items displayed in the displayed area.
The ViewportWidth is the width of the display area.
The ScollableHeight is the total number of items that can be displayed.
The ScrollableWidth is the width of the largest item.
The VerticalOffsetChanged and the HorizontalOffsetChanged events occur when the VerticalOffset value or the HorizontalOffset value is changed.
The VerticalScrollSettingsChanged and the HorizontalScrollSettingsChanged occur when the ViewportHeight value or the ViewportWidth value is changed.
Call the EnsureItemVisible(ItemIndex) method to be sure that the item at the ItemIndex index is displayed in the display area of the control.
This method will return the ContainerItem from its index.
This property will return the index of the current item.
This property will return the ContainerItem that is the current item
The HoldFocusItem is the item that has the focus or that contains a control that has the focus.
The HoldFocusItem is the current item.
The HoldFocusItemChanged event occurs when the HoldFocusItem is changed and therefore it occurs when the CurrentItem is changed.
This property contains the name of the current cell.
The OnCurrentCellNameChanged event occurs when the current cell name change.
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 returns 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.
This method allows retrieving a ContainerItem from an element of the ItemsSource.
For instance, you can write:
ContainerItem firstContainerItem = MyGridBody.GetElement(MyGridBody.Items[0]);
The GetItemSource method is the opposite of the GetElement method.
This method retrieves the source that was used to generated a ContainerItem.
If set to "On", the HandyContainer will not generate all the ContainerItems from the ItemsSource elements. Only the Items displayed are generated.
When working using the VirtualMode, the VirtualPageSize property contains the number of ContainerItems that are generated from the ItemsSource
When working using the VirtualMode, the VirtualPageStartIndex property contains the index of the first element of the ItemsSource from which the ContainersItems are generated.
These properties allow to show or hide the scrollbars.
This event occurs when an item is clicked.
The 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.
The item that is currently selected (if any). If several items are selected, this property holds the last item that was selected.
The items that are currently selected (if any).
This event occurs when the SelectedItems collection has changed
This event occurs just before the SelectedItems collection has changed
This event occurs when the IsExpanded property of a ContainerItem has changed.
This method allows focusing a cell and making it the current cell.
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.
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.
The method returns a cell from its name.
If the item has children items (i.e. if the item is a node), this property allows to get or to set whether the node is open or not (i.e. whether the children items are displayed or not)
This event occurs when the IsExpanded property value has changed.
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.
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.
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 ContainersItems are generated. Read the description of the Items property of the HandyContainer above to know more.
This property will return the item that is just below.
This property will return the item that is just above
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 ContainersItems 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 ContainersItems 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, 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.
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:
MyGridBody.EnsureItemVisible(100);
((ContainerItem) MyGridBody.GetItemFromIndex(100)).FocusCell("City");
In order to know when the current cell has changed, we have to monitor two events: the CurrentCellNameChanged and the HoldFocusItemChanged
The HoldFocusItemChanged event will occur each time the current item will have changed.
The CurrentCellNameChanged event will occur each time the current cell name will have changed.
The CurrentCell is City8 and the user clicks the Address8 cell.
The CurrentCellNameChanged event is occurring but the HoldFocusItemChanged event is not occurring.
The CurrentCell is City8 and the user clicks the City7 cell.
The HoldFocusItemChanged event is occurring but the CurrentCellNameChanged event is not occurring.
The CurrentCell is City8 and the user clicks the Address7 cell.
Both the HoldFocusItemChanged and the CurrentCellNameChanged events are occurring.
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
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 23 Apr 2009 Editor: Smitha Vijayan |
Copyright 2009 by Jeff Karlson Everything else Copyright © CodeProject, 1999-2009 Web20 | Advertise on the Code Project |