Freigeben über


Writing a Custom CollectionViewLoader for the DomainCollectionView and MVVM

The question I’ve been fielding most lately is how to write a custom CollectionViewLoader. Once you see it done, it’s a pretty simple task. Without an example it’s hard to know where to start. In a previous post, I discussed how the view, loader, and source related to each other. In this post, I’ll show how that translates to the code you write.

The Loader as part of your View Model

Out of the three parts of the DomainCollectionView (DCV) triad, the loader is the piece mostly closely aligned with your View Model. In fact, I wrote the default implementation (the DomainCollectionViewLoader) with callbacks so you could put all the code you cared about directly in your VM. That said, feel free to blur the lines between your VM and your CollectionViewLoader until you find the balance of functionality and reusability you feel comfortable with.

The CollectionViewLoader

A good place to start is to take a look at public surface area of the CollectionViewLoader type you’ll be extending.

   public abstract class CollectionViewLoader
  {
    public event EventHandler CanLoadChanged;
    public event AsyncCompletedEventHandler LoadCompleted;

    public abstract bool CanLoad { get; }

    public abstract void Load(object userState);
  }

It’s a simple abstract class with one method, one property, and two events. The Load method is responsible for asynchronously loading data into the source collection of the collection view. Since the Load method is asynchronous, it needs to raise a LoadCompleted event when it completes. The CanLoad property tells the DCV whether it can safely invoke the Load method. There are a number of CanXx properties in the ICollectionView interface, and changes to the CanLoad property will propagate to all of them. For example, since re-sorting the DCV will result in a Load call, CanSort will be set to false when CanLoad is false.

The Sample

I’ve also had a number of questions lately about how to use the DCV with the MVVM pattern I proposed in a recent post. This will be a two-birds-with-one-stone sample where I’ll show how to write a custom CollectionViewLoader that works with the MVVM service pattern. It’s worth noting this sample is a variant of the one I published with my original DCV post.

I’ll start with the custom loader code and work back through the rest of the sample.

   public class SampleCollectionViewLoader<TEntity>
    : CollectionViewLoader where TEntity : Entity
  {
    private Action<Action<ServiceLoadResult<TEntity>>, object> _load;
    private Action<object> _cancelLoad;
    private Action<ServiceLoadResult<TEntity>> _onLoadCompleted;

    private object _currentUserState;

    public SampleCollectionViewLoader(
        Action<Action<ServiceLoadResult<TEntity>>, object> load,
        Action<object> cancelLoad,
        Action<ServiceLoadResult<TEntity>> onLoadCompleted)
    {
      if (load == null)
      {
        throw new ArgumentNullException("load");
      }
      if (onLoadCompleted == null)
      {
        throw new ArgumentNullException("onLoadCompleted");
      }

      this._load = load;
      this._cancelLoad = cancelLoad;
      this._onLoadCompleted = onLoadCompleted;
    }

    public override bool CanLoad
    {
      get { return true; }
    }

    private object CurrentUserState
    {
      get
      {
        return this._currentUserState;
      }

      set
      {
        if (this._currentUserState != value)
        {
          if (this._cancelLoad != null)
          {
            this._cancelLoad(this._currentUserState);
          }
        }

        this._currentUserState = value;
      }
    }

    public override void Load(object userState)
    {
      if (!this.CanLoad)
      {
        throw new InvalidOperationException(
                    "Load cannot be called when CanLoad is false");
      }
      this.CurrentUserState = userState;
      this._load(this.OnLoadCompleted, userState);
    }

    private void OnLoadCompleted(ServiceLoadResult<TEntity> result)
    {
      this._onLoadCompleted(result);

      if (this.CurrentUserState == result.UserState)
      {
        this.CurrentUserState = null;
      }

      base.OnLoadCompleted(result);
    }
  }

This custom CollectionViewLoader in this sample is pretty straightforward and follows the pattern used in the DomainCollectionViewLoader to call back into the view model. It has three callbacks; one for loading, one for canceling, and one for load completion. As you can see, the signature of the callbacks differs from the ones in the DomainCollectionViewLoader. This is necessary to work with the service layer in a natural way which I’ll explain more in the next section.

There are a couple more things to note about this implementation. First, I haven’t done anything with the CanLoad property. In the sample I include a IsGridEnabled flag in the SampleViewModel that serves roughly the same purpose (and behaves a little nicer from a UI perspective when using the standard Silverlight DataGrid). Second, the bulk of the code that’s not calling into the view model is related to cancellation. I’ll talk a little more about that at the end.

The next part of the sample to look at is the service interface. It’s pretty simple and encapsulates loading sample entities.

   public interface ISampleService
  {
    EntityContainer EntityContainer { get; }

    void LoadSampleEntities(
      QueryBuilder<SampleEntity> query,
      Action<ServiceLoadResult<SampleEntity>> callback,
      object state);
  }

