Bagikan melalui


Simplifying hierarchical ViewModels with collection transforms

Under the Model-View-ViewModel (MVVM) design pattern, the ViewModel typically provides a programmatic interface to backend state (the Model) and state changes to which the View can easily be bound, effectively separating the view logic from the actual presentation. With its powerful data binding system and the declarative presentation markup made possible by Xaml, MVVM is particularly useful when developing with WPF. In such a scenario, the ViewModel typically exposes the Model (a data model, a service, etc.) state through CLR properties and state changes through the PropertyChanged event of INotifyPropertyChanged.  State is affected via ICommands also exposed by CLR properties on the ViewModel. Creating such a ViewModel for a Model with a simple structure is usually rather straight forward. However, without some careful planning, creating a ViewModel for a Model that is hierarchical in nature can become relatively complex. Issues such as maintaining consistency between the Model’s structure and the ViewModel’s structure in the midst of changes being made to both the Model and the ViewModel quickly arise.

Fortunately, various approaches exist to help mitigate this complexity in different situations. Often, the Model’s hierarchical structure is formed by Model elements having collections of child Model elements. For example, an address book might have a collection of contacts, which in turn have collections of addresses and phone numbers. In the approach I describe here, the complexity of such a hierarchical Model is managed through collection transforms. A collection transform “transforms” a source collection into a readonly target collection and keeps itself up-to-date based on collection change notifications from the source collection. In a sense, this is similar to the Select extension method provided by Linq, except that future changes to the source collection are reflected in the target (transformed) collection. When this approach is applied to hierarchical ViewModels, the individual ViewModel elements (the address book, the individual contacts, and the individual addresses) can be changed through the ViewModel, but the overall structure of the ViewModel hierarchy can only be affected by the structure of the Model hierarchy. In practice, this is both functional and simple to implement.

The first thing needed is the TransformedCollection class. This is a readonly collection that supports change notifications:

public class TransformedCollection<TSource, TTarget> : ReadOnlyCollection<TTarget>, INotifyCollectionChanged, IDisposable{ public TransformedCollection(IEnumerable<TSource> sourceCollection, Func<TSource, TTarget> setup, Action<TTarget> teardown = null) ...}

 

This is a generic, ReadOnlyCollection with type parameters for the type of element in the source collection (the Model in our case) and the type of element in the target collection (the ViewModel in our case). The constructor takes the source collection, a delegate that transforms a source element into a target element, and an optional delegate that cleans up target elements when they are removed (such as disposing them, if necessary). The sample code has the full implementation.

The simplest way to use this class is to use it directly (do not subclass it). This scenario can further be simplified with an extension method:

public static TransformedCollection<TSource, TTarget> Transform<TSource, TTarget>(this IEnumerable<TSource> sourceCollection, Func<TSource, TTarget> setup, Action<TTarget> teardown = null){ return new TransformedCollection<TSource, TTarget>(sourceCollection, setup, teardown);}

 

Using the TransformedCollection directly works well when the ViewModel classes do not have corresponding interfaces. For example, consider the following (simplified) data model:

public class AddressBook{ private readonly ObservableCollection<Contact> contacts; public AddressBook() { this.contacts = new ObservableCollection<Contact>(); } public ObservableCollection<Contact> Contacts { get { return this.contacts; } }} 

 

A corresponding ViewModel might look like:

public class AddressBookViewModel{ private readonly TransformedCollection<Contact, ContactViewModel> contactViewModels; public AddressBookViewModel(AddressBook addressBook) { this.contactViewModels = addressBook.Contacts.Transform( contact => new ContactViewModel(contact)), contactViewModel => contactViewModel.Dispose()); } public TransformedCollection<Contact, ContactViewModel> ContactViewModels { get { return this.contactViewModels; } }}

 

If the ViewModel classes have corresponding interfaces, the situation is slightly more complex, but certainly manageable:

public interface IContactViewModelCollection : IEnumerable<IContactViewModel>, INotifyCollectionChanged { }public interface IAddressBookViewModel{ IContactViewModelCollection ContactViewModels { get; }}public class AddressBookViewModel : IAddressBookViewModel{ private readonly ContactViewModelCollection contactViewModels; public AddressBookViewModel(AddressBook addressBook) { this.contactViewModels = new ContactViewModelCollection(addressBook.Contacts); } public IContactViewModelCollection ContactViewModels { get { return this.contactViewModels; } } private class ContactViewModelCollection : TransformedCollection<Contact, ContactViewModel>, IContactViewModelCollection { public ContactViewModelCollection(IEnumerable<Contact> contacts) : base( contacts, contact => new ContactViewModel(contact), contactViewModel => contactViewModel.Dispose()) { } IEnumerator<IContactViewModel> IEnumerable<IContactViewModel>.GetEnumerator() { return this.GetEnumerator(); } }}

 

In both cases above, the ViewModel should dispose the TransformedCollection instance at some point, but I omitted this for clarity. The sample code properly disposes the TransformedCollections.

The fact that the TransformedCollection is readonly may seem like a limitation that would be difficult to work with, but in practice I’ve found it to reduce complexity at a negligible cost. Since the ViewModel references to the Model, it’s easy enough for the ViewModel to expose commands that affect the underlying Model which is in turn reflected in the ViewModel. For example, an AddContactCommand exposed by the AddressBookViewModel can add a Contact to the underlying AddressBook, which is then reflected in the AddressBookViewModel’s ContactViewModelCollection. Again, this is demonstrated in the sample code. As always, feedback is welcome.