At the Silverlight Firestarter ‘10, John Papa did a great talk on MVVM Patterns for Silverlight and WP7 that featured WCF RIA Services as the data layer. For that talk, I helped him streamline the service layer abstraction to correctly work with the RIA client. However, there were a number of great RIA features like querying and change tracking that weren’t surfacing in the view model layer. It was such a shame that I wanted to take a second pass over the pattern to see if I could make it better. I was able to sort out things a little better leading up to Mix ‘11 and the result is the new QueryBuilder type we shipped in the RIA Toolkit.

I’ve put together a reworked version of John’s BookShelf sample to highlight the changes.

The BookShelf Sample

The primary type we’re interested in here is the IBookDataService. Also interesting are the types that implement and consume it. Thankfully this is a small list composed of the interface, the design-time implementation, the actual implementation, and the view model. I’ll make changes to each and try explain the rationale in this post. Typically the deciding factor is whether a type can be returned by a mock or design-time implementation (and to a lesser extent, whether the design is awesome).

In addition, I wrote up some result types that bring a lot of the value of the RIA Operation types to the view model layer. I’ll cover those in a section at the end.

IBookDataService

Since the changes to the service interface drive the changes elsewhere, we should start by looking at the IBookDataService.

  public interface IBookDataService
  {
    EntityContainer EntityContainer { get; }

    void SubmitChanges(
Action<ServiceSubmitChangesResult> callback,
object state); void LoadBooksByCategory(
int categoryId,
QueryBuilder<Book> query,
Action<ServiceLoadResult<Book>> callback,
object state); void LoadBooksOfTheDay(
Action<ServiceLoadResult<BookOfDay>> callback,
object state); void LoadCategories(
Action<ServiceLoadResult<Category>> callback,
object state); void LoadCheckouts(
Action<ServiceLoadResult<Checkout>> callback,
object state); }

There are a few things worth noting. First, the EntityContainer is available as part of the interface. If you aren’t familiar with it, the EntityContainer is the type the DomainContext uses to store EntitySets, implement change tracking, and compose change sets. Luckily, the EntityContainer is designed such that it can live as easily in a mock implementation as the real one. Adding it to the service layer provides a lot of power while still enabling both separation and testability.

Second, all the callbacks pass a service result type. These types contain a distilled version of their RIA counterparts (LoadOperation, etc.) and I’ll cover them at the end.

Finally, the LoadBooksByCategory method takes both the categoryId and a QueryBuilder as arguments. This not only allows the service layer to filter the books by category, but it also allows the view model layer to put together custom queries that will be run on the server (sorting, paging, etc.).

DesignBookDataService

The design-time service implementation is simple and closely resembles the original version.

  public class DesignBookDataService : IBookDataService
  {
    private readonly EntityContainer _entityContainer =
new BookClubContext.BookClubContextEntityContainer(); public EntityContainer EntityContainer { get { return this._entityContainer; } } ...
    public void LoadBooksByCategory(
int categoryId,
QueryBuilder<Book> query,
Action<ServiceLoadResult<Book>> callback,
object state) { this.Load(query.ApplyTo(new DesignBooks()), callback, state); } ...
    private void Load<T>(
IEnumerable<T> entities,
Action<ServiceLoadResult<T>> callback,
object state)
where T : Entity { this.EntityContainer.LoadEntities(entities); callback(new ServiceLoadResult<T>(entities, state)); } }

The EntityContainer is the same internal type as the one use in the BookClubContext, but could easily be mocked if the design-time service and generated proxies lived in different assemblies.

The LoadBooksByCategory method applies the query to a static data-set before invoking a generic Load method. The generic version makes sure the entities are contained in the EntityContainer and then asynchronously invokes the callback.

BookDataService

The actual service implementation is greatly simplified and is now much closer to the design-time version.

  public class BookDataService : IBookDataService
  {
    private readonly BookClubContext _context = new BookClubContext();

    ...

    public EntityContainer EntityContainer
    {
      get { return this._context.EntityContainer; }
    }

    ...
    public void LoadBooksByCategory(
int categoryId,
QueryBuilder<Book> query,
Action<ServiceLoadResult<Book>> callback,
object state) { this.Load(
query.ApplyTo(this._context.GetBooksByCategoryQuery(categoryId)),
lo => { callback(this.CreateResult(lo, true)); }, state); } ...
    private void Load<T>(
EntityQuery<T> query,
Action<LoadOperation<T>> callback,
object state)
where T : Entity { ...
      this._context.Load(query, lo =>
        {
          ...
callback(lo); }, state); }
private ServiceLoadResult<T> CreateResult<T>(
LoadOperation<T> op,
bool returnEditableCollection = false)
where T : Entity { if (op.HasError) { op.MarkErrorAsHandled(); } return new ServiceLoadResult<T>( returnEditableCollection ?
new EntityList<T>(
this.EntityContainer.GetEntitySet<T>(),
op.Entities) :
op.Entities, op.TotalEntityCount, op.ValidationErrors, op.Error, op.IsCanceled, op.UserState); } }

