1. Introduction
Tutorial overview
This article is the second part of a tutorial explaining how to build a full featured data grid using Silverlight and the GOA Toolkit.
In the first part of this tutorial, we explained the way of creating a read-only grid's body. In this second part, we will focus on the implementation of editing and validation features. In the third part, we will turn our attention to the headers of the grid.
All along this article, I will assume that you have completed the first part of the tutorial. If it is not the case, I strongly advise to do so. You can access it here: Build Your Own DataGrid for Silverlight: Step 1.
Get Started
This tutorial was written using the free edition of the GOA Toolkit 2009 Vol. 1 Build 251. If you have already implemented the first step of the tutorial before this article was released, you may need to upgrade. Check that you are working with the GOA Toolkit 2009 Vol. 1 Build 251 or after (and not build 212).
Be sure to have installed this release or a more recent one on your computer (www.netikatech.com/downloads).
The steps in the implementation of the Editing and the Validation process are not very difficult, but the steps are numerous. If you are lost when reading this article, or if you have difficulties to understand the purpose of what we are doing, we suggest that you consult the "cells' editing process" and the "items' validation process" pictures at the end of this tutorial. These two pictures will give you an overview of the editing and validation processes. It may be a good idea to print these two pictures before starting reading this article and to keep an eye on them while you are performing each step of the tutorial.
This second article starts exactly where the first one stopped. Before going further, you should open the GridBody solution that was created during the first step of the tutorial.
2. Basic Cell Editing
Cell States
During the first part of the tutorial, we have implemented two possible common states for the cells: Standard
and Focused
.
In the code of the cell, we have made the distinction between the two states by using the isFocused
private field. Now that we are going to implement editing features, we must add a new state to the cells: the Edited
state.
The isFocused
boolean field is too poor to allow us to keep track of a the new Edited
state. We have to replace the isFocused
field by something more elaborate: the CellState
enum.
Let's first add this enum to our GoaOpen\Extensions\Grid folder:
namespace Open.Windows.Controls
{
public enum CellState
{
Standard = 0,
Focused,
Edited
}
}
Let's also replace the isFocused
field of the Cell
class by a CellState
property and update the OnGotFocus
and the OnLostFocus
methods.
public CellState CellState
{
get;
private set;
}
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (this.CellState != CellState.Focused)
{
if (this.CellState != CellState.Edited)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
}
HandyContainer parentContainer =
HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
parentContainer.CurrentCellName = this.Name;
parentContainer.EnsureCellIsVisible(this);
}
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this,
currentFocusedElement as DependencyObject))
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
Note that, in the OnGotFocus
method, we have added a new condition: this.CellState != CellState.Edited
.
This is because we do not want the OnGotFocus
method to revert the cell's state back to the "Focused" state when the cell will be edited.
TextCell
Edit when click
We would like that when the user clicks on the current cell, the cell state becomes "Edited
". In the case of the TextCell
, it means that when the user will click on the cell, a TextBox
will be displayed in the cell in order that the user can edit the text of the cell.
BeginEdit method
The first thing to do is to add a BeginEdit
method to the Cell
class. This method will be called each time the cell state must become Edited
.
internal bool BeginEdit()
{
if (this.CellState == CellState.Edited)
return true;
if (this.IsTabStop)
{
VisualStateManager.GoToState(this, "Edited", true);
bool isEditStarted = OnBeginEdit();
if (isEditStarted)
this.CellState = CellState.Edited;
return isEditStarted;
}
return false;
}
protected abstract bool OnBeginEdit();
In this method, we first check if the cell state is already "Edited
". If the cell state is "Edited
", then we have nothing to do and we exit the method.
Then, we check if the cell can have the focus (IsTabStop
). If the cell cannot have the focus, then it cannot be edited.
Next, we change the Visual State of the cell by calling the GoToState
method of the VisualStateManager
. This will allow us to perform some actions in the template of the cell (which is defined in the style of the cell at the end of the generic.xaml file). For instance, in the case of the TextCell
, we will add a TextBox
to the Template of the cell and will display it when the state of the cell becomes "Edited
". In the case of other kinds of cells, we can perform other actions such as display a ComboBox
or a DatePicker
.
After that, we call the OnBeginEdit
abstract method. This method must be overridden for each type of cell. In the case of the TextCell
, we will use this method to initialize the TextBox
that is displayed when the cell is edited.
TextCell style
We must now update the TextCell
style in order to take into account the Edited
state. Let's go to the end of our generic.xaml file and modify the TextCell
style. In the Template of the TextCell
style, let's locate the TextElement
and replace it with the following elements:
<Grid>
<TextBlock
x:Name="TextElement"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<TextBox
x:Name="TextBoxElement"
Visibility="Collapsed"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"/>
</Grid>
We now have two elements bound to the Text
property of the TextCell
: the TextElement TextBlock
and the TextBoxElement TextBox
. By default, the TextBox
element is collapsed (hidden). We must add the "Edited
" Visual State to the Template and make the TextBoxElement
visible when this state becomes "Active
":
<vsm:VisualState x:Name="Edited">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TextBoxElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
After these changes, the TextCell
style will look like this:
<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:VisualState x:Name="Edited">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="TextBoxElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<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>
<Grid>
<TextBlock
x:Name="TextElement"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"/>
<TextBox
x:Name="TextBoxElement"
Visibility="Collapsed"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"/>
</Grid>
<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>
OnApplyTemplate method
We must be able to access the TextBoxElement
we have just added in the Template of the TextCell
from our code.
Let's add a TextBoxElement
field to the TextCell
class. We will retrieve a reference to the TextBoxElement
as soon as the template will be applied to the TextCell
, i.e., in the OnApplyTemplate
method of the TextCell
:
private TextBox textBoxElement;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
textBoxElement = GetTemplateChild("TextBoxElement") as TextBox;
}
OnBeginEdit method
In the TextCell
class, we also need to override the OnBeginEdit
method of the Cell
class in order to initialize the TextBoxElement
when the cell state of the cell becomes "Edited
".
protected override bool OnBeginEdit()
{
if (textBoxElement != null)
{
if (textBoxElement.Focus())
{
textBoxElement.SelectionStart = textBoxElement.Text.Length;
return true;
}
}
return false;
}
In this method, we put the focus on the TextBoxElement
and put the selection at the end of the existing text. This way, the user can start editing the text immediately. According to your preference, you may wish that the whole text of the TextBoxElement
is selected when the user starts editing the cell. In that case, you have to call the SelectAll
method of TextBoxElement
in the code above.
We must also override the OnBeginEdit
method of the CheckBoxCell
class; otherwise, we will not be able to build our project. As we do not take care of the CheckBoxCell
at this time, let's just implement a minimal OnBeginEdit
method:
protected override bool OnBeginEdit()
{
return true;
}
Start editing on click
Our initial requirement was that the user is able to start editing the current cell by clicking on it. Therefore, we have to override the OnMouseLeftButtonDown
of the TextCell
in order to implement this feature.
(Be careful: currently, you should have the CheckBoxCell
file opened, but you must override the OnMouseLeftButtonDown
method inside the TextCell
file. Do not forget to open the correct file before applying your changes.)
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (this.CellState == CellState.Focused)
BeginEdit();
if (this.CellState == CellState.Edited)
e.Handled = true;
}
Note that in the method above, we check that the cell is the current cell (this.CellState == CellState.Focused
) before calling the BeginEdit
method. This is because we do not want the cell to be edited as soon as the user clicks on it. It is only when the user clicks on the current cell that the cell becomes edited.
Let's build our project.
As we have changed the code of some classes, we may need to add some using
clauses at the top of the corresponding files. If you are not able to build the project, and have errors like: "The type or namespace name 'ATypeName' could not be found (are you missing a using directive or an assembly reference?)", it means that there are some using
clauses missing in the corresponding file.
The quickest way to resolve the error is:
- Double click the error in order that the corresponding file is opened.
- In the code window, right click the keyword causing the error (it will be already highlighted).
- In the context menu that opens beside the faulted keyword, click the "Resolve" item.
Let's try our changes.
If we start our program and click on a TextCell
, the cell becomes the current cell. If we click a second time on the cell, the textbox is displayed, allowing us to edit its content.
Nevertheless, we are facing several problems:
- The style of the
TextBox
displayed in the cell is ugly. It is too high, and displayed with unattractive borders and an unwanted background color. - The characters that we type in the textbox are lost as soon as we leave the cell.
CellTextBoxStyle
The default TextBox
style that is currently used in the TextCell
style does not suit the cell's look. Let's create a new style for the textbox that will be used inside the TextCell
and any other cell that needs to display a TextBox
.
In the generic.xaml file of the GoaOpen project, just before the TextCell
style, let's add the new style:
<Style x:Key="CellTextBoxStyle" TargetType="TextBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0" />
<Setter Property="BorderBrush" Value="{StaticResource DefaultListControlStroke}" />
<Setter Property="SelectionBackground" Value="{StaticResource DefaultDownColor}" />
<Setter Property="SelectionForeground" Value="{StaticResource DefaultForeground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Grid x:Name="RootElement">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" />
<vsm:VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0"
Storyboard.TargetName="ContentElement"
Storyboard.TargetProperty="Opacity"
To="0.6"/>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Border x:Name="DisabledVisual"
Background="{StaticResource DefaultDisabled}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1" CornerRadius="1"
Visibility="Collapsed"
IsHitTestVisible="False"/>
<ScrollViewer x:Name="ContentElement"
Style="{StaticResource ScrollViewerStyle}"
Padding="{TemplateBinding Padding}"
IsTabStop="False"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This style was created from the standard TextBox
style. We have removed the unnecessary states, borders, and background.
In the Template property of the style of the TextCell
, let's apply this new style to the TextBoxElement
:
<TextBox
x:Name="TextBoxElement"
Style="{StaticResource CellTextBoxStyle}"
Visibility="Collapsed"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"/>
If we start our application again and edit a TextCell
, we can see that the look of the edited cell is a lot better than before.
Apply the edit changes
Now that we have a cell that can be edited, we would like to implement the following features:
- If the current cell is edited and the user navigates to another cell, the changes the user has made to the text of the cell are applied to the property the cell is bound to. For instance, if the cell is bound to the
FirstName
property of the Person
class, we would like that the FirstName
property value is updated when the user navigates to another cell. - If the current cell is edited and the user presses the "Esc" key, the cell leaves the
Edited
state; it goes back to the Focus
state and all the changes made by the user to the text of the cell are discarded.
TwoWay binding
When we applied binding to our cells in the first part of this tutorial, we did not specify the mode of binding.
Let's take the FirstName
property of the Person
class as a sample. This property is bound to the Text
property of the FirstName TextCell
. Thanks to the bindings we have set in the ItemTemplate
of our grid's body, any change applied to the value of the Person
's FirstName
property is also applied to the Text
of the FirstName TextCell
. As we would like that the user is able to edit the cells, we would also like that any change applied to the Text
property of the TextCell
is also applied to the FirstName
property of Person
.
In order to achieve this, we have to tell the binding to do so. This change is easy to apply as binding provides a "TwoWay
" mode that behaves exactly this way. Let's apply this change to all our cells except the ChildrenCount
cell.
Open the Page.xaml file of the GridBody project and replace the ItemDataTemplate
of the HandyContainer
with the following one:
<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, Mode=TwoWay}"
x:Name="FirstName"/>
<o:TextCell Text="{Binding LastName, Mode=TwoWay}"
x:Name="LastName"/>
<o:TextCell Text="{Binding Address, Mode=TwoWay}"
x:Name="Address"/>
<o:TextCell Text="{Binding City, Mode=TwoWay}"
x:Name="City"/>
<o:TextCell Text="{Binding ZipCode, Mode=TwoWay}"
x:Name="ZipCode"/>
<o:CheckBoxCell IsChecked="{Binding IsCustomer, Mode=TwoWay}"
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, Mode=TwoWay}"
x:Name="CountryName"/>
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"/>
</g:GStackPanel>
</o:HandyDataPresenter>
</Grid>
</g:ItemDataTemplate>
CommitEdit
The same way we have created a BeginEdit
method that allows us to "force" the cell to edit, we are going to implement a CommitEdit
method that will force the cell to apply the user changes and to leave the Edited
state.
Let's add the CommitEdit
method to the Cell
class:
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
OnCommitEdit();
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
return true;
}
protected abstract void OnCommitEdit();
This method has a keepFocus
parameter allowing to know if, after having left the Edited
stated, the state of the cell must be set back to the Focused
state or the Standard
state.
The same way we have created an OnBeginEdit
method when we have created the BeginEdit
method, we have also created an OnCommitEdit
abstract method. This method must be overridden in the inherited cells in order to apply the change made by the user. In the case of the TextCell
, the implementation of the OnCommitEdit
is very short:
protected override void OnCommitEdit()
{
if (textBoxElement != null)
this.Text = textBoxElement.Text;
}
As we do not take care of the CheckBox
cell at this time, let's implement a minimal OnCommitEdit
to the CheckBoxCell
class:
protected override void OnCommitEdit()
{
}
Our requirement was to apply the changes made by the user when she/he navigates to another cell. This can be translated to the following requirement: call the CommitEdit
method when the current cell is not the current cell anymore. It can also be translated to: call the CommitEdit
method in the OnLostFocus
method of the cell.
So, let's modify the OnLostFocus
method of the Cell
class accordingly:
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
if (CellState == CellState.Edited)
CommitEdit(false);
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
}
CancelEdit
We have a BeginEdit
method to force the cell to go to the "Edited
" state, and a CommitEdit
method to force an edited cell to commit the changes made by the user and leave the edited state. We also need a method that forces an edited cell to discard the changes and leave the Edited
state.
We will implement this method on the Cell
class the same way as the two other ones, and we will also create an OnCancelEdit
abstract method that must be overridden in the inherited cells' classes.
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
OnCancelEdit();
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
return true;
}
protected abstract void OnCancelEdit();
The OnCancelEdit
method implementation in the TextCell
class is as short as the OnCommitEdit
implementation:
protected override void OnCancelEdit()
{
if (textBoxElement != null)
textBoxElement.Text = this.Text;
}
For now, we will implement an empty OnCancelEdit
method in the CheckBoxCell
class:
protected override void OnCancelEdit()
{
}
Our second requirement was: "If the current cell is edited and the user presses the "Esc" key, the cell leaves the Edited
state; it goes back to the Focused
state and all the changes made by the user on the text of the cell are discarded."
This can be implemented by overriding the OnKeyDown
method of the Cell
class and by calling the CancelEdit
method appropriately:
protected override void OnKeyDown(KeyEventArgs e)
{
if (CellState == CellState.Edited)
{
switch (e.Key)
{
case Key.Escape:
CancelEdit(true);
e.Handled = true;
break;
}
}
base.OnKeyDown(e);
}
If we start our application now, we can test the new features we have implemented:
- Double-click the LastName2 cell to edit it (we click it a first time to make it the current cell, and then we click it a second time to edit it).
- Change the text of the cell by typing something on the keyboard.
- Navigate to another cell either using the keyboard navigation keys or by clicking it.
This time, the changes we have made are kept when we leave the cell.
Let's repeat the same process but using the Esc key:
- Double-click the LastName2 cell to edit it (we click it a first time to make it the current cell, and then we click it a second time to edit it).
- Change the text of the cell by typing something on the keyboard.
- Press the Esc key.
When the Esc key is pressed, the changes are discarded and the cell leaves the Edited
state.
Editing without clicking
We cannot force a user to click a cell each time he needs to edit it. It would be a lot easier if the current cell automatically switches to the Edited
state as soon as the user has typed something on the keyboard.
This can be implemented in the TextCell
class by overriding the OnKeyDown
method:
protected override void OnKeyDown(KeyEventArgs e)
{
if (CellState != CellState.Edited)
{
switch (e.Key)
{
case Key.Left:
case Key.Up:
case Key.Down:
case Key.Right:
case Key.PageDown:
case Key.PageUp:
case Key.Home:
case Key.End:
case Key.Enter:
case Key.Ctrl:
case Key.Shift:
case Key.Alt:
case Key.Escape:
case Key.Tab:
break;
default:
if (BeginEdit() && (textBoxElement != null))
textBoxElement.Text = "";
break;
}
}
base.OnKeyDown(e);
}
When the user presses a key, the BeginEdit
method is called, forcing the cell to go to the Edited
state. Furthermore, the textBoxElement
's text is cleared. This way, the text typed by the user will replace the current text of the cell.
Note that, in the KeyDown
method, some keys such as the navigation keys (left, up) are not taken into account because they are used in other processes.
We can test this new feature by starting the application, navigating to any TextCell
, and typing some text. We will notice that the cell will automatically switch to the Edited
state and that the text typed will replace the text of the cell.
CheckBoxCell
Introduction
It is now time to implement our editing features to the CheckBoxCell
class as well. Nevertheless, our requirements will not be exactly the same for the CheckBoxCell
as for the TextCell
.
When the user clicks on a TextCell
, the cell becomes the current cell. He/she then can start editing it either directly by typing something or by clicking the cell a second time. However, concerning the CheckBox
cell, this behavior may seem strange to the user. It would mean that the user will have to first click the cell to make it the current cell and then click it a second time to change its value. This is not the behavior the user will expect. When watching a checkbox inside a cell, the user will expect that, when he clicks the cell, its value changes whether the cell is the current cell or not. We will have to take this fact into account when implementing the editing features on the CheckBoxCell
.
CheckBoxCell style
This time, we do not have to add an editing control inside the template of the cell. We will handle the editing process ourselves.
Therefore, the CheckBoxCell
style modification is short. We just have to add the "Edited
" VisualState
to the "CommonStates
" VisualStateGroups
:
<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:VisualState x:Name="Edited">
<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>
EditedValue
The IsChecked
property is the value of the CheckBoxCell
. As we must be able to cancel the changes the user makes while editing the cell, we cannot store the changes he makes directly in the IsChecked
property. We will use an EditedValue
property instead:
private bool editedValue;
private bool EditedValue
{
get { return editedValue; }
set
{
if (editedValue != value)
{
editedValue = value;
isOnReadOnlyChange = true;
if (editedValue)
CheckMarkVisibility = Visibility.Visible;
else
CheckMarkVisibility = Visibility.Collapsed;
isOnReadOnlyChange = false;
}
}
}
The setter of the property is implemented in such a way that when the edited value is modified, the CheckMarkVisibility
property is updated in order that the user can see the changes.
OnBeginEdit, OnCancelEdit, and OnCommitEdit methods
The OnBeginEdit
, OnCancelEdit
, and OnCommitEdit
methods are easy to implement.
protected override bool OnBeginEdit()
{
if (this.CellState != CellState.Edited)
editedValue = this.IsChecked;
return true;
}
protected override void OnCancelEdit()
{
EditedValue = this.IsChecked;
}
protected override void OnCommitEdit()
{
this.IsChecked = EditedValue;
}
OnMouseLeftButtonDown method
As explained before, when the user clicks the cell, the cell must be forced to switch to the Edited
state even if it is not the current cell. Nevertheless, this is not as easy to implement as it seems to be.
A cell can be edited only if it is the current cell and the current cell is the cell that has the focus. This means that if the CheckBoxCell
that is clicked by the user is not the current cell, we must "put" the focus on that cell and, just after, edit the cell. However, putting the focus on a cell is a process that is not perfectly synchronous. We cannot call the Focus
method on the cell and then assume that the cell has got the focus and that all the related events (LostFocus
, GotFocus
...) have been called. It will not work. Especially, the LostFocus
and GotFocus
events are not called synchronously and, therefore, we cannot edit the cell just after the Focus
method has been called because it will not be the current cell yet.
Therefore, when the user clicks the cell, we will put the focus on the cell and, then, we will allow Silverlight to finish the focus process before starting to edit the cell. We will implement this by calling the BeginEdit
method asynchronously using the BeginInvoke
method of the Dispatcher.
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
{
BeginEdit();
if (this.CellState == CellState.Edited)
EditedValue = !EditedValue;
}
else
Dispatcher.BeginInvoke(new Action(MouseDownBeginEdit));
}
private void MouseDownBeginEdit()
{
if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
{
BeginEdit();
if (this.CellState == CellState.Edited)
EditedValue = !EditedValue;
}
}
OnKeyDown method
The OnKeyDown
method is implemented the same way as for the TextCell
.
The value of the CheckBoxCell
is modified when the user presses the space bar key.
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Space:
if (BeginEdit())
{
EditedValue = !EditedValue;
e.Handled = true;
}
break;
}
base.OnKeyDown(e);
}
We can test our changes by starting our application and clicking a CheckBoxCell
. Alternatively, we can navigate to a CheckBoxCell
using the keyboard navigation keys and we can change its value by pressing the space bar.
3. Advanced editing features
IsDirty
Introduction
During the program flow, it is often important to know if the current cell is edited and if its value has changed.
For instance, this value is useful when the application is closed. By knowing that the current cell is edited and that its value has changed, we can take appropriate actions such as warn the user to apply the changes.
We will say that a cell is dirty if it is edited and its value has changed.
IsCurrentCellDirty property
As its name suggests, the IsCurrentCellDirty
property will allow knowing if the current cell of a HandyContainer
is dirty.
Let's implement this property in our HandyContainer
partial class (the one located in the GoaOpen\Extensions\Grid folder):
public bool IsCurrentCellDirty
{
get { return CurrentDirtyCell != null; }
}
private Cell currentDirtyCell;
internal Cell CurrentDirtyCell
{
get { return currentDirtyCell; }
set
{
if (currentDirtyCell != value)
{
currentDirtyCell = value;
OnIsCurrentCellDirtyChanged(EventArgs.Empty);
}
}
}
public event EventHandler IsCurrentCellDirtyChanged;
protected virtual void OnIsCurrentCellDirtyChanged(EventArgs e)
{
if (IsCurrentCellDirtyChanged != null)
IsCurrentCellDirtyChanged(this, e);
}
As it will be important to have a reference to the current dirty cell in the next steps of this tutorial, we have implemented an internal CurrentDirtyCell
property. This property will be filled with the current cell if it is dirty. If there is no current cell or if it is not dirty, the value of the property will be null
.
The value of IsCurrentCellDirty
is calculated from the CurrentDirtyCell
value. We have also added an IsCurrentCellDirtyChanged
event.
IsDirty Cell property
The IsDirty
property of the Cell
class allows knowing if the cell is dirty or not. This value is synchronized with the CurrentDirtyCell
value of the HandyContainer
containing the Cell
:
private bool isDirty;
public bool IsDirty
{
get { return isDirty; }
internal set
{
if (isDirty != value)
{
isDirty = value;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (isDirty)
parentContainer.CurrentDirtyCell = this;
else
parentContainer.CurrentDirtyCell = null;
}
}
}
}
When the CancelEdit
or the CommitEdit
methods of the Cell
are called, the IsDirty
property value is set back to false
:
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCommitEdit();
IsDirty = false;
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
return true;
}
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCancelEdit();
IsDirty = false;
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
}
return true;
}
In order to know that the cell is dirty, the TextCell
must monitor the TextChanged
event of the TextBoxElement
. If the cell is in the Edited
state and the TextBoxElement
text is changed, then it means that the cell is dirty.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
textBoxElement = GetTemplateChild("TextBoxElement") as TextBox;
if (textBoxElement != null)
textBoxElement.TextChanged +=
new TextChangedEventHandler(textBoxElement_TextChanged);
}
private void textBoxElement_TextChanged(object sender, TextChangedEventArgs e)
{
if (this.CellState == CellState.Edited)
this.IsDirty = true;
}
The CheckBoxCell
is dirty when its EditedValue
is changed:
private bool EditedValue
{
get { return editedValue; }
set
{
if (editedValue != value)
{
editedValue = value;
isOnReadOnlyChange = true;
if (editedValue)
CheckMarkVisibility = Visibility.Visible;
else
CheckMarkVisibility = Visibility.Collapsed;
IsDirty = true;
isOnReadOnlyChange = false;
}
}
}
Editing the cells programmatically
Until now, the cells are able to switch to or from the Edited
state only when the user is interacting with the cells. It is important that we are able to do so from the code of our programs. Therefore, we will add a BeginEdit
, CommitEdit
, and CancelEdit
method to the HandyContainer
. By calling the BeginEdit
method, we will force the current cell to switch to the Edited
state. By calling the CommitEdit
and the CancelEdit
method, we will force the current cell to commit or cancel the changes the user has made and to leave the Edited
state.
Let's add these three methods to our HandyContainer
partial class:
public bool BeginEdit()
{
Cell currentCell = GetCurrentCell();
if (currentCell != null)
return currentCell.BeginEdit();
return false;
}
public bool CommitEdit()
{
return CommitEdit(true);
}
public bool CommitEdit(bool keepFocus)
{
Cell currentCell = GetCurrentCell();
if (currentCell != null)
return CurrentDirtyCell.CommitEdit(keepFocus);
return true;
}
public bool CancelEdit()
{
return CancelEdit(true);
}
public bool CancelEdit(bool keepFocus)
{
Cell currentCell = GetCurrentCell();
if (currentCell != null)
return CurrentDirtyCell.CancelEdit(keepFocus);
return true;
}
private Cell GetCurrentCell()
{
ContainerItem currentItem = this.CurrentItem;
if ((currentItem != null) && !String.IsNullOrEmpty(this.CurrentCellName))
return currentItem.FindCell(this.CurrentCellName);
return null;
}
CanEdit
It is essential to be able to prevent the user from editing a cell in some circumstances. For instance, in our sample, we would like that the user is unable to edit the ChildrenCount
cell as the Children.Count
property of the Country
class is read-only.
Let's add a CanEdit
dependency property to our Cell
class. We are implementing a dependency property (rather than a "standard" property) because we would like to be able to set its value in XAML.
public static readonly DependencyProperty CanEditProperty;
static Cell()
{
CanEditProperty = DependencyProperty.Register("CanEdit",
typeof(bool), typeof(Cell), new PropertyMetadata(true));
}
public bool CanEdit
{
get { return (bool)GetValue(CanEditProperty); }
set { SetValue(CanEditProperty, value); }
}
Then, let's update the BeginEdit
method of the Cell
class:
internal bool BeginEdit()
{
if (this.CellState == CellState.Edited)
return true;
if (this.IsTabStop && CanEdit)
{
...
Let's now add a False
value to the CanEdit
property of the ChildrenCount TextCell
.
- Open the Page.xaml file of our GridBody project.
- Locate the
ChildrenCount
TextCell
in the ItemTemplate
of the HandyContainer
. - Apply the change:
<o:TextCell Text="{Binding Children.Count}"
x:Name="ChildrenCount"
CanEdit="False"/>
We can test our change:
- Start the application.
- Try to edit one of the
ChildrenCount
cells.
As expected, it is not possible to edit the cell.
Events
Introduction
We still have to implement events in order to be informed when the current cell is edited. Furthermore, even if we have implemented the CanEdit
property, it may be too poor in some circumstances. For instance, it should be possible to abort the editing process only under certain conditions.
Therefore, we will add the following events:
CurrentCellBeginEditing
: this event will occur at the very beginning of the editing process. The EventArg
associated to this event will expose a Cancel
property, allowing canceling the editing process from code.CurrentCellBeginEdited
: this event will occur at the end of the begin edit process once the cell is in the Edited
state.CurrentCellEndEdit
: this event will occur at the end of the edit process once the cell has left the Edited
state.
Furthermore, we will also implement the IsCurrentCellInEditMode
property. This property will allow knowing if the current cell is edited. Do not confuse between the IsCurrentCellDirty
property (the current cell is edited and its value has changed) and the IsCurrentCellInEditMode
property (the current cell is edited, but we do not care if its value has changed or not).
Implement the events in the HandyContainer
internal void _OnCurrentCellBeginEditing(GCancelEventArgs e)
{
OnCurrentCellBeginEditing(e);
}
public event EventHandler<GCancelEventArgs> CurrentCellBeginEditing;
protected virtual void OnCurrentCellBeginEditing(GCancelEventArgs e)
{
if (CurrentCellBeginEditing != null)
CurrentCellBeginEditing(this, e);
}
internal void _OnCurrentCellBeginEdited(EventArgs e)
{
CurrentEditedCell = this.GetCurrentCell();
OnCurrentCellBeginEdited(e);
}
public event EventHandler CurrentCellBeginEdited;
protected virtual void OnCurrentCellBeginEdited(EventArgs e)
{
if (CurrentCellBeginEdited != null)
CurrentCellBeginEdited(this, e);
}
public event EventHandler CurrentCellEndEdit;
internal void _OnCurrentCellEndEdit(EventArgs e)
{
CurrentEditedCell = null;
OnCurrentCellEndEdit(e);
}
internal void OnCurrentCellEndEdit(EventArgs e)
{
if (CurrentCellEndEdit != null)
CurrentCellEndEdit(this, e);
}
public bool IsCurrentCellInEditMode
{
get { return CurrentEditedCell != null; }
}
internal Cell CurrentEditedCell
{
get;
set;
}
The _OnCurrentCellBeginEditing
, _OnCurrentCellBeginEdited
, and _OnCurrentCellEndEdit
internal methods will be called from the Cell
class. The CurrentEditedCell
value is updated in the _OnCurrentCellBeginEdited
and the _OnCurrentCellEndEdit
methods.
Calling _OnCurrentCellBeginEditing, _OnCurrentCellBeginEdited, and _OnCurrentCellEndEdit from the Cell class
The _OnCurrentCellBeginEditing
and the _OnCurrentCellBeginEdited
methods are both called in the BeginEdit
method of the Cell
class:
internal bool BeginEdit()
{
if (this.CellState == CellState.Edited)
return true;
if (this.IsTabStop && this.CanEdit)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
GCancelEventArgs cancelEventArgs = new GCancelEventArgs(false);
parentContainer._OnCurrentCellBeginEditing(cancelEventArgs);
if (cancelEventArgs.Cancel)
return false;
VisualStateManager.GoToState(this, "Edited", true);
bool isEditStarted = OnBeginEdit();
if (isEditStarted)
{
this.CellState = CellState.Edited;
parentContainer._OnCurrentCellBeginEdited(EventArgs.Empty);
}
return isEditStarted;
}
return false;
}
The _OnCurrentCellEndEdit
method is called in both the CancelEdit
and the CommitEdit
methods of the Cell
class:
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCommitEdit();
IsDirty = false;
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);
}
return true;
}
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCancelEdit();
IsDirty = false;
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);
}
return true;
}
Update the CommitEdit and CancelEdit methods
Now that we have implemented a CurrentEditedCell
property, let's make the CommitEdit
and the CancelEdit
methods of the HandyContainer
more efficient.
public bool CommitEdit(bool keepFocus)
{
if (CurrentEditedCell != null)
return CurrentEditedCell.CommitEdit(keepFocus);
return true;
}
public bool CancelEdit(bool keepFocus)
{
if (CurrentEditedCell != null)
return CurrentEditedCell.CancelEdit(keepFocus);
return true;
}
4. Cells' validation
Introduction
We must provide a way to validate the data the user has typed in a cell. We should also be able to inform the user that the data he/she has typed is wrong, and we should as well be able to force the user to enter valid data before performing other actions.
Silverlight already provides a validation mechanism called during the binding process. We will see how to use it within our grid. Nevertheless, this mechanism is too poor for our use. We will also implement our own validation methods and events to complete the existing validation mechanism.
CurrentCellValidating event
Introduction
This event will occur just before the data the user has typed is "sent" to the property the cell is bound to. The EventArgs
associated to this event will have a Cancel
property, allowing us to cancel the validation process and force the cell to stay edited.
CellValidatingEventArgs
Let's add a new CellValidatingEventArgs
class to the GoaOpen\Extensions\Grid folder. This specialized EventArgs
will be used with the CurrentCellValidating
event.
using Netika.Windows.Controls;
namespace Open.Windows.Controls
{
public class CellValidatingEventArgs : GCancelEventArgs
{
public CellValidatingEventArgs(object oldValue, object newValue, bool cancel)
: base(cancel)
{
OldValue = oldValue;
NewValue = newValue;
}
public object OldValue
{
get;
private set;
}
public object NewValue
{
get;
private set;
}
}
}
The CellValidatingEventArgs
inherits from the GCancelEventArgs
because the GCancelEventArgs
already implements a Cancel
property.
We also have implemented an OldValue
and NewValue
properties. These properties will allow us to know what the current value of the cell is (the value typed by the user) and what the previous value of the cell was (the value before the user typed something in the cell).
CurrentCellValidating event
Let's implement the CurrentCellValidating
event in our HandyContainer
partial class.
internal void _OnCurrentCellValidating(CellValidatingEventArgs e)
{
OnCurrentCellValidating(e);
}
public event EventHandler<CellValidatingEventArgs> CurrentCellValidating;
protected virtual void OnCurrentCellValidating(CellValidatingEventArgs e)
{
if (CurrentCellValidating != null)
CurrentCellValidating(this, e);
}
The _OnCurrentCellValidating
method will be called from the Cell
class.
Calling the _OnCurrentCellValidating method
We need to call the _OnCurrentCellValidating
method from the CommitEdit
method of the Cell
class. After this call, if the Cancel
value of the EventArgs
is set to true
, it means that the CommitEdit
process must be canceled.
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
CellValidatingEventArgs cancelEventArgs =
new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
parentContainer._OnCurrentCellValidating(cancelEventArgs);
if (cancelEventArgs.Cancel)
return false;
OnCommitEdit();
IsDirty = false;
}
}
if (this.CellState == CellState.Edited)
. . .
The newValue
and the oldValue
parameters of the CellValidatingEventArgs
are filled with the CurrentValue
and the DirtyValue
of the cell.
These properties are not implemented yet. The CurrentValue
property must be filled with the value of the cell without taking into account any change the user would have made by editing the cell. The DirtyValue
property must be filled by the value of the cell taking into account the changes the user would have made by editing the cell.
Implement CurrentValue and DirtyValue
The CurrentValue
and the DirtyValue
properties must be implemented in the TextCell
and in the CheckBoxCell
.
Let's first add abstract properties in the Cell
class:
protected abstract object CurrentValue
{
get;
}
protected abstract object DirtyValue
{
get;
}
Let's then implement these properties in the TextCell
class:
protected override object CurrentValue
{
get { return this.Text; }
}
protected override object DirtyValue
{
get
{
if (textBoxElement != null)
return textBoxElement.Text;
return this.Text;
}
}
Let's also implement these properties in the CheckBoxCell
class:
protected override object CurrentValue
{
get { return this.IsChecked; }
}
protected override object DirtyValue
{
get { return EditedValue; }
}
Testing the CurrentCellValidating event
Let's test our changes by using the CurrentCellValidating
event inside our GridBody project.
Let's first modify the Page.xaml of the GridBody project to handle the event:
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items"
HandyDefaultItemStyle="Node"
HandyStyle="GridBodyStyle"
CurrentCellValidating="MyGridBody_CurrentCellValidating">
And then, let's add some code to the Page.xaml.cs file in order to validate the value of our first name cell.
private void MyGridBody_CurrentCellValidating(object sender,
Open.Windows.Controls.CellValidatingEventArgs e)
{
if (MyGridBody.CurrentCellName == "FirstName")
{
string newValue = e.NewValue as string;
if (string.IsNullOrEmpty(newValue))
e.Cancel = true;
}
}
The way, we have implemented the CurrentCellValidating
event, and the user will not be able to fill the cell with an empty First Name.
Let's try our changes:
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Navigate to the LastName3 cell in order to commit the changes made
We see that the grid is not working very well. The commit process of the FirstName3 cell has been canceled and the cell remains edited, but nothing prevents us from navigating to the LastName3 cell. At the end, we have the FirstName3 cell that is still edited, but the current cell is the LastName3 cell.
Therefore, before going further, we have to implement a new behavior inside our grid's body: we cannot let the user navigate to another cell if the current cell has not been validated (i.e., committed).
Canceling navigation
Introduction
We have implemented a way to validate the cells of our grid during the edit process, and we have implemented a way to cancel the commit process of the cell if the cell value is not correct. Nevertheless, canceling the commit process is not enough. We also need to be able to cancel the navigation process.
Fortunately, our code manages the navigation between the cells, and therefore we will be able to add conditions to prevent the navigation when necessary.
Canceling when the user clicks a cell
In order to cancel the navigation when the user clicks a cell, we need to modify the OnMouseLeftButtonDown
method of the Cell
class.
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
this.Focus();
else
e.Handled = true;
}
}
}
protected bool CanNavigateOnMouseDown(DependencyObject clickSource)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
bool canNavigate = true;
if (parentContainer.IsCurrentCellInEditMode)
{
if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, clickSource))
canNavigate = parentContainer.CommitEdit(false);
}
return canNavigate;
}
return false;
}
We have added a CanNavigateOnMouseDown
method. In case the current cell is edited and the user clicks another cell, we force the current cell to commit. It is only if the commit process ran to the end (returns true
) that we allow the navigation.
Let's try our changes:
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Navigate to the LastName3 cell by clicking on it
This time the navigation has been canceled, and the FirstName3 cell remains the current cell.
Canceling when the user clicks the CheckBoxCell
Let's test the navigation cancelation when we click a CheckBoxCell
.
- Add a Breakpoint at the beginning of the
MouseDownBeginEdit
method of the CheckBoxCell
class - Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Navigate to a
CheckBoxCell
cell by clicking on it
The grid is not working well in this case: the navigation is canceled, but the MouseDownBeginEdit
method is called (we can see that, thanks to the breakpoint we have added).
This is because we have handled the OnMouseLeftButtonDown
method of the CheckBoxCell
in such a way that the editing process starts as soon as the user clicks on the cell.
Let's improve the OnMouseLeftDown
method of our CheckBoxCell
class:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
{
BeginEdit();
if (this.CellState == CellState.Edited)
EditedValue = !EditedValue;
}
else if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject) &&
IsTabStop && CanEdit)
Dispatcher.BeginInvoke(new Action(MouseDownBeginEdit));
}
Canceling when the FocusCell method is called
In Step 1 of this tutorial, we enhanced the ContainerItem
class by adding a FocusCell
method. This method is used internally, and can be used externally to programmatically navigate to a cell.
We need to enhance this method in order that "it does not work" if the current cell cannot be committed.
public bool FocusCell(string cellName)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) &&
parentContainer.IsCurrentCellInEditMode &&
(parentContainer.CurrentEditedCell.Name != cellName))
{
bool canNavigate = parentContainer.CommitEdit(false);
if (!canNavigate)
return false;
}
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))
{
if (parentContainer != null)
parentContainer._OnNavigatorSetKeyboardFocus(this);
}
return true;
}
}
}
return false;
}
Canceling when an item is clicked
In step 1, we allowed an item to get the focus when clicked. In our sample, this can happen, for instance, if the user clicks exactly on the line between two items (i.e., between two rows).
Nevertheless, we cannot let an item get the focus if the current cell cannot be committed.
We have to override the OnMouseLeftButtonDown
method in our ContainerItem
partial class in order to avoid that.
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
bool canNavigate = true;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (!TreeHelper.IsChildOf(parentContainer.CurrentDirtyCell,
e.OriginalSource as DependencyObject))
canNavigate = parentContainer.CommitEdit(false);
}
if (canNavigate)
base.OnMouseLeftButtonDown(e);
else
e.Handled = true;
}
Canceling the GridSpatialNavigator's navigation
Let's first add a ValidateCell
method to our GridSpatialNavigator
:
protected static bool ValidateCell(IKeyNavigatorContainer container)
{
HandyContainer parentContainer =
HandyContainer.GetParentContainer((FrameworkElement)container);
return parentContainer.CommitEdit(true);
}
This method will return false
if the current cell cannot be committed.
Let's now modify the KeyDown
and ActiveKeyDown
methods. If the user has pressed a navigation key (Tab, Up, Down), we will first call the ValidateCell
method in order to check that the current cell can be committed. It is only if the ValidateCell
method returns true
that the KeyDown
or the ActiveKeyDown
method of the ancestor SpatialNavigator
will be called:
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;
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateCell(container))
base.ActiveKeyDown(container, e);
else
e.Handled = true;
break;
default:
base.ActiveKeyDown(container, e);
break;
}
}
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;
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateCell(container))
base.KeyDown(container, e);
else
e.Handled = true;
break;
default:
base.KeyDown(container, e);
break;
}
}
else
ProcessKey(container, e);
}
We also have to modify the ProcessKey
method the same way:
private void ProcessKey(IKeyNavigatorContainer container, GKeyEventArgs e)
{
if (!ValidateCell(container))
{
e.Handled = true;
return;
}
GStackPanel gStackPanel = (GStackPanel)container;
HandyContainer parentContainer = HandyContainer.GetParentContainer(gStackPanel);
if (gStackPanel.Children.Count > 0)
{
...
Canceling the RowSpatialNavigator's navigation
Let's add the same ValidateCell
method to our RowSpatialNavigator
that the one we have added to our GridSpatialNavigator
. (If you think that it is strange to twice add the same method, you think right. The ValidateCell
method of the GridSpatialNavigator
will be modified later on.)
protected static bool ValidateCell(IKeyNavigatorContainer container)
{
HandyContainer parentContainer =
HandyContainer.GetParentContainer((FrameworkElement)container);
return parentContainer.CommitEdit(true);
}
Let's also modify the KeyDown
and the ActiveKeyDown
methods of the RowSpatialNavigator
the same way it was modified in our GridSpatialNavigator
:
public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
{
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateCell(container))
base.ActiveKeyDown(container, e);
else
e.Handled = true;
break;
default:
base.ActiveKeyDown(container, e);
break;
}
}
}
public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
{
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateCell(container))
base.KeyDown(container, e);
else
e.Handled = true;
break;
default:
base.KeyDown(container, e);
break;
}
}
}
Testing the navigation canceling
Let's try one more time that our changes work.
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Try to navigate to another cell by:
- Clicking on it
- Pressing the up, down, left, right, Home, or End key
- Pressing the Tab or the Shift-Tab key
- Pressing the Ctrl-Home or Ctrl-End key
In all of these cases, the FirstName3 cell remains the current cell.
Canceling the grid's scrolling
For now, when a cell is dirty, the user is allowed to scroll the grid using the vertical scrollbar. This behavior can have unwanted effects. For instance, a user can edit a cell with a wrong value, scroll the grid to a location where the edited cell is not visible and then click another cell. In this case, the validation process will start on a cell that is not visible anymore.
Rather than implement long and difficult algorithms that take care of situations like the one described before and, for instance, scroll back the grid to a location where the edited cell is visible, we will handle it the easy way: if a cell cannot be committed, the user cannot scroll the grid.
This can be implemented very easily by overriding the OnVerticalOffsetChanging
method of our HandyContainer
partial class.
protected override void OnVerticalOffsetChanging(CancelOffsetEventArgs e)
{
if (!CommitEdit())
e.Cancel = true;
base.OnVerticalOffsetChanging(e);
}
Canceling nodes expand and collapse
When a cell is edited and cannot be committed, we have cancelled the scrolling of the grid to avoid an edited cell being moved to the not displayed area of the grid.
When expanding a node that is displayed abode the edited cell, we make the children items of the node appear between the node and the edited cell. In this case, the location of the edited cell can be moved to the not displayed area of the grid.
When collapsing the parent node (if any) of an item holding an edited cell, we make the item and therefore the edited cell disappear.
For these reasons, we will not allow to collapse or expand a node if the current cell is edited and cannot be committed.
Let's override the OnIsExpandedChanging
method of the ContainerItem
class:
protected override void OnIsExpandedChanging(
Netika.Windows.Controls.IsExpandedChangingEventArgs e)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) && (!parentContainer.CommitEdit()))
e.Cancel = true;
else
base.OnIsExpandedChanging(e);
}
Alerting the user
Introduction
When a cell cannot be committed, the cell remains edited. Nevertheless, the user can be confused by this behavior and wonder why he cannot navigate to another cell. In order that the user understands that the value he has typed is wrong, let's make the border of the cell red when it cannot be committed.
ValidStates
In order to be able to apply a visual look to a cell that is not valid, we will add a "ValidStates
" VisualStateGroup
to the Templates of our cells. This group will have two possible states: "Valid
" and "NotValid
". In case a cell is in the NotValid
state, a visual element warning the user is displayed.
Let's implement this change in the TextCell
style. The "ValidStates
" VisualStateGroup
is added at the end of VisualStateManager.VisualStateGroups
. The visual element warning the user is a red rectangle named ValidElement
.
<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:VisualState x:Name="Edited">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="TextBoxElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<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:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
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
Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
IsHitTestVisible="false"
Margin="0,1,1,0"
Visibility="Collapsed"/>
<Grid>
<TextBlock
x:Name="TextElement"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<TextBox
x:Name="TextBoxElement"
Style="{StaticResource CellTextBoxStyle}"
Visibility="Collapsed"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"/>
</Grid>
<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>
Let's implement the same change in the CheckBoxCell
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:VisualState x:Name="Edited">
<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:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
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"/>
<Rectangle Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
Margin="0,1,1,0"
IsHitTestVisible="false"
Visibility="Collapsed"/>
<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>
IsValid cell property
We still need to switch from the Valid
to the NotValid
state appropriately. Let's add an IsValid
property to the Cell
class.
private bool isValid = true;
public bool IsValid
{
get { return isValid; }
internal set
{
if (isValid != value)
{
isValid = value;
if (isValid)
VisualStateManager.GoToState(this, "Valid", true);
else
VisualStateManager.GoToState(this, "NotValid", true);
}
}
}
The IsValid
method must be set in the CancelEdit
and in the CommitEdit
methods:
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCancelEdit();
IsValid = true;
IsDirty = false;
}
. . .
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
CellValidatingEventArgs cancelEventArgs =
new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
parentContainer._OnCurrentCellValidating(cancelEventArgs);
if (cancelEventArgs.Cancel)
{
IsValid = false;
return false;
}
OnCommitEdit();
IsValid = true;
IsDirty = false;
}
}
...
Let's try our changes:
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Try to navigate to another cell
- Keep the application running
This time, the border of FirstName3 is displayed in red.
- Type something in the FirstName3 cell
- Navigate to another cell
The background of the cell is displayed normally.
Using binding validation
Introduction
Silverlight already provides a validation mechanism called during the binding process.
In order to make it work, let's modify the binding on the LastName cell (make the modification in the Page.xaml file of the GridBody project):
<o:TextCell
Text="{Binding LastName, Mode=TwoWay,
NotifyOnValidationError=true, ValidatesOnExceptions=true}"
x:Name="LastName"/>
The NotifyOnValidationError
property will tell the binding to call the BindingValidationError
event on the cell (see below to know more) when an error occurs (it is a little more complicated than this because the event is a routed event; if you want to know more about the binding validation process, please read the Silverlight documentation). The ValidateOnExceptions
property will tell the binding that if an exception is thrown during the binding process, it must process this exception as a validation error.
Let's modify the LastName
property of our Person
class in order that it throws an exception if we try to fill it with a null
or an empty string:
private string lastName;
public string LastName
{
get { return lastName; }
set
{
if (lastName != value)
{
if (string.IsNullOrEmpty(value))
throw new Exception("Last name cannot be empty");
lastName = value;
OnPropertyChanged("LastName");
}
}
}
Finally, let's handle the BindingValidationError
event in our cell class:
public Cell()
{
this.BindingValidationError +=
new EventHandler<ValidationErrorEventArgs>(Cell_BindingValidationError);
}
private void Cell_BindingValidationError(object sender, ValidationErrorEventArgs e)
{
if (e.Action == ValidationErrorEventAction.Added)
MessageBox.Show("Invalid Data");
else if (e.Action == ValidationErrorEventAction.Removed)
MessageBox.Show("Valid Data");
}
Let's try our changes:
- Start the application
- Edit the LastName3 cell
- Delete the content of the cell
- Navigate to another cell
An "Invalid Data" message box is displayed.
Now that we have played with the binding validation process built in Silverlight, we need to handle the BindingValidationError
properly in order that it fits into our validation flow.
Handle the BindingValidationError event correctly
Let's first change our Cell_BindingValidationError
method in order that it sets the isBindingValid
flag:
private bool isBindingValid = true;
private void Cell_BindingValidationError(object sender,
ValidationErrorEventArgs e)
{
if (e.Action == ValidationErrorEventAction.Added)
isBindingValid = false;
else if (e.Action == ValidationErrorEventAction.Removed)
isBindingValid = true;
}
Next, let's modify our CommitEdit
method in order to take into account the isBindingValid
flag value:
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
CellValidatingEventArgs cancelEventArgs =
new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
parentContainer._OnCurrentCellValidating(cancelEventArgs);
if (cancelEventArgs.Cancel)
{
IsValid = false;
return false;
}
OnCommitEdit();
if (!isBindingValid)
{
IsValid = false;
return false;
}
IsValid = true;
IsDirty = false;
}
}
. . .
Note that we have processed the isBindingValid
value after the call to the OnCommitEdit
method. Indeed, the BindingValidationError
event will be called after we have tried to apply the value typed by the user to the property the cell is bound to. Therefore, the OnCommitEdit
method must be called before we check the isBindingValid
flag.
If we try the application now, the validation of the LastName cell processes correctly:
- Start the application
- Edit the LastName3 cell
- Delete the content of the cell
- Navigate to another cell
The LastName3 cell remains edited and it remains the current cell. The border of the cell is displayed in red.
CurrentCellValidated event
Introduction
When a cell is committed, at least two validation actions are started: the OnCurrentCellValidating
event and the binding validation.
We need an event to know when the validation actions are finished in order to be able to start posting validation actions. This is the purpose of the CurrentCellValidated
event.
Implementing the CurrentCellValidated event
The currentCellValidated
event will be implemented in our HandyContainer
partial class the same way the other events were implemented:
internal void _OnCurrentCellValidated(EventArgs e)
{
OnCurrentCellValidated(e);
}
public event EventHandler CurrentCellValidated;
protected virtual void OnCurrentCellValidated(EventArgs e)
{
if (CurrentCellValidated != null)
CurrentCellValidated(this, e);
}
Calling the _OnCurrentCellValidated method
This method must be called in the CommitEdit
method of the Cell
class after the validation has processed:
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
CellValidatingEventArgs cancelEventArgs =
new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
parentContainer._OnCurrentCellValidating(cancelEventArgs);
if (cancelEventArgs.Cancel)
{
IsValid = false;
return false;
}
OnCommitEdit();
if (!isBindingValid)
{
IsValid = false;
return false;
}
IsValid = true;
IsDirty = false;
parentContainer._OnCurrentCellValidated(EventArgs.Empty);
}
}
. . .
5. Items' validation
Introduction
Knowing the states of the cells and when they change is not enough. Each cell is part of a whole: the item (i.e., a row). A modification in one cell may have consequences for the whole item. Therefore, it is important to known when an item has been modified and to be able to take actions on the item appropriately.
IsDirty item
Introduction
We will say that an item is dirty if the value of at least one of its cells has been changed. As soon as the item has been validated (see below), the item is not dirty anymore.
HandyContainer IsCurrentItemDirty property
We will implement the IsCurrentItemDirty
property the same way we have implemented the IsCurrentCellDirty
property.
public bool IsCurrentItemDirty
{
get { return CurrentDirtyItem != null; }
}
private ContainerItem currentDirtyItem;
internal ContainerItem CurrentDirtyItem
{
get { return currentDirtyItem; }
set
{
if (currentDirtyItem != value)
{
currentDirtyItem = value;
OnIsCurrentItemDirtyChanged(EventArgs.Empty);
}
}
}
public event EventHandler IsCurrentItemDirtyChanged;
protected virtual void OnIsCurrentItemDirtyChanged(EventArgs e)
{
if (IsCurrentItemDirtyChanged != null)
IsCurrentItemDirtyChanged(this, e);
}
We have implemented an internal CurrentDirtyItem
property. This property will be filled with the current item if it is dirty. If there is no current item or if it is not dirty, the value of the property will be null
.
The value of IsCurrentItemDirty
is calculated from the CurrentDirtyItem
value.
We have also added an IsCurrentItemDirtyChanged
event.
IsDirty item's property
The IsDirty
property of the ContainerItem
is a little more than just a flag.
We have to take the following rules into account:
- If a cell becomes dirty, the item (i.e., the row) holding that item also becomes dirty.
- If a cell is committed, it is not dirty anymore, but the item holding that cell remains dirty until it has been validated.
- If a cell is canceled, it is not dirty anymore. If the item holding that cell was already dirty before the cell was edited, the item remains dirty. If the item holding the cell was not dirty before the cell was edited, the item is not dirty anymore.
In order to be able to implement these rules, we will add an IsDirtyCount
property to the ContainerItem
class. As soon as a cell of the item becomes dirty, the IsDirtyCount
property of the ContainerItem
is incremented by one. If the editing of a cell is cancelled, the IsDirtyCount
property is decremented by one. A ContainerItem
is dirty if the IsDirtyCount
property value is greater than 0.
When the ContainerItem
is validated (see below), the IsDirtyCount
property is reset to 0.
private int isDirtyCount;
internal int IsDirtyCount
{
get { return isDirtyCount; }
set
{
if (isDirtyCount != value)
{
isDirtyCount = value;
HandyContainer parentContainer =
HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (isDirtyCount > 0)
parentContainer.CurrentDirtyItem = this;
else
parentContainer.CurrentDirtyItem = null;
}
}
}
}
public bool IsDirty
{
get { return IsDirtyCount > 0; }
}
Updating the IsDirtyCount property from the Cell class
As we will need to access the parent ContainerItem
from a cell, let's create a static GetParentContainerItem
method in the ContainerItem
class the same way we have created a static GetParentContainer
method in the HandyContainer
class:
public static ContainerItem GetParentContainerItem(FrameworkElement element)
{
DependencyObject parentElement = element;
while (parentElement != null)
{
ContainerItem parentContainerItem = parentElement as ContainerItem;
if (parentContainerItem != null)
return parentContainerItem;
parentElement = VisualTreeHelper.GetParent(parentElement);
}
return null;
}
The next thing to do is to increment the IsDirtyCount
of the parent ContainerItem
of the cell when the cell becomes dirty. Let's modify the implementation of the IsDirty
property of the Cell
class:
public bool IsDirty
{
get { return isDirty; }
internal set
{
if (isDirty != value)
{
isDirty = value;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (isDirty)
parentContainer.CurrentDirtyCell = this;
else
parentContainer.CurrentDirtyCell = null;
}
if (isDirty)
{
ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
if (parentRow != null)
parentRow.IsDirtyCount++;
}
}
}
}
Next, we have to decrease the IsDirtyCount
value of the parent ContainerItem
when the CancelEdit
method of the cell is called:
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCancelEdit();
IsValid = true;
IsDirty = false;
ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
if (parentRow != null)
parentRow.IsDirtyCount--;
}
if (this.CellState == CellState.Edited)
{
...
Edited item
Introduction
We will say that an item is edited if one of its cells is edited.
HandyContainer IsCurrentItemEdited property
Let's first add a IsCurrentItemEdited
property to our HandyContainer
class. As usual, we will also implement a CurrentEditedItem
property and a IsCurrentItemEditedChanged
event:
public bool IsCurrentItemEdited
{
get { return CurrentEditedItem != null; }
}
private ContainerItem currentEditedItem;
internal ContainerItem CurrentEditedItem
{
get { return currentEditedItem; }
set
{
if (currentEditedItem != value)
{
currentEditedItem = value;
OnIsCurrentItemEditedChanged(EventArgs.Empty);
}
}
}
public event EventHandler IsCurrentItemEditedChanged;
protected virtual void OnIsCurrentItemEditedChanged(EventArgs e)
{
if (IsCurrentItemEditedChanged != null)
IsCurrentItemEditedChanged(this, e);
}
Update the CurrentEditedItem property
Let's modify the _OnCurrentCellBeginEdited
and the _OnCurrentCellEndEdit
methods of the HandyContainer
in order to keep the CurrentEditedItem
property value up-to-date:
internal void _OnCurrentCellBeginEdited(EventArgs e)
{
CurrentEditedCell = this.GetCurrentCell();
CurrentEditedItem = ContainerItem.GetParentContainerItem(CurrentEditedCell);
OnCurrentCellBeginEdited(e);
}
internal void _OnCurrentCellEndEdit(EventArgs e)
{
CurrentEditedCell = null;
CurrentEditedItem = null;
OnCurrentCellEndEdit(e);
}
Validating the ContainerItem
Introduction
When an item is dirty, we must be able to validate it. The validation of the item can occur automatically. For instance, when the user clicks on another item, the item is validated before the other item becomes the current item. The validation process of the current item can also be forced by code by calling a Validate
method on the HandyContainer
. Of course, the purpose of the validation process is to be able to take actions and to be able to cancel navigation during this process. Therefore, we will also have to add validation events.
CurrentItemValidating and CurrentItemValidated events
The same way we have added the CurrentCellValidating
and CurrentCellValidated
events to the HandyContainer
class, we also need to add the CurrentItemValidating
and CurrentItemValidated
events to that class. The CurrentItemValidating
event can be canceled. In that case, the validation process stops and the navigation process (if any) is canceled.
The implementation of these events in the HandyContainer
follows the same pattern as the implementation of the other events.
First, let's add an ItemValidatingEventArgs
class to our GoaOpen\Extensions\Grid folder:
using Netika.Windows.Controls;
namespace Open.Windows.Controls
{
public class ItemValidatingEventArgs : GCancelEventArgs
{
public ItemValidatingEventArgs(ContainerItem item, bool cancel)
: base(cancel)
{
Item = item;
}
public ContainerItem Item
{
get;
private set;
}
}
}
Then, let's modify our HandyContainer
partial class and add the CurrentItemValidating
and the CurrentItemValidated
events:
internal void _OnCurrentItemValidating(ItemValidatingEventArgs e)
{
OnCurrentItemValidating(e);
}
public event EventHandler<ItemValidatingEventArgs> CurrentItemValidating;
protected virtual void OnCurrentItemValidating(ItemValidatingEventArgs e)
{
if (CurrentItemValidating != null)
CurrentItemValidating(this, e);
}
internal void _OnCurrentItemValidated(EventArgs e)
{
OnCurrentItemValidated(e);
}
public event EventHandler CurrentItemValidated;
protected virtual void OnCurrentItemValidated(EventArgs e)
{
if (CurrentItemValidated != null)
CurrentItemValidated(this, e);
}
The _OnCurrentItemValidating
and the _OnCurrentItemValidated
methods will be called from the ContainerItem
class.
ContainerItem Validate method
The Validate
method of the ContainerItem
is the method that will be called each time an item needs to be validated.
The Validate
method of the ContainerItem
must extend the behavior of the Cell
's CommitEdit
method. If a cell of the ContainerItem
is edited, a call to the Validate
method of the ContainerItem
must have the same result on the cell as a direct call to the CommitEdit
method of the cell. Therefore, even if the item is not dirty, if the item holds an edited cell, the CommitEdit
method of the cell must be called.
internal bool Validate(bool keepFocus)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (parentContainer.IsCurrentCellInEditMode &&
!parentContainer.CommitEdit(keepFocus))
return false;
if (!IsDirty)
return true;
ItemValidatingEventArgs eventArgs = new ItemValidatingEventArgs(this, false);
parentContainer._OnCurrentItemValidating(eventArgs);
if (eventArgs.Cancel)
return false;
IsDirtyCount = 0;
parentContainer._OnCurrentItemValidated(EventArgs.Empty);
}
return true;
}
HandyContainer Validate method
The same way we have a CommitEdit
method to "validate" the current cell of the HandyContainer
, we must have a Validate
method to validate the current item.
Note that the Validate
method must be called only if the current item is edited or if it is dirty. Therefore, let's first add the IsCurrentItemEditedOrDirty
and CurrentEditedOrDirtyItem
properties to the HandyContainer
.
internal bool IsCurrentItemEditedOrDirty
{
get { return IsCurrentItemEdited || IsCurrentItemDirty; }
}
internal ContainerItem CurrentEditedOrDirtyItem
{
get { return CurrentDirtyItem ?? CurrentEditedItem; }
}
Then, let's implement the Validate
method:
public bool Validate()
{
return Validate(true);
}
public bool Validate(bool keepFocus)
{
if (CurrentEditedOrDirtyItem != null)
return CurrentEditedOrDirtyItem.Validate(keepFocus);
return true;
}
Canceling the navigation process
Introduction
Earlier in this tutorial, at several places, we modified some methods in order to ensure that the current item could be committed before allowing the user to navigate to another part of the grid.
We now have to do the same kind of modification to take care if the current item can be validated before allowing navigation.
Canceling when the user clicks a cell
We have to modify the OnMouseLeftButtonDown
method of the Cell
class in such a way that if the current item is edited or if it is dirty:
- if the user has clicked a cell that is held by another item, we validate the current item before allowing the navigation to process
- if the user has clicked a cell that is held by the current item, we commit the current cell before allowing the navigation to process
- if the user has clicked the current cell, we allow the navigation to process.
If the current item is not edited or if it is not dirty, we allow the navigation process without any validation.
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
this.Focus();
else
e.Handled = true;
}
}
}
protected bool CanNavigateOnMouseDown(DependencyObject clickSource)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
bool canNavigate = true;
if (parentContainer.IsCurrentItemEditedOrDirty)
{
bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem ==
ContainerItem.GetParentContainerItem(this);
if (isEditedSameItem)
{
if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, clickSource))
canNavigate = parentContainer.CommitEdit(false);
}
else
canNavigate = parentContainer.Validate(false);
}
return canNavigate;
}
return false;
}
Canceling navigation when the FocusCell method is called
The ContainerItem
's FocusCell
method is used internally, and can be used externally to programmatically navigate to a cell. We need to enhance this method in order that it "does not work" if the cell to focus is located in another item than the current item and if the current item cannot be validated.
Let's modify the FocusCell
method of the ContainerItem
class:
public bool FocusCell(string cellName)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) && parentContainer.IsCurrentItemEditedOrDirty)
{
bool canNavigate = true;
bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem == this;
if (isEditedSameItem)
{
if (parentContainer.IsCurrentCellInEditMode &&
(parentContainer.CurrentEditedCell.Name != cellName))
canNavigate = parentContainer.CommitEdit(false);
}
else
canNavigate = parentContainer.Validate(false);
if (!canNavigate)
return false;
}
object focusedElement = FocusManager.GetFocusedElement();
FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
if (firstChild != null)
{
. . .
Canceling navigation when an item is clicked
Earlier in this tutorial, we have overridden the OnMouseLeftButtonDown
method in our ContainerItem
partial class in order that an item cannot get the focus if the current cell cannot be committed.
We now have to enhance this method in order that if the current item is edited or dirty:
- if the user has clicked another item than the current item, we validate the current item before allowing the navigation to process
- if the user has clicked somewhere in the current item but has not clicked the current cell, we commit the current cell before allowing the navigation to process
- if the user has clicked the current cell, we allow the navigation to process
If the current item is not dirty or edited, we allow the navigation process without any validation.
Let's modify the OnMouseLeftButtonDown
method of the ContainerItem
class:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
bool canNavigate = true;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) && (parentContainer.IsCurrentItemEditedOrDirty))
{
bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem == this;
if (isEditedSameItem)
{
if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell,
e.OriginalSource as DependencyObject))
canNavigate = parentContainer.CommitEdit(false);
}
else
canNavigate = parentContainer.Validate(false);
}
if (canNavigate)
base.OnMouseLeftButtonDown(e);
else
e.Handled = true;
}
Canceling navigation in the GridSpatialNavigator
Until now, before allowing the GridSpatialNavigator
to process the navigation, we checked that the current cell could be committed by calling the ValidateCell
method of the GridSpatialNavigator
.
Nevertheless, the GridSpatialNavigator
handles navigation between the items of the HandyContainer
and, therefore, we must validate the current item (and not only the current cell) before allowing the navigation to process.
Let's first replace the ValidateCell
method of the GridSpatialNavigator
with a ValidateItem
method:
protected static bool ValidateItem(IKeyNavigatorContainer container)
{
HandyContainer parentContainer =
HandyContainer.GetParentContainer((FrameworkElement)container);
if (parentContainer.IsCurrentItemEditedOrDirty)
return parentContainer.Validate(true);
return true;
}
Then, in the ActiveKeyDown
and the KeyDown
methods, we must call the ValidateItem
method to ensure that the current item validates before calling the ancestor method:
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;
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateItem(container))
base.ActiveKeyDown(container, e);
else
e.Handled = true;
break;
default:
base.ActiveKeyDown(container, e);
break;
}
}
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;
switch (e.Key)
{
case Key.Down:
case Key.Up:
case Key.PageDown:
case Key.PageUp:
case Key.Enter:
case Key.Home:
case Key.End:
case Key.Right:
case Key.Left:
case Key.Tab:
if (ValidateItem(container))
base.KeyDown(container, e);
else
e.Handled = true;
break;
default:
base.KeyDown(container, e);
break;
}
}
else
ProcessKey(container, e);
}
Eventually, we also need to modify the ProcessKey
method.
We cannot just replace the call to the ValidateCell
method at the beginning of the ProcessKey
method by a call to the ValidateItem
method. Indeed, the Tab key navigation is processed by the ProcessKey
method, and a press to the Tab key does not always imply that we will navigate to another item. Therefore, in the case of the Tab key, we will have to call either the ValidateItem
method or the CommitEdit
method of the cell.
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))
{
if (!ValidateItem(container))
{
e.Handled = true;
return;
}
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))
{
if (!ValidateItem(container))
{
e.Handled = true;
return;
}
gStackPanel.MoveToLastIndex();
ContainerItem lastContainerItem =
(ContainerItem)gStackPanel.Children[gStackPanel.Children.Count - 1];
parentContainer.CurrentCellName = lastContainerItem.GetLastCellName();
if (lastContainerItem.FocusCell(parentContainer.CurrentCellName))
e.Handled = true;
}
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()))
{
if (!ValidateItem(container))
{
e.Handled = true;
return;
}
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 (!parentContainer.CommitEdit(true))
e.Handled = true;
}
}
else
{
if (String.IsNullOrEmpty(parentContainer.CurrentCellName) ||
(parentContainer.CurrentCellName ==
currentItem.GetLastCellName()))
{
if (!ValidateItem(container))
{
e.Handled = true;
return;
}
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;
}
}
}
else
{
if (!parentContainer.CommitEdit(true))
e.Handled = true;
}
}
}
}
}
}
Canceling the Grid's scrolling
To avoid confusing cases such as a cell that is committed when it is not visible, we have implemented a commit of the current cell as soon as the user tries to scroll the grid vertically. For the same reason, we have not allowed the grid to scroll if the current cell cannot be committed.
We have to improve this behavior in order to take into account items validation. The current item must be validated as soon as the user scrolls the grid vertically, and the grid is not allowed to scroll if the current item cannot be validated.
Let's modify the OnVerticalOffsetChanging
method of our HandyContainer
partial class in order to implement these new rules:
protected override void OnVerticalOffsetChanging(CancelOffsetEventArgs e)
{
if (this.IsCurrentItemEditedOrDirty && !this.Validate())
e.Cancel = true;
base.OnVerticalOffsetChanging(e);
}
Canceling nodes expand and collapse
Expanding or collapsing a node is not allowed if the current cell cannot be committed. We have to extend this behavior in order that a node cannot be expanded or collapsed if the current item cannot be validated.
Let's modify the OnIsExpandedChanging
method of our ContainerItem
partial class:
protected override void OnIsExpandedChanging(
Netika.Windows.Controls.IsExpandedChangingEventArgs e)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) && (!parentContainer.Validate()))
e.Cancel = true;
else
base.OnIsExpandedChanging(e);
}
Testing item validation
Let's test our changes by using the CurrentItemValidating
event inside our GridBody project.
Let's first add a Validate
method to our Person
class:
public bool Validate()
{
int zipCodeValue = 0;
int.TryParse(zipCode, out zipCodeValue);
if ((city.ToUpper() == "NEW YORK") &&
((zipCodeValue < 10001) || (zipCodeValue > 10292)))
return false;
return true;
}
In this method, we check that if the city typed by the user is "New York", then the zip code is in the expected range (between 10001 and 10292). In a real application, we would, of course, check the zip code for every city.
Let's modify the Page.xaml of the GridBody project to handle the CurrentItemValidating
event:
<handycontainer x:name="MyGridBody" virtualmode="On" alternatetype="Items"
handydefaultitemstyle="Node" handystyle="GridBodyStyle"
currentcellvalidating="MyGridBody_CurrentCellValidating"
currentitemvalidating="MyGridBody_CurrentItemValidating" />
<handycontainer x:name="MyGridBody" virtualmode="On" alternatetype="Items"
handydefaultitemstyle="Node" handystyle="GridBodyStyle"
currentcellvalidating="MyGridBody_CurrentCellValidating"
currentitemvalidating="MyGridBody_CurrentItemValidating" />
And then, let's add some code to the Page.xaml.cs file in order to validate the value of our item:
private void MyGridBody_CurrentItemValidating(object sender, ItemValidatingEventArgs e)
{
Person currentPerson = HandyContainer.GetItemSource(e.Item) as Person;
if ((currentPerson != null) && (!currentPerson.Validate()))
e.Cancel = true;
}
Let's now try the items validation process:
- Start the application
- Edit the City4 cell and type New York
- Edit the ZipCode4 cell and type 0
- Try to navigate to another cell
We are able to navigate from cell to cell inside the current item, but as the item is not valid, we cannot navigate to another item. We cannot scroll the grid vertically.
- Type a valid value in the ZipCode4 cell such as 10001
Now, we are able to navigate to another item and the grid can be scrolled vertically.
Alerting the user
Introduction
The user can be confused by the fact of not being able to navigate or to scroll when an item cannot be validated. In order that the user understands that there is something wrong with the current item, let's display the border of the item red when it cannot be validated.
IsValid property
Let's add an IsValid
property to our ContainerItem
partial class. This method will switch the ContainerItem
from the Valid
state to the NotValid
state appropriately.
private bool isValid = true;
public bool IsValid
{
get { return isValid; }
internal set
{
if (isValid != value)
{
isValid = value;
if (isValid)
VisualStateManager.GoToState(this, "Valid", true);
else
VisualStateManager.GoToState(this, "NotValid", true);
}
}
}
Implementing the Valid and NotValid states
In both the Container_RowItemStyle
and the Container_RowNodeStyle
styles, we need to implement the Valid
and NotValid
states. This means adding a new ValidStates
VisualStateGroup
:
<vsm:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
As well as a ValidElement
Rectangle
:
<Rectangle Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
IsHitTestVisible="false"
Visibility="Collapsed"
Margin="0,1,1,0"/>
Eventually, our Container_RowItemStyle
and Container_RowNodeStyle
styles will look like this:
<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:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
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>
<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 Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
IsHitTestVisible="false"
Visibility="Collapsed"
Margin="0,1,1,0"
Grid.RowSpan="2"/>
<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:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
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>
<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
Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
IsHitTestVisible="false"
Visibility="Collapsed"
Margin="0,1,1,0"/>
<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>
Updating the IsValid value
Let's update the Validate
method of the ContainerItem
class in order to update the IsValid
property value appropriately:
internal bool Validate(bool keepFocus)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (parentContainer.IsCurrentCellInEditMode &&
!parentContainer.CommitEdit(keepFocus))
return false;
if (!IsDirty)
return true;
ItemValidatingEventArgs eventArgs = new ItemValidatingEventArgs(this, false);
parentContainer._OnCurrentItemValidating(eventArgs);
if (eventArgs.Cancel)
{
IsValid = false;
return false;
}
IsValid = true;
IsDirtyCount = 0;
parentContainer._OnCurrentItemValidated(EventArgs.Empty);
}
return true;
}
Testing our changes
Let's test our changes in order to see if the right border is displayed appropriately:
- Start the application
- Edit the City4 cell and type New York
- Edit the ZipCode4 cell and type 0
- Try to navigate to another item
A red border is displayed around the current item.
- Type a valid value in the ZipCode4 cell such as 10001
- Navigate to another item
The red border is not displayed any more.
7. Polishing the editing process
Focusing an external control
Introduction
There is still a case that we do not have taken into account in the editing process: what happens when another control is focused? Let's illustrate this case by adding a button (or whatever control that can be focused) beside our grid's body:
<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">
<g:GDockPanel>
<Button Content="Focus Button"
g:GDockPanel.Dock="Top"
Margin="5" Width="200"/>
<o:HandyContainer
x:Name="MyGridBody"
VirtualMode="On"
AlternateType="Items"
HandyDefaultItemStyle="Node"
HandyStyle="GridBodyStyle"
CurrentCellValidating="MyGridBody_CurrentCellValidating"
CurrentItemValidating="MyGridBody_CurrentItemValidating"
g:GDockPanel.Dock="Fill">
. . .
</o:HandyContainer>
</g:GDockPanel>
</Grid>
</UserControl>
Let's watch what happens if we click the button while an item is edited:
- Start the application
- Edit the Address3 cell and type something
- Click the button at the top of the page
The cell is committed and the button gets focus. This is the expected behavior. Let's now watch what happens if we click the button while a cell is edited and the cell cannot be committed:
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Click the button at the top of the page
The button gets the focus and, as the cell cannot be committed, a red border is displayed around the FirstName3 cell. Let's now click somewhere in the grid (but not in the FirstName3 cell). Nothing happens.
This is, at the same time, an expected behavior and an unwanted behavior. As programmers, we understand what happens - as the cell is not committed, another cell cannot have the focus, and clicking on the grid has no effect. Therefore, it is an expected behavior. However, as a user, we would like that the FirstName3 cell gets the focus back as soon as we click somewhere on the grid. So, it is also an unwanted behavior.
Let's try to press the Tab key. This time, the focus is "moved" to the grid but not at the right place. Either the item at the top of the grid or the item holding the edited cell gets the focus (according to the actions you have made before editing the cell, another item may get the focus). We would have expected that the FirstName3 cell would have gotten the focus.
Finally, let's watch what happens when an item cannot be validated:
- Start the application
- Edit the City 3 cell
- Type New York
- Click the button at the top of the page
The button gets the focus, but the item is not validated. Nothing warns the user that the values he has typed are not correct. If we click the grid, then the item is validated or not - according to the location where the grid is clicked. As a user, we would have expected the grid to be validated when the button is pressed (as soon as the grid has lost the focus).
Validating when the focus is lost
Let's first override the OnGotFocus
and the OnLostFocus
methods of our HandyContainer
partial class in order to validate the grid when it loses the focus:
private bool hasFocus = false;
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
hasFocus = true;
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
hasFocus = false;
Validate(false);
}
}
Let's try our changes:
- Start the application
- Edit the City 3 cell
- Type New York
- Click the button at the top of the page
This time, the grid is validated when we click the button, and a red border is displayed around the invalid item.
ResumeEdit method
In the cases described above, we would like that the editing process resume as soon as we click somewhere on the grid, or as soon as the grid gets the focus back. Let's add a ResumeEdit
method to the Cell
class. This method will be called to start editing a cell again when needed.
internal bool ResumeEdit()
{
return OnBeginEdit();
}
Let's also add a ResumeEdit
method to the HandyContainer
. The method will check if the grid contains a dirty cell, and if any, it will resume the editing of the cell. If there is no dirty cell, it will then check if the grid contains a dirty item, and if any, it will set the focus back to this item:
internal bool ResumeEdit()
{
if (this.CurrentDirtyCell != null)
return CurrentDirtyCell.ResumeEdit();
else if (this.CurrentDirtyItem != null)
{
FrameworkElement focusElement =
FocusManager.GetFocusedElement() as FrameworkElement;
if (ContainerItem.GetParentContainerItem(focusElement) != this.CurrentDirtyItem)
return this.CurrentDirtyItem.Focus();
}
return false;
}
Calling the ResumeEdit method
We now need to call the ResumeEdit
method when:
- The grid or one of its children gets the focus
- The grid or one of its children is clicked
Nevertheless, the ResumeEdit
method must not be called if the grid already holds the focus (i.e., the grid or one of its child elements is already focused).
HandyContainer OnMouseLeftButtonDown method
Let's override the OnMouseLeftButtonDown
method of the HandyContainer
to call the ResumeEdit
method:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (!hasFocus)
ResumeEdit();
}
HandyContainer OnGotFocus method
We also need to modify the OnGotFocus
method in order to call the ResumeEdit
method if the grid gets the focus:
private bool hasFocus = false;
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (!hasFocus)
{
hasFocus = true;
ResumeEdit();
}
}
ContainerItem OnMouseLeftButtonDown method
We need to change the OnMouseLeftButtonDown
method of the ContainerItem
as well:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
bool canNavigate = true;
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) &&
(parentContainer.IsCurrentItemEditedOrDirty))
{
bool isEditedSameItem =
parentContainer.CurrentEditedOrDirtyItem == this;
if (isEditedSameItem)
{
if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell,
e.OriginalSource as DependencyObject))
canNavigate = parentContainer.CommitEdit(false);
}
else
canNavigate = parentContainer.Validate(false);
}
if (canNavigate)
base.OnMouseLeftButtonDown(e);
else
{
e.Handled = true;
FrameworkElement focusElement =
FocusManager.GetFocusedElement() as FrameworkElement;
if (!TreeHelper.IsChildOf(parentContainer, focusElement))
parentContainer.ResumeEdit();
}
}
Cell OnMouseLeftButtonDown method
Eventually, we have to change the OnMouseLeftButtonDown
method of the Cell
class:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
object currentFocusedElement = FocusManager.GetFocusedElement();
if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
this.Focus();
else
{
e.Handled = true;
FrameworkElement focusElement =
FocusManager.GetFocusedElement() as FrameworkElement;
if (!TreeHelper.IsChildOf(parentContainer, focusElement))
parentContainer.ResumeEdit();
}
}
}
}
Testing our changes
Let's test our changes by replaying the tests we have done at the beginning of this chapter. Let's watch what happens if we click the button while a cell is edited and the cell cannot be committed:
- Start the application
- Edit the FirstName3 cell
- Delete the content of the cell
- Click the button at the top of the page
The button gets the focus and, as the cell cannot be committed, a red border is displayed around the FirstName3 cell. Let's now click somewhere in the grid (but not in the FirstName3 cell). This time, the FirstName3 cell is focused and edited again. This is the wanted behavior. Let's try to press the Tab key. Click the button again and then press the Tab key. The FirstName3 cell is focused and edited again. This is the wanted behavior. Finally, let's watch what happens when an item cannot be validated:
- Type something in the FirstName3 cell in order that it can be committed
- Edit the City 3 cell
- Type New York
- Click the button at the top of the page
The item is validated. If we click the grid or if we press the Tab key, the invalid item is focused.
Strengthen the navigation process
Introduction
In order to have our grid working the way we wanted when a cell was edited, we had to modify the code of the grid at several places to cancel the navigation when a cell cannot be committed or when an item cannot be validated. This is clearly the weakest point of our grid's code.
- If we need to change the behavior of the navigation, we will have to take care of the different places in the code where the navigation can be canceled.
- Nothing prevents a programmer using the grid the wrong way and, for instance, to set the focus on a cell directly (using the
Focus
method of the cell) without using the FocusCell
method of the container item. In that case, the navigation checks (current cell commit and item validation) will be bypassed.
Let's add some "fences" to our code to prevent wrong uses of the grid.
Forbidding focusing a cell in edit mode
The first thing we must take care is to never allow a cell to become the current cell if another cell is edited (and has not been committed). Let's modify the OnGotFocus
method of the Cell
class:
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if ((parentContainer != null) &&
(parentContainer.CurrentEditedCell != null) &&
(parentContainer.CurrentEditedCell != this))
throw new InvalidOperationException(
"Inavalid navigation operation");
if ((this.CellState != CellState.Focused) &&
(this.CellState != CellState.Edited))
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
if (parentContainer != null)
{
parentContainer.CurrentCellName = this.Name;
parentContainer.EnsureCellIsVisible(this);
}
}
}
Forbidding focusing an item
We must also never allow another item to become the current item if the current item is dirty (and it has not been validated). Let's override the OnGotFocus
method of our ContainerItem
partial class to avoid this:
protected override void OnGotFocus(RoutedEventArgs e)
{
HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
if ((parentContainer.CurrentDirtyItem != null) &&
(parentContainer.CurrentDirtyItem != this))
throw new InvalidOperationException(
"Invalid navigation operation");
}
base.OnGotFocus(e);
}
Preventing focusing an item or a cell
In the two steps above, we have not allowed cells and items to get the focus by throwing an exception when it happens. This was the suppression part of our work. Let's now implement the prevention part. When a cell of an item becomes edited, we will prevent the other cells from getting the focus by setting their IsTabStop
property to false
and, when an item becomes dirty or edited, we will also prevent the other items from getting the focus by setting their IsTabStop
property to false
. Let's add a FocusProtect()
and a FocusUnProtect()
method to the Cell
class:
private bool isFocusProtected;
private bool isTabStop;
internal void FocusProtect()
{
if (isFocusProtected)
return;
isTabStop = this.IsTabStop;
this.IsTabStop = false;
isFocusProtected = true;
}
internal void FocusUnProtect()
{
if (!isFocusProtected)
return;
IsTabStop = isTabStop;
isFocusProtected = false;
}
The FocusProtect
method will be called when we would like to prevent a cell from getting the focus, and the FocusUnProtect
method will be called when the cell can get the focus again. Let's also add a FocusProtect
and a FocusUnProtect
method to the ContainerItem
class:
private bool isFocusProtected;
private bool isTabStop;
internal void FocusProtect(Cell skipedCell)
{
if (isFocusProtected)
return;
isTabStop = this.IsTabStop;
this.IsTabStop = false;
List<Cell> cells = this.GetCells();
foreach (Cell cell in cells)
if (cell != skipedCell)
cell.FocusProtect();
isFocusProtected = true;
}
internal void FocusUnProtect(Cell skipedCell)
{
if (!isFocusProtected)
return;
IsTabStop = isTabStop;
List<Cell> cells = this.GetCells();
foreach (Cell cell in cells)
if (cell != skipedCell)
cell.FocusUnProtect();
isFocusProtected = false;
}
Let's add an ItemsFocusProtect
and an ItemsFocusUnProtected
method to the HandyContainer
class. The ItemsFocusProtect
method will "protect" all the items except the one that is dirty or edited. The edited or dirty item will be processed separately:
private void ItemsFocusProtect()
{
Panel itemsHost = this.ItemsHost;
UIElementCollection itemsHostChildren = itemsHost.Children;
foreach (ContainerItem item in itemsHostChildren)
if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
item.FocusProtect(null);
}
private void ItemsFocusUnProtect()
{
Panel itemsHost = this.ItemsHost;
UIElementCollection itemsHostChildren = itemsHost.Children;
foreach (ContainerItem item in itemsHostChildren)
if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
item.FocusUnProtect(null);
}
Let's now call those methods when needed. When a cell is edited, we must prevent the other cells from getting the focus, and we must prevent the items from getting the focus. Let's modify the _OnCurrentCellBeginEdited
method of the HandyContainer
:
internal void _OnCurrentCellBeginEdited(EventArgs e)
{
CurrentEditedCell = this.GetCurrentCell();
CurrentEditedItem = ContainerItem.GetParentContainerItem(CurrentEditedCell);
CurrentEditedItem.FocusProtect(CurrentEditedCell);
if (CurrentDirtyItem == null)
ItemsFocusProtect();
OnCurrentCellBeginEdited(e);
}
Note that the ItemsFocusProtect
method is called only if the current item is not dirty. Indeed, if the item is already dirty, it means that the ItemsFocusProtect
method will already have been called before. When a cell is not edited anymore, we can remove the "focus protection" on the other cells. The items will be unprotected only if the current item is not dirty. Let's modify the _OnCurrentCellEndEdit
method of the HandyContainer
:
internal void _OnCurrentCellEndEdit(EventArgs e)
{
CurrentEditedItem.FocusUnProtect(CurrentEditedCell);
if (currentDirtyItem == null)
ItemsFocusUnProtect();
CurrentEditedCell = null;
CurrentEditedItem = null;
OnCurrentCellEndEdit(e);
}
Eventually, when an item is not dirty anymore, we can remove the "focus protection" from the other items. Let's modify the CurrentDirtyItem
method of the HandyContainer
:
internal ContainerItem CurrentDirtyItem
{
get { return currentDirtyItem; }
set
{
if (currentDirtyItem != value)
{
if (value == null)
ItemsFocusUnProtect();
currentDirtyItem = value;
OnIsCurrentItemDirtyChanged(EventArgs.Empty);
}
}
}
The fences we have added are not perfect. If you would like to be able to change the IsTabStop
property of a cell or an item when a cell is edited, you will have to perfect the methods we have added. However, the way is mapped. Let's try our changes.
- Start the application
- Edit the FirstName3 cell
- Press the Tab key
The LastName3 cell does not get the focus, but the focus is sent "outside" the application. The other navigation keys are working fine. Let's see what happens when we press the Tab key:
- Add a Breakpoint at the beginning of the
KeyDown
method of the RowSpatialNavigator
class - Edit the FirstName3 cell
- Press the Tab key
The breakpoint is never reached. When the FirstName3 cell is edited, the IsTabStop
property of all the other cells and items is set to false
. When we press the Tab key, as no control can get the focus, Silverlight immediately moves the focus on to the next "thing" that can get the focus: the browser.
If we add a new button to our application "after" our grid, the problem vanishes. In that case, when we edit the LastName3 cell, there is a control that can get the focus (the new button) and the focus process proceeds as expected.
In order to get rid of this problem, we will add a fake control to our grid. We will make this control appear when no cell or item can get the focus except the edited cell. This way, there will always be a control that can get the focus when pressing the Tab key. This trick will allow us to resolve our problem.
Let's first create our control. In the GoaOpen\Extensions\Grid folder, add a GridLastFocusControl
class:
using System;
using System.Windows;
using System.Windows.Controls;
namespace Open.Windows.Controls
{
public class GridLastFocusControl: Control
{
public GridLastFocusControl()
{
this.DefaultStyleKey = typeof(GridLastFocusControl);
}
protected override void OnGotFocus(RoutedEventArgs e)
{
throw new InvalidOperationException(
"GridLastFocusControl cannot get focus");
}
}
}
In our generic.xaml file, let's add an empty style for our GridLastFocusControl
:
<Style TargetType="o:GridLastFocusControl">
</Style>
In the generic.xaml file, let's also modify the GridBodyStyle
in order to add a GridLastFocusControl
to the Control Template of the HandyContainer
:
<Style x:Key="GridBodyStyle" TargetType="o:HandyContainer">
...
<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>
<o:GridLastFocusControl Width="0" Height="0"
Visibility="Collapsed"
x:Name="LastFocusControl"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Let's override the OnApplyTemplate
method of our HandyContainer
partial class in order to have a reference to the LastFocusControl
:
Control lastFocusControl;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
lastFocusControl = this.GetTemplateChild("LastFocusControl") as Control;
}
Eventually, let's modify the ItemsFocusProtect
and the ItemsFocusUnProtect
methods of our HandyContainer
class in order to switch the lastFocusControl
Visibility
property value from Visible
to Collapse
and from Collapse
to Visible
when needed:
private void ItemsFocusProtect()
{
Panel itemsHost = this.ItemsHost;
UIElementCollection itemsHostChildren = itemsHost.Children;
foreach (ContainerItem item in itemsHostChildren)
if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
item.FocusProtect(null);
if (lastFocusControl != null)
lastFocusControl.Visibility = Visibility.Visible;
}
private void ItemsFocusUnProtect()
{
Panel itemsHost = this.ItemsHost;
UIElementCollection itemsHostChildren = itemsHost.Children;
foreach (ContainerItem item in itemsHostChildren)
if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
item.FocusUnProtect(null);
if (lastFocusControl != null)
lastFocusControl.Visibility = Visibility.Collapsed;
}
Let's try our changes.
- Start the application
- Edit the FirstName3 cell
- Press the Tab key
This time, the LastName3 cell gets the focus.
Create a CanNavigate method
Another way to improve the code would be to create a "general" CanNavigate
method and replace the individual checks we make at different places by a call to this method.
As a reminder, we check if the navigation needs to be canceled at the following places:
- When the user clicks a cell
- When the user clicks a
CheckBoxCell
- When an item is clicked
- In the
GridSpatialNavigator
- In the
RowSpatialNavigator
- In the
FocusCell
method
We will not implement this method in this tutorial. This is an excellent exercise for the reader. If you need to deeply change the way the navigation is processed by the grid, we suggest you to first implement a general CanNavigate
method yourself. If you are able to complete this task, it will mean that you will have enough knowledge to change the way the navigation works.
8. Speed up the grid
Introduction
Until now, we applied changes to our grid without taking care of the negative impact they could have on the performance of the grid. This is not without any consequences. In fact, our grid is almost twice slower than it was at the end of Step 1! It is slower to start, and it is slower to scroll. The decrease comes mainly from one bottleneck: the use of a TextBox
inside the TextCell
's ControlTemplate
. The way we have implemented the TextCell
's ControlTemplate
, a TextBox
is created for every TextCell
displayed inside the grid. This means that if the grid displays, for instance, 30 rows (i.e., items) and each row holds 6 TextCell
s, the grid will have to create and manage 180 textboxes at a time! In fact, it will have to manage even more textboxes because the grid maintains a cache of some rows.
Removing the TextBox from the ControlTemplate
Adding a TextBox
in the TextCell
's ControlTemplate
was an easy and quick way to implement the Edited
state of the TextCell
. However, it is not essential to proceed this way. Let's remove the TextBox
from the ControlTemplate
. We will add it dynamically from the TextCell
code only when needed. We will also need to remove the StoryBoard
that makes the TextBox
visible in the "Edited
" VisualState
.
<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:VisualState x:Name="Edited">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<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:VisualStateGroup x:Name="ValidStates">
<vsm:VisualState x:Name="Valid"/>
<vsm:VisualState x:Name="NotValid">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ValidElement"
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
Name="ValidElement"
Stroke="Red"
StrokeThickness="2"
IsHitTestVisible="false"
Margin="0,1,1,0"
Visibility="Collapsed"/>
<Grid x:Name="TextContainerElement">
<TextBlock
x:Name="TextElement"
Text="{TemplateBinding Text}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}"/>
</Grid>
<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 we have given a name to the Grid
containing the TextElement
. This is because we will need a reference to this grid in order to be able to dynamically add the TextBox
inside it.
Create the TextBox from code
Let's modify the code of the cell in order to dynamically create the TextBox
when the cell is edited.
OnApplyTemplate method
We first need to modify the OnApplyTemplate
method to remove the reference to the TextBoxElement
that does not exist anymore. We also must add a new reference to the new TextContainerElement
:
private Panel textContainerElement;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
textContainerElement = GetTemplateChild("TextContainerElement") as Panel;
}
OnBeginEdit method
When the OnBeginEdit
method is called, we have to dynamically create the TextBox
and insert it in the TextContainerElement
:
private TextBox textBoxElement;
protected override bool OnBeginEdit()
{
if (textContainerElement != null)
{
if (textBoxElement == null)
{
textBoxElement = new TextBox();
textBoxElement.Style =
ResourceHelper.FindResource("CellTextBoxStyle") as Style;
textBoxElement.Text = this.Text;
textBoxElement.Margin = this.Padding;
textBoxElement.HorizontalAlignment = this.HorizontalContentAlignment;
textBoxElement.VerticalAlignment = this.VerticalContentAlignment;
textBoxElement.Foreground = this.Foreground;
textBoxElement.TextChanged +=
new TextChangedEventHandler(textBoxElement_TextChanged);
textContainerElement.Children.Add(textBoxElement);
}
if (textBoxElement.Focus())
{
textBoxElement.SelectionStart = textBoxElement.Text.Length;
return true;
}
}
return false;
}
In the code above, as soon as we have created the TextBox
element, we initialize some of its properties. Then, we add the TextBox
to the children collection of the textContainerElement
panel.
Also note the use of the ResourceHelper.FindResource
method to find the CellTextBoxStyle
style to apply to the Style
property of the TextBox
.
OnCommitEdit and OnCancelEdit methods
It could be tempting to modify the OnCommitEdit
and the OnCancelEdit
methods in order to remove the TextBox
after the editing process has completed. Nevertheless, the editing process is only partially completed when OnCommitEdit
is called. If you take a look at the CommitEdit
method of the Cell
class, you will see that the isBindingValid
flag (the result of the binding validation process) is checked after the call to the OnCommitEdit
method. At this point of the code, the commit process can still be cancelled. Furthermore, the OnCommitEdit
method and the OnCancelEdit
method are called only if the cell is dirty.
AfterEdit method
Let's add an AfterEdit
virtual method to our Cell
class.
protected virtual void AfterEdit()
{
}
Let's call the AfterEdit
method at the end of the CommitEdit
method and the CancelEdit
method.
internal bool CommitEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
HandyContainer parentContainer =
HandyContainer.GetParentContainer(this);
if (parentContainer != null)
{
CellValidatingEventArgs cancelEventArgs =
new CellValidatingEventArgs(this.CurrentValue,
this.DirtyValue, false);
parentContainer._OnCurrentCellValidating(cancelEventArgs);
if (cancelEventArgs.Cancel)
{
IsValid = false;
return false;
}
OnCommitEdit();
if (!isBindingValid)
{
IsValid = false;
return false;
}
IsValid = true;
IsDirty = false;
parentContainer._OnCurrentCellValidated(EventArgs.Empty);
}
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
AfterEdit();
HandyContainer parentContainer =
HandyContainer.GetParentContainer(this);
parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);
}
return true;
}
internal virtual bool CancelEdit(bool keepFocus)
{
if (this.CellState != CellState.Edited)
return true;
if (IsDirty)
{
OnCancelEdit();
IsValid = true;
IsDirty = false;
ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
if (parentRow != null)
parentRow.IsDirtyCount--;
}
if (this.CellState == CellState.Edited)
{
if (keepFocus)
{
VisualStateManager.GoToState(this, "Focused", true);
this.CellState = CellState.Focused;
bool gotFocus = this.Focus();
Debug.Assert(gotFocus);
}
else
{
VisualStateManager.GoToState(this, "Standard", true);
this.CellState = CellState.Standard;
}
AfterEdit();
HandyContainer parentContainer =
HandyContainer.GetParentContainer(this);
parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);
}
return true;
}
Override the AfterEdit method
Let's now override the AfterEdit
method in the TextCell
class and remove the TextBox
from the TextContainerElement
:
protected override void AfterEdit()
{
if ((textBoxElement != null) && (textContainerElement != null))
{
if (textBoxElement == FocusManager.GetFocusedElement())
this.Focus();
textContainerElement.Children.Remove(textBoxElement);
textBoxElement.TextChanged -=
new TextChangedEventHandler(textBoxElement_TextChanged);
textBoxElement = null;
}
}
9. Polishing the TextCell
Double click support
Introduction
Most of the time, double-click is not supported in web applications. Nevertheless, most users would like that when they double-click a TextCell
, the entire content of the cell is selected. Let's implement this feature.
Simulate a double-click event
As Silverlight does not implement any double click events, we will have to simulate it ourselves. In order to implement this feature, we will have to modify the OnMouseLeftButtonDown
method of our TextCell
class.
private long lastTick;
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (this.CellState == CellState.Focused)
BeginEdit();
if (this.CellState == CellState.Edited)
{
e.Handled = true;
if (DateTime.Now.Ticks - lastTick < 2000000)
DoubleClick();
}
lastTick = DateTime.Now.Ticks;
}
Each time this method is called, we store the time when the method was called in the lastTick
field. If the method is called twice within a small amount of time, we will consider that the cell has been double clicked. Implementing a full double-click support would imply more checks and conditions. For instance, in the code above, we have not handled the case of a user clicking the cell three times within a small amount of time. Is-it a double double-click? However, our basic implementation is strong enough to handle our particular case.
DoubleClick method
The implementation of the DoubleClick
method is easy:
private void DoubleClick()
{
if (textBoxElement != null)
textBoxElement.SelectAll();
}
Let's try our change:
- Start the application
- Click a cell to make it the current cell
- Double click another cell
The cell is edited and the text of the cell is selected. Nevertheless, the following situation does not work:
- Click a cell to make it the current cell
- Double click the current cell
In that case, the text of the cell is not selected. This comes from the fact that the textBoxElement
is displayed between the two clicks. During the first call to the OnMouseLeftButtonDown
method, the BeginEdit
method is called and the TextBox
is displayed inside the cell. Then, the OnMouseLeftButtonDown
method is not called a second time because the second click is trapped by the TextBox
and not the TextCell
.
CellTextBox
In order to handle the case when the TextBox
is displayed between two clicks, let's create our own TextBox
.
using System.Windows.Controls;
using System;
namespace Open.Windows.Controls
{
public class CellTextBox: TextBox
{
internal long LastTick
{
get;
set;
}
protected override void OnMouseLeftButtonDown(
System.Windows.Input.MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (DateTime.Now.Ticks - LastTick < 2000000)
DoubleClick();
LastTick = DateTime.Now.Ticks;
}
private void DoubleClick()
{
this.SelectAll();
}
}
}
The CellTextBox
implements a LastTick
property that we will be filled with the lastTick
value of the TextCell
. Let's modify the textBoxElement
field and the OnBeginEdit
method of the TextCell
in order to use a CellTextBox
rather than a standard TextBox
in our TextCell
:
private CellTextBox textBoxElement;
protected override bool OnBeginEdit()
{
if (textContainerElement != null)
{
if (textBoxElement == null)
{
textBoxElement = new CellTextBox();
textBoxElement.Style =
ResourceHelper.FindResource("CellTextBoxStyle") as Style;
textBoxElement.Text = this.Text;
Let's also modify the OnMouseLeftButtonDown
method of the TextCell
class in order to initialize the LastTick
property of the CellTextBox
:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (this.CellState == CellState.Focused)
BeginEdit();
if (this.CellState == CellState.Edited)
{
e.Handled = true;
if (DateTime.Now.Ticks - lastTick < 2000000)
DoubleClick();
}
lastTick = DateTime.Now.Ticks;
if (textBoxElement != null)
textBoxElement.LastTick = lastTick;
}
Let's start the application to check our changes:
- Click a cell to make it the current cell
- Double click the current cell
This time, the text of the cell is selected.
10. Using the Editing and Validation features
Cells' editing process
In order to help you understanding and use the editing process of the grid, here is a picture of it.
- The green circles are the possible starting points.
- The green rectangles are the methods and internal processes.
- The blue rectangles are the events.
- A process is ended by a red circle.
Only the most common events are displayed in the picture.
Items' validation process
11. Conclusion
This article allowed us to implement editing and validation into our grid body. We have made several assumptions such as:
- Validation must be made as soon as the focus leaves a cell.
- If a cell or an item cannot be validated, navigation to another cell or item is not allowed.
- Only the current cell may be edited.
- When the user clicks a
TextCell
, it becomes the current cell. The user must click this cell one more time in order to edit it. - When the user clicks a
CheckBoxCell
, it becomes the current cell and it is instantly edited. - ...
It is up to you to modify these default behaviors (and others) in order that the data grid fits your wishes.
12. History
6 June 2009
- Updated the
AfterEdit
method (added condition about focus).
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.