Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using the Silverlight DataGrid with View Model / MVVM

0.00/5 (No votes)
8 Aug 2010 6  
The Silverlight DataGrid using inline editing, paging, sorting, and button events, using the View Model.

[Click here for a live sample]

Also see:

View Model and the Silverlight DataGrid

The Silverlight DataGrid is a powerful control that allows inline editing, paging, sorting, and drag and drop re-ordering of columns. It can be challenging to understand when using normal code-behind techniques, but sometimes downright baffling when using View Model / MVVM style programming.

In the following examples, we will not directly reference the DataGrid in the View Model. The code we will create can be consumed by any collection control such as a ListBox.

img32.jpg

Note: If you are new to View Model Style programming, it is recommended that you read: Silverlight View Model Style: An (Overly) Simplified Explanation.

It's not the DataGrid You Want to Manipulate - It's the Collection Bound to the DataGrid

One of the primary things to understand about the Silverlight DataGrid is that, it always uses a "View of the Data" that implements an ICollectionView. According to http://msdn.microsoft.com/en-us/library/system.componentmodel.icollectionview(VS.95).aspx:

"The DataGrid control uses this interface to access the indicated functionality in the data source assigned to its ItemsSource property. If the ItemsSource implements IList, but does not implement ICollectionView, the DataGrid wraps the ItemsSource in an internal ICollectionView implementation."

In the initial examples, we will simply bind a collection to the DataGrid and allow this automatic CollectionView to be created. In the Sorting example, we will implement our own CollectionViewSource, so that we can hook in to, and detect, events.

We will discover that when we have control over the collection the DataGrid is bound to in the View Model, it gives us all the control we need to implement the functionality we desire.

