Columns & Rows, Part IV

Silverlight 2.0 DataGrid CRUD Operations

asp:Feature

Columns & Rows: Part IV

Silverlight 2.0 DataGrid CRUD Operations

By Bilal Haidar

Silverlight 2.0 DataGrid not only displays data in read-only mode, it allows Excel-like inline editing of records. You also can add records to, or delete records from, the DataGrid. Having such a full-fledged DataGrid control enriches data-centered applications by giving users an easy way to view and manipulate data in an application. Performing create, read, update, and delete (CRUD) operations is the topic of this fourth installment in the series on Silverlight 2.0 DataGrid. I will discuss each operation in detail.

In the first three installments of this series, I introduced the DataGrid major properties, adding a DataGrid control to a page, defining its columns in XAML and at runtime, and making use of the DataGrid control to implement a Master-Detail scenario.

I presented the DataGrid control as a read-only control with no ability to edit/delete/insert data. In this article, youll learn how to use the DataGrid controls inline edit feature, how to delete rows, and how to enable the DataGrid control so that you can insert new records.

References to Silverlight 2.0

This article assumes a fair knowledge of using Silverlight 2.0 especially data-binding features and is not intended as an introduction to Silverlight 2.0. If you need more information about Silverlight 2.0, go to the official Silverlight website at http://www.silverlight.net, where you'll find dozens of articles and videos to get you started. Another major resource for learning Silverlight 2.0 is http://www.silverlightshow.net; it focuses on delivering rich and comprehensive Silverlight tutorials.