Again we can see the LoadBooksByCategory method applying the query; this time to an EntityQuery. It also creates a callback that it can pass through to the DomainContext.Load method.

It’s worth noting the CreateResult method has the option to return an editable collection or a read-only one. When we take a look at the view model, you’ll be able to see it working against both.

BookViewModel

The view model has changed a number of things to consume the new interface, but it’s all still pretty straightforward.

  public class BookViewModel : ViewModel
  {
    private const int _pageSize = 10;

    private int _pageIndex;

    protected IPageConductor PageConductor { get; set; }
    protected IBookDataService BookDataService { get; set; }

    ... 
public BookViewModel( IPageConductor pageConductor, IBookDataService bookDataService) { PageConductor = pageConductor; BookDataService = bookDataService; BookDataService.EntityContainer.PropertyChanged +=
BookDataService_PropertyChanged; RegisterCommands(); LoadData(); } private void BookDataService_PropertyChanged(
object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "HasChanges") { HasChanges = BookDataService.EntityContainer.HasChanges; SaveBooksCommand.RaiseCanExecuteChanged(); } } ...
    public void LoadBooksByCategory()
    {
      _pageIndex = 0;
      Books = null;
      if (SelectedCategory != null)
      {
        BookDataService.LoadBooksByCategory(
          SelectedCategory.CategoryID,
          new QueryBuilder<Book>().Take(_pageSize),
          LoadBooksCallback,
          null);
      }
    }

    private void OnLoadMoreBooksByCategory()
    {
      if (SelectedCategory != null)
      {
        _pageIndex++;
        BookDataService.LoadBooksByCategory(
          SelectedCategory.CategoryID,
          new QueryBuilder<Book>()
.Skip(_pageIndex * _pageSize).Take(_pageSize), LoadBooksCallback, null); } } public void LoadBooksByTitle() { _pageIndex = 0; Books = null; if (SelectedCategory != null) { BookDataService.LoadBooksByCategory( SelectedCategory.CategoryID, new QueryBuilder<Book>()
.Where(b => b.Title.Contains(TitleFilter)), LoadBooksCallback, null); } } private void LoadBooksCallback(ServiceLoadResult<Book> result) { if (result.Error != null) { // handle error } else if (!result.Cancelled) { if (Books == null) { Books = result.Entities as ICollection<Book>; } else { foreach (var book in result.Entities) { Books.Add(book); } } SelectedBook = Books.FirstOrDefault(); } } private ICollection<Book> _books; public ICollection<Book> Books { get { return _books; } set { _books = value; RaisePropertyChanged("Books"); } } ...
}

The first place to highlight is in the constructor where the view model subscribes to property change events on the EntityContainer. This allows the view model to listen directly for changes made to any of the entities it is working against.

The second point of interest is the three, similarly-implemented load methods. The significant difference between each implementation is the query it composes to pass to the IBookDataService. In all three cases, the composed query will be run on the server.

Finally, the LoadBooksCallback casts result.Entities to an ICollection<Book>. This is because Books is a collection that supports the adding and removing of entities. In this case, the collection is simply aggregating the results of multiple queries, but the same pattern could be used for the creation of new entities. The reason this pattern works is because the BookDataService is returning an EntityList from CreateResult (above), and EntityList knows how to correctly handle the addition and removal of entities.

Service Result Types

I added three result types to the sample (to the Ria.Common project), ServiceInvokeResult, ServiceLoadResult, and ServiceSubmitChangesResult. While the base operation types have some great features, they aren’t convenient to use in mock or design-time implementations. In addition, there is a lot of useful information in these types that is important to return from the service layer. Each of the result types has properties that closely parallel their operation types. For instance, the ServiceLoadResult has properties for the Error and Cancelled states as well as the enumerable Entities. As can be seen in the LoadBooksCallback above, the pattern for consuming the result type is very similar to the standard RIA pattern; check for errors, check for cancelation, and then process the entities.

QueryBuilder and the DomainCollectionView

Though it isn’t apparent in this sample, the QueryBuilder also acts as a bridge between the DomainCollectionView (read about the DomainCollectionView) and the service layer. Each of the query extensions provided for the DCV work against the QueryBuilder as well. For instance, SortAndPageBy could be applied to the query in the view model before passing the QueryBuilder to the service layer.

Summary

I covered a lot in this post and I wouldn’t be surprised if there are still questions to be answered. It’s not my desire to present a canonical MVVM pattern with this post, but the design here is well-worn and a good place to start. Hopefully types, ideas, and patterns in this post prove useful to you. Please let me know what think and if you have any questions.

[Interesting Variations]

Since writing this, there have been a few intrepid developers who’ve written variations on the pattern worth mentioning.