This is the second part of a two-part series on working with collections in WCF RIA Services.
In the first part of the article series, we’ve learned about the simpler collection types: EntitySet and EntityList. In this part, we’ll dig deeper into the more advanced types: the ICollectionView and the DomainCollectionView.
ICollectionView
The ICollectionView isn’t a new interface: implementations are used as a view over a collection by a number of existing Silverlight controls, like the DataGrid. We now have the ability to use it directly from our ViewModel, to enable binding to an ICollectionView implementation (example implementations available in Silverlight are the CollectionViewSource and PagedCollectionView).
It can be initialized as such:
private ICollectionView CreateView(IEnumerable source) { CollectionViewSource cvs = new CollectionViewSource(); cvs.Source = source; return cvs.View; } private ICollectionView _books; public ICollectionView Books { get { if (this._books == null) { this._books = CreateView(this.Context.Books); } return this._books; } }
When you load Books, this is automatically reflected in this View:
public CollectionViewViewModel() { InstantiateCommands(); // load books Context.Load<Book>(Context.GetBooksQuery().Take(10)); }
ICollectionView: adding and removing data.
Adding and removing books should be done by adding or removing them directly on the Context. Again, this will automatically be reflected, as our CollectionViewSource tracks the underlying Entity Set.
So what’s so special about this? Up until now, this isn’t really different from simply using an Entity Set, right? Well, the real power lies in the fact that an ICollectionView allows you to add filters, sorting and grouping directly on the View.
Filtering an ICollectionView
An ICollectionView has a Filter property, which is a Predicate<object>. Let’s rewrite our code a bit, so it looks like this:
private ICollectionView _books; public ICollectionView Books { get { if (this._books == null) { this._books = CreateView(this.Context.Books); this._books.Filter = new Predicate<object>(BookCorrespondsToFilter); } return this._books; } } public bool BookCorrespondsToFilter(object obj) { Book book = obj as Book; return book.Title.Contains("Silverlight"); }
For every book in the collection, the BookCorrespondsToFilter method is executed to check if the Books’ Title contains the word “Silverlight”. If it doesn’t, it won’t be part of the View over the Books Entity Set.
Now, code like this works IF you know in advance what your filter will be. In a lot of applications however, you’d want the user to be able to define the filter himself. So let’s change the code a bit: I’ve added a “filterActive” property, which is set to true when the user clicks the “Add filter” property:
public bool BookCorrespondsToFilter(object obj) { Book book = obj as Book; if (filterActive) { return book.Title.Contains("Silverlight"); } return true; }
AddFilter = new RelayCommand(() => { filterActive = true; });
When you click this button… nothing happens. Why is this?
When you change something to the filter, or you change something to an underlying Book entity (for example: change its title), the ICollectionView implementation will not automatically execute the filter again: the Filter method is only executed when you add an Entity to the underlying Entity Set. This means you have to specifically tell it to recheck the already loaded entities against the new filter. This can be done by calling the Refresh() method on your ICollectionView:
Refresh = new RelayCommand(() =>
{
Books.Refresh();
});
Now, the View will be recreated, resulting in the filter method getting executed again for each Book in the backing Books Entity Set. Again, this is only necessary when you change the Filter, or when you change a property on an underlying entity which is used in the filter.
Sorting and grouping an ICollectionView
Two more interesting properties exist on an ICollectionView: SortDescriptions and GroupDescriptions. These can be manipulated to change the way your View sorts or groups the underlying entities from the Entity Set.
Sorting can be achieved easily by clicking the column header of the DataGrid you’re binding the ICollectionView to – this will automatically add the necessary Sort Descriptions, and sort your View. Next to that, it can also be achieved through code (interesting when you need to add sorting to a View which is bound to, for example, a ListBox, an thus doesn’t have column headers to click) as such:
AddSort = new RelayCommand(() => { Books.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending)); });
Grouping your collection can be done in a similar way:
AddGrouping = new RelayCommand(() => { Books.GroupDescriptions.Add(new PropertyGroupDescription("Author")); });
This results in a sorted, grouped View of your data:
Do keep in mind that grouping your collection will disable UI virtualization – so beware when working with large datasets, as grouping them will diminish performance quite fast. For large datasets which require grouping: always combine this with paging.
The ICollectionView is a great collection type to use when you need sorting, filtering, grouping, … on your collection. However, it only works on in-memory collections, meaning all your data has to be on the client. This is suitable for a lot of scenarios, but for some this is not feasible. For these scenarios, have a look at the following paragraph: the DomainCollectionView.
DomainCollectionView
A lot of business applications work with large result sets: hundreds, thousands or even millions of records that need to be sorted, filtered, grouped, … In these scenarios, the ICollectionView isn’t a feasible solution, as it requires all the data to be on the client. We need a collection type that allows for server-side sorting, filtering and, most importantly: paging.
This is where the DomainCollectionView comes into play, as it takes care of those concerns. The DomainCollectionView can be found in the WCF RIA Services Toolkit, in the Microsoft.Windows.Data.DomainServices assembly (the assembly is included in the sample solution). Setting it up requires a little more work than the other collection types, but it’s still quite easy once you get the hang of it. The DomainCollection gets initialized with a Source and a Loader (default: the CollectionViewLoader).
public DomainCollectionView<Book> Books { get { return this.view; } }
The Source component defines the source entities for your View – it can be any IEnumerable, but typically this is a collection type which implements INotifyCollectionChanged (EntityList is often used):
this.source = new EntityList<Book>(Context.Books);
The Loader takes care of loading the data. When you use the CollectionViewLoader, which is the default, you pass in two callbacks: OnLoad, and OnLoadCompleted, respectively defining what should happen when data must be loaded, and what should happen when a load operation is completed (you could also use a simple load operation instead of the CollectionViewLoader if you want to).
this.loader = new DomainCollectionViewLoader<Book>( this.OnLoadBooks, this.OnLoadBooksCompleted);
private LoadOperation<Book> OnLoadBooks() { return this.Context.Load(this.query.SortPageAndCount(this.view)); } private void OnLoadBooksCompleted(LoadOperation<Book> op) { if (op.HasError) { op.MarkErrorAsHandled(); } else if (!op.IsCanceled) { this.source.Source = op.Entities; if (op.TotalEntityCount != -1) { this.Books.SetTotalItemCount(op.TotalEntityCount); } } }
As you can see, OnLoadBooks makes sure the query is executed including sorting (Sort Descriptions), Paging (only the correct page gets loaded), and a total count (needed for the DataPager).
When the books have been loaded, the Source collection is set to the loaded entities, and the TotalItemCount is set via TotalEntityCount.
… and with these two components, the DomainCollectionView is initialized as such:
this.view = new DomainCollectionView<Book>(loader, source);
All that’s left now is triggering the initial load, which can be done as such (as you can see, the page size is set to 5):
using (this.view.DeferRefresh()) { this.view.PageSize = 5; this.view.MoveToFirstPage(); }
(DeferRefresh() allows us to defer the refreshing of the view until all operations in the using statement are executed)
So, in essence, what happens when you page, sort, … (anything that should trigger a View refresh) is that the Loader is executed and loads the data. Once this is done, the Source property gets updated, which in turn notifies the View it is updated, and the control your View is bound to reflects these changes.
(note: if you’re already using the April version of the WCF RIA Services Toolkit, SortPageAndCount has been replaced by SortAndPageBy)
DomainCollectionView: adding and removing data.
It’s possible to add and remove data on the client by manipulating the View (and thus the underlying Entity List), with code as such:
AddBook = new RelayCommand(() => { // you can add books like this, but DCV is server side oriented: to get correct // behaviour, you should add it to the Context and submit the changes, after which // the next query will fetch the book you just added. Book book = Books.AddNew() as Book; book.Author = "Kevin Dockx"; book.ASIN = "123456"; book.Title = "Silverlight for dummies"; }); DeleteBook = new RelayCommand(() => { // deleting an item can be done like this, but should be done directly on the context // & submitted to the server Books.RemoveAt(0); });
However, this will result in a View which is out of sync with your current Filters, Sort Descriptions, …: you’ll end up with more or less items in your View than there should be according to the pagesize, or you’ll end up with unsorted items, unfiltered items, … and this is very normal: after all, the DomainCollectionView is designed to work on server-side entities, and by manipulating the collection on the client, you’re trying to manipulate the DomainCollectionView to work in a way it wasn’t designed to work.
The correct way to add or remove entities is by adding or removing them on the server, eg: add an Entity to your Context (or remove one from it), submit the changes to your server, and refresh the DomainCollectionView.
DomainCollectionView: Filtering, Sorting and Grouping your data.
Now, how do you filter data using this approach? Pretty easy actually: you add the necessary Where-clause to your EntityQuery, as such:
AddFilter = new RelayCommand(() => { // filters in DCV should be done by adding a Where clause to the query, as DCV is mainly used for // server side logic this.query = Context.GetOrderedBooksQuery().Where(b => b.Title.Contains("Silverlight")); this.view.MoveToFirstPage(); });
This leaves sorting and grouping. Sorting is provided out-of-the-box: clicking a column header will result in Sort Descriptions being added to the Books collection, which will be taken into account the next time data is fetched – and clicking a column header will automatically refetch the data.
In some applications, a requirement is to move to the first page of the collection when sorting is applied. If something like that is needed, you could write an event handler as such:
INotifyCollectionChanged notifyingSortDescriptions = (INotifyCollectionChanged)this.Books.SortDescriptions; notifyingSortDescriptions.CollectionChanged += (sender, e) => { this.view.MoveToFirstPage(); };
Just as with the ICollectionView, you can also add sort descriptions to the DomainCollectionView in code:
AddSort = new RelayCommand(() => { Books.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending)); Books.Refresh(); });
Adding group descriptions is done in a comparable way:
AddGrouping = new RelayCommand(() => { Books.GroupDescriptions.Add(new PropertyGroupDescription("Author")); Books.Refresh(); });
Bringing it all together, we end up with a server-side pageable, sorted, grouped collection:
Conclusion
As of WCF RIA Services SP1, new collection types have been added (and others have been changed), allowing for great integration with MVVM, from better binding options to a server-side pageable and sortable collection type. If you’re serious about using WCF RIA Services in a LOB context with MVVM, looking into these new collection types is a must.