In addition, you can download the Microsoft Silverlight 2.0 SDK Documentation, which covers all features of Silverlight (http://www.microsoft.com/downloads/details.aspx?familyid=BCE7684A-507B-4FC6-BC99-6933CD690CAB&displaylang=en).

ObservableCollection Overview

The ObservableCollection class is part of the System.Collections.ObjectModel namespace. It represents a dynamic and generic collection of data items and inherits from the Collection class, as the following example shows:

public class ObservableCollection : Collection, INotifyCollectionChanged, INotifyPropertyChanged

{ }

In addition to inheriting from the Collection class, the ObservableCollection class also inherits from the INotifyCollectionChanged interface, and that's what makes it a dynamic collection.

This interface provides the CollectionChanged event that is to be raised by collections implementing this interface whenever a new item is added or removed from the collection. The ObservableCollection class is no different; it implements the INotifyCollectionChanged interface and raises the CollectionChanged event whenever a new item is added or removed from its items.

There is an advantage to implementing the aforementioned interface with the ObservableCollection. When binding a UI FrameworkElement to an instance of an ObservableCollection, the UI will be notified automatically by the collection that an item has been added or deleted. The collection will raise the CollectionChanged event and update itself, incorporating all the changes in the data source. This is what makes ObservableCollection class more beneficial in data-binding scenarios than the generic List or any other IEnumerable collection. For additional details about the INotifyCollectionChanged interface, visit http://msdn.microsoft.com/en-us/library/system.collections.specialized.inotifycollectionchanged.aspx.

The ObservableCollection class also implements the INotifyPropertyChanged interface. This interface is similar to INotifyCollectionChanged in terms of notifying the UI of any change in the collection data. However, it operates on the property level and provides the PropertyChanged event. This event is to be raised by any data object used as a data source and that needs to send notification to the UI FrameworkElement objects to inform them of any change in the underlying data.

The ObservableCollection class implements this interface and raises the PropertyChanged events internally whenever one of its properties changes. For more information about the INotifyPropertyChanged interface, visit http://msdn.microsoft.com/en-us/library/system.collections.specialized.inotifycollectionchanged.aspx.

I recommend that you use the ObservableCollection class when working with Silverlight data-binding because of the automated notification system that is present for you out of the box.

DataGrid Inline Editing

As mentioned, the DataGrid control enables inline editing similar to that found in Microsoft Excel spreadsheets. To edit a cell's data, simply double-click the cell, and the cell changes from read-only mode to editable-mode.

To enable inline editing in the DataGrid control, set the value of the IsReadOnly property to false. By default this property is set to a value of true, which disables DataGrid inline editing. Because the Silverlight 2.0 DataGrid control has limited events fired when it comes to handling changed data, I'll provide a workaround on the data source object to handle different states.

Throughout this series of articles, I've used a collection of Employee objects. The Employee class adds a property named RowState that is of type DataRowState enumeration. This enumeration is defined as:

public enum DataRowState

{

Unchanged,

Added,

Deleted,

Modified

}

The enumeration contains these entries:

         Unchanged, which is assigned on an Employee object when a record is loaded from a data source.

         Added, which is assigned on an Employee object when it is added to the list.

         Deleted, which is assigned on an Employee object when it is deleted and removed from list.

         Modified, which is assigned on an Employee object when any of the objects properties is edited.

To change the RowState of an Employee object, let's look at a sample property implemented inside the Employee class:

private string firstName = "";

public string FirstName

{

get { return this.firstName; }

set

{

if (string.IsNullOrEmpty(value))

return;

if (this.rowState == DataRowState.Deleted)

return;

if (this.rowState == DataRowState.Unchanged)

this.rowState = DataRowState.Modified;

this.firstName = value;

this.NotifyPropertyChanged("FirstName");

}

}

Of importance is the Set section of the FirstName property. If the current object's RowState property is DataRowState.Deleted, the property returns without any changes done on the private member variable. On the other hand, if the object is an already-loaded object from a data source and its RowState is DataRowState.Unchanged, then change the object's RowState and set it to DataRowState.Modified. Finally, update the private member variable, then fire a private utility method that will eventually fire the PropertyChanged event. Remember, the Employee class implements the INotifyPropertyChanged; thus it needs to always notify the UI FrameworkElement objects of any change in the data object's property.

Now that you know what goes on behind the scenes when any of the object properties change, let's step through an example to show how to enable editing in the DataGrid control.

IsReadOnly="False">

As mentioned, to enable inline editing on the DataGrid control, simply set the IsReadOnly property to false. In addition, follow the steps explained previously on how to construct the data source object by adding the RowState property, so that you can catch and control any object that has been modified. This is important when you need to reflect the changes back into the data store.

Once the IsReadOnly property is set to false, double-clicking any cell will place the cell into editable mode, as shown in Figure 1. As you can see, one of the cells contains the mouse cursor, which means the cell is in edit mode.

Figure 1: The DataGrid in edit mode

It's important to mention again that in edit mode, the DataGridTextColumn presents data in a TextBox control. The DataGridCheckBoxColumn presents data in an enabled CheckBox control, allowing you to flip between Checked and Unchecked states. What is left is the DataGridTemplateColumn, where you should set a DataTemplate to its CellEditingTemplate to prepare the UI to be displayed when the column is in edit mode.

There are other ways to handle editing data in Silverlight DataGrid, including ADO.NET Data Services. But what I've presented here is a simple yet flexible object-oriented way to handle changes to objects.

Deleting Records from the DataGrid Control

To delete a row displayed by the DataGrid control, we'll hook to the KeyDown event of the DataGrid control, inherited from the UIElement class. The strategy behind deleting rows from the DataGrid control is to allow a user to press the Delete key on the keyboard while a row is selected.

Once a key on the keyboard is pressed while the UIElement has the focus, the KeyDown event handler fires. allowing the developer to grab the key pressed. First, we'll hook the DataGrid controls KeyDown event to an event handler in the code-behind:

// Attach to events

this.dgEmployees.KeyDown += dgEmployees_KeyDown;

Before we explain the implementation details of the dgEmployees_KeyDown event handler, we need to make sure that any row can be deleted only when the row is in read-only mode and not in editable mode. To support this condition, we'll hook into the DataGrid controls BeginningEdit and CurrentCellChanged events and handle them as:

private bool editing;

First, we'll define the editing private member. This member will define whether the DataGrid control is in edit mode or not. The BeginningEdit event is fired before the DataGrid enters the edit mode.

void dgEmployees_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)

