Introducing An MVVM-Friendly DomainDataSource: The DomainCollectionView

[This sample references out-of-date API. This newer post discusses the updates made for the V1 SP2 Preview and shows how to implement filtering.]

There’ve been plenty of talk about an MVVM-friendly DomainDataSource. I’m pretty sure it means completely different things to different people. I even took a stab at defining it a while back. What we’ve included in the Microsoft.Windows.Data.DomainServices assembly in the Toolkit closely follows that outline.

Since this is the first look at this component, I feel compelled to tell you what we’ve provided is more like an engine than a car. There’s a lot of power and flexibility in the DomainCollectionView design, but there are still a lot of ways to misuse it. We haven’t spent nearly as much time nailing down the use cases as we did on the DomainDataSource (for good or for bad).

Without further introduction, let’s dig in.

What is the DomainCollectionView?

The DomainCollectionView (herein referred to as the DCV) is a collection view implementation. The generic version of the class implements all the following interfaces. Most controls are designed to recognize and work against these interfaces.

  • ICollectionView
  • IEditableCollectionView
  • IPagedCollectionView
  • IEnumerable
  • IEnumerable<T>
  • INotifyPropertyChanged
  • INotifyCollectionChanged

For fear of pushing you to the brink of insanity, I’ll forego listing the full API. Instead, I’ll just say there are a lot of properties and methods for you to get acquainted with. Here’s a list of the ones you’ll commonly find yourself using.

  • CurrentItem, CurrentPosition, and MoveCurrentToXx: This is treated as the selected item in most list controls
  • GroupDescriptions: This collection defines the way items are grouped and also affects server-side sorting
  • PageIndex, MoveToXxPage: Calling MoveToPage will prompt the view to load the page at the specified index
  • PageSize: This defines the number of items to load on each page
  • Refresh, DeferRefresh: Calling Refresh will prompt the view to load using the current view settings
  • SortDescriptions: This collection defines the way items are sorted and affects server-side sorting
  • SourceCollection: This is the collection that backs the view and only items in the collection will appear in the view
  • TotalItemCount, SetTotalItemCount: This is used to calculate the total number of pages

How Do I Use a DomainCollectionView?

The DCV is designed using the following triad.

image

In the design, the View component is the DCV. It’s primary functions are to funnel interface calls to the Loader and to respond to changes in the Loader and Source components.

The Loader component is the CollectionViewLoader. It is responsible for handling an asynchronous Load call and raising the LoadCompleted event upon completion. The DomainCollectionViewLoader is the default implementation and can be created with two callbacks allowing you to handle loading and load completion.

The Source component can be any collection that implements IEnumerable. However, observable collections tend to provide the best results. When the Loader gets data, it should update the source and the source should notify the view. EntityList works well as the source, especially in paging scenarios.

image

Your responsibilities fall mostly along the bottom of the triangle. When the view is refreshed (by a you or a control), it will ask you to load the appropriate data. This usually breaks down into the following steps.

  1. Create an EntityQuery and apply the view state to the query. There are a number of EntityQuery extension methods, including Sort and Page, to make this easy on you.
  2. Load the query.
  3. In the completion callback, update the source collection with the new data. You may need to update the view as well if you queried for the TotalEntityCount (used in paging).

Let’s See Some Code

At this point, it will probably be easiest to see the pieces working together in code. This sample shows a read-only DataGrid where sorting and paging are both being done on the server. Here’s the DomainService I’ll be working with. The only thing to note here is I had to override Count to support paging since I’m deriving from the base DomainService.

   [EnableClientAccess()]
  public class SampleDomainService : DomainService
  {
    // Overridden to support paging over POCO entities.
    // If you're extending a derived DomainService
    // implementation this is already done for you.
    protected override int Count<T>(IQueryable<T> query)
    {
      return query.Count();
    }

    public IQueryable<SampleEntity> GetAllEntities() {…}
  }

On the client, I’m using a stripped-down version of the MVVM pattern for simplicity. The view isn’t terribly interesting, but I’ll start there anyway. Both the DataGrid and the DataPager are bound to the view model.

   <UserControl ...>

    <UserControl.DataContext>
        <vm:SampleViewModel />
    </UserControl.DataContext>
    
    ...

    
    <Grid ...>
      <Border ...>
        <Grid>
          ...
          <sdk:DataGrid Name="dataGrid1" Grid.Row="0"
                        ItemsSource="{Binding CollectionView}"
                        IsEnabled="{Binding IsGridEnabled}"
                        IsReadOnly="True"/>
          <sdk:DataPager Name="dataPager1" Grid.Row="1"
                         Source="{Binding CollectionView}"
                        IsEnabled="{Binding IsGridEnabled}"/>
        </Grid>
      </Border>
    </Grid>
  </UserControl>

image

Finally, we’ll get to the interesting piece, the view model. The two properties the view is bound to, CollectionView and IsGridEnabled, are the least interesting part so I’ll skip right past those to the constructor. This is where all the relationships get set up.

   public SampleViewModel()
  {
    this._source = new EntityList<SampleEntity>(
      this._context.SampleEntities);

    this._loader = new DomainCollectionViewLoader<SampleEntity>(
      this.LoadSampleEntities,
      this.OnLoadSampleEntitiesCompleted);

    this._view = new DomainCollectionView<SampleEntity>(
      this._loader,
      this._source);

    // Swap out the loader for design-time scenarios
    if (DesignerProperties.IsInDesignTool)
    {
      DesignTimeLoader loader = new DesignTimeLoader();
      this._view =
        new DomainCollectionView<SampleEntity>(loader, loader.Source);
    }

    // Go back to the first page when the sorting changes
    INotifyCollectionChanged notifyingSortDescriptions =
      (INotifyCollectionChanged)this.CollectionView.SortDescriptions;
    notifyingSortDescriptions.CollectionChanged +=
      (sender, e) => this._view.MoveToFirstPage();

    using (this.CollectionView.DeferRefresh())
    {
      this._view.PageSize = 10;
      this._view.MoveToFirstPage();
    }
  }

You can start to see things come together in the first couple of lines. The source is initialized with its backing EntitySet, the loader is initialized with a couple of callbacks, and the view is initialized with references to the first two.

The second things to notice is the block that adds design-time data. I’ve created a custom loader that returns a few sample entities and, at design time, construct a view using it instead. Not to get too sidetracked, but this snippet shows how easy it is to create a design-time loader.

   private class DesignTimeLoader : CollectionViewLoader
  {
    private static readonly IEnumerable<SampleEntity> source = new[] {…}

    public IEnumerable<SampleEntity> Source
    {
      get { return DesignTimeLoader.source; }
    }

    public override bool CanLoad { get { return true; } }
    public override void Load(object userState)
    {
      this.OnLoadCompleted(
        new AsyncCompletedEventArgs(null, false, userState));
    }
  }

Also, it’s worth noting there are plenty of other approaches to obtain sample data. I chose this one to highlight the simplicity of writing a custom loader and because it only required a few lines of code.

Finally, there are two more things to look at near the end of the constructor. First, I’ve added an event handler to the SortDescriptions collection. Now, every time the collection is updated with a new SortDescription, I’ll make sure the table moves back to the first page. This isn’t a necessary addition, but I liked the behavior a little bit better. Once everything is set up, I make a couple updates to the view in a DeferRefresh block. Any time you’re updating more than one view property and then reloading, it’s good practice to wrap it with a DeferRefresh. This tells the view to forego all recalculations until you’ve finished your update. Upon exiting the using block, the Refresh method will be invoked to update the view.

The next piece we’ll look at are the two callbacks I passed into the constructor of the DomainCollectionViewLoader. As far as RIA data loading goes, they’re both pretty standard.

   private LoadOperation<SampleEntity> LoadSampleEntities()
  {
    this.IsGridEnabled = false;

    return this._context.Load(
      this._context.GetAllEntitiesQuery().SortPageAndCount(this._view));
  }

  private void OnLoadSampleEntitiesCompleted(LoadOperation<SampleEntity> op)
  {
    this.IsGridEnabled = true;

    if (op.HasError)
    {
      // TODO: handle errors
      op.MarkErrorAsHandled();
    }
    else if (!op.IsCanceled)
    {
      this._source.Source = op.Entities;

      if (op.TotalEntityCount != -1)
      {
        this._view.SetTotalItemCount(op.TotalEntityCount);
      }
    }
  }

In the LoadSampleEntities callback, the most interesting part is the SortPageAndCount method. There are a number of EntityQuery extensions included in the CollectionViewExtensions class (Sort, Page, etc.) that understand how to apply the state of the view to the query. For instance, SortPageAndCount orders according to the sort and group descriptions, skip/takes using the page index and count, and requests the total number of items if that has not already been determined.

The OnLoadSampleEntitiesCompleted is also pretty simple. In the successful load block, the source is updated with the entities that were just loaded. Also, if the TotalEntityCount was requested, it is used to update the view’s TotalItemCount ( which is used by the DataPager to determine the total number of pages). Enabling and disabling the grid is not a required step, but I liked how it looked.

Those are the significant bits of the sample. With a little setup and a couple callbacks, the DCV makes server-side paging simple and obtainable. If you’re interested in seeing it run, here’s a link to the source.

Cool, I Want More

The DomainCollectionView in Microsoft.Windows.Data.DomainServices in the Toolkit supports the view functions of sorting, grouping, and paging. In addition, you can apply server-side filtering as part of your load callback (using a .Where clause). These four pieces will be the core of nearly every query you put together on the client.

Even with all that, there are a number of features that existed in the DomainDataSource that could be pulled into the DCV. I included an extensive list of features in this post. Let me know what’s missing and what you’d like added the most.

As I mentioned earlier, this stuff is powerful and flexible to allows you to do cool stuff, but doesn’t work terribly hard to prevent you from doing things wrong. I’ve tried to provide some guidance with this post and the sample to keep things running smooth, but I’m sure you’ll take this in directions I haven’t anticipated. If you run into bugs or have questions about the best way to do things, send them my way (Email Kyle).