In MVVM when using lists of POCO elements in the backend, usually we have to wrap these in view models to use them in ObservableCollections. Fortunately, we can automate the synchronization between our model list and view model list.


If you have a strict separation between your back-end models and your view model logic - which is generally a good idea - a recurring task is to map lists of model types to an ObersvableColletion in your ViewModel. Usually, these models are then also wrapped in their respective ViewModel types. This leads to the fact that additional house-keeping is needed, to update the original model list when elements in the ObservableCollection are added, deleted or moved. One typical way is to rebuild the model list when it is needed, as shown in the following snippet. When we consider the following classes

// models
class Person
{
    public string Name { get; set; }

    public string PhoneNumber { get; set; }
}

class Contacts
{
    List<Person> People { get; } = new List<Person>();
}

// corresponding view models
class PersonViewModel : ViewModelBase
{
    public Person Model { get; }
}

class ContactsViewModel : ViewModelBase
{
    ObservableCollection<PersonViewModel> People { get; }
}

typical synchronization code encountered in the wild could look like this:

Contacts Model { get; }

ObservableCollection<PersonViewModel> People { get; }

public ViewModel(Contacts model)
{
    Model = model;
    foreach(Person p in model.People)
    {
        People.Add(new PersonViewModel(p));
    }
}

public void UpdateModel() {
    Model.People.Clear();
    foreach(PersonViewModel pvm in People)
    {
        Model.People.Add(pvm.Model);
    }
}

But this results in the fact that our model is not valid (at least regarding to what the user currently sees in the UI) until we call the UpdateModel method every time we want to use the model again (e.g. in a back-end call).

Fortunately, we can create a custom ObservableCollection implementation which automatically synchronizes the two lists so that we have the same state in the model and the view model at all times. This is a neat little class I have been using in projects for some time already. Basically, it is a wrapper around a model in a list which itself is a ObservableCollection which syncs changes of the list structure back to the model list. For this it needs three things:

  • the model list to wrap (obviously)
  • a function to create ViewModels from a model instance (to auto-fill the collection when it is wrapped)
  • a function to extract the wrapped model from a given ViewModel (to apply the same action to the model in the underlying list which has been applied to the ViewModel)

To synchronize changes to the ObservableCollection back we use the CollectionChanged event, catch the Models using the provided function from the affected ViewModels and carry out the same actions to the wrapped model list. For our sample classes provided earlier, we could then us eit like this:

 List<Person> list = new List<Person>() { ... };
 ObservableCollection<PersonViewModel> collection = 
 	new SyncCollection<PersonViewModel, Person>(
 	list, 
    	(pmodel) => new PersonViewModel(pmodel),
    	(pvm) => pvm.Model);

 // now all changes to collection are carried through to the model list
 // e.g. adding a new ViewModel will add the corresponding Model in the wrapped list, etc.

The full code is here (be aware, that this version does not support the actions Replace and Move):

/// <summary>
/// An observable collection which automatically syncs to the underlying models collection
/// </summary>
public class SyncCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
{
    IList<TModel> modelCollection;
    Func<TViewModel, TModel> modelExtractorFunc;

    /// <summary>
    /// Creates a new instance of SyncCollection
    /// </summary>
    /// <param name="modelCollection">The list of Models to sync to</param>
    /// <param name="viewModelCreatorFunc">Creates a new ViewModel instance for the given Model</param>
    /// <param name="modelExtractorFunc">Returns the model which is wrapped by the given ViewModel</param>
    public SyncCollection(IList<TModel> modelCollection, Func<TModel, TViewModel> viewModelCreatorFunc, Func<TViewModel, TModel> modelExtractorFunc)
    {
        if (modelCollection == null)
            throw new ArgumentNullException("modelCollection");
        if (viewModelCreatorFunc == null)
            throw new ArgumentNullException("vmCreatorFunc");
        if (modelExtractorFunc == null)
            throw new ArgumentNullException("modelExtractorFunc");

        this.modelCollection = modelCollection;
        this.modelExtractorFunc = modelExtractorFunc;

        // create ViewModels for all Model items in the modelCollection
        foreach (var model in modelCollection)
            Add(viewModelCreatorFunc(model));

        CollectionChanged += SyncCollection_CollectionChanged;
    }

    private void SyncCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // update the modelCollection accordingly

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                for (int i = 0; i < e.NewItems.Count; i++)
                    modelCollection.Insert(i + e.NewStartingIndex, modelExtractorFunc((TViewModel)e.NewItems[i]));
                break;
            case NotifyCollectionChangedAction.Remove:
                // NOTE: currently this ignores the index (works when there are no duplicates in the list)
                foreach (var vm in e.OldItems.OfType<TViewModel>())
                    modelCollection.Remove(modelExtractorFunc(vm));
                break;
            case NotifyCollectionChangedAction.Replace:
                throw new NotImplementedException();
            case NotifyCollectionChangedAction.Move:
                throw new NotImplementedException();
            case NotifyCollectionChangedAction.Reset:
                modelCollection.Clear();
                foreach (var viewModel in this)
                    modelCollection.Add(modelExtractorFunc(viewModel));
                break;
        }
    }
}