(For more information about the methods and properties available on the DataGrid, see: http://msdn.microsoft.com/en-us/library/system.windows.controls.datagrid_methods(v=VS.95).aspx)

This tutorial uses Visual Studio 2010 (or higher) and Expression Blend 4 (or higher).

The Application

The application allows for sorting, by clicking on the column headers, and paging using the buttons on the bottom of the DataGrid.

You can drag the column headers and reorder them.

The Comment field only shows the first 25 characters unless you are editing the comment.

Clicking on a Comment field twice will allow you to edit the comment. Clicking away (anywhere on the application that is not on the box being edited) will save the comment.

Clicking on the Clear Button will turn the Comment field for that row into ****.

Display Items on a DataGrid

Let's start with simply displaying items on a DataGrid.

First, we create a Silverlight project with a website.

Then, we create a database called RIATasks, and add a table called RIAComments:

USE [RIATasks]
GO
CREATE TABLE [dbo].[RIAComments](
    [CommentID] [int] IDENTITY(1,1) NOT NULL,
    [Comment] [nvarchar](max) NOT NULL,
    [CommentUpdateDate] [datetime] NOT NULL,
 CONSTRAINT [PK_RIAComments] PRIMARY KEY CLUSTERED 
(
    [CommentID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
       IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, 
       ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Note: The download contains a file ReadMe_DatabaseSetup.txt, that contains the script for the table, and a script to create sample data.

Add a LINQ to SQL class to the RIADataGrid.Web site called RIATasksDB.dbml.

Select Server Explorer.

Create a connection to the RIATasks database, and drag the Tasks table to the Object Relational Designer surface.

Save and Close the file.

We will now create a Web Service method that will return the items in the table using the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using Microsoft.VisualBasic;

namespace RIADataGrid.Web
{
    [WebService(Namespace = "http://OpenLightGroup.net/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class WebService : System.Web.Services.WebService
    {
        // Web Methods

        #region GetRIAComments
        [WebMethod]
        public List<RIAComment> GetRIAComments()
        {
            RIATasksDBDataContext DB = new RIATasksDBDataContext();

            var colRIAComments = (from RIAComments in DB.RIAComments
                                  select RIAComments).ToList();

            return colRIAComments;
        }
        #endregion
    }
}

In the Silverlight Application project, we add a Service Reference. We name the Service Reference wsRIARIAComments.

Also, add to the Silverlight Application project, an assembly reference to Microsoft.VisualBasic.

We will now create a Model in the Silverlight Application project, to call the GetRIAComments method in the Web Service.

Create a folder called Models, and a class called RIACommentsModel.cs with the following code:

using Microsoft.VisualBasic;
using System.Linq;
using System;
using System.Collections.Generic;
using System.ServiceModel;
using RIADataGrid.wsRIARIAComments;
using RIADataGrid;

namespace RIADataGrid
{
    public class RIACommentsModel
    {
        #region GetRIAComments
        public static void GetRIAComments(
           EventHandler<GetRIACommentsCompletedEventArgs> eh)
        {
            // Set up web service call
            WebServiceSoapClient WS = new WebServiceSoapClient();

            // Set the EndpointAddress
            WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());

            WS.GetRIACommentsCompleted += eh;
            WS.GetRIACommentsAsync();
        }
        #endregion

        // Utility

        #region GetBaseAddress
        private static Uri GetBaseAddress()
        {
            // Get the web address of the .xap that launched this application     
            string strBaseWebAddress = App.Current.Host.Source.AbsoluteUri;
            // Find the position of the ClientBin directory        
            int PositionOfClientBin =
                App.Current.Host.Source.AbsoluteUri.ToLower().IndexOf(@"/clientbin");
            // Strip off everything after the ClientBin directory         
            strBaseWebAddress = Strings.Left(strBaseWebAddress, PositionOfClientBin);
            // Create a URI
            Uri UriWebService = new Uri(String.Format(@"{0}/WebService.asmx", 
                                        strBaseWebAddress));
            // Return the base address          
            return UriWebService;
        }
        #endregion
    }
}

This will allow us to call the GetRIAComments Web Service method from the View Model.

Next, we create the View Model; it is the place where our programming logic will be contained.

Create a folder called ViewModels, and a class called MainPageModel.cs with the following code:

using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows;
using RIADataGrid.wsRIARIAComments;
using Microsoft.VisualBasic;
using System.Windows.Controls;
using System.Windows.Data;
using System.Collections.Specialized;

namespace RIADataGrid
{
    public class MainPageModel : INotifyPropertyChanged
    {
        public MainPageModel()
        {   
            // The following line prevents Expression Blend
            // from showing an error when in design mode
            if (!DesignerProperties.IsInDesignTool)
            {
                // Get the RIAComments
                GetRIAComments();
            }
        }

        // Operations

        #region GetRIAComments
        private void GetRIAComments()
        {
            // Call the Model to get the collection of RIAComments
            RIACommentsModel.GetRIAComments((Sender, EventArgs) =>
            {
                if (EventArgs.Error == null)
                {
                    // Clear the current RIAComments
                    colRIAComments.Clear();

                    // loop thru each item
                    foreach (var RIAComment in EventArgs.Result)
                    {
                        // Add to the colRIAComments collection
                        colRIAComments.Add(RIAComment);
                    }                                       
                }
            });
        }
        #endregion

        // Collections

        #region colRIAComments
        private ObservableCollection<RIAComment> _colRIAComments
            = new ObservableCollection<RIAComment>();
        public ObservableCollection<RIAComment> colRIAComments
        {
            get { return _colRIAComments; }
            private set
            {
                if (colRIAComments == value)
                {
                    return;
                }
                _colRIAComments = value;
                this.NotifyPropertyChanged("colRIAComments");
            }
        }
        #endregion

        // Utility

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
        #endregion
    }
}

We will expand on this class, and others, in the later sections; for now, it just fills a collection called colRIAComments with the results of the Web Service.

The final part is to create the View:

Right-click on MainPage.xaml to open it in Expression Blend.

Click on LayoutRoot in the Objects and Timeline window.

In its Properties, select DataContext.

Set it to MainPageModel.

Select the Data tab and Create Sample Data from Class...

Select MainPageModel.

Click the Assets button and select the DataGrid.

Draw a DataGrid on the main page.

Drag colRIAComments from the sample data...

...and drop it on the DataGrid.

The sample data will display on the DataGrid.

Note: if you do not see Columns under the DataGrid in the Objects and Timeline window, close and re-open Expression Blend. When you re-open the project in Expression Blend, you will see the Columns section.

Format the Columns

In the Objects and Timeline window, select the Comment column:

Set its Properties to match the picture above.

Note: It is important that the Layout be set to Width 1 and Star. Without these settings, the Clear button (used later in this tutorial) will not work.

Next, in the Objects and Timeline window, select the CommentID column. Set its Properties to match the picture above.

Finally, in the Objects and Timeline window, select the CommentUpdateDate column. Set its Properties to match the picture above.

Move the ID column above the Comment column.

Right-click on the web project in the Projects window and set it to Startup Project.

Right-click on the .aspx page (in your project, it may be called "...TestPage.aspx") and set it to Startup.

Hit F5 to build and run the project. The project will run and open in your web browser.

Inline Editing

We will now alter the project to enable Inline Editing of the Comments field. Actually, it is already enabled because we un-checked the IsReadOnly box on the column definition. We will now enable the updated Comments to be saved to the database.

In addition, we will alter the GetRIAComments Web Service to only show the first 25 characters of the Comments field. This will allow the application to move faster, as it will have less data to display. When a user edits a Comment, it will call a Web Service method that will return the full Comment, and display it for the user to edit.

We will also implement a method to ensure that we are only updating a record that has not changed since we last retrieved it. We do this by only updating a record if the last update time matches.

Adding an Errors Property

We learned from the tutorial, Central Silverlight Business Rules Validation, that we can add an Errors property to the Partial Class of a LINQ to SQL table.

We create a folder called Classes and a file called DataBaseParticalClass.cs and use the following code for it:

using System.Collections.Generic;

namespace RIADataGrid.Web
{
    #region public partial class RIAComment
    public partial class RIAComment
    {
        public List<string> Errors = new List<string>();
    }
    #endregion
}

Alter the Web Service

Now we will alter the existing Web Service method and add another two:

#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments()
{
    // Create a collection to hold the results
    List<RIAComment> colResult = new List<RIAComment>();

    RIATasksDBDataContext DB = new RIATasksDBDataContext();

    var colRIAComments = from RIAComments in DB.RIAComments
                         select RIAComments;

    // Loop thru the Tasks
    foreach (var item in colRIAComments)
    {
        // Create a Task
        RIAComment OutputRIAComment = new RIAComment();

        // Get only the first 25 charaters of the comment
        OutputRIAComment.CommentID = item.CommentID;
        OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
        OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;

        // Add to the final results
        colResult.Add(OutputRIAComment);
    }

    return colResult;
}
#endregion

#region GetRIAComment
[WebMethod]
public RIAComment GetRIAComment(int RIACommentID)
{
    RIATasksDBDataContext DB = new RIATasksDBDataContext();

    var result = (from RIAComments in DB.RIAComments
                  where RIAComments.CommentID == RIACommentID
                  select RIAComments).FirstOrDefault();

    return result;
}
#endregion

#region UpdateRIAComment
[WebMethod]
public RIAComment UpdateRIAComment(RIAComment objRIAComment)
{
    DateTime dtCurrentDate = DateTime.Now;
    RIATasksDBDataContext DB = new RIATasksDBDataContext();

    var result = (from RIAComments in DB.RIAComments
                  where RIAComments.CommentID == objRIAComment.CommentID
                  // This will only perform the update if the CommentUpdateDate matches
                  // the existing CommentUpdateDate in the database
                  where RIAComments.CommentUpdateDate == objRIAComment.CommentUpdateDate
                  select RIAComments).FirstOrDefault();

    if (result != null)
    {               
        result.Comment = Strings.Left(objRIAComment.Comment, 10000);
        result.CommentUpdateDate = dtCurrentDate;
        
        DB.SubmitChanges();

        // Update the CommentUpdateDate on the object that will be returned
        objRIAComment.CommentUpdateDate = dtCurrentDate;
    }
    else
    {
        // The record could not be found because the CommentUpdateDate did not match
        // Or the record was deleted
        objRIAComment.Errors.Add("The record was not updated");
    }

    // Update comments to only show 25 characters
    objRIAComment.Comment = Strings.Left(objRIAComment.Comment, 25) + " ...";

    return objRIAComment;
}
#endregion

Below is an overview of the web methods:

  • GetRIAComments - Altered to only return the first 25 characters and to add "..." to the end of the Comment.
  • GetRIAComment - Retrieves a single Comment. This is used to get the full Comment when a user is using Inline Editing.
  • UpdateRIAComment - Updates a Comment only if the CommentUpdateDate field matches. It also returns the updated Comment if the update is successful. If it is not, the Errors property is filled with an error message.

Update the Web Reference

Right-click on the wsRIARIAComments web reference and select Update Service Reference.

Update the Model

Add the following methods to the existing Model:

#region GetRIAComment
public static void GetRIAComment(int RIACommentID, 
       EventHandler<GetRIACommentCompletedEventArgs> eh)
{
    // Set up web service call
    WebServiceSoapClient WS = new WebServiceSoapClient();

    // Set the EndpointAddress
    WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());

    WS.GetRIACommentCompleted += eh;
    WS.GetRIACommentAsync(RIACommentID);
}
#endregion

#region UpdateRIAComment
public static void UpdateRIAComment(RIAComment objRIAComment, 
       EventHandler<UpdateRIACommentCompletedEventArgs> eh)
{
    // Set up web service call
    WebServiceSoapClient WS = new WebServiceSoapClient();

    // Set the EndpointAddress
    WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());

    WS.UpdateRIACommentCompleted += eh;
    WS.UpdateRIACommentAsync(objRIAComment);
}
#endregion

These methods simply call the Web Service methods that were added.

Note: because the parameters for the GetRIAComments Web Service method did not change, we do not need to change its method in the Model.

The View Model

We will now update the View Model. First, we will add a Helper Class that will help us implement ICommands. ICommands are used to raise events in the View Model, from the View.

The DelegateCommand Helper Class

Create a folder called Classes and a file called DelegateCommand.cs.

Replace all the code with the following code:

using System.Windows.Input;
using System;
 
// From http://johnpapa.net/silverlight/
//          5-simple-steps-to-commanding-in-silverlight/
namespace RIADataGrid
{
    public class DelegateCommand : ICommand
    {
        Func<object, bool> canExecute;
        Action<object> executeAction;
        bool canExecuteCache;
 
        public DelegateCommand(Action<object> executeAction, 
                               Func<object, bool> canExecute)
        {
            this.executeAction = executeAction;
            this.canExecute = canExecute;
        }
 
        #region ICommand Members
 
        public bool CanExecute(object parameter)
        {
            bool temp = canExecute(parameter);
 
            if (canExecuteCache != temp)
            {
                canExecuteCache = temp;
                if (CanExecuteChanged != null)
                {
                    CanExecuteChanged(this, new EventArgs());
                }
            }
 
            return canExecuteCache;
        }
 
        public event EventHandler CanExecuteChanged;
 
        public void Execute(object parameter)
        {
            executeAction(parameter);
        }
 
        #endregion
    }
}

This class allows us to easily invoke ICommands. You can get more information on this class at: http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/.

The View Model

Add the following properties to the View Model:

#region MessageVisibility
private Visibility _MessageVisibility
    = Visibility.Collapsed;
public Visibility MessageVisibility
{
    get { return _MessageVisibility; }
    private set
    {
        if (_MessageVisibility == value)
        {
            return;
        }
        _MessageVisibility = value;
        this.NotifyPropertyChanged("MessageVisibility");
    }
}
#endregion

#region Errors
private ObservableCollection<string> _Errors
    = new ObservableCollection<string>();
public ObservableCollection<string> Errors
{
    get { return _Errors; }
    private set
    {
        if (_Errors == value)
        {
            return;
        }
        _Errors = value;
        this.NotifyPropertyChanged("Errors");
    }
}
#endregion

This adds a property to hold any errors returned, and a property to allow the error list to show (or not).

Add the following methods to the class:

#region GetRIAComment
private void GetRIAComment(RIAComment Comment)
{
    // Call the Model to get the full RIAComment
    RIACommentsModel.GetRIAComment(Comment.CommentID, (Sender, EventArgs) =>
    {
        if (EventArgs.Error == null)
        {
            // Find the comment in the colRIAComments collection
            var CommentInCollection = (from comment in colRIAComments
                                       where comment.CommentID == EventArgs.Result.CommentID
                                       select comment).FirstOrDefault();

            if (CommentInCollection != null)
            {
                CommentInCollection.Comment = EventArgs.Result.Comment;
            }
        }
    });
}
#endregion

#region UpdateRIAComment
private void UpdateRIAComment(RIAComment objRIAComment)
{
    // Call the Model to UpdateRIAComment the RIAComment
    RIACommentsModel.UpdateRIAComment(objRIAComment, (Sender, EventArgs) =>
    {
        if (EventArgs.Error == null)
        {
            // Find the comment 
            var CommentInCollection = (from comment in colRIAComments
                                       where comment.CommentID == EventArgs.Result.CommentID
                                       select comment).FirstOrDefault();

            if (CommentInCollection != null)
            {
                // Update the Comment
                CommentInCollection.Comment = EventArgs.Result.Comment;
                CommentInCollection.CommentUpdateDate = EventArgs.Result.CommentUpdateDate;

            }

            // Show any errors
            Errors = EventArgs.Result.Errors;
            // Set the visibility of the Message ListBox
            MessageVisibility = (Errors.Count > 0) ? 
                                   Visibility.Visible : Visibility.Collapsed;

        }
    });
}
#endregion

These methods pull up the full content of the Comments field, and update a Comment by calling the respective methods in the Model.

Add the following code to the class:

#region GetRIACommentsCommand
public ICommand GetRIACommentsCommand { get; set; }
public void GetRIAComments(object param)
{
    GetRIAComments();
}

private bool CanGetRIAComments(object param)
{
    return true;
}
#endregion

#region GetRIACommentCommand
public ICommand GetRIACommentCommand { get; set; }
public void GetRIAComment(object param)
{
    GetRIAComment((RIAComment)param);
}

private bool CanGetRIAComment(object param)
{
    return true;
}
#endregion

#region UpdateRIACommentCommand
public ICommand UpdateRIACommentCommand { get; set; }
public void UpdateRIAComment(object param)
{
    // This is an Update
    UpdateRIAComment((RIAComment)param);
}

private bool CanUpdateRIAComment(object param)
{
    // Do not allow if there is no Current RIAComment
    return (param as RIAComment != null);
}
#endregion

This code implements the ICommands. We will raise these ICommands from the View using Behaviors.

Add the following code to the constructor of the View Model:

GetRIACommentsCommand = new DelegateCommand(GetRIAComments, CanGetRIAComments);
GetRIACommentCommand = new DelegateCommand(GetRIAComment, CanGetRIAComment);
UpdateRIACommentCommand = new DelegateCommand(UpdateRIAComment, CanUpdateRIAComment);

This code uses the DelegateCommand helper class to set up the ICommands.

The View

We will now complete the View. You will want to build the project, and then close and open the MainPage.xaml file to get Expression Blend to recognize the new properties.

In the Data Context window, click on the Errors collection...

...and drag it to the design surface. A ListBox will automatically be created and bound to the collection.

After the ListBox is created, size the ListBox so that it is in the lower right hand corner.

In the Data Context window, click on the MessageVisibility property, and drag and drop it on the Errors listbox.

A Create Data Binding box will come up. Select Visibility for Property of [ListBox] and click OK.

Add the Behaviors

From Assets, select the InvokeCommand Action Behavior.

Drop it on the DataGrid in the Objects and Timeline window.

in the Properties for the Behavior, select PreparingCellForEdit as the Event Name.

Click the Data bind icon next to Command.

Bind it to the GetRIAComment command.

Select Advanced options next to CommandParameter, and then select Data Binding... from the popup menu.

Bind it to the SelectedItem of the DataGrid.

Repeat the process with another InvokeCommand Action Behavior:

  • Select RowEditEnded as the Event Name
  • Select UpdateRIACommentsCommand for the Command
  • Select DataGrid and SelectedItem for CommandParameter

Hit F5 to build and run the application. You will see that only the first 25 characters are shown for each Comment.

When you click twice on a row, you will see the full Comment and you can change it.

If you alter the update time of a record in the database after you have started editing it, it will not save and it will show an error.

DataGrid Paging

Right now, all the records show in the DataGrid at the same time. This would be a problem if we had a lot of records (the application would move slower). We will now implement paging of the records.

In the Web Service, replace the code for the GetRIAComments web method with the following code:

#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments(int intPage)
{
    // Create a collection to hold the results
    List<RIAComment> colResult = new List<RIAComment>();

    RIATasksDBDataContext DB = new RIATasksDBDataContext();

    var colRIAComments = from RIAComments in DB.RIAComments
                         select RIAComments;

    // Compute the CurrentPage
    int CurrentPage = ((intPage * 5) - 5);
    // Implement paging
    colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);

    // Loop thru the Tasks
    foreach (var item in colRIAComments)
    {
        // Create a Task
        RIAComment OutputRIAComment = new RIAComment();

        // Get only the first 25 charaters of the comment
        OutputRIAComment.CommentID = item.CommentID;
        OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
        OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;

        // Add to the final results
        colResult.Add(OutputRIAComment);
    }

    return colResult;
}
#endregion

Right-click on the wsRIARIAComments web reference and select Update Service Reference.

Alter the GetRIAComments method in the Model to the following:

#region GetRIAComments
public static void GetRIAComments(int intPage, 
       EventHandler<GetRIACommentsCompletedEventArgs> eh)
{
    // Set up web service call
    WebServiceSoapClient WS = new WebServiceSoapClient();

    // Set the EndpointAddress
    WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());

    WS.GetRIACommentsCompleted += eh;
    WS.GetRIACommentsAsync(intPage);
}
#endregion

In the View Model, add the following property:

#region CurrentPage
private int _CurrentPage = 1;
public int CurrentPage
{
    get { return _CurrentPage; }
    private set
    {
        if (CurrentPage == value)
        {
            return;
        }
        _CurrentPage = value;
        this.NotifyPropertyChanged("CurrentPage");
    }
}
#endregion

Alter the GetRIAComments method to the following:

#region GetRIAComments
private void GetRIAComments()
{
    // Call the Model to get the collection of RIAComments
    RIACommentsModel.GetRIAComments(CurrentPage, (Sender, EventArgs) =>
    {
        if (EventArgs.Error == null)
        {
            // Clear the current RIAComments
            colRIAComments.Clear();

            // loop thru each item
            foreach (var RIAComment in EventArgs.Result)
            {
                // Add to the colRIAComments collection
                colRIAComments.Add(RIAComment);
            }
        }
    });
}
#endregion

Add the following ICommands:

#region PreviousPageCommand
public ICommand PreviousPageCommand { get; set; }
public void PreviousPage(object param)
{
    CurrentPage--;
    GetRIAComments();
}

private bool CanPreviousPage(object param)
{
    // Must not already be on the first page
    return (CurrentPage > 1);
}
#endregion

#region NextPageCommand
public ICommand NextPageCommand { get; set; }
public void NextPage(object param)
{
    CurrentPage++;
    GetRIAComments();
}

private bool CanNextPage(object param)
{
    // There must be records to move to the next page
    return (colRIAComments.Count > 0);
}
#endregion

Lastly, add the following code to the constructor:

PreviousPageCommand = new DelegateCommand(PreviousPage, CanPreviousPage);
NextPageCommand = new DelegateCommand(NextPage, CanNextPage);

The View

Draw two buttons on the page and label them.

Drop an InvokeCommandAction Behavior on each button.

In the Properties for each Behavior, select Click for the EventName, and click the Advanced Options box next to Command.

Select Data Binding...

And bind to the appropriate command (either PreviousPageCommand or NextPageCommand).

Repeat the procedure for the other Button.

The paging will now work.

Calling an ICommand from a Button Click Inside the DataGrid

Next, we want to put a button on each row of the DataGrid that will set the current Comment to **** when the button is clicked.

First, add the following code to the View Model:

#region ClearCommand
public ICommand ClearCommand { get; set; }
public void Clear(object param)
{
    // Clear the Comment
    RIAComment objRIAComment = (RIAComment)param;
    objRIAComment.Comment = "****";
}

private bool CanClear(object param)
{
    // Do not allow if there is no Current RIAComment
    return (param as RIAComment != null);
}
#endregion

Add the following line in the constructor of the View Model:

ClearCommand = new DelegateCommand(Clear, CanClear);

Right-click on the DataGrid, and in the Objects and Timeline window, create a DataGridTextColumn.

After you add the column, drag it so that it is the first column.

Note, if you get a Value does not fall within the expected range. error...

you have a duplicate or mis-numbered DisplayIndex. You can fix this by numbering the columns correctly in the XAML view (starting with 0 for the first column).

It is important that each row in the DataGrid be bound to the data; otherwise, you will get a Value cannot be null error when you try to run the project.

In the Properties for the row (the new one you just added), select Advanced options next to Binding.

Bind it to colRIAComments.

After you set the Binding, set the remaining properties like the picture above.

Right-click on the column again, and then Edit a Copy of the Edit CellStyle.

On the Create Style Resource box, select New...

Click OK.

Back on the Create Style Resource box, click OK.

You are now editing the Style; right-click on the Style and select Edit Current.

Click on ContentPresenter. This will shift the main design page so that you are editing the Content Template.

Draw a Button in the design page.

In the Properties for the Button:

  • Set the Margins to 5
  • Set the Content to Clear
  • Click on the Button then set the DataContext...

... to the ViewModel that the main page is using (MainPageModel).

You do this so that the Behavior that you will add in the next step will be able to see the ICommands on the main View Model.

Otherwise, the scope of the Button (and any Behaviors attached to it) would be limited to only the elements in the row in the DataGrid.

However, note that what we are doing is instantiating a new instance of the MainPageModel. The ICommand that we will raise will not be on the instance of the MainPageModel that is connected to the .xaml page that the UI is on, but to another copy.

Also note that a new instance of the MainPageModel will be created for each row, and that in this code example, there is a call in the constructor of the class to get the RIAComments that will be called each time. So when this application loads, it will call the GetRIAComments Web Service 6 extra times. It will only do this on the first page, but you may not like this behavior at all.

The solution is to make a separate ViewModel that does not cause any unwanted effects, if it is instatiated multiple times. I cover how that works in this tutorial: Deleting a Silverlight DataGrid Row With a Button on the Row.

Drop an InvokeCommandAction Behavior on the Button:

In the Properties for the Behavior, set the EventName to Click, and bind the Command to the ClearCommand.

However, your problem is, now the Button is using the "scope of the main page", so you no longer have access to the row that you are currently on, so you can't pass it as a parameter.

No problem, the contentPresenter (the parent of the Button) is still bound to the "context of the current row", so you can simply pass its DataContext as a parameter.

In the Properties for the Behavior, click on Advanced options next to CommandParameter.

Bind the parameter to the DataContext of the ContentPresenter.

Hit F5 to build and run the project.

When you click the Clear button on a row, it will change the Comment in the row to ****.

You could easily change the ICommand raised by the Button to Update or Delete a Comment.

Sorting

We saved sorting for last because it will actually require the most code. However, in this case, we will not alter the View at all (except to change the binding of the DataGrid). What we need to do is create our own collection that implements ICollectionView.

The reason we need to do this is so that we can hook into events in our collection, and detect when the user is sorting the DataGrid. Right now, the DataGrid will sort. However, it will only sort the current page. Instead of allowing the DataGrid to handle the sorting automatically, we will call the Web Service and sort the records there, and then pull up the current page with the sort applied.

This will provide a correct sorting experience. For example, if you are on page two and sort the records by ID number, you will see page two of the entire sorted collection.

Alter the GetRIAComments Web Service method to allow sorting parameters to be passed and applied to the query:

#region GetRIAComments
[WebMethod]
public List<RIAComment> GetRIAComments(int intPage, 
                   string SortProperty, string SortDirection)
{
    // Create a collection to hold the results
    List<RIAComment> colResult = new List<RIAComment>();

    RIATasksDBDataContext DB = new RIATasksDBDataContext();

    var colRIAComments = from RIAComments in DB.RIAComments
                         select RIAComments;

    if (SortDirection == "Descending")
    {
        switch (SortProperty)
        {
            case "CommentID":
                colRIAComments = colRIAComments.OrderByDescending(x => x.CommentID);
                break;
            case "Comment":
                colRIAComments = colRIAComments.OrderByDescending(x => x.Comment);
                break;
            case "CommentUpdateDate":
                colRIAComments = 
                  colRIAComments.OrderByDescending(x => x.CommentUpdateDate);
                break;
            default:
                colRIAComments = colRIAComments.OrderByDescending(x => x.CommentID);
                break;
        }
    }
    else
    {
        switch (SortProperty)
        {
            case "CommentID":
                colRIAComments = colRIAComments.OrderBy(x => x.CommentID);
                break;
            case "Comment":
                colRIAComments = colRIAComments.OrderBy(x => x.Comment);
                break;
            case "CommentUpdateDate":
                colRIAComments = colRIAComments.OrderBy(x => x.CommentUpdateDate);
                break;
            default:
                colRIAComments = colRIAComments.OrderBy(x => x.CommentID);
                break;
        }
    }

    // Compute the CurrentPage
    int CurrentPage = ((intPage * 5) - 5);
    // Implement paging
    colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);

    // Loop thru the Tasks
    foreach (var item in colRIAComments)
    {
        // Create a Task
        RIAComment OutputRIAComment = new RIAComment();

        // Get only the first 25 charaters of the comment
        OutputRIAComment.CommentID = item.CommentID;
        OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
        OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;

        // Add to the final results
        colResult.Add(OutputRIAComment);
    }

    return colResult;
}
#endregion

