This article is compatible with the latest version of Silverlight.
Introduction
In Part 1 of this series I covered developing the framework for a Line-Of-Business application in Silverlight, including the user interface framework, communication with a WCF service, a login screen, and a basic inventory list. In this article I will take you through the steps to extend the functionality of the inventory list to page, sort and group the results.
Source code* and Live Demo**
*To run the source code take a look at the "Using the sample application" section of the previous article first.
**To login in the sample application use the following Username: demo and Password: demo.
Be sure to check all articles of the series: Part 1, Part 2, Part 3, Part 4, Part 5 and Part 6
Limitations of the Standard DataGrid Control
It never ceases to amaze me how Microsoft repeatedly releases grid controls that don’t provide the ability to group similar rows together under a heading (such as the ASP.NET GridView control, the DataGrid and DataGridView Windows Forms controls, and until recently WPF didn’t even have a data grid in the SDK!). Luckily Microsoft provided us with a data grid in Silverlight 2, but following tradition it doesn’t support grouping either. I consider grouping to be fairly essential functionality in a data grid control - every single LOB application I have developed has required the ability to group similar records together. Of course this opens the door to third party solutions, and when it comes to third party data grids you generally have no shortage of options to choose from, and Silverlight is no exception. Luckily for the budget conscious there is a perfect solution – the free (and open source!) AgDataGrid Suite from DevExpress. Whilst it has some shortcomings (discussed shortly) it does provide us with the grouping feature that we desire, and you can’t argue with the price. It’s still in beta, but I have found it to be a stable alternative to the standard DataGrid control, so we will replace the existing DataGrid control in the inventory list screen with the DevExpress one to allow for the grouping functionality.
Another limitation of the standard DataGrid control is the lack of scroll wheel support. Fortunately the DevExpress data grid supports scrolling through the rows with the scroll wheel on your mouse, so in addition to grouping functionality we also gain scroll wheel support by implementing the DevExpress data grid.
Limitations of the DevExpress DataGrid
Unfortunately, like the standard DataGrid control the AgDataGrid is not without its flaws either. As it is open source I have chosen to fix some flaws myself and work around the others.
The first issue I encountered when migrating to the AgDataGrid was that the property names were not aligned between the two. This is a minor annoyance really as once you’ve migrated (if you need to migrate at all) it becomes a non-issue but I thought I’d point out that you might need to change various parts of your code to accommodate the new grid. Examples of changes you’ll need to make include renaming the Header property for each column to HeaderContent, the IsReadOnly property to AllowEditing, etc.
My biggest issue when migrating however was due to the manner in which binding is implemented in the AgDataGrid. Whereas previously I could use the Silverlight standard binding syntax, such as:
Binding="{Binding ProductNumber}"
I now needed to set the FieldName property of each column as provided by the AgDataGrid instead:
FieldName="ProductNumber"
This doesn’t seem like a particularly big deal but it becomes an issue when you have a template column or want to use a value converter. In these cases we will need to handle the PrepareCellDisplayElement event for each of these columns which isn’t particularly elegant. In the case of our inventory list example, the Product Name and the List Price columns now both require code behind in order to display correctly, as the Product Name column is a template column (to display a hyperlink to the item details screen) and the List Price column requires the data to be formatted as currency (previously done via a value converter, now needs to be a template column). Note that I test for null results in the PrepareCellDisplayElement event handler as the event is sometimes raised even though a row has no data (ie. a blank row). Hopefully standard binding syntax will be added in a future version, and if not remember the grid is open source so you are welcome to add support for it yourself.
On that note, in order to support all the features I required I did need to add a few features to the grid myself. As we are paging the results we need to know if the sorting or grouping was changed, since this could mean a different set of results should appear in the current page and we would need to request these from the server. However there was no event raised when the grid sorting or grouping is changed so these events needed to be added. Another issue I found was that the style of the hyperlink in the Product Name template column was being overridden by the grid, causing the hyperlink to display as black text. I made a simple change (which wasn’t ideal but served the purpose) where the font for the cell was not set if the column had a template assigned to it. These changes are detailed at the end of the article.
Something to note is that automatic column widths are handled differently between the standard DataGrid and the AgDataGrid controls. The standard DataGrid control will size the columns to the widest content of each column, whilst the AgDataGrid control simply splits the width of the data grid evenly between each column regardless of the length of the contents. Therefore you may be better off setting fixed widths for the columns when using the AgDataGrid. The AgDataGrid does look a bit better with its implementation of automatic width columns as unlike the standard DataGrid control the last column extends to the width of the grid.
One last issue that I worked around was to do with the scroll bar. If the user scrolled down the list and the list was repopulated (for example if the page was changed, etc), the scroll position would stay where it was. This was particularly an issue if the new set of results was shorter than the previous one as the scroll bar would be hidden but you would not see any of the results – a very confusing scenario for the user. Therefore each time we repopulate the data grid we need to make the top row visible using the MakeRowVisible function. Making row 0 visible would show the top row of the results, but if the results were grouped then the top group header would be above this row (which actually has a row handle of -1) and would need to be set to be visible instead.
Implementing Paging
When displaying a list of items such as a product inventory you could expect that there are likely to be hundreds or possibly thousands of results. Transferring all of these from the server to the Silverlight client could be unnecessarily bandwidth intensive and time consuming, thus a better solution is to page the results – transferring only a subset of the results to the client until the user requests more.
There are two ways you could implement this – allow the user to navigate between pages of results using Next Page and Previous Page buttons (such as a page of Google results), or automatically retrieving pages of results in the background as the user scrolls down, appending the new pages to the bottom of the existing ones. Manish Dalal details the latter method on his blog (see Resources for the link), but I will be implementing the former method for this project.
The user interface for my implementation of paging is rather simple, consisting of a First Page button (|<), a Previous Page button (<<), a Next Page button (>>), a Last Page button (>|) and a text block to display the current page number and total number of pages (eg. Page 1 of 5). You could make this a bit fancier by allowing the user to select a particular page number (such as Google does) but we will keep this simple for the time being. When the current page is at the start or end of the set of results we disable the buttons accordingly so the user stays within the available number of pages. This is what we end up with:
When interacting with the web service we need to request a specific page of results (by page number), and as a part of the returned data (we are using an out parameter on the web service function) return the total number of rows (of items in the entire result set, not just in that page). This is so we can calculate the total number of pages on the client (alternatively you could calculate this on the server and just return the total number of pages instead). We also pass through to the server the maximum number of results that should be returned per page. The server could specify this, but it permits the ability for the user to specify how many results they want to appear per page as future functionality.
How a page of results are obtained from the database will be discussed in the section on querying the entity framework below.
Implementing Grouping and Sorting
Grouping and sorting is handled by the AgDataGrid control, though as previously mentioned we needed to add a couple of events to the grid so we could be notified of the changes. Sorting by column is as simple as clicking on that column’s header. Grouping by a column is a little more complicated, where a column header needs to be dragged onto a grouping panel. To ungroup by a column you need to drag the column group from the grouping panel back onto the column headers. Multiple levels of grouping is permitted. The grouping panel takes up valuable screen real-estate so I’ve decided to hide it by default. Note the Grouping toggle button in the toolbar – this is used to show and hide the grouping panel so it can be displayed on demand.
When you change the sorting or grouping you are actually reorganising the entire set of results accordingly, and the results previously in the current page may not be the same results that belong in the page now. We need to handle our new SortingChanged and GroupingChanged events so we can go off to the server and get the appropriate results when the sorting or grouping is changed. If there is only 1 page of results in total there is no point in going back to the server as the results in the page will not be any different. However, if the sorting or grouping is changed when there are multiple pages of results we will always go back to page 1 again to start from the beginning.
When the grouping is changed you’ll note that it has a corresponding effect on the column sorting. Grouping is essentially just a type of sorting, but with headers displayed to separate the results by group. This is just a display issue, therefore when the grouping is changed the server only needs to know that the sorting of the results has changed, simplifying things greatly. Grouped columns have precedence over sorted columns in the sort order, so you will notice that the grouped columns are always listed first in the sorted columns collection for the data grid.
How sorted results are obtained from the database will be discussed in the section on querying the entity framework below.
Styling the Data Grid
Both the standard DataGrid and the AgDataGrid controls are styled “out of the box” for cell selection and editing, which is not the look we are after in this scenario. When the user clicks a cell, we just want the whole row to be selected, and the cell that was clicked should look no different to any other cell in the row. Therefore we need to alter the FocusedCellStyle style of the data grid by creating a new style for it in our App.xaml file, and setting the Style property of the data grid to use it instead. Note that we don’t need to redefine all the style elements for the data grid in the App.xaml file, just the ones we want to alter. The data grid will otherwise use the default styles for the other elements.
So how do we know what elements there are to style are how they are styled? The styles are located in the generic.xaml file in the themes folder in the source code for the AgDataGrid control and you can copy the ones you want to change out of there and into your App.xaml file. Then you can make your modifications, and don’t forget to set the Style property on your control to point to these new ones.
So to stop the selected cell being highlighted we just set its style to have white text with a transparent background – blending in perfectly with the selected row style. There are some additional stylings (such as the row height) that we can improve but these will be covered in a future article in this series.
Dynamically Querying the Entity Framework
On the server we have our web service that queries the database via the Entity Framework. We need to translate what data the client wants (based upon the page number and the search filter) and how it should be sorted (based upon the grouping and sorting of the data grid) to a means of querying the Entity Framework. This is actually harder than it might sound as we are essentially dynamically building up a query based upon a variable set of parameters. Whilst LINQ allows us to build our query piece by piece, it doesn’t (easily) support ordering by field name(s) defined in strings rather than by lambda expressions. We could use Entity SQL to build up our queries but that isn’t particularly elegant and is open to SQL injection attacks. Therefore we need to build the lambda expressions dynamically from the string parameter inputs. But let’s start from the beginning.
Our results come from three tables in the database: Product, ProductSubcategory, and ProductCategory. These are joined together using the Include function:
IQueryable<InventoryBO.ProductSummary> qry =
from p in context.Product.Include("ProductSubcategory").Include("ProductCategory")
and we populate a collection of ProductSummary DTOs from the results.
If a filter string is provided we need to generate a lambda expression for the where clause:
if (filter != null && filter.Length != 0)
qry = qry.Where(p => p.Name.ToLower().Contains(filter.ToLower()));
Note that I convert both the product name and the filter to lowercase for the equality test. This is because the collation of the AdventureWorks database is case sensitive so we need to work around this by converting both to lowercase before testing if the values are equal.
Now we need to order the results. We are passing through the required ordering from the client to the server as a string, eg. CategoryName ASC, SubcategoryName DESC. This string can’t be thrown at LINQ as it wants a strongly typed expression. We need to build the lambda expression dynamically from this string which is pretty mind bending when you try. After a lot of failed attempts (one method I tried seemed to work until I tried to sort by the List Price field which failed – it only worked for strings) I came across the Dynamic LINQ parser library (providing a set of extension methods to LINQ) in a set of C# examples from Microsoft (link in the Resources at the end of the article) which solved my problem.
IOrderedQueryable<InventoryBO.ProductSummary> orderedQuery =
(IOrderedQueryable<InventoryBO.ProductSummary>)DynamicQueryable.OrderBy(qry, sortBy)
After filtering and sorting the result set all we need to do is obtain the subset of results according to the page that the user is requesting, using the Skip and Take functions:
if (pageNumber != 1)
qry = qry.Skip((pageNumber - 1) * itemsPerPage);
qry = qry.Take(itemsPerPage);
Note that I don’t skip any records if the first page has been requested. You can specify to skip 0 records, however the skipping process is quite computationally expensive on the database (compare with and without it in the SQL Profiler if you wish) so if no records need to be skipped then I leave it out.
This was all rather complicated to develop, but once done you have a pattern you can reuse.
Other Notes and Additions
I added a “wait indicator” in the bottom right hand corner similar to that on YouTube for when the application is sending/retrieving data to/from the server. How this works is discussed on my blog (link in the Resources at the end of the article).
AgDataGrid Modifications
Whilst the AgDataGrid control is open source, the license to distribute the modified source code is not well defined so I have chosen not to include the modified source code with the demonstration project (just the compiled DLL). A summary of the modifications I made however are as follows:
Columns.cs
OnPrepareCellDisplayElement:
if (this.cellDisplayTemplate == null) // Added by Chris Anderson
new FontLayoutHelper(cell).SetFont(displayElement);
DataGrid.xaml.cs
Class level:
// Added by Chris Anderson
public event GroupingChangedEventHandler GroupingChanged;
public event SortingChangedEventHandler SortingChanged;
// Added by Chris Anderson
protected virtual void OnGroupingChanged()
{
RaiseGroupingChanged();
}
// Added by Chris Anderson
protected virtual void OnSortingChanged()
{
RaiseSortingChanged();
}
// Added by Chris Anderson
protected virtual void RaiseGroupingChanged()
{
if (GroupingChanged != null)
GroupingChanged(this, new GroupingEventArgs(GroupedColumns));
}
// Added by Chris Anderson
protected virtual void RaiseSortingChanged()
{
if (SortingChanged != null)
SortingChanged(this, new SortingEventArgs(SortedColumns));
}
SortGroupColumnPropertiesChanged:
// Added By Chris Anderson
if (changeGrouping)
OnGroupingChanged();
else
OnSortingChanged();
Events.cs
Class Level:
// Added by Chris Anderson
public delegate void GroupingChangedEventHandler(object sender,
GroupingEventArgs e);
public delegate void SortingChangedEventHandler(object sender,
SortingEventArgs e);
public class GroupingEventArgs : EventArgs
{
public ReadOnlyCollection<AgDataGridColumn> GroupedColumns { get; set; }
public GroupingEventArgs(ReadOnlyCollection<AgDataGridColumn> groupedColumns)
{
GroupedColumns = groupedColumns;
}
}
public class SortingEventArgs : EventArgs
{
public ReadOnlyCollection<AgDataGridColumn> SortedColumns { get; set; }
public SortingEventArgs(ReadOnlyCollection<AgDataGridColumn> sortedColumns)
{
SortedColumns = sortedColumns;
}
}
public class GroupingEventArgs : EventArgs
{
public ReadOnlyCollection<AgDataGridColumn> GroupedColumns { get; set; }
public GroupingEventArgs(ReadOnlyCollection<AgDataGridColumn> groupedColumns)
{
GroupedColumns = groupedColumns;
}
}
public class SortingEventArgs : EventArgs
{
public ReadOnlyCollection<AgDataGridColumn> SortedColumns { get; set; }
public SortingEventArgs(ReadOnlyCollection<AgDataGridColumn> sortedColumns)
{
SortedColumns = sortedColumns;
}
}
Conclusion
We have now added paging, grouping, and sorting of results to our inventory list. To implement the grouping functionality we needed to exchange the standard DataGrid control with the open source AgDataGrid control, with a few minor modifications. From these changes we now have this screen for our inventory list:
Stay tuned for my next article which will cover securing your web service calls, sharing business logic between the client and the server, and much more.
Resources
DevExpress AgDataGrid control:
http://www.devexpress.com/Products/NET/Controls/Silverlight/Grid/
Alternative Paging Method:
http://weblogs.asp.net/manishdalal/archive/2008/10/09/stealth-paging-datagrid.aspx
DynamicQuery:
http://code.msdn.microsoft.com/csharpsamples
Wait Indicator Animation:
http://chrisa.wordpress.com/2008/10/09/a-wait-indicator-in-silverlight/