{

// Grid is considered in edit-mode

this.editing = true;

}

The event handler noted previously sets the private member editing to a value of true, meaning that the DataGrid control is now in edit mode. The CurrentCellChanged event fires when the DataGrid controls CurrentCell property changes.

void dgEmployees_CurrentCellChanged(object sender, EventArgs e)

{

this.editing = false;

}

This means that if a cell is in edit mode and a user selects another cell, the DataGrid control will turn into read-only mode. This is exactly what we need to capture. In fact, we need to set the private member editing to false when the CurrentCell property on the DataGrid control changes, so that the DataGrid control is not in edit mode anymore.

Now that you understand the role of private member editing, let's discuss the dgEmployees_KeyDown event handler. The event handler allows a row to be deleted only if the key pressed on the keyboard is the Key.Delete key, there is a row selected on the DataGrid control, and the DataGrid control is not in edit mode.

private void dgEmployees_KeyDown(object sender, KeyEventArgs e)

{

// If the user selected a row and clicked

// on the Delete key, deleting a row\

if ((e.Key == Key.Delete) && (this.dgEmployees.SelectedItem != null) && (!editing))

{

.

.

Give the end user a warning that he is about to delete a row, and if he accepts, then proceed.

#region Handle Delete

HtmlPage.Window.Dispatcher.BeginInvoke(delegate

{

result = MessageBox.Show("Are you sure you want to delete this record?",

"Delete a Record",

MessageBoxButton.OKCancel);

if (result == MessageBoxResult.Cancel)

return;

Figure 2 shows the warning the user will receive upon pressing the Delete key while selecting a record in read-only mode. Retrieve the data source bound to the DataGrid control.

Figure 2: Deleting a record from the DataGrid control

// Get the datasource of the DataGrid

ObservableCollection employees =

(ObservableCollection)this.dgEmployees.ItemsSource;

Cast the selected data item on the DataGrid to its bound object type, in this case an Employee object. Remember, the DataGrid control is bound to a collection of ObservableCollection (i.e., a collection of Employee records).

// Get the Employee to be deleted

Employee originalEmployee = (Employee)this.dgEmployees.SelectedItem;

Retrieve the corresponding selected Employee object from the data source.

// Update the Employee to be now in delete mode

Employee deletedEmployee =

(from emp in employees

where emp.EmployeeID == originalEmployee.EmployeeID

select emp).Single();

Call the Delete method on the Employee object. This method will internally set the object's RowState property to DataRowState.Deleted.

// Set the RowState on the Employee Instance

deletedEmployee.Delete();

Rebind the DataGrid control to the updated data source, thus causing the LoadingRow event on the DataGrid control to fire. Why? We will see in a moment why we need this event to fire.

this.dgEmployees.ItemsSource = null;

this.dgEmployees.ItemsSource = employees;

// Empty objects

employees = null;

});

#endregion

}

}

We've used the LoadingRow event on the DataGrid, which fires when a new DataGridRow is initialized and before it's added to the DataGrid's collection of rows, to attach to the DataGridRows Loaded event.

private void dgEmployees_LoadingRow(object sender, DataGridRowEventArgs e)

{

e.Row.Loaded += new RoutedEventHandler(Row_Loaded);

}

When a DataGridRow finishes its initialization and is added and loaded into the DataGrid control, the row's Loaded event fires.

void Row_Loaded(object sender, RoutedEventArgs e)

{

// Hide the row that has the RowState DataRowState.Deleted

DataGridRow row = sender as DataGridRow;

Employee employee = row.DataContext as Employee;

if (employee.RowState == DataRowState.Deleted)

row.Visibility = Visibility.Collapsed;

}

We're using the row's Loaded event to hide any data row whose underlying data source object, in this case an Employee object, has the RowState property set to DataRowState.Deleted. In this way, only modified, unchanged, or new records will be displayed and presented by the DataGrid control.

This explains why we have reset the ItemsSource property on the DataGrid control so that the DataGrids LoadingRow event fires, thus firing each row's Loaded event and hiding any deleted row.