Next, we’ll take a look at the view model. It’s going to be the place where the SampleCollectionViewLoader, the SampleService, and all the callbacks are tied together.

   public SampleViewModel()
  {
    this._source = new EntityList<SampleEntity>(
      this._service.EntityContainer.GetEntitySet<SampleEntity>());

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

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

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

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

The source is created using an EntitySet provided by the EntityContainer in the ISampleService. The loader now creates a SampleCollectionViewLoader and passes in three view model callbacks. We’ll take a look at two of those callbacks next.

   private void LoadSampleEntities(
    Action<ServiceLoadResult<SampleEntity>> onLoadCompleted,
    object userState)
  {
    this.IsGridEnabled = false;

    this._service.LoadSampleEntities(
      new QueryBuilder<SampleEntity>().SortAndPageBy(this._view),
      onLoadCompleted,
      userState);
  }

  private void OnLoadSampleEntitiesCompleted(
    ServiceLoadResult<SampleEntity> result)
  {
    this.IsGridEnabled = true;

    if (result.Error != null)
    {
      // TODO: handle errors
    }
    else if (!result.Cancelled)
    {
      this._source.Source = result.Entities;

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

If you’ve followed my previous posts on the subject (linked prolifically above), these two implementations will look very familiar. The only significant difference is that the loader passes a completion callback and user state through to the LoadSampleEntities method for it to pass to the service layer. When the service load completes, it will tell the loader which will in-turn tell the view model.

A Note on Cancellation

Cancellation is a tricky subject on its own, but I’ll try to address how it fits into this pattern. Since a CollectionViewLoader is only dealing with one set of data, each subsequent load should cancel the previous one. In the sample for the MVVM pattern, I had cancellation in the service layer. It’s an approach that worked on the assumption you only invoke one current load per entity type. It’s pretty easy to see how this assumption fails to scale, though, so other approaches are necessary. If you find yourself using the same load method for multiple purposes, I’d recommend adding a cancel method to the service layer that looks like CancelLoadXx(object userState) . You can then invoke this from the cancel callback you pass to the loader.

Summary

Like I mentioned at the beginning of this post, I expect you to write custom CollectionViewLoaders that support the kinds of things you want to do in your view model. Hopefully this post gives you a template to start from. Feel free to customize it however you see fit.

Here’s the source for the sample. It shows a little more detail on things fit together. Let me know what you think and if you have questions.

https://code.msdn.microsoft.com/Writing-a-Custom-e0769d37

Comments

  • Anonymous
    May 31, 2011
    I am hoping to upgrade some code to use this in the near future. One thing I am unclear on though. The model I would like to see is view models all the way down. So given an entity (let's say Account) that has a child collection of entities (lets say Address(es)). I want to have an AccountViewModel that has a property ObservableCollection<AddressViewModel> not ObservableCollection<Address> -- make sense? Additionally, the hierarchy should be able to continue. Any hints on how to do the conversion, where to do it, etc. Am I missing something? Thanks for the great work. For the record, I normally expose an Entity property on the ViewModel -- there is a lot of VM-like goodness on the entities, but there are a lot of additional things that really belong in a ViewModel rather than the client-side entity.

  • Anonymous
    May 31, 2011
    @Robert I'd like your thoughts on this, but I'd recommend treating your Entity types like extensions of your view models. However, instead of trying to get everything generated into the proxy, I'd recommend adding properties via partial classes. In all but the most complex cases, this should be a reasonable approach. If you want hierarchies of view models, I'd recommend hanging them off a view model (instead of a model). This might lead to parallel hierarchies (VM hierarchy will parallel M hierarchy) so be careful you're not wasting too much effort.

  • Anonymous
    May 31, 2011
    I have used that approach in the past, but my impression is that it gets out of hand in some more complex cases. Let me try to illustrate one. We have a dashboard (ish) page that uses entities that are defined in a RIA Services class library. Using slightly different names for similar concepts, the dashboard has Products and Editions. I have a View for an Edition which has a hyperlink button that navigates to Url that edits. But the Url is a facet of the application, not the class library, so I need to pass in a Url service. Additionally, the view has a toggle button that activates and deactivates (both an edit and submit in a single click), so I need to pass down the DataService (I like your version of Papa's btw). I believe for the majority of cases the Entities plus a little partial class extension give you what you need, but in some cases I have seen the partial class feel like it is taking on too much responsibility. I have been planning to look into your WrappingCollection as a base and then tracking via INCC the members.

  • Anonymous
    October 21, 2011
    Hi Kyle, are you have a sample that explain how the DCV works with a generic ViewModel (ViewModel<TEntity>), wich is bind to a runtime constructed DataGrid ? Excuse me for my poor english. Thanks in advance.

  • Anonymous
    November 15, 2011
    Kyle, I am finally getting a chance to implement this with a custom loader. Can you tell me what:    this._source = new EntityList<SampleEntity>(      this._service.EntityContainer.GetEntitySet<SampleEntity>()); buys you instead of the: this._source = this._service.EntityContainer.GetEntitySet<SampleEntity>(); ? Thanks for the great work on this.

  • Anonymous
    November 16, 2011
    @Robert The EntityList is a view over a backing EntitySet. It allows you to display a subset of the entities in the set and is good for data-shaping (filtering, paging, etc) scenarios. Here's a complete description of the differences. blogs.msdn.com/.../collection-binding-options-in-wcf-ria-services-sp1.aspx

  • Anonymous
    January 27, 2013
    I have a problem... when i use IEnumerable with Source, this work only in Create DomainCollectionView:            _usersView = new DomainCollectionView<eUser>(                _userLoader, _users); Where _users is IEnumerable... this work fine also when _users are loaded... but when I Load IEnumerable after associate a DomainCollectionView, the objeto don't refresh... Can you help-me?

  • Anonymous
    January 28, 2013
    @Bier If you want your DCV to refresh based on an IEnumerable, your enumerable collection must implement the INotifyCollectionChanged interface as well. Since this isn't always the easiest interface to implement, you can make _users an ObservableCollection and just re-fill it every time the data changes.