Click here to Skip to main content
Click here to Skip to main content

Build Your Own DataGrid for Silverlight: Step 2

, 3 Jun 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
Implementation of editing and validation features.

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;
        //we do not want that the ContainerItem 
        //OnMouseLeftButtonDown is called
}

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()
{
    //Reset the EditedValue to be sure that 
    //the CheckMarkVisibility value returns to the old value
    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;
                //in that case we do not want that the event goes further
        }
    }
}

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);
    //The following condition is not perfectly correct. It will be enhanced later on
    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;
                //in that case we do not want that the event goes further
        }
    }
}

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"/>
                        <!-- Item content -->
                        <g:GContentPresenter
                          Grid.RowSpan="2" 
                          x:Name="ELEMENT_ContentPresenter"
                          Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          OrientatedHorizontalAlignment=
                                "{TemplateBinding HorizontalContentAlignment}"
                          OrientatedMargin="{TemplateBinding Padding}"
                          OrientatedVerticalAlignment=
                                "{TemplateBinding VerticalContentAlignment}"  
                          PresenterOrientation=
                                "{TemplateBinding PresenterOrientation}"/>
                        <Rectangle x:Name="BorderElement" 
                           Grid.RowSpan="2" 
                           Stroke="{TemplateBinding BorderBrush}" 
                           StrokeThickness="{TemplateBinding BorderThickness}" 
                           Margin="-1,0,0,-1"/>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

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

                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="IsExpandedStates">
                            <vsm:VisualState x:Name="NotIsExpanded"/>
                            <vsm:VisualState x:Name="IsExpanded">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="CheckedArrow" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowUnchecked" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ExpandedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="AlternateStates">
                            <vsm:VisualState x:Name="NotIsAlternate"/>
                            <vsm:VisualState x:Name="IsAlternate">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="AlternateBackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="BackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="InvertedStates">
                            <vsm:VisualState x:Name="InvertedItemsFlowDirection">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowCheckedToTop" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowCheckedToBottom" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="NormalItemsFlowDirection"/>
                        </vsm:VisualStateGroup>
                        <vsm: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; //in that case we do not want that the event goes further
                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 TextCells, 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;
        //we do not want that the ContainerItem
        //OnMouseLeftButtonDown is called

        if (DateTime.Now.Ticks - lastTick < 2000000)
        //It is a double click
            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)
            //It is a double click
                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;
        //we do not want that the ContainerItem OnMouseLeftButtonDown is called

        if (DateTime.Now.Ticks - lastTick < 2000000) //It is a double click
            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.

CellEditingProcess.jpg

Items' validation process

ItemValidation.jpg

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).

License

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

Share

About the Author

Jeff Karlson
Web Developer
Belgium Belgium
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.141022.2 | Last Updated 4 Jun 2009
Article Copyright 2009 by Jeff Karlson
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid