MVVM Pattern for RIA Services
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.
- Remco combined the pattern with my guidance on using Tasks (from the VS Async CTP) to create a Task Based MVVM Sample.
- Colin riffed in a couple directions first combining the pattern with MEF and then splitting it into a number of class libraries.
Comments
Anonymous
May 03, 2011
The comment has been removedAnonymous
May 05, 2011
Hi Kyle, Where can i get more info about QueryBuilder? ThanksAnonymous
May 05, 2011
The comment has been removedAnonymous
August 23, 2011
Hi Kyle. It would be nice to see how you recommend combining this with DomainCollectinView sample! ThanksAnonymous
August 23, 2011
@Kjartan This post and sample cover it. blogs.msdn.com/.../writing-a-custom-collectionviewloader-for-the-domaincollectionview-and-mvvm.aspxAnonymous
September 13, 2011
I am quite new to RIA and MVVM and I wonder why did you choose on the server side to use dbContext and then write so many classes like BookClubContext and then the UnitOfWorkDomainService and then all the interfaces involve with that and of course also the mapping classes .... what is gained from this approach ? wouldn't it be easier and less code to use the EDMX generated classes and create a domain service that inherits from LinqToEntitiesDomainService ? after all the DB do exists , so I don't see the benefits of using "Code First" approach here. maybe I am missing something... thanks tshaimanAnonymous
September 18, 2011
Your architecture is great. I have an application where I handle the creation of orders: an order has a customer,and a list of orderDetails. The order creation process involves a series of phases: you chose or create a new customer, then add some orderDetails(product, quantity,...) I don't like that the Order object I'm creating is tracked continuously but I would like to call the web service only once and when my order is complete. This means that I need a system to instanciate at some point a new Order that is not yet tracked, so in my IService I created a new method: Order CreateDetachedOrder() that in my Service implementation behaves like this: public Order CreateDetachedOrder() { Order order = new Order(); this._context.Orders.Detach(order); return order; } so I'm free to work against that object in my UI for a while, without worrying about to call SubmitChanges() for other reasons. When I'll chose to actually persist my order I'll attach the entity as new. Do you think that this is a good, clean way of handling this or I'm missing something?Anonymous
September 18, 2011
I think I just did a bad question... that was my ignorance of the framework... observed a couple of behaviors and misunderstood a bitAnonymous
September 20, 2011
@Jack Did you figure it out? You shouldn't need to do anything special to work with a detached object; just create it. It will not be tracked until you add it to the context.Anonymous
September 24, 2011
Yes! Thanks! ...I have another problem, with QueryBuilder this time, I can not figure out how should I concatenate where-Or queryies, I posted this question on stackoverflow here: http://bit.ly/nnf2QA I know that it existes a PredicateBuilder class in LinqKit but it doesn't work in silverlight 4Anonymous
September 26, 2011
@Jack Yeah, 'or' queries are tricky. That predicate builder sounds like a reasonable option. I'd recommend downloading the source and seeing if it will compile in Silverlight.Anonymous
November 29, 2011
Kyle McClellan. Please Help Me. I am don't understand process to add new Entity to domain context. Must I use EntityContainer or add to BookDataService.cs new methods for adding new Entity. Excuse Me, i am very bad speak EnglishAnonymous
December 12, 2011
@Evgeniy The pattern is pretty easy. The current sample is set up to allow new Books to be added (though there isn't any UI to do it). 1) Make sure you DomainService has an insert method (BookClubService.InsertBook) 2) Return an editable collection from your Service to your ViewModel (BookDataService.LoadBooksByCategory) 3) Just add to the collection directly (BookViewModel.Books) 4) Save your changes back to the server (BookDataService.SubmitChanges)