Right-click on the wsRIARIAComments web reference and select Update Service Reference.

Alter the GetRIAComments method in the Model to the following:

#region GetRIAComments
public static void GetRIAComments(int intPage, string SortProperty, 
       string SortDirection, EventHandler<GetRIACommentsCompletedEventArgs> eh)
{
    // Set up web service call
    WebServiceSoapClient WS = new WebServiceSoapClient();

    // Set the EndpointAddress
    WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());

    WS.GetRIACommentsCompleted += eh;
    WS.GetRIACommentsAsync(intPage, SortProperty, SortDirection);
}
#endregion

This allows the sorting parameters to be passed to the GetRIAComments web service method.

Add the following property to the View Model:

#region ViewSource
private CollectionViewSource _ViewSource = new CollectionViewSource();
public CollectionViewSource ViewSource
{
    get { return _ViewSource; }
    private set
    {
        _ViewSource = value;
        this.NotifyPropertyChanged("ViewSource");
    }
}
#endregion

This provides a CollectionViewSource that we can bind the DataGrid to. When we do this, the DataGrid will no longer use its internal View Source. We will then be able to hook into sorting events that we would otherwise not have access to.

Add the following properties to the View Model:

#region SortProperty
private string _SortProperty;
public string SortProperty
{
    get { return _SortProperty; }
    private set
    {
        if (SortProperty == value)
        {
            return;
        }
        _SortProperty = value;
        this.NotifyPropertyChanged("SortProperty");
    }
}
#endregion

#region SortDirection
private string _SortDirection;
public string SortDirection
{
    get { return _SortDirection; }
    private set
    {
        if (SortDirection == value)
        {
            return;
        }
        _SortDirection = value;
        this.NotifyPropertyChanged("SortDirection");
    }
}
#endregion

This allows us to store the current field that is being sorted and the direction it is being sorted.

Add the following code to the constructor of the View Model:

// Connect the ViewSource to the Comments collection
// The DataGrid will be bound to the ViewSource so that it
// can implement sorting that we can wire-up an event handler to
ViewSource.Source = colRIAComments;

// Wire-up an event handler to the SortDescriptions collection on the ViewSource
INotifyCollectionChanged sortchangeNotifier = 
          ViewSource.View.SortDescriptions as INotifyCollectionChanged;

// Call View_CollectionChanged when the SortDescriptions collection is changed
sortchangeNotifier.CollectionChanged += 
          new NotifyCollectionChangedEventHandler(View_CollectionChanged);

This code assigns colRIAComments to the CollectionViewSource (ViewSource).

It then hooks in a handler to any change in the SortDescriptions of the CollectionViewSource. The CollectionViewSource will automatically change the collection of SortDescriptions when the DataGrid, that will be bound to the collection, changes its sorting (when the user clicks the headers on the DataGrid).

Add the following code to respond to the event:

#region View_CollectionChanged
void View_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    // Clear the underlying collection to prevent the ViewSource
    // from sorting the collection currently displayed in the ViewSource
    // and then sorting it again after the web service call 
    colRIAComments.Clear();

    // When the sort is in Add mode, it is the second (the last step)
    // of defining a new Sort
    if (e.Action == NotifyCollectionChangedAction.Add)
    {
        // re-query the datasource
        GetRIAComments();
    }
}
#endregion

This method is called multiple times (once for each Action that is placed in the SortDescriptions collection).

The NotifyCollectionChangedAction.Add Action is placed in the collection last (after the sort is changed), and that is when we want to re-query the web service and pull up the records, passing it the current sort.

Add the following method to the class:

#region SetSortProperties
private void SetSortProperties()
{
    // Set the Sort PropertyName and Direction 
    // If there is anything in the SortDescriptions collection
    // Items are placed here when the Control attached to this ViewSource
    // is sorted (for example, by clicking on the Header in the DataGrid)
    if (ViewSource.View != null)
    {
        if (ViewSource.View.SortDescriptions.Count > 0)
        {
            SortProperty = ViewSource.View.SortDescriptions[0].PropertyName;
            SortDirection = ViewSource.View.SortDescriptions[0].Direction.ToString();
        }
    }
}
#endregion