The employee data source object that the deleted row corresponds to is never removed from the data source collection bound to the DataGrid control. Rather the record is marked as deleted and will be deleted later when all the changes on the DataGrid control are submitted, as you'll see later in this article.

Inserting Records Using the DataGrid Control

In the previous installments of this series, you learned how to use the DataGrid control to display read-only data records. In the previous sections, you learned how to edit and delete data records displayed by the DataGrid control. So far, we've covered the R, U, and D letters of CRUD; what remains is the C letter (i.e., creating data records using the DataGrid control).

By default, the DataGrid control doesn't provide a way to add new records in its configuration properties. Therefore, you need a workaround in the underlying data source. We'll follow a technique demonstrated by Mike Taulty in his video on editing/insert/deleting in Silverlight DataGrid, which you can view or download at http://channel9.msdn.com/posts/mtaulty/Silverlight-InsertUpdateDelete-with-the-DataGrid/.

With this technique, you create a new class named BlankRowCollection that inherits from ObservableCollection class in such a way that, whenever a new instance of this custom collection is created, an empty row is added as the last row in the displayed DataGrid control. Thus, a user will always have the chance to add any record at any time. Whenever the Empty record is filled by the user, a new empty record is added automatically to the end of the DataGrid control. In the following section, I'll step through the BlankRowCollection class implementation.

The BlankRowCollection class is defined as:

public class BlankRowCollection : ObservableCollection

where T: INotifyPropertyChanged, new()

