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:

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

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

Collapse
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:

Collapse




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":

Collapse





Visible






Collapsed






Visible





After these changes, the TextCell style will look like this:

Collapse

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:

Collapse
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".

Collapse
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:

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

Collapse
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:

Collapse

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:

Collapse

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:

Collapse

































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:

Collapse
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:

Collapse
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:

Collapse
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:

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

Collapse
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:

Collapse
protected override void OnCancelEdit()
{
if (textBoxElement != null)
textBoxElement.Text = this.Text;
}
For now, we will implement an empty OnCancelEdit method in the CheckBoxCell class:

Collapse
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:

Collapse
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:

Collapse
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:

Collapse








Visible










Visible







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:

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

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

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

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