This tutorial is part of a set. You can read step 2 here: Build Your Own DataGrid for Silverlight: Step 2.
1. Introduction
Why would I create a grid myself?
Before diving into this tutorial, let's have a look at some of the benefits of writing our own data grid control:
- Out-of-the-box grids never have all the features we need. Either they are really poor and do not fit our needs, or they are enclosed in huge assemblies that make the size of our application just too big. On top of that, they require a long time to learn.
- On the opposite, by following the steps of this tutorial, we will build a grid that we will fully domesticate. At any time, we will be able to add features that we really need and only the ones we need, keeping the project at reasonable size. Furthermore, we will learn the way some of the key components of Silverlight and the GOA Toolkit work, and we will be able to apply our knowledge to build other high level controls.
However, pay attention to the fact that in order to complete this tutorial, we will need the free edition of the GOA Toolkit for Silverlight (http://www.netikatech.com/products/toolkit). This library will allow us to take shortcuts, and without them, this tutorial would have the size of an entire book. It will allow us to create up to five instances of our GridBody.
Grid's Body
Just to be sure that we all use the same words to designate the same things, here is a picture describing the elements of the data grid:
In this first part of the tutorial, we will focus on the implementation of a read-only body. In the second part, we will discuss on how to add editing features to our grid, and in the third part, we will turn our attention to the headers.
2. Getting started
Download and install the GOA Toolkit
This tutorial was written using GOA Toolkit 2009 Vol. 1 Build 212. Be sure to have installed this release or a more recent one on your computer. You can download a trial setup from the NETiKA TECH web site: www.netikatech.com/downloads.
If you do not know the GOA Toolkit at all, we suggest that you spend some time to quickly scan the tutorial that is provided with the GOA Toolkit. This tutorial is installed during the setup of the GOA Toolkit. You can access it through the Start menu:
Create a new solution in Visual Studio
- In Visual Studio, create a new Silverlight project and name it "GridBody".
- Add references to the GoaEssentials assembly and to the GoaOpen project.
The easiest way to achieve this is to follow the steps described in the HowTos documentation:
- Open the HowTos documentation
- Select the How To Start node on the left of the application screen
- Follow the instructions on the right
At the end of this process, we should have a solution having a hierarchy like this one:
3. Basic grid body
GOA Toolkit architecture quick overview
If you have not read the GOA Toolkit tutorial, here is a summary that you should read.
The GOA Toolkit focuses on controls that are able to display several items. Lists, menus, tabs, toolbars and data grids are controls of this type. In the GOA Toolkit, this kind of control is called a List control.
The GOA Toolkit is subdivided into two libraries: GOA Essentials and GOA Open.
Base components requiring care in their development and maintenance are grouped in GOA Essentials. These components are at the heart of the GOA Toolkit and they should not be modified without watching out.
GOA Open is built on top of GOA Essentials. GOA Open is provided with its source code. It is made of high level controls such as menus or toolbars. They are all built the same way. If you learn how a GOA Open control is built, you may apply your knowledge on other controls. Most of the time, GOA open controls are made of one or several GOA Essentials components on which styles have been applied.
GOA Open provides three sets of List controls.
Commands
These controls are mainly used to perform actions inside the application. Menus and toolbars are part of this set.
Containers
Container controls are used to display data in various ways. Lists, trees, or combos are part of this set.
Navigators
Navigator controls allow navigating between sets of data or parts of the application. TabStrips, TabTrees, TabLists, or NavigationBars are part of this set.
Implementing a very basic grid's body
Our grid's body will be implemented using a HandyContainer
control. This is the control that best fits our needs.
Let's first see what it is possible to do with this control without changing anything.
Let's add a HandyContainer
to the Page.xaml of our GridBody application. At the same time, we will add the necessary XMLNS references at the top of the XAML, and remove the Width
and Height
settings.
<UserControl x:Class="GridBody.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
<Grid x:Name="LayoutRoot" Background="White">
<o:HandyContainer
x:Name="MyGridBody">
</o:HandyContainer>
</Grid>
</UserControl>
Of course, if we start our application now, our page will not display anything. We need to attach data to it.
Preparing the data
We need data to be able to test our grid. We are going to fill it with a collection of persons.
Person class
Here is the code of the Person
class. We have to add this class to the GridBody project.
using Open.Windows.Controls;
namespace GridBody
{
public class Person : ContainerDataItem
{
public Person(string firstName, string lastName, string address,
string city, string zipCode, bool isCustomer, string comment)
{
this.firstName = firstName;
this.lastName = lastName;
this.address = address;
this.city = city;
this.zipCode = zipCode;
this.isCustomer = isCustomer;
this.comment = comment;
}
private string firstName;
public string FirstName
{
get { return firstName; }
set
{
if (firstName != value)
{
firstName = value;
OnPropertyChanged("FirstName");
}
}
}
private string lastName;
public string LastName
{
get { return lastName; }
set
{
if (lastName != value)
{
lastName = value;
OnPropertyChanged("LastName");
}
}
}
private string address;
public string Address
{
get { return address; }
set
{
if (address != value)
{
address = value;
OnPropertyChanged("Address");
}
}
}
private string city;
public string City
{
get { return city; }
set
{
if (city != value)
{
city = value;
OnPropertyChanged("City");
}
}
}
private string zipCode;
public string ZipCode
{
get { return zipCode; }
set
{
if (zipCode != value)
{
zipCode = value;
OnPropertyChanged("ZipCode");
}
}
}
private bool isCustomer;
public bool IsCustomer
{
get { return isCustomer; }
set
{
if (isCustomer != value)
{
isCustomer = value;
OnPropertyChanged("IsCustomer");
}
}
}
private string comment;
public string Comment
{
get { return comment; }
set
{
if (comment != value)
{
comment = value;
OnPropertyChanged("Comment");
}
}
}
}
}
Note that we have made the Person
class inherit from the ContainerDataItem
class. This is not mandatory, but it is recommended. It is the easiest way to create a data class that can work with a HandyContainer
control. If you choose not to inherit from the ContainerDataItem
class, you should at least implement the INotifyPropertyChanged
interface in your class. This interface holds a PropertyChanged
event the purpose of which is to notify the grid when the value of a property has been modified. This interface is already implemented in the ContainerDataItem
. In order to make it work properly, you need to call the OnPropertyChanged
method in the setter of each property.
Fill the HandyContainer
In order to fill the grid body, we are going to fill the ItemsSource
property of the HandyContainer
control with a collection of persons. We could use any kind of collection that implements the IList
interface. However, if we would like the GridBody to be able to handle changes in the collection (such as when we add or remove a person), the collection should implement INotifyCollectionChange
. This is the case of the ObservableCollection
.
Nevertheless, the ObservableCollection
provided with Silverlight is limited. Using this collection, you can only add or remove elements one at a time.
On the opposite, the HandyContainer
is able to manage the manipulation of several items at a time. Therefore, we will use a GObservableCollection
(provided with the Goa Toolkit). This collection implements all the interfaces needed, and provides methods to manipulate several items at a time.
So, let's create our collection of persons and fill the ItemsSource
of the GridBody in the constructor of the Page
of our application:
public partial class Page : UserControl
{
private GObservableCollection<Person> personCollection;
public Page()
{
InitializeComponent();
personCollection = new GObservableCollection<Person>();
for (int personIndex = 0; personIndex < 1000; personIndex++)
personCollection.Add(new Person("FirstName" + personIndex,
"LastName" + personIndex,
"Address" + personIndex,
"City" + personIndex,
"ZipCode" + personIndex,
personIndex % 2 == 0,
"Comment" + personIndex));
MyGridBody.ItemsSource = personCollection;
}
}
As the purpose of this tutorial is not to explain how to connect and retrieve data from a database or an application server, the data is generated from code.
If we start our application now, we will face two problems:
- The application is slow to start.
- The grid does not display the persons' data, but it displays the name of the type of the
Person
class.
Before going further, let's start by correcting these two problems.
VirtualMode
When displaying and manipulating UIElements, Silverlight is not as fast as a desktop application. This is the reason why our application is slow when it starts. The HandyContainer
control creates a UIElement for each person of the collection. As our collection holds 1000 persons, 1000 UIElements must be created. Creating 1000 UIElements is a "long" process, and it slows our application down.
Fortunately, the HandyContainer
control implements a VirtualMode. When it is in Virtual Mode, only the items that fit inside the displayed area of the control are created. This way, the number of UIElements that must be created and manipulated is cut down to a more acceptable value.
Applying this change is fast, and can be made directly in the XAML of the page of our application.
<UserControl x:Class="GridBody.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
<Grid x:Name="LayoutRoot" Background="White">
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On">
</o:HandyContainer>
</Grid>
</UserControl>
If we start the application now, we notice that it is a lot faster to start. Furthermore, now that the Virtual Mode is on, the performance of the grid will depend a lot less on the number of elements in our data collection.
ItemsTemplate
We have not told the GridBody how it must display the person's data.
This can be done by using the ItemTemplate
property of the control. The ItemTemplate
is the data template that must be applied to each item in order to display the data (the person) it is linked to.
We are going to create a DataTemplate
that uses TextBlock
s and Border
s 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.
DefaultItemModel
We would like to remove the space that is displayed between the items (i.e., the rows). This can be done by removing the padding on each item.
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 item of a HandyContainer
. In order to do this, we have to fill the DefaultItemModel
property of the control with a ContainerItem
. Each property value (except for the style) that we apply to the ContainerItem
of the DefaultItemModel
will also be applied to each item of the HandyContainer
:
<o:HandyContainer.DefaultItemModel>
<o:ContainerItem HandyStyle="StandardItem"
Padding="0" HorizontalAlignment="Left"/>
</o:HandyContainer.DefaultItemModel>
If we start our application now, the data is better displayed.
AlternateType
It is not easy to see where an item starts and where it finishes. It would be a lot easier if one item out of two has another background. The AlternateType
property of the HandyContainer
allows us to accomplish this:
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items">
Cells
Let's start the application and watch the result of our work. We are still far from a data grid, but it is an interesting start.
What is missing? First, we would like that the cells inside the items are real cells and not TextBlock
s with a border. The cells should be able to display different kind of data - not just text. The user should be able to navigate from one cell to another. At the same time, we would like to keep the flexibility of the ItemTemplate
. Using panels inside an item template allows us to easily set the location of each cell. We are not limited to display the cells on a single row like in a standard grid.
But before implementing those features, let's explore another possibility of the HandyContainer
: the nodes.
Nodes
We would like that our grid is also able to display hierarchical data. The items of the HandyContainer
are able to manage this. Each item can be a node.
As a sample, we are going to make our persons members of countries, and display them grouped by the countries they belong to. Let's create a very simple Country
class and add it to the GridBody project:
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<Country> countryCollection;
public Page()
{
InitializeComponent();
countryCollection = new GObservableCollection<Country>();
for (int countryIndex = 0; countryIndex < 100; countryIndex++)
{
Country country = new Country("CountryName" + countryIndex);
for (int personIndex = 0; personIndex < 10; personIndex++)
country.Children.Add(new Person("FirstName" + personIndex,
"LastName" + personIndex,
"Address" + personIndex,
"City" + personIndex,
"ZipCode" + personIndex,
personIndex % 2 == 0,
"Comment" + personIndex));
country.IsExpanded = true;
countryCollection.Add(country);
}
MyGridBody.ItemsSource = countryCollection;
}
}
As the Country
class inherits from the ContainerDataItem
class, we were automatically able to use two very interesting properties in the code above: Children
and IsExpanded
. The Children
property allows defining the children of an element. Once its children property is filled, the HandyContainer
manages the item as a node. The IsExpanded
property allows defining whether the node (i.e., the item) is expanded (opened) or not.
DataPresenter
However, if we start the application, the countries nodes are not displayed. This is because we still need to change the ItemTemplate
of the GridBody
and describe the way the countries will be displayed.
In the ItemTemplate
, we must be able to describe at the same time how the persons and the countries are displayed. This is done by the use of the HandyDataPresenter
.
<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 HandyDataPresenter
s 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>
4. Cells
Introduction
It is now time to start implementing our cells. The first things we need are cells that can display different kinds of data. In this tutorial, we will implement the TextCell
class and the CheckBoxCell
class. You will be able to easily implement the other kinds of cells yourself.
We will add all our new features directly in the GoaOpen project. This project is the open part of the GOA Toolkit.
Let's create a new Extensions folder inside the GoaOpen project. We will put all our GOA improvements in that folder. Let's also add a Grid subfolder to the Extensions folder. This folder will hold all the improvements related to our Grid.
Preparations
TreeHelper class
Let's first implement a helper class that we will use at several places in our code.
The TreeHelper
class implements a IsChildOf
method which allows to know if an element of the tree is a child of another element of the tree. For instance, if the button "button1
" is a child of the canvas canvas1
, the following call will return true
:
TreeHelper.IsChildOf(canvas1, button1)
Add this class inside the Extensions\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;
}
}
}
Preparing the HandyContainer
Before implementing the cells, we need to add a few methods and properties to the HandyContainer
.
The HandyContainer
class is located in the GoaControls\HandyList\HandyList\HandyContainer folder of the GoaOpen project.
We will not add our methods directly to the HandyContainer file. In order to keep our changes apart from the code provided in GoaOpen, we will add a new HandyContainer file in the Extensions\Grid folder we have just created.
Let's modify the existing HandyContainer.cs file (the one that is located in the GoaControls\HandyList\HandyList\HandyContainer folder) in order that it contains a partial class:
namespace Open.Windows.Controls
{
public partial class HandyContainer : HandyListControl
{
public static readonly DependencyProperty HandyStyleProperty;
public static readonly DependencyProperty HandyDefaultItemStyleProperty;
Let's create a new HandyContainer
partial class in the new HandyContainer file (the one that we just created in the the Extensions\Grid folder).
using System;
using System.Windows;
using Netika.Windows.Controls;
using System.Windows.Media;
using System.Windows.Input;
namespace Open.Windows.Controls
{
public partial class HandyContainer : HandyListControl
{
}
}
GetParentContainer method
The GetParentContainer
static method will allow finding the parent HandyContainer
of a Framework element. For instance, if the "cell1
" cell is a cell of the "GridBody1
" HandyContainer
, the following call will return a reference to GridBody1 HandyContainer
:
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;
}
}
}
CurrentCellName property
Remember that one of our requirements was that we would like that the location of the cells could be set by the use of panels inside the ItemTemplate
of the GridBody
. This means that the cells will not necessarily be displayed side by side in a single line. Therefore, we cannot designate a cell by using an index as it is usually done in a standard Grid
. In the case of elaborated layouts, it will not be clear which cell is designated by which index.
Therefore, we will force the use of a name for each cell, and will provide ways to manipulate the cells from their name.
At this time, we will add a CurrentCellName
property to the HandyContainer
. The CurrentCell
is the cell of the grid that holds the focus. The CurrentCellName
property will be filled with the name of the current cell.
We will also add a CurrentCellNameChanged
event. This event is raised when the current cell is changed.
public event EventHandler CurrentCellNameChanged;
private string currentCellName;
public string CurrentCellName
{
get { return currentCellName; }
internal set
{
if (currentCellName != value)
{
currentCellName = value;
OnCurrentCellNameChanged(EventArgs.Empty);
}
}
}
protected virtual void OnCurrentCellNameChanged(EventArgs e)
{
if (CurrentCellNameChanged != null)
CurrentCellNameChanged(this, e);
}
Cells
In the GoaOpen project, let's first create a Cell
abstract class that implements features shared by all the cells, whatever the data type they display is. The TextCell
class and the CheckBoxCell
class will inherit from the Cell
class.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Open.Windows.Controls
{
public abstract class Cell : Control
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (string.IsNullOrEmpty(this.Name))
throw new InvalidCastException("A cell must have a name");
}
private bool isFocused;
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (!isFocused)
{
VisualStateManager.GoToState(this, "Focused", true);
isFocused = true;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
parentContainer.CurrentCellName = this.Name;
}
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
isFocused = false;
VisualStateManager.GoToState(this, "Standard", true);
}
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
this.Focus();
}
}
}
}
In the OnApplyTemplate
, we make sure that the cell has a name. Cells are referenced by their names, and defining a name on each cell is mandatory.
The cell that holds the focus (this means that either the cell or one of the controls it contains has the focus) is the current cell. Therefore, when the cell gets the focus (watch the OnGotFocus
method), we notify its parent HandyContainer
by setting the value of the CurrentCellName
property.
Furthermore, we call the VisualStateManager.GoToState
method to switch the state of the cell to "Focused
". This way, we will be able to modify the look of the cell (we will do this in the style applied to the cell) when it becomes the current cell.
When the focus leaves the cell (watch the OnLostFocus
event), we switch the state of the cell back the "Standard
" value.
The OnMouseLeftButtonDown
method puts the focus on the cell when the user clicks on it.
TextCell
Code
The code of the TextCell
is very simple. A Text
property allows defining the text that the cell must display.
In the constructor, we define the default style that must be used by the TextCell
. We will add this style to the generic.xaml file in the next step.
using System.Windows;
namespace Open.Windows.Controls
{
public class TextCell : Cell
{
public static readonly DependencyProperty TextProperty;
static TextCell()
{
TextProperty = DependencyProperty.Register("Text",
typeof(string), typeof(TextCell), null);
}
public TextCell()
{
DefaultStyleKey = typeof(TextCell);
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
}
Style
We also need to implement the Style
of the TextCell
.
GOA Open is delivered with two generic files: generic.xaml and genericSL.xaml. The first file is the one used by default. It contains the default styles that are applied to the GOA open controls.
The genericSL.xaml file contains alternative styles for the GOA Open controls. When these styles are applied to the GOA controls, they have a look that is close to the look of the standard Silverlight controls. If you do not know how to use the styles provided in the genericSL.xaml file instead of the ones provided in the default generic.xaml file, read the instructions in the ReadMe.Txt file of the GoaOpen project.
In this tutorial, we will assume that you use the styles provided in the default generic.xaml file of the GoaOpen project. If this is not the case, we recommend reactivating them.
If you download the code of this tutorial, you will see that we also provide a genericSL file containing the Standard Sliverlight style for the grid.
Let's add at the end of the generic.xaml file a separator that clearly separates our styles from the other provided GoaOpen styles:
. . .
</Setter.Value>
</Setter>
</Style>
</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:
- We have set a default
Width
value. This way if the Width
of the cell is not defined in the ItemTemplate
, the default width will be applied. - The
TextCell
holds a TextBlock
that will display the value of the Text
property of the TextCell
. - The
FocusElement
is a dotted rectangle. It is collapsed by default, and becomes visible when the CommonStates
value becomes "Focused
". - A vertical line, built using a
Rectangle
that has a width of 1 pixel, is displayed at the right of the cell. This line is used to draw the right border of the cell.
After all these modifications, let's try to start our application again. But before that, we need to change the ItemTemplate
of the GridBody
control that is on the Page
of the GridBody Tutorial project.
Let's replace all the Border
/TextBlock
pairs with TextCell
. Do not forget to give a name to each cell:
<g:ItemDataTemplate>
<Grid>
<o:HandyDataPresenter DataType="GridBody.Person">
<g:GDockPanel>
<g:GStackPanel Orientation="Horizontal"
g:GDockPanel.Dock="Top">
<o:TextCell Text="{Binding FirstName}"
x:Name="FirstName"/>
<o:TextCell Text="{Binding LastName}"
x:Name="LastName"/>
<o:TextCell Text="{Binding Address}"
x:Name="Address"/>
<o:TextCell Text="{Binding City}"
x:Name="City"/>
<o:TextCell Text="{Binding ZipCode}"
x:Name="ZipCode"/>
</g:GStackPanel>
<o:TextCell Text="{Binding Comment}"
g:GDockPanel.Dock="Fill"
x:Name="Comment" Width="Auto"/>
</g:GDockPanel>
</o:HandyDataPresenter>
<o:HandyDataPresenter DataType="GridBody.Country">
<g:GStackPanel Orientation="Horizontal">
<o:TextCell Text="{Binding Name}"
x:Name="CountryName"/>
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"/>
</g:GStackPanel>
</o:HandyDataPresenter>
</Grid>
</g:ItemDataTemplate>
If we start the application now, we will notice that the cells are correctly displayed, and that when we click on a cell, it gets the focus. Nevertheless, the display is not perfect. We would like to have the ability to add horizontal lines between the rows.
But before doing this, let's write the code of the other kind of cells we would like to implement: the CheckBoxCell
.
CheckBoxCell
Code
Let's add a CheckBoxCell
class to the GoaOpen project.
using System;
using System.Windows;
namespace Open.Windows.Controls
{
public class CheckBoxCell : Cell
{
public static readonly DependencyProperty IsCheckedProperty;
public static readonly DependencyProperty CheckMarkVisibilityProperty;
private bool isOnReadOnlyChange;
static CheckBoxCell()
{
IsCheckedProperty = DependencyProperty.Register("IsChecked",
typeof(bool),
typeof(CheckBoxCell),
new PropertyMetadata(new PropertyChangedCallback(OnIsCheckedChanged)));
CheckMarkVisibilityProperty =
DependencyProperty.Register("CheckMarkVisibility",
typeof(Visibility),
typeof(CheckBoxCell),
new PropertyMetadata(Visibility.Collapsed,
new PropertyChangedCallback(OnCheckMarkVisibilityChanged)));
}
public CheckBoxCell()
{
DefaultStyleKey = typeof(CheckBoxCell);
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
private static void OnIsCheckedChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
CheckBoxCell cell = (CheckBoxCell)d;
cell.OnIsCheckedChanged((bool)e.NewValue);
}
protected virtual void OnIsCheckedChanged(bool isChecked)
{
isOnReadOnlyChange = true;
if (isChecked)
CheckMarkVisibility = Visibility.Visible;
else
CheckMarkVisibility = Visibility.Collapsed;
isOnReadOnlyChange = false;
}
public Visibility CheckMarkVisibility
{
get { return (Visibility)GetValue(CheckMarkVisibilityProperty); }
private set { SetValue(CheckMarkVisibilityProperty, value); }
}
private static void OnCheckMarkVisibilityChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
CheckBoxCell cell = (CheckBoxCell)d;
if (!cell.isOnReadOnlyChange)
throw new InvalidOperationException("Property is read only");
}
}
}
The code of this cell is quite simple. We have implemented two properties: IsChecked
and CheckMarkVisibility
. The IsChecked
property will be bound to the data. The CheckMarkVisibility
property allows defining if the CheckMark
that the cell will display is displayed or not. The CheckMarkVisibility
property value will depend on the IsChecked
property value.
Alternatively, we could have used two states: IsChecked
and IsNotChecked
, and use the same kind of process as with the focus element.
Style
<Style TargetType="o:CheckBoxCell">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush"
Value="{StaticResource DefaultListControlStroke}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground"
Value="{StaticResource DefaultForeground}"/>
<Setter Property="Cursor" Value="Arrow" />
<Setter Property="Width" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="o:CheckBoxCell">
<Grid Background="Transparent">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Standard"/>
<vsm:VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="focusElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Rectangle
x:Name="ShadowVisual"
Fill="{StaticResource DefaultShadow}"
Height="12"
Width="12"
RadiusX="2"
RadiusY="2"
Margin="1,1,-1,-1"/>
<Border
x:Name="BackgroundVisual"
Background="{TemplateBinding Background}"
Height="12"
Width="12"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="2"
BorderThickness="{TemplateBinding BorderThickness}"/>
<Grid
x:Name="CheckMark"
Width="8"
Height="8"
Visibility="{TemplateBinding CheckMarkVisibility}" >
<Path
Stretch="Fill"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
Data="M129.13295,140.87834 L132.875,145 L139.0639,137" />
</Grid>
<Rectangle
x:Name="ReflectVisual"
Fill="{StaticResource DefaultReflectVertical}"
Height="5"
Width="10"
Margin="1,1,1,6"
RadiusX="2"
RadiusY="2"/>
<Rectangle
Name="focusElement"
Stroke="{StaticResource DefaultFocus}"
StrokeThickness="1"
Fill="{TemplateBinding Background}"
IsHitTestVisible="false"
StrokeDashCap="Round"
Margin="0,1,1,0"
StrokeDashArray=".2 2"
Visibility="Collapsed" />
<Rectangle
Name="CellRightBorder"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="0.5"
Width="1"
HorizontalAlignment="Right"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now that we have already made the style of the TextBoxCell
, the CheckBoxCell
style seems quite simple.
- We have set a default
Width
value. - The visibility of the
CheckMark
element is bound to the CheckMarkVisibility
property. - The
FocusElement
and the vertical right border are managed exactly the same way in the TextCell
and in the CheckBoxCell
.
In order to see what a CheckBoxCell
looks like, let's add one in the ItemTemplate
of the GridBody
control of the GridBody project, and let's bind it to the IsCustomer
property of the Person
s:
<g:ItemDataTemplate>
<Grid>
<o:HandyDataPresenter DataType="GridBody.Person">
<g:GDockPanel>
<g:GStackPanel Orientation="Horizontal"
g:GDockPanel.Dock="Top">
<o:TextCell Text="{Binding FirstName}"
x:Name="FirstName"/>
<o:TextCell Text="{Binding LastName}"
x:Name="LastName"/>
<o:TextCell Text="{Binding Address}"
x:Name="Address"/>
<o:TextCell Text="{Binding City}"
x:Name="City"/>
<o:TextCell Text="{Binding ZipCode}"
x:Name="ZipCode"/>
<o:CheckBoxCell IsChecked="{Binding IsCustomer}"
x:Name="IsCustomer"/>
</g:GStackPanel>
<o:TextCell Text="{Binding Comment}"
g:GDockPanel.Dock="Fill"
x:Name="Comment" Width="Auto"/>
</g:GDockPanel>
</o:HandyDataPresenter>
<o:HandyDataPresenter DataType="GridBody.Country">
<g:GStackPanel Orientation="Horizontal">
<o:TextCell Text="{Binding Name}"
x:Name="CountryName"/>
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"/>
</g:GStackPanel>
</o:HandyDataPresenter>
</Grid>
</g:ItemDataTemplate>
5. HandyContainer Grid style
Introduction
We have not created a style for the HandyContainer
used to build our GridBody
yet.
GoaOpen already provides several styles for the HandyContainer
. The HandyStyle
property allows choosing between the provided styles. This property is an enumerator. A style is associated to each enumerator of the enumeration. When you choose a value for the HandyStyle
property, the HandyContainer
will look for the corresponding style in the generic.xaml file and apply it.
Until now, the ListStyle
was applied to the HandyContainer
that we have used to create our GridBody
. Nevertheless, we would like not to use the ListStyle
but a style of our own that we can change when we need to.
GridBody style
At this time, we will just make a copy of the ListStyle
that is provided in the generic.xaml file of GoaOpen.
- Find the
ListStyle
in the generic.xaml file - Copy it at the end of the file (just after the
CheckBoxCell
style) - Rename it to
GridBodyStyle
.
<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"/>
<Setter Property="HandyScrollerStyle" Value="StandardScrollerStyle"/>
<Setter Property="HandyItemsPanelModel" Value="StandardPanel" />
<Setter Property="HandyStatersModel" Value="StandardStaters"/>
<Setter Property="HandyDefaultItemStyle" Value="Calculated"/>
<Setter Property="HandyItemContainerStyle" Value="StandardItem"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="SelectionMode" Value="Single"/>
<Setter Property="IsTabStop" Value="False" />
<Setter Property="ShowColSeparators" Value="False"/>
<Setter Property="ShowRowSeparators" Value="False"/>
<Setter Property="ColSpace" Value="5"/>
<Setter Property="RowSpace" Value="5"/>
<Setter Property="ItemContainerDefinedStyle"
Value="{StaticResource EmptyStyle}"/>
<Setter Property="SeparatorStyle"
Value="{StaticResource Container_SeparatorStyle}"/>
<Setter Property="StandardItemStyle"
Value="{StaticResource Container_ItemStyle}"/>
<Setter Property="ListItemStyle"
Value="{StaticResource Container_ListItemStyle}"/>
<Setter Property="DetailsItemStyle"
Value="{StaticResource Container_ItemDetailStyle}"/>
<Setter Property="CheckBoxStyle"
Value="{StaticResource Container_CheckBoxStyle}"/>
<Setter Property="RadioButtonStyle"
Value="{StaticResource Container_RadioButtonStyle}"/>
<Setter Property="ToggleButtonStyle"
Value="{StaticResource Container_ToggleButtonStyle}"/>
<Setter Property="NodeStyle"
Value="{StaticResource Container_NodeStyle}"/>
<Setter Property="DropDownListStyle"
Value="{StaticResource Container_DropDownListStyle}"/>
<Setter Property="DropDownButtonStyle"
Value="{StaticResource Container_DropDownButtonStyle}"/>
<Setter Property="ColSeparatorsStyle"
Value="{StaticResource StandardColSeparatorStyle}"/>
<Setter Property="RowSeparatorsStyle"
Value="{StaticResource StandardRowSeparatorStyle}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="o:HandyContainer">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<Grid x:Name="ELEMENT_Root">
<g:Scroller
x:Name="ElementScroller"
Style="{TemplateBinding ScrollerStyle}"
Background="Transparent"
BorderThickness="0"
Margin="{TemplateBinding Padding}">
<g:GItemsPresenter
x:Name="ELEMENT_ItemsPresenter"
Opacity="{TemplateBinding Opacity}"
Cursor="{TemplateBinding Cursor}"
HorizontalAlignment =
"{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment =
"{TemplateBinding VerticalContentAlignment}"/>
</g:Scroller>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
HandyContainerStyle enum
We need that the new predefined style "GridBodyStyle
" can be applied to the HandyContainer
by choosing a new value for the HandyStyle
property of the HandyContainer
. To do this, we must add the GridBodyStyle
enumerator to the HandyContainerStyle
enum.
- In the GoaOpen project, locate the HandyContainerStyle file and open it (it is located in the GoaControls\HandyList\HandyList\HandyContainer folder).
- Add the new
GridBodyStyle
enumerator at the end of the list:
namespace Open.Windows.Controls
{
public enum HandyContainerStyle
{
None = 0,
ListStyle,
ShelfStyle,
VerticalShelfStyle,
ComboListStyle,
GridBodyStyle
}
}
Apply the style
Let's apply this new style to the MyGridBody
control contained in the Page
of the GridBody project:
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items"
HandyDefaultItemStyle="Node"
HandyStyle="GridBodyStyle">
Now, the GridBodyStyle
will be applied to the HandyContainer
. As we have not modified the style yet, at this time, we will not see any difference if we start our application.
6. ContainerItem styles
Introduction
The HandyContainer
control has a HandyDefaultItemStyle
property which allows choosing which style is applied to the items it contains. We have already used this property in our introduction when we applied the node style to the items. This property is an enumerator. It can have the following values: None
, Calculated
, ItemContainer
, Separator
, StandardItem
, ListItem
, DetailsItem
, CheckBox
, RadioButton
, ToggleButton
, Node
, DropDownList
, DropDownButton
.
Each one of these values is associated to a property of the HandyContainer
containing a style. The following properties are defined:
ItemContainerDefinedStyle
SeparatorStyle
StandardItemStyle
ListItemStyle
DetailsItemStyle
CheckBoxStyle
RadioButtonStyle
ToggleButtonStyle
NodeStyle
DropDownListStyle
DropDownButtonStyle
When we select the StandardItem
value for the HandyDefaultItemStyle
property of the HandyContainer
, the style that is defined in the StandardItemStyle
property is applied to each item of the HandyContainer
. When we select the Node
value for the HandyDefaultItemStyle
property of the HandyContainer
, the style that is defined in the NodeStyle
property is applied to each item of the HandyContainer
, and so on.
Until now, we have worked with the StandardItemStyle
(default style) and the NodeStyle
.
The same way we have defined our own GridBodyStyle
to apply to the HandyContainer
, we would like to define our own StandardItemStyle
and NodeStyles
that we can change when we need to.
Creating the styles
If you look at the GridBodyStyle
that we have created in the generic.xaml file, you will see these two properties defined:
<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 choose the StandardItem
value for the HandyDefaultItemStyle
, the new "Container_RowItemStyle
" style will be applied to each item of the HandyContainer
, and when we choose the "Node
" value for the HandyDefaultItemStyle
, the "Container_RowNodeStyle
" style will be applied to each item.
We still need to create both styles.
Here is the code for them. You can copy and paste them in the generic.xaml file.
Be careful to paste them just before the GridBodyStyle
style. As the GridBodyStyle
makes references to Container_RowItemStyle
and Container_RowNodeStyle
, it is better to define the two item styles before the HandyContainer
style.
<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"/>
<g:GContentPresenter
Grid.RowSpan="2"
x:Name="ELEMENT_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
OrientatedHorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
OrientatedMargin="{TemplateBinding Padding}"
OrientatedVerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"
PresenterOrientation=
"{TemplateBinding PresenterOrientation}"/>
<Rectangle x:Name="BorderElement" Grid.RowSpan="2"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"
Margin="-1,0,0,-1"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Container_RowNodeStyle" TargetType="o:HandyListItem">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Cursor" Value="Arrow" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0"/>
<Setter Property="Foreground"
Value="{StaticResource DefaultForeground}"/>
<Setter Property="Background" Value="White" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Indentation" Value="10" />
<Setter Property="IsTabStop" Value="True" />
<Setter Property="IsKeyActivable" Value="True"/>
<Setter Property="ItemUnpressDropDownBehavior" Value="CloseAll" />
<Setter Property="BorderBrush"
Value="{StaticResource DefaultListControlStroke}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="o:HandyListItem">
<Grid x:Name="LayoutRoot" Background="Transparent">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal"/>
<vsm:VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0"
Storyboard.TargetName=
"ELEMENT_ContentPresenter"
Storyboard.TargetProperty="Opacity"
To="0.6"/>
<DoubleAnimation Duration="0"
Storyboard.TargetName="ExpandedVisual"
Storyboard.TargetProperty=
"Opacity" To="0.6"/>
<DoubleAnimation Duration="0"
Storyboard.TargetName="SelectedVisual"
Storyboard.TargetProperty="Opacity"
To="0.6"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ExpandedReflectVisual"
Storyboard.TargetProperty=
"Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="SelectedReflectVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Duration="0"
Storyboard.TargetName="HasItem"
Storyboard.TargetProperty="Opacity"
To="0.6"/>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="FocusStates">
<vsm:VisualState x:Name="NotFocused"/>
<vsm:VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="MouseOverStates">
<vsm:VisualState x:Name="NotMouseOver"/>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="MouseOverVisual"
Storyboard.TargetProperty=
"Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ExpandedOverVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="PressedStates">
<vsm:VisualState x:Name="NotPressed"/>
<vsm:VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PressedVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectedStates">
<vsm:VisualState x:Name="NotSelected"/>
<vsm:VisualState x:Name="Selected">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="SelectedVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="HasItemsStates">
<vsm:VisualState x:Name="NotHasItems">
<Storyboard>
<DoubleAnimation Duration="0"
Storyboard.TargetName="ExpandedVisual"
Storyboard.TargetProperty="Opacity"
To="0"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="HasItems">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="HasItem"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="IsExpandedStates">
<vsm:VisualState x:Name="NotIsExpanded"/>
<vsm:VisualState x:Name="IsExpanded">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="CheckedArrow"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ArrowUnchecked"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ExpandedVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="AlternateStates">
<vsm:VisualState x:Name="NotIsAlternate"/>
<vsm:VisualState x:Name="IsAlternate">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="AlternateBackgroundVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="BackgroundVisual"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="InvertedStates">
<vsm:VisualState x:Name="InvertedItemsFlowDirection">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ArrowCheckedToTop"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ArrowCheckedToBottom"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="NormalItemsFlowDirection"/>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Rectangle Width="{TemplateBinding FullIndentation}" />
<Grid MinWidth="16" Margin="0,0,1,0">
<Grid x:Name="HasItem"
Visibility="Collapsed"
Height="16" Width="16"
Margin="0,0,0,0">
<Path x:Name="ArrowUnchecked"
HorizontalAlignment="Right" Height="8"
Width="8"
Fill="{StaticResource DefaultForeground}"
Stretch="Fill"
Data="M 4 0 L 8 4 L 4 8 Z" />
<Grid x:Name="CheckedArrow"
Visibility="Collapsed">
<Path x:Name="ArrowCheckedToTop"
HorizontalAlignment="Right"
Height="8" Width="8"
Fill="{StaticResource DefaultForeground}"
Stretch="Fill"
Data="M 8 4 L 0 4 L 4 0 z"
Visibility="Collapsed"/>
<Path x:Name="ArrowCheckedToBottom"
HorizontalAlignment="Right"
Height="8" Width="8"
Fill="{StaticResource DefaultForeground}"
Stretch="Fill"
Data="M 0 4 L 8 4 L 4 8 Z" />
</Grid>
<ToggleButton x:Name="ELEMENT_ExpandButton"
Height="16" Width="16"
Style="{StaticResource EmptyToggleButtonStyle}"
IsChecked="{TemplateBinding IsExpanded}"
IsThreeState="False" IsTabStop="False"/>
</Grid>
</Grid>
<Grid>
<Border x:Name="BackgroundVisual"
Background="{TemplateBinding Background}" />
<Rectangle
Fill="{StaticResource DefaultAlternativeBackground}"
x:Name="AlternateBackgroundVisual"
Visibility="Collapsed"/>
<Grid x:Name="ExpandedVisual"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Rectangle Fill="{StaticResource DefaultBackground}"
Grid.RowSpan="2"/>
<Rectangle x:Name="ExpandedOverVisual"
Fill=
"{StaticResource DefaultDarkGradientBottomVertical}"
Grid.RowSpan="2"
Visibility="Collapsed"
Margin="0,0,0,1"/>
<Rectangle x:Name="ExpandedReflectVisual"
Fill="{StaticResource DefaultReflectVertical}"
Margin="0,1,0,0"/>
</Grid>
<Grid x:Name="SelectedVisual"
Visibility="Collapsed" >
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Rectangle
Fill="{StaticResource DefaultDownColor}"
Grid.RowSpan="2"/>
<Rectangle x:Name="SelectedReflectVisual"
Fill="{StaticResource DefaultReflectVertical}"
Margin="0,1,1,0"
RadiusX="1" RadiusY="1"/>
</Grid>
<Rectangle x:Name="MouseOverVisual"
Fill="{StaticResource DefaultDarkGradientBottomVertical}"
Visibility="Collapsed" Margin="0,0,1,0"/>
<Grid x:Name="PressedVisual"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Rectangle Fill="{StaticResource DefaultDownColor}"
Grid.RowSpan="2"/>
<Rectangle
Fill="{StaticResource DefaultDarkGradientBottomVertical}"
Grid.Row="1" Margin="0,0,1,0"/>
<Rectangle Fill="{StaticResource DefaultReflectVertical}"
Margin="0,1,1,0"
RadiusX="1" RadiusY="1"/>
</Grid>
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="0.5"
Height="1"/>
<Rectangle x:Name="FocusVisual"
Stroke="{StaticResource DefaultFocus}"
StrokeDashCap="Round" Margin="0,1,1,0"
StrokeDashArray=".2 2"
Visibility="Collapsed"/>
<g:GContentPresenter
x:Name="ELEMENT_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Cursor="{TemplateBinding Cursor}"
OrientatedHorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
OrientatedMargin="{TemplateBinding Padding}"
OrientatedVerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"
PresenterOrientation=
"{TemplateBinding PresenterOrientation}"/>
<Rectangle x:Name="BorderElement"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"
Margin="-1,0,0,-1"/>
</Grid>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In order to avoid a boring editing process during this tutorial, we provide you the final Container_RowItemStyle
and the Container_RowNodeStyle
styles. This way, you can just make a copy/paste of them in the generic.xaml.
The Container_RowItemStyle
and the Container_RowNodeStyle
styles have been created by copying Container_ItemStyle
and Container_NodeStyle
and by adjusting a few elements in order that they look more like a grid row:
- We have set the value of the padding and the margin properties to 0 in order that there is no space displayed between the items (i.e., the rows) of the grid or between the border of the items and the cells they contain.
- We have also set the
HorizontalAlignement
value to Left
.
Thanks to these changes, we can now remove the DefaultItemModel
that was associated to the GridBody
of the GridBody project at the beginning of this tutorial.
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items"
HandyDefaultItemStyle="Node"
HandyStyle="GridBodyStyle">
<o:HandyContainer.ItemTemplate>
<g:ItemDataTemplate>
<Grid>
<o:HandyDataPresenter DataType="GridBody.Person">
<g:GDockPanel>
<g:GStackPanel Orientation="Horizontal"
g:GDockPanel.Dock="Top">
<o:TextCell Text="{Binding FirstName}"
x:Name="FirstName"/>
<o:TextCell Text="{Binding LastName}"
x:Name="LastName"/>
<o:TextCell Text="{Binding Address}"
x:Name="Address"/>
<o:TextCell Text="{Binding City}"
x:Name="City"/>
<o:TextCell Text="{Binding ZipCode}"
x:Name="ZipCode"/>
<o:CheckBoxCell
IsChecked="{Binding IsCustomer}"
x:Name="IsCustomer"/>
</g:GStackPanel>
<o:TextCell Text="{Binding Comment}"
g:GDockPanel.Dock="Fill" x:Name="Comment"
Width="Auto"/>
</g:GDockPanel>
</o:HandyDataPresenter>
<o:HandyDataPresenter DataType="GridBody.Country">
<g:GStackPanel Orientation="Horizontal">
<o:TextCell Text="{Binding Name}"
x:Name="CountryName"/>
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"/>
</g:GStackPanel>
</o:HandyDataPresenter>
</Grid>
</g:ItemDataTemplate>
</o:HandyContainer.ItemTemplate>
</o:HandyContainer>
Furthermore, we have added to the styles a Rectangle
named BorderElement
, and that will display the border of the items. We have also applied some small changes in order that the elements are well positioned inside the items.
We will not spend our time to explain each element and property of these styles. These styles are large, but there is nothing extraordinary with them. Nevertheless, if you have time, you can read them carefully, or better, try to modify them in order to deeply understand how they work.
Both styles are built the same way. The main difference between the two is that the Container_RowNodeStyle
style can handle nodes:
- The content of a node child is indented.
- If a node has children, an arrow is displayed in front of it, allowing expanding or collapsing the node.
If we start our application again, we can see that borders are displayed around the items of the grid (i.e., the rows) and that our grid looks nicer.
Missing line
However, there is a line missing between the two rows of the cells displayed inside the person items:
This is logical. In our styles, we have built the right border for the cells and the borders for the items (i.e., the rows), but we have to add the border between the rows of the cells ourselves.
If we look at the ItemDataTemplate
below, we will see that we have added a rectangle just before the TextCell
displaying the comment. This rectangle is used to display the separator line.
Do not forget to apply the same change to your ItemDataTemplate
.
<g:ItemDataTemplate>
<Grid>
<o:HandyDataPresenter DataType="GridBody.Person">
<g:GDockPanel>
<g:GStackPanel Orientation="Horizontal"
g:GDockPanel.Dock="Top">
<o:TextCell Text="{Binding FirstName}"
x:Name="FirstName"/>
<o:TextCell Text="{Binding LastName}"
x:Name="LastName"/>
<o:TextCell Text="{Binding Address}"
x:Name="Address"/>
<o:TextCell Text="{Binding City}"
x:Name="City"/>
<o:TextCell Text="{Binding ZipCode}"
x:Name="ZipCode"/>
<o:CheckBoxCell IsChecked="{Binding IsCustomer}"
x:Name="IsCustomer"/>
</g:GStackPanel>
<Rectangle Height="1"
Stroke="{StaticResource DefaultListControlStroke}"
StrokeThickness="0.5" Margin="-1,0,0,-1"
g:GDockPanel.Dock="Top"/>
<o:TextCell Text="{Binding Comment}"
g:GDockPanel.Dock="Fill" x:Name="Comment"
Width="Auto"/>
</g:GDockPanel>
</o:HandyDataPresenter>
<o:HandyDataPresenter DataType="GridBody.Country">
<g:GStackPanel Orientation="Horizontal">
<o:TextCell Text="{Binding Name}"
x:Name="CountryName"/>
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"/>
</g:GStackPanel>
</o:HandyDataPresenter>
</Grid>
</g:ItemDataTemplate>
If we try to start our application now, it will not work. The stroke value of the rectangle we have just added is linked to a DefaultListControlStroke
static resource that we have not defined yet.
The styles defined in the generic.xaml files of GoaOpen reference brushes and colors that are defined at the top of the file. This way, changing the default colors of the styles is easy: we just have to change the brushes and colors defined at the top of the file.
If we look at the top of the file, we will see that there are a lot of other predefined brushes and colors: such as "Background:Beige, StandardColor: Brown, ActionColor: Green" or "All Grey". In order to use these predefined brushes and colors instead of the default ones, you have to comment the default predefined brushes and colors and uncomment the ones you would like to use.
In order that our separator rectangle looks nice, we have applied the DefaultListControlStroke
resource value to its stroke. Nevertheless, as the DefaultListControlStroke
is defined in the generic.xaml file of the GoaOpen project, it is not accessible from our GridBody project. We have to make a copy of it.
Let's add it to the App.xaml file of our GridBody project.
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GridBody.App"
>
<Application.Resources>
<SolidColorBrush x:Key="DefaultListControlStroke"
Color="#FF99B0BB" />
</Application.Resources>
</Application>
7. Cell navigation
Introduction
We now have a grid body that looks well.
We have cells inside the items (i.e., the rows) of the grid body. We can navigate using the keyboard between the items of the grid, and we can navigate between the cells of an item by clicking on them.
The main missing feature that we must implement before finishing the first part of this tutorial is the ability to navigate from cell to cell using standard navigation keys such as the right and left arrows and the Home or End keys.
Keyboard navigation between cells inside an item
SpatialNavigator
In order to be able to navigate between the cells inside an item, we can use the SpatialNavigator
. The SpatialNavigator
is a class that can manage the key navigation between the children of a panel. By "connecting" the SpatialNavigator
to a panel, we automatically allow the user to navigate between the children of the panel using its keyboard (arrow keys, Home and End keys).
When moving the focus from one child to another, the SpatialNavigator
takes into account the location of the children, and moves the focus to the nearest element in the direction represented by the key pressed by the user. For instance, if the user presses the "down arrow" key, the SpatialNavigator
will find the closest element below the currently focused element, and will move the focus to it.
Let's add SpatialNavigator
s 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:
- Click on a cell in order that it becomes the current cell (it gets the focus).
- Use the right arrow, the left arrow, the Home key, or the End key to navigate between the cells.
Nevertheless, there is nothing to ensure that when we move the current cell from cell to cell, it keeps visible. We can verify this by performing the following actions:
- Start the application.
- Resize the grid in order that the last cells (of the rows) are not visible.
- Click on the first cell of a row in order that it becomes the current cell (it gets the focus).
- Use the end key to navigate to the last cell.
The last cell becomes the current cell, but the HandyContainer
does not scroll on the right in order to make it visible. In order to correct this problem, we are going to add the EnsureCellIsVisible
method to the HandyContainer
, and the GetPosition
method to the Cell
class.
GetPosition
The GetPosition
static method is used to know the position of the cell in comparison to another UIElement
.
Let's add this method to the code of the Cell
class:
public static Point GetPosition(Cell cell, UIElement element)
{
Point result = new Point();
MatrixTransform transform = null;
try
{
transform = cell.TransformToVisual(element) as MatrixTransform;
}
catch
{
}
result.X = transform.Matrix.OffsetX;
result.Y = transform.Matrix.OffsetY;
return result;
}
EnsureCellIsVisible
The purpose of the EnsureCellsVisible
method is to modify the HorizontalOffset
value of the HandyContainer
in order to make a cell visible.
In the EnsureCellIsVisible
method, we first call the Cell.GetPosition
method in order to have the position of the cell inside the ItemsHost
(the ItemsHost
is the panel that is inside the HandyContainer
and that contains the items of the HandyContainer
).
If the position of the Left
of the cell is to the left of the "left border" of the ItemsHost
, we change the HorizontalOffset
in order that the left of the cell is exactly at the "left border" of the ItemsHost
. If the position of the Right
of the cell is to the right of the "right border" of the ItemsHost
, we change the HorizontalOffset
in order that the right of the cell is exactly at the "right border" of the ItemsHost
.
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;
}
Call EnsureCellIsVisible
We need now to call EnsureCellIsVisible
when a cell becomes the current cell, i.e., when it gets the focus. Let's modify the code of the OnGotFocus
method of the Cell
class, as follows:
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (!isFocused)
{
VisualStateManager.GoToState(this, "Focused", true);
isFocused = true;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
parentContainer.CurrentCellName = this.Name;
parentContainer.EnsureCellIsVisible(this);
}
}
}
We can now restart our application and ensure that the current cell keeps visible.
- Start the application.
- Resize the grid in order that the last cells (of the rows) are not visible.
- Click on the first cell of a row in order that it becomes the current cell (it gets the focus).
- Use the End key to navigate to the last cell.
This time. the horizontal offset of the HandyContainer
is automatically modified in order that the current cell keeps visible.
Keyboard navigation between the cells of two items
Keeping cells in the same "column"
When moving from one item to another item using the up or down arrow key. or using the PageUp or PageDown key, we would like that the current cell of the source row becomes the current cell of the target row.
For instance, at this time, if the current cell is Address6 and I press the up arrow, the focus is moved to the item that is displayed above the current item. but no cell is focused anymore. In this case, we would like that Address5 becomes the current cell.
The first thing to do to be able to manage this case is to be able to tell an item (i.e., a row) which one of its cells must become the current cell. Let's extend the ContainerItem
class in order to be able to manage the cells it contains.
Extend the ContainerItem class
The ContainerItem
is the type that is used when creating the items of the HandyContainer
. We will add a feature to this class the same way we have added features to the HandyContainer
class.
First, let's create a ContainerItem
partial class in the Extensions\Grid folder of the GoaOpen project.
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
{
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;
}
GridSpatialNavigator
When the user presses the Up, Down arrow key, or the Page Up or the Page Down key, the focus is moved from item to item. This is possible because a SpatialNavigator
is linked to the ItemsHost
of the HandyContainer
. The ItemsHost
is the panel that is inside the HandyContainer
and that contains the items of the HandyContainer
.
We are going to enhance the SpatialNavigator
that is linked to the ItemHost
in order that it works as we would like.
Let's first create a GridSpatialNavigator
that inherits from the SpatialNavigator
in the Extensions\Grid folder.
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:
- A
TweenChildrenAnimator
is used to manage the animation of the items. - A
SpatialNavigator
is used to manage the key navigation between the items.
We would like that our GridSpatialNavigator
is used instead of the standard SpatialNavigator
. We do not want to change anything else at this time.
Let's modify the GridBodyStyle
that we have created at the end of the generic.xaml file.
Just before the line defining the Template
property of the GridBody
(<Setter Property="Template">
), let's add a new setter that describes the ItemsHost
to use:
<Setter Property="ItemsPanelModel">
<Setter.Value>
<g:GStackPanelModel>
<g:GStackPanelModel.ChildrenAnimator>
<g:TweenChildrenAnimator Duration="00:00:0.1"
TransitionType="Linear" />
</g:GStackPanelModel.ChildrenAnimator>
<g:GStackPanelModel.KeyNavigator>
<o:GridSpatialNavigator/>
</g:GStackPanelModel.KeyNavigator>
</g:GStackPanelModel>
</Setter.Value>
</Setter>
If we look at the GridBodyStyle
style, we will also see this property:
<Setter Property="HandyItemsPanelModel" Value="StandardPanel" />
The HandyItemsPanelModel
is an enum property. It allows to choose the panel that must be used as the ItemsHost
. It works the same way as the HandyStyle
property.
If we do not change the value of the HandyItemsPanelModel
property, the value we have set in the ItemsPanelProperty
will not be taken into account, and the value of the HandyItemPanelModel
property will be used instead. Of course, we could have enhanced the HandyItemsPanelModel
property in order that it allows us to select our new ItemsPanelModel
, but this is not the purpose of this tutorial.
Let's just set the HandyItemsPanelModel
property to the "None
" value. This way, it will not interfere with the ItemsPanelModel
property:
<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 the GOA Toolkit to be able to create a clone of the SpatialNavigator
when needed.
We will know that the GridSpatialNavigator
has processed a key and moved the focus to another item when the OnNavigatorSetKeyboardFocus
method of our HandyContainer
will be called. This is where we will take actions in order that the right cell has the focus.
But before doing this, to be sure we fully understand what we are doing, let's recall the different steps of the process when a user presses a key:
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 Down, Up, Page Down, or the Page Up keys, we force the cell having the CurrentCellName
name to have the focus.
Let's test our changes:
- Start the application.
- Click on the Address8 cell.
- Press the up arrow key.
The Address7 cell becomes the current cell (i.e., the cell that has the focus). This is the behavior we expected.
Ctrl-Home and Ctrl-End keys
Introduction
Let's start our application and analyze what happens when we press the Ctrl-Home and the Ctrl-End keys.
- Start the application.
- Click on the Address8 cell.
- Press the Ctrl-Home key.
FirstName8 becomes the current cell. If we had pressed the Ctrl-End key, the cell at the end of the current row would have become the current cell. This behavior is easily understandable: the SpatialNavigator
s that we have defined in our ItemTemplate
process the key pressed by the user and change the focus of the cells. Nevertheless, we would like that when the user presses the Ctrl-Home key, the first cell of the first row of the grid becomes the current cell - not the first cell of the current row. In the same way, we would like that when the user presses the Ctrl-End key, the last cell of the last row of the grid becomes the current cell - not the last cell of the current row.
Extend the ContainerItem
Our requirements are "when the user press the Ctrl-Home key, the first cell of the first row of the grid becomes the current cell" and "when the user presses the Ctrl-End key, the last cell of the last row of the grid becomes the current cell".
But, what are the first cell and the last cell? Remember that the location of the cells is set using panels inside the ItemTemplate
of the HandyContainer
. It means that cells are not necessarily located on one line, side by side.
We will postulate that the first cell of a row is the cell that is the closest to the top left corner of the row (i.e., the item), and that the last cell is the one that is close to the bottom right corner of the row.
Let's enhance our ContainerItem
partial class and write methods to find the first cell and the last cell of an item (i.e., a row).
private class CellPosition
{
public CellPosition(Cell cell, Point position)
{
Cell = cell;
Position = position;
}
public Cell Cell
{
get;
private set;
}
public Point Position
{
get;
private set;
}
}
private class CellPositionComparer : IComparer<CellPosition>
{
public int Compare(CellPosition x, CellPosition y)
{
if (x.Position.Y > y.Position.Y)
return 1;
else if (x.Position.Y < y.Position.Y)
return -1;
if (x.Position.X > y.Position.X)
return 1;
else if (x.Position.X < y.Position.X)
return -1;
return 0;
}
}
public string GetFirstCellName()
{
this.UpdateLayout();
List<CellPosition> cellPositions = new List<CellPosition>();
List<Cell> cells = GetCells();
UIElement rootVisual = Application.Current.RootVisual;
foreach (Cell cell in cells)
{
cellPositions.Add(new CellPosition(cell, Cell.GetPosition(cell, rootVisual)));
}
cellPositions.Sort(new CellPositionComparer());
foreach (CellPosition cellPosition in cellPositions)
{
if (cellPosition.Cell.IsTabStop)
return cellPosition.Cell.Name;
}
return null;
}
public string GetLastCellName()
{
this.UpdateLayout();
List<CellPosition> cellPositions = new List<CellPosition>();
List<Cell> cells = GetCells();
UIElement rootVisual = Application.Current.RootVisual;
foreach (Cell cell in cells)
{
cellPositions.Add(new CellPosition(cell, Cell.GetPosition(cell, rootVisual)));
}
cellPositions.Sort(new CellPositionComparer());
for (int cellIndex = cellPositions.Count - 1; cellIndex >= 0; cellIndex--)
{
CellPosition cellPosition = cellPositions[cellIndex];
if (cellPosition.Cell.IsTabStop)
return cellPosition.Cell.Name;
}
return null;
}
List<Cell> cellCollection;
private List<Cell> GetCells()
{
if (cellCollection == null)
{
cellCollection = new List<Cell>();
DependencyObject firstChild = GetFirstTreeChild();
if (firstChild != null)
AddChildrenCells(firstChild, cellCollection);
}
return cellCollection;
}
private void AddChildrenCells(DependencyObject parent, List<Cell> cellsCollection)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int index = 0; index < childrenCount; index++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, index);
Cell childCell = child as Cell;
if (childCell != null)
cellsCollection.Add(childCell);
else
AddChildrenCells(child, cellsCollection);
}
}
GetFirstCellName
first retrieves all the available cells by calling the GetCells
method. It then gets the position of all the cells and sorts them using the CellPositionCompare
comparer. Then, it returns the first cell that can have the focus (IsTapStop == true
). GetLastCellName
works the same way. The GetCells
method scans the VisualTree to find all the cells that are children of the ContainerItem
. In order to avoid scanning the VisualTree each time the GetCells
method is called, the result of the scan is cached in the cellCollection
collection.
Nevertheless, we must take into account that if a new template is applied to the ContainerItem
, cellCollection
will not be up-to-date anymore. Therefore, we must override the OnApplyTemplate
method and clear the collection cache:
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();
}
Enhance the GridSpatialNavigator
Now that we have methods that allow us to find the first and the last cell of a ContainerItem
, we can enhance our GridSpatialNavigator
in order that it takes care of the Ctrl-Home and Ctrl-End keys.
Let's first modify the KeyDown
and ActiveKeyDown
methods in order that if the user presses the Ctrl-Home or the Ctrl-End key, the default behavior of the navigator is not processed any more.
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 linked to. In our case, this panel is the ItemsHost
of our HandyContainer
and the panel is a GStackPanel
.
In the ProcessKey
method, the first thing to do is to move to the first item (or the last item) of the ItemsHost
. This is what we do when we call the gStackPanel.MoveToFirstIndex()
(or the gStackPanel.MoveToLastIndex()
) method.
Then, we have to find the first cell (or the last cell) and put the focus on it to make it the current cell. We must not forget to update the CurrentCellName
property value of the HandyContainer
at the same time.
RowSpatialNavigator
If we start our application now and try to navigate to the first cell of the first row by pressing the Ctrl-Home key, it does not work. The same problem occurs if we try to navigate to the last cell of the last row by pressing the Ctrl-End key. This is because the SpatialNavigator
s that we have defined in our ItemTemplate
are still processing the key pressed by the use. We have to replace these SpatialNavigator
s by SpatialNavigator
s of our own that do not process the Ctrl-Home and the Crl-End keys.
Let's create a new RowSpatialNavigator
class in the Extensions\Grid folder of the GoaOpen project, and modify the ActiveKeyDown
and KeyDown
methods in order that the Ctrl-Home and Ctrl-End keys are not processed anymore.
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 SpatialNavigator
s that we have defined in our ItemTemplate
of our GridBody
with 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>
_OnNavigatorSetKeyboardFocus
We are almost done but, not completely yet.
Let's try our changes:
- Start the application.
- Set the Address 8 cell as the current cell by clicking on it.
- Press the Ctrl-Home key.
The first cell of the first item becomes the current cell, as expected. Nevertheless, the selection does not follow our change. The item holding Address 8 is still selected. We can see it because its background remains orange.
The current value of the SelectionMode
property of the HandyContainer
is set to "Single
". It means that only one item can be selected at a time and that the selection "follows" the focus.
When we press the other navigation keys, the selection "follows" the focus. For instance, if we click on Address8, then on the Address7 cell and then on the Address6 cell, those cells gets the focus, and the items that contain those cells become the selected item: their backgrounds become orange. The same happens if we press the up or the down arrow key.
However, when we press the Ctrl-Home or the Ctrl-End key, the selection does not follow the focused cell. This is because, in the GridSpatialNavigator
, we have substituted our own code to the standard SpatialNavigator
code. In our code, in the ProcessKey
method, we have forgotten to "tell" the HandyNavigator
that we have changed the current item. This can be done by calling the "OnNavigatorSetKeyboardFocus
" method of the HandyContainer
.
Nevertheless, we will not modify the ProcessKey
method of the GridSpatialNavigator
to call this method, but we will make the change in the FocusCell
method of the ContainerItem
method.
This way, we will not have to take care of OnNavigatorSetKeyboardFocus
anymore. The FocusCell
method will call it when necessary.
As the OnNavigatorSetKeyboardFocus
method is a protected
method, let's first add an internal
_OnNavigatorSetKeyboardFocus
method to our HandyContainer
partial class:
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.
Tab key
Let's try to use the tab key inside our grid:
- Start the application.
- Click the Address8 cell to make it the current cell.
- Press the Tab key.
The City8 cell becomes the current cell. This is the behavior we expected.
The Address8 cell becomes the current cell again. This is also the behavior we expected.
- Now, click the Comment8 cell to make it the current cell.
- Press the Tab key.
The focus is moved to the first item of the grid. This is not at all the behavior we expected.
- Click the FirstName8 cell to make it the current cell.
- Press the Shift-Tab key.
The focus is moved to the item holding the FirstName8 cell. This is not the behavior we expected.
When the first cell of an item is the current cell and if we press the Shift-Tab key, we would like that the last cell of the previous item becomes the current cell. When the last cell of an item is the current cell and if we press the Tab key, we would like that the first cell of the next item becomes the current cell.
Let's modify our GridSpatialNavigator
in order to implement these two features.
GridSpatialNavigator
First, let's modify the ActiveKeyDown
and KeyDown
methods to be sure that the ProcessKey
method is called when the user presses the Tab key:
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 GetFirstCellName
(or GetLastCellName
) of the ContainerItem
.
Next, we get the previous item (or the next item) by using the PrevNode
(or NextNode
) property of the current item. After that, we make sure that the last cell (or the first cell) of the previous item (or the next item) is the current cell.
We also call the gStackPanel.EnsureVisible
method in order to be sure that the new current item is located in the display area of the ItemHost
of the HandyContainer
control.
Enter key
When used inside a grid, usually the Enter key has the same behavior as the down arrow key. Let's modify the ActiveKeyDown
and KeyDown
methods of our GridSpatialNavigator
to implement this feature.
public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
LastKeyProcessed = e.Key;
LastModifier = Keyboard.Modifiers;
if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
((Keyboard.Modifiers & ModifierKeys.Control)
!= ModifierKeys.Control)) && (e.Key != Key.Tab))
{
if (e.Key == Key.Enter)
e.Key = Key.Down;
LastKeyProcessed = e.Key;
base.ActiveKeyDown(container, e);
}
else
ProcessKey(container, e);
}
public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
LastKeyProcessed = e.Key;
LastModifier = Keyboard.Modifiers;
if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
((Keyboard.Modifiers & ModifierKeys.Control)
!= ModifierKeys.Control)) && (e.Key != Key.Tab))
{
if (e.Key == Key.Enter)
e.Key = Key.Down;
LastKeyProcessed = e.Key;
base.KeyDown(container, e);
}
else
ProcessKey(container, e);
}
Focus on the item
Introduction
In some cases, the focus can go on the item rather than on the cell.
We can test the two following cases:
- Click exactly on the line separating the two items
- Click on the FirstName0 cell and then press the up arrow key.
These two cases are easily explainable.
In the first case, by clicking on the line, we click on an item rather than on a cell. The item gets the focus.
In the second case, the cells contained in the first row are not the same as the cells contained in the second row. Therefore, when moving from one row to another, the GridBody "does not know" on which cell of the item to put the focus.
We could easily modify the two behaviors described above by -for instance- forcing the first cell of an item to become the current cell if no other cell has the focus.
Nevertheless, we will let you implement this feature yourself if you want.
In this tutorial, we will postulate that the fact that an item gets the focus rather than a cell is not an unwanted behavior. Rather than avoiding this to happen, we will provide the user an easy way to move the focus to the first cell or the last cell of the item when it happens.
Modify the ContainerItem class
Let's modify the ContainerItem
class to allow the user to use the "Home" and the "End" keys to make the first or the last cell of the item the current cell.
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled)
{
if ((e.Key == Key.Home) &&
((Keyboard.Modifiers & ModifierKeys.Control)
!= ModifierKeys.Control))
{
string firstCellName = GetFirstCellName();
if (!string.IsNullOrEmpty(firstCellName))
{
if (FocusCell(firstCellName))
e.Handled = true;
}
}
if ((e.Key == Key.End) &&
((Keyboard.Modifiers & ModifierKeys.Control)
!= ModifierKeys.Control))
{
string lastCellName = GetLastCellName();
if (!string.IsNullOrEmpty(lastCellName))
{
if (FocusCell(lastCellName))
e.Handled = true;
}
}
}
}
8. Helper methods
In this section, we will add a few methods to help manipulate the grid from code.
FindCell
Let's add a FindCell
method to our ContainerItem
class. This method will accept a cell name as parameter, and will return the cell having this name.
public Cell FindCell(string cellName)
{
FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
if (firstChild != null)
return firstChild.FindName(cellName) as Cell;
return null;
}
CurrentItem
The current item is the ContainerItem
that has the focus, or it is the ContainerItem
that contains a cell that has the focus.
The HoldFocusItem
property of the HandyContainer
contains the item that has the focus or the item that contains a control that has the focus. The HoldFocusItem
property does not return a ContainerItem
, but it returns the element of the ItemsSource
collection that is linked to it.
Therefore, we can retrieve the current item from the HoldFocusItem
. Nevertheless, we will add a CurrentItem
property to our HandyContainer
partial class to provide a convenient way to access the CurrentItem
.
The GetElement
method of the HandyContainer
allows finding a ContainerItem
from the element of the ItemsSource
collection that is linked to it. We are using this method to retrieve the ContainerItem
from the HoldFocusItem
.
public ContainerItem CurrentItem
{
get { return this.GetElement(this.HoldFocusItem) as ContainerItem; }
}
CurrentItemIndex
The CurrentItemIndex
is the index of the current item. This index is the number of items that are stacked on top of the current item.
This number depends on the fact that some nodes may be collapsed or not. In the first picture below, the index of the current item is 12, whereas in the second picture below, the index of the current item is 2.
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;
}
}
9. How to use the GridBody
Create a GridBody
As a reminder, here is the list of the steps needed to create a GridBody
:
- Add a
HandyContainer
to your page and change its HandyStyle
to the "GridBodyStyle
" value.
- If you expect to handle a lot of data, set the
VirtualMode
property of the HandyContainer
to "On
". - If you expect to display hierarchical data in the grid body, set the
HandyDefaultItemStyle
property value to "Node
". - If you would like that the items are displayed using an alternate background, set the
AlternateType
property value to "Items
".
- Prepare the data.
- Create the data classes that will hold the data displayed in the
Grid
. These classes should inherit from ContainerDataItem
(in our tutorial, we created the Person
and Country
classes). - Fill the
ItemsSource
of the GridBody
with a collection holding your data. Ideally, the collection will be a GObservableCollection
, but it is not mandatory.
- Fill the
ItemTemplate
of the GridBody
.
- Preferably use an
ItemDataTemplate
rather than a DataTemplate
. - If you are displaying items of different kinds (for instance, persons and countries), use
HandyContentPresenter
s to separate their description in the ItemTemplate
. - Place the
Cell
s inside the ItemDataTemplate
using panels, and link RowSpatialNavigator
s to these panels. - Do not forget to name your cells.
- Do not forget to bind the data to your cells.
GridBody, ContainerItems, and Cells members
You can use all the methods, properties, and events that we have implemented, as well as the ones that are already implemented in the HandyContainer
and ContainerItems
classes and their ancestors. We will provide here a small description of the most important ones. You can read the Help file provided with GOA Toolkit to have a look at all of them.
GridBody members
VerticalOffset, HorizontalOffset, ViewportHeight, ViewportWidth, ScrollableHeight, and ScrollableWidth
VerticalOffset
is the index of the item that is displayed at the top of the displayed area.HorizontalOffset
is the distance (in pixels) between the left of the items and the left of the displayed area.ViewportHeight
is the number of items displayed in the displayed area.ViewportWidth
is the width of the display area.ScollableHeight
is the total number of items that can be displayed.ScrollableWidth
is the width of the largest item.
VerticalOffsetChanged, HorizontalOffsetChanged ,VerticalScrollSettingsChanged, HorizontalScrollSettingsChanged
- The
VerticalOffsetChanged
and the HorizontalOffsetChanged
events occur when the VerticalOffset
value or the HorizontalOffset
value is changed. VerticalScrollSettingsChanged
and HorizontalScrollSettingsChanged
occur when the ViewportHeight
value or the ViewportWidth
value is changed.
EnsureItemVisible
Call the EnsureItemVisible(ItemIndex)
method to be sure that the item at the ItemIndex
index is displayed in the display area of the control.
GetItemFormIndex
This method will return the ContainerItem
from its index.
CurrentItemIndex
This property will return the index of the current item.
CurrentItem
This property will return the ContainerItem
that is the current item.
HoldFocusItemChanged
HoldFocusItem
is the item that has the focus or that contains a control that has the focus. The HoldFocusItemChanged
event occurs when the HoldFocusItem
is changed, and therefore it occurs when the CurrentItem
is changed.
CurrentCellName
This property contains the name of the current cell.
OnCurrentCellNameChanged
The OnCurrentCellNameChanged
event occurs when the current cell name changes.
Items
The Items
property contains all the items "linked" to the HandyContainer
.
This property can be disturbing at first because when the ItemsSource
of the HandyContainer
is set, it will not return a collection of all the ContainerItem
s of the HandyContainer
, but it will return a collection of all the elements from which the ContainersItem
s are generated (these elements come from the ItemsSource
).
If you wonder why the Items
property does not return a collection of ContainerItem
s, remind that the HandyContainer
can work in VirtualMode. In this case, only a part of the ContainerItem
s are generated from the ItemsSource
.
GetElement
This method allows retrieving a ContainerItem
from an element of the ItemsSource
.
For instance, you can write:
ContainerItem firstContainerItem = MyGridBody.GetElement(MyGridBody.Items[0]);
GetItemSource (static method)
The GetItemSource
method is the opposite of the GetElement
method. This method retrieves the source that was used to generated a ContainerItem
.
VirtualMode
If set to "On
", the HandyContainer
will not generate all the ContainerItem
s from the ItemsSource
elements. Only the items displayed are generated.
VirtualPageSize
When working using the VirtualMode, the VirtualPageSize
property contains the number of ContainerItem
s that are generated from the ItemsSource
.
VirtualPageStartIndex
When working using the VirtualMode, the VirtualPageStartIndex
property contains the index of the first element of the ItemsSource
from which the ContainersItem
s are generated.
VerticalScrollbarVisibility and HorizontalScrollbarVisibility
These properties allow to show or hide the scrollbars.
ItemClick
This event occurs when an item is clicked.
SelectionMode
SelectionMode
allows defining the way the items (i.e., the rows) can be selected by the user. In this tutorial, the default value (single selection mode) was used, but you can use another one such as None
or Multiple
.
SelectedItem
The item that is currently selected (if any). If several items are selected, this property holds the last item that was selected.
SelectedItems
The items that are currently selected (if any).
SelectedItemChanged
This event occurs when the SelectedItems
collection has changed.
SelectedItemChanging
This event occurs just before the SelectedItems
collection has changed
UIItemIsExpandedChanged
This event occurs when the IsExpanded
property of a ContainerItem
has changed.
ContainerItems members
FocusCell
This method allows focusing a cell and making it the current cell.
GetFirstCellName
The method returns the name of the first cell of the ContainerItem
. The first cell is the cell that is the closest to the top left corner.
GetLastCellName
The method returns the name of the last cell of the ContainerItem
. The last cell is the cell that is the closest to the right bottom corner.
FindCell
The method returns a cell from its name.
IsExpanded
If the item has children items (i.e., if the item is a node), this property allows to get or set whether the node is open or not (i.e., whether the children items are displayed or not).
IsExpandedChanged
This event occurs when the IsExpanded
property value has changed.
CollapseAll
When the CollapseAll
method of an item is called the IsExpanded
property value of the item and the IsExpanded
property of all its children (and grandchildren) are set to false
.
ExpandAll
When the CollapseAll
method of an item is called, the IsExpanded
property value of the item and the IsExpanded
property of all its children (and grandchildren) are set to true
.
Items
The Items
property contains all the children items "linked" to the Item
.
When the ItemsSource
of the HandyContainer
is set, it will not return a collection of ContainerItem
s, but it will return a collection of all the elements from which the ContainerItem
s are generated. Read the description of the Items
property of the HandyContainer
above to know more.
NextNode
This property will return the item that is just below the current item.
PrevNode
This property will return the item that is just above the current item.
Virtual Mode
Working with a HandyContainer
when the VirtualMode is set to "On
" can be disturbing at first.
The main concept that must be understood is that all the ContainerItem
s 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 ContainerItem
s 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 ContainerItem
s 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 ContainerItem
s, but the source element they are linked to. This is the case of the HoldFocusItem
, SelectedItem
, Items
, PressedItem
, and MouseOverItem
properties, for instance. If you have the reference to an element of the ItemsSource
collection and want to find the ContainerItem
that is linked to it, use the GetElement
method of the HandyContainer
. Nevertheless, this method will return a ContainerItem
only if it is located in the display area of the control and has been generated.
Exercises
Setting the current cell from the outside
Before setting the current cell, you must make sure the ContainerItem
holding the cell is located in the display area of the grid's body. Then, you can use the FocusCell
method of the ContainerItem
.
Let's suppose we would like that the City cell of the 100th item becomes the current cell. We can write:
MyGridBody.EnsureItemVisible(100);
((ContainerItem) MyGridBody.GetItemFromIndex(100)).FocusCell("City");
Knowing when the current cell has changed
In order to know when the current cell has changed, we have to monitor two events: CurrentCellNameChanged
and HoldFocusItemChanged
. The HoldFocusItemChanged
event will occur each time the current item changes. The CurrentCellNameChanged
event will occur each time the current cell name changes.
Examples
- The
CurrentCell
is City8, and the user clicks the Address8 cell. The CurrentCellNameChanged
event occurs, but the HoldFocusItemChanged
event does not.
- The
CurrentCell
is City8, and the user clicks the City7 cell. The HoldFocusItemChanged
event occurs, but the CurrentCellNameChanged
event does not.
- The
CurrentCell
is City8, and the user clicks the Address7 cell.
Both the HoldFocusItemChanged
and the CurrentCellNameChanged
events occur.
10. Conclusion
Congratulation for having reached the end of this long tutorial. Now that we have laid the grounds of our data grid, the next tutorials will be shorter. Do not miss them.
This tutorial is part of a set. You can read Step 2 here: Build Your Own DataGrid for Silverlight: Step 2.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.