{

As the previous code shows, the BlankRowCollection class inherits from the ObservableCollection class and specifies a condition on its data item generic type to be a class that implements the INotifyPropertyChanged interface and has an empty constructor.

public BlankRowCollection()

{

t = new T();

base.InsertItem(0, t);

t.PropertyChanged += OnPropertyChanged;

}

In the empty constructor of the custom collection:

         Create a new instance of the data item generic type using its default constructor.

         Call the ObservableCollection InsertItem method to add the new data item into the list of items of the collection.

         Hook into the PropertyChanged event of the data item generic type.

This explains why we force the BlankRowCollection to have its input data item generic type implement the INotifyPropertyChanged: We need to hook into its PropertyChanged event. In addition, we force the condition that the input data item generic type must contain an empty constructor, so that we can create a new default instance represented by an empty row displayed by the DataGrid control.

Following on the third bullet in the previously shown list, the OnPropertyChanged event handler is defined as:

protected void OnPropertyChanged(object sender, PropertyChangedEventArgs e)

{

t.PropertyChanged -= OnPropertyChanged;

t = new T();

t.PropertyChanged += OnPropertyChanged;

base.InsertItem(this.Items.Count, t);

}

When a user starts typing in any of the above-created empty rows column cells, the PropertyChanged event handler, on underlying data item, fires on the spot. What this code does is unhook the PropertyChanged event of the previous empty row, create a new empty data item, hook into its PropertyChanged event, and add the new empty item as the last row entry on the DataGrid control. This allows a user to add additional rows. Therefore, the trick behind hooking into the PropertyChanged event of a new data item is:

         Unhook to the previous row's data item PropertyChanged event that is already filled by the user.

         Create a new empty row to provide additional rows for data entry.

In addition to the previous, the BlankRowCollection class overrides the InsertItem() method as follows:

protected override void InsertItem(int index, T item)

{

if (this.Items.Count == index)

index = this.Items.Count - 1;

base.InsertItem(index, item);

}

The InsertItem() method is overridden to ensure that the new empty row always shows as the last row of the DataGrid control. This method decrements the position of any item that is being added to the end of the list by 1, thus making sure the new empty row is always showing as the last row of DataGrid control.

To add a DataGrid control with the option of adding new data records available, simply bind the DataGrid control to a collection of type BlankRowCollection.

this.dgEmployees.ItemsSource = EmployeeManager.GetEmployeeListAdd();

The GetEmployeeListAdd() method returns an instance of type BlankRowCollection and is defined as:

public static BlankRowCollection GetEmployeeListAdd()

{

BlankRowCollection employees = new BlankRowCollection(){

new Employee(Guid.NewGuid(), "Bilal", "Haidar", "Lead Software Developer", "Project

Manager", 28, false, new DateTime(2006, 4, 16), new List() {"MVP", "MCT"}),

new Employee(Guid.NewGuid(), "Wessam", "Zeidan", "IT Manager", "IT Manager", 28,

false, new DateTime(2006, 4, 16), new List() {"ICQ", "MSN"}),

new Employee(Guid.NewGuid(), "Haissam", "Abdul Malak", "Senior Software

Developer", "Software Developer", 25, false, new DateTime(2006, 4, 16), new

List(){"PMV", "Gmail"}),

new Employee(Guid.NewGuid(), "Sami", "Souki", "Lead Software Developer", "Project

Manager", 21, true, new DateTime(2006, 4, 16), null),

new Employee(Guid.NewGuid(), "Yousef", "Daher", "Junior Software Developer",

"Junior Software Developer", 21, false, new DateTime(2006, 4, 16), null),

new Employee(Guid.NewGuid(), "Dalida", "James", "Software Developer", "Software

Developer", 25, true, new DateTime(2006, 4, 16), null),

new Employee(Guid.NewGuid(), "Remee", "Hopkins", "Senoir Software Developer",

"Project Manager", 28, true, new DateTime(2006, 4, 16), new List() {"MVP", "MCP"}),

new Employee(Guid.NewGuid(), "Natalie", "Farah", "Project Manager", "Project

Manager", 26, false, new DateTime(2006, 4, 16), null)

};

return employees;

}

Figure 3: The DataGrid control with inline insert enabled

Figure 3 shows the result of enabling a DataGrid control with the option to allow a user to add new inline records. As you can see, the DataGrid control is populated with some records and the last row is an empty one, with default values shown for each column cell corresponding to the underlying data object properties default values. Figure 3 also shows the Save Data button in the lower left corner of the page. This buttons handler is defined as:

private void btnSave_Click(object sender, RoutedEventArgs e)

{

BlankRowCollection employees =

this.dgEmployees.ItemsSource as BlankRowCollection;

for (int i = 0; i < employees.Count - 1; i++)

{

switch (employees[i].RowState)

{

case DataRowState.Added:

this.txtUpdates.Text += string.Format("Employee {0} has been added.\n",

employees[i].LastName);

break;

case DataRowState.Deleted:

this.txtUpdates.Text += string.Format("Employee {0} has been deleted.\n",

employees[i].FirstName);

break;

case DataRowState.Modified:

this.txtUpdates.Text += string.Format("Employee {0} has been updated.\n",

employees[i].FirstName);

break;

default:

break;

}

}

}

The method starts by casting the DataGrid control's data source into a collection of type BlankRowCollection instance. Then it loops over the data items in the collection and uses a switch statement to decide on the data item's RowState property. This way, you can know what records were newly added, deleted, edited, and unchanged and accordingly perform your updates into the back-end database using several Silverlight 2.0 methods, including calling an ASMX Web Service, WCF Service, or ADO.NET Data Services.

To try out a working example of adding, editing, and deleting rows in a DataGrid control, you can browse a live sample at http://bhaidar.net/SilverlightDataGrid/GridCRUD.aspx.

This article shows how easy it is to delete, edit, and insert new records with the Silverlight 2.0 DataGrid. I introduced the magical ObservableCollection class, which has a built-in notification mechanism that helps the binding in Silverlight reflect changes happening at the data sources. And I provided step-by-step examples to demonstrate that Silverlight 2.0 DataGrid has the capability to perform the delete, edit, and insert actions.

Source code accompanying this article is available for download.

Bilal Haidar ([email protected]) is a Microsoft MVP in ASP/ASP.NET. He is an MCP, MCTS, MCPD, MCT, and Telerik MVP. He is a lead software developer at CCC, a multinational construction company based in Athens, Greece.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish