[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.
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
{
#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)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentsCompleted += eh;
WS.GetRIACommentsAsync();
}
#endregion
#region GetBaseAddress
private static Uri GetBaseAddress()
{
string strBaseWebAddress = App.Current.Host.Source.AbsoluteUri;
int PositionOfClientBin =
App.Current.Host.Source.AbsoluteUri.ToLower().IndexOf(@"/clientbin");
strBaseWebAddress = Strings.Left(strBaseWebAddress, PositionOfClientBin);
Uri UriWebService = new Uri(String.Format(@"{0}/WebService.asmx",
strBaseWebAddress));
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()
{
if (!DesignerProperties.IsInDesignTool)
{
GetRIAComments();
}
}
#region GetRIAComments
private void GetRIAComments()
{
RIACommentsModel.GetRIAComments((Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
colRIAComments.Clear();
foreach (var RIAComment in EventArgs.Result)
{
colRIAComments.Add(RIAComment);
}
}
});
}
#endregion
#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
#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()
{
List<RIAComment> colResult = new List<RIAComment>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = from RIAComments in DB.RIAComments
select RIAComments;
foreach (var item in colRIAComments)
{
RIAComment OutputRIAComment = new RIAComment();
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
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
where RIAComments.CommentUpdateDate == objRIAComment.CommentUpdateDate
select RIAComments).FirstOrDefault();
if (result != null)
{
result.Comment = Strings.Left(objRIAComment.Comment, 10000);
result.CommentUpdateDate = dtCurrentDate;
DB.SubmitChanges();
objRIAComment.CommentUpdateDate = dtCurrentDate;
}
else
{
objRIAComment.Errors.Add("The record was not updated");
}
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)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetRIACommentCompleted += eh;
WS.GetRIACommentAsync(RIACommentID);
}
#endregion
#region UpdateRIAComment
public static void UpdateRIAComment(RIAComment objRIAComment,
EventHandler<UpdateRIACommentCompletedEventArgs> eh)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
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 ICommand
s. ICommand
s 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;
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 ICommand
s. 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)
{
RIACommentsModel.GetRIAComment(Comment.CommentID, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
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)
{
RIACommentsModel.UpdateRIAComment(objRIAComment, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
var CommentInCollection = (from comment in colRIAComments
where comment.CommentID == EventArgs.Result.CommentID
select comment).FirstOrDefault();
if (CommentInCollection != null)
{
CommentInCollection.Comment = EventArgs.Result.Comment;
CommentInCollection.CommentUpdateDate = EventArgs.Result.CommentUpdateDate;
}
Errors = EventArgs.Result.Errors;
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)
{
UpdateRIAComment((RIAComment)param);
}
private bool CanUpdateRIAComment(object param)
{
return (param as RIAComment != null);
}
#endregion
This code implements the ICommand
s. We will raise these ICommand
s 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 ICommand
s.
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)
{
List<RIAComment> colResult = new List<RIAComment>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colRIAComments = from RIAComments in DB.RIAComments
select RIAComments;
int CurrentPage = ((intPage * 5) - 5);
colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);
foreach (var item in colRIAComments)
{
RIAComment OutputRIAComment = new RIAComment();
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
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)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
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()
{
RIACommentsModel.GetRIAComments(CurrentPage, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
colRIAComments.Clear();
foreach (var RIAComment in EventArgs.Result)
{
colRIAComments.Add(RIAComment);
}
}
});
}
#endregion
Add the following ICommand
s:
#region PreviousPageCommand
public ICommand PreviousPageCommand { get; set; }
public void PreviousPage(object param)
{
CurrentPage--;
GetRIAComments();
}
private bool CanPreviousPage(object param)
{
return (CurrentPage > 1);
}
#endregion
#region NextPageCommand
public ICommand NextPageCommand { get; set; }
public void NextPage(object param)
{
CurrentPage++;
GetRIAComments();
}
private bool CanNextPage(object param)
{
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)
{
RIAComment objRIAComment = (RIAComment)param;
objRIAComment.Comment = "****";
}
private bool CanClear(object param)
{
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 ICommand
s 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)
{
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;
}
}
int CurrentPage = ((intPage * 5) - 5);
colRIAComments = colRIAComments.Skip(CurrentPage).Take(5);
foreach (var item in colRIAComments)
{
RIAComment OutputRIAComment = new RIAComment();
OutputRIAComment.CommentID = item.CommentID;
OutputRIAComment.Comment = Strings.Left(item.Comment, 25) + " ...";
OutputRIAComment.CommentUpdateDate = item.CommentUpdateDate;
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)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
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:
ViewSource.Source = colRIAComments;
INotifyCollectionChanged sortchangeNotifier =
ViewSource.View.SortDescriptions as INotifyCollectionChanged;
sortchangeNotifier.CollectionChanged +=
new NotifyCollectionChangedEventHandler(View_CollectionChanged);
This code assigns colRIAComment
s 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)
{
colRIAComments.Clear();
if (e.Action == NotifyCollectionChangedAction.Add)
{
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()
{
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()
{
SetSortProperties();
RIACommentsModel.GetRIAComments(CurrentPage, SortProperty,
SortDirection, (Sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
colRIAComments.Clear();
foreach (var RIAComment in EventArgs.Result)
{
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.