This will look in the SortDescriptions collection and pull out the property (field name) being sorted and the direction. It places these values in the SortProperty and SortDirection properties (created earlier). These will be passed to the Web Service.

This method will be called by the following method that is changed to:

#region GetRIAComments
private void GetRIAComments()
{
    // Set Sort Properties if there are any
    SetSortProperties();

    // Call the Model to get the collection of RIAComments
    RIACommentsModel.GetRIAComments(CurrentPage, SortProperty, 
                     SortDirection, (Sender, EventArgs) =>
    {
        if (EventArgs.Error == null)
        {
            // Clear the current RIAComments
            colRIAComments.Clear();

            // loop thru each item
            foreach (var RIAComment in EventArgs.Result)
            {
                // Add to the colRIAComments collection
                colRIAComments.Add(RIAComment);
            }
        }
    });
}
#endregion

This method calls the Web Service and pulls up the page of records.

The last step is to build the project, then click on the DataGrid, and in the Properties, next to ItemsSource, select Advanced options.

Select Data Binding...

Bind it to ViewSource > View.

Now, if you go to the last page and sort, it will sort the entire collection, not just the records that appear on the last page.

Style

You can easily theme the entire application. First, install the Silverlight Toolkit.

Locate the theme.

Drop it on the [UserControl].

The application will be themed.

View Model - Less Code, Really!

Hopefully, you can see that View Model is not hard at all. It really is not complicated once you see how it is done. Expression Blend was designed to work in "View Model Style", so you should have an easier time using Expression Blend when you use this simple pattern.

While it may seem easier to implement a DataGrid using code-behind, you will usually find that you will need to create a lot of code to locate and modify values and properties in the DataGrid.

Controls such as the DataGrid are designed to Bind to collections. View Model is designed to implement binding. It's the Binding that saves you code. Once a Binding is created, it will perform functionality automatically. You do not have to explicitly write code for each piece of functionality. Most importantly, you will Bind to, and gather parameters, directly from the DataGrid element that you are ultimately concerned with, rather than hooking into an event and then hunting for the real value you are looking for.

Furthermore, you will realize that a lot of programmatic functionality is best implemented on the underlying data source, not the DataGrid itself.

Also note, this example uses standard Web Services; you can easily use WCF or WCF RIA Services. The View and the View Model would still be exactly the same.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here