background image
  logo  
Icon

VMCollectionWrapper - Synchronize a Model collection with a ViewModel collection

Tue Sep 21th 2010 by Brett
There has been a lot of debate around what to do with collections of Model objects live under a ViewModel. Consider the situation where a Person has a collection of Nicknames and you have created a PersonViewModel. The debate centers around whether you should expose the Nicknames to binding directly, or if you should create a collection of NicknameViewModels to which binding should occur.

I confess to doing both. In some cases my needs were so simple (perhaps I just needed to display the nicknames) that I would just create a simple property in my ViewModel to expose the nicknames (e.g. public IEnumerable { get { return Model.Nicknames;}}). In 20 seconds I have what I need. Unfortunately this has always left a bad taste in my mouth, and inevitably I end up refactoring them to ViewModels because I need to add some minor functionality I cannot accomplish through data binding alone.

In doing this over-and-over again I found myself:
  1. Creating a new ViewModel that wrapped the Model - easy enough - esp. if I only care about a few properties
  2. Creating a colleciton to hold the ViewModels and populating it from the Models
  3. In cases where I cared about observing changes to this collection in the model - writing code to syncronize the collections. For example adding a Nickname and committing it to the model would require a change to both collections.


Obviously you cannot eliminate #1, but in many cases #2 and #3 where extremely repetitive. I decided to author a collection that would handle this and thus VMCollectionWrapper was born. VMCollectionWrapper can be used in your ViewModel as follows:
public class PersonViewModel : ViewModelBase
{
     public PersonViewModel(Person p)
     {
          //create and populate the VM Collection.  VMCollectionWrapper is observable
          Nicknames = new VMCollectionWrapper<PersonViewModel, Person>(p.NickNames);
     }

     public VMCollectionWrapper<PersonViewModel, Person> Nicknames { get; set; }
}
The code for VMCollectionWrapper looks like this:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace theSilverMethod.MvvM
{
    /// <summary>
    /// Wraps a collection of models with viewmodels.  The underlying collection is updated automatically when 
    /// this collection is changed.  
    /// </summary>
    /// <typeparam name="T">The ViewModel type.  Must implment IViewModelModelProp"/></typeparam>
    /// <typeparam name="U">The model type</typeparam>
    public class VMCollectionWrapper<T,U> : ObservableCollection<T>
        where T : IViewModelModelProp<U>, new()
    {
        private bool _ignoreChanges;
        private IList<U> _wrappedCollection;
        
        public VMCollectionWrapper(IList<U> wrappedModelCollection)
        {
            this.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(VMCollectionWrapper_CollectionChanged);
            _wrappedCollection = wrappedModelCollection;
            if (_wrappedCollection is INotifyCollectionChanged)
            {
                INotifyCollectionChanged childWatch = _wrappedCollection as INotifyCollectionChanged;
                childWatch.CollectionChanged += new NotifyCollectionChangedEventHandler(wrappedCollection_CollectionChanged);
            }
            _ignoreChanges = true;
            foreach (U model in _wrappedCollection) this.Add(new T() { Model = model });
            _ignoreChanges = false;
        }

        /// <summary>
        /// Synchronizes chages from the Models collection to the ViewModels collection
        /// </summary>
        void wrappedCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_ignoreChanges)
                return;
            _ignoreChanges = true;
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                this.Clear();
                foreach (U model in _wrappedCollection)
                    this.Add(new T() { Model = model });
            }
            else
            {
                var toRemove = new List<T>();
                if (e.OldItems != null && e.OldItems.Count > 0)
                    foreach (U model in e.OldItems)
                        foreach (T viewModel in this)
                            if (viewModel.Model.Equals(model))
                                toRemove.Add(viewModel);
                foreach (T viewModel in toRemove)
                    this.Remove(viewModel);

                if (e.NewItems != null && e.NewItems.Count > 0)
                    foreach (U model in e.NewItems)
                        this.Add(new T() { Model = model });
            }
            _ignoreChanges = false;
        }

        
        /// <summary>
        /// Synchronizes changes from the ViewModels collection to the Models colleciton
        /// </summary>
        void VMCollectionWrapper_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (_ignoreChanges)
                return;

            _ignoreChanges = true;

            // If a reset, then e.OldItems is empty. Just clear and reload.
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                _wrappedCollection.Clear();

                foreach (T viewModel in this)
                    _wrappedCollection.Add(viewModel.Model);
            }
            else
            {
                // Remove items from the models collection
                var toRemove = new List<U>();

                if (null != e.OldItems && e.OldItems.Count > 0)
                    foreach (T viewModel in e.OldItems)
                        foreach (U model in _wrappedCollection)
                            if (viewModel.Model.Equals(model))
                                toRemove.Add(model);

                foreach (U model in toRemove)
                    _wrappedCollection.Remove(model);

                // Add new viewmodel items to the models collection
                if (null != e.NewItems && e.NewItems.Count > 0)
                    foreach (T viewModel in e.NewItems)
                        _wrappedCollection.Add(viewModel.Model);
            }

            _ignoreChanges = false;

        }

        

    }

   
    public interface IViewModelModelProp <T>
    {
        T Model { get; set; }
    }
}

You will notice that the ViewModel you are using needs to implement IViewModelModelProp which requires that the ViewModel has a property called Model that is correctly typed. This is necessary for syncronization of the collections because we need a way to determine which ViewModels belong to which Views.

Ultimately this doesn't work for every situation, but for the ones it does it sure saves a lot of coding.

Unit tests are here.

2 Comments     follow

Hello, nice article.
I implemented some time ago an article about "Using MVVM to provide undo/redo" that used something similar.
In my version all modifications to the WrapperCollention where converted to commands (for the Undo thing) and send to the wrapped list. The WrapperColletion was actualized using NotifyCollectionChanged. This avoids using _ignoreChanges but maybe is more complex.
My collention also implemented

public interface MirrorCollectionConversor
{
V GetViewItem(D modelItem, int index);
D GetModelItem(V viewItem, int index);
}

to convert between ViewItems a Model items, but maybe your IViewModelModelProp is simpler.
link: http://blog.notifychanged.com/2009/01/30/viewmodelling-lists
posted by danice on Wed Sep 22th 2010 at 3:38 PM
Thanks! Thats funny because I went back and forth several times about where to put the Interface for extracting the model out of the ViewModel. In the actual project I was working on when I authored VMCollectionWrapper I already had a property on my ViewModel called Model so I went that way as it was less code.

In thinking about responsibilities of the classes I think your decision was probably more "responsible" as the VMCollectionWrapper would be less dependent on the implementation of the ViewModel.
posted by Brett on Thu Sep 23th 2010 at 3:28 PM
Login to post comments
 

Recent Posts

By Month