MVVM Sync Collections

C#WpfMvvm

C# Problem Overview


Is there a standardized way to sync a collection of Model objects with a collection of matching ModelView objects in C# and WPF? I'm looking for some kind of class that would keep the following two collections synced up assuming I only have a few apples and I can keep them all in memory.

Another way to say it, I want to make sure if I add an Apple to the Apples collection I would like to have an AppleModelView added to the AppleModelViews collection. I could write my own by listening to each collections' CollectionChanged event. This seems like a common scenario that someone smarter than me has defined "the right way" to do it.

public class BasketModel
{
    public ObservableCollection<Apple> Apples { get; }
}

public class BasketModelView
{
    public ObservableCollection<AppleModelView> AppleModelViews { get; }
}

C# Solutions


Solution 1 - C#

I use lazily constructed, auto-updating collections:

public class BasketModelView
{
    private readonly Lazy<ObservableCollection<AppleModelView>> _appleViews;

    public BasketModelView(BasketModel basket)
    {
        Func<AppleModel, AppleModelView> viewModelCreator = model => new AppleModelView(model);
        Func<ObservableCollection<AppleModelView>> collectionCreator =
            () => new ObservableViewModelCollection<AppleModelView, AppleModel>(basket.Apples, viewModelCreator);

        _appleViews = new Lazy<ObservableCollection<AppleModelView>>(collectionCreator);
    }

    public ObservableCollection<AppleModelView> Apples
    {
        get
        {
            return _appleViews.Value;
        }
    }
}

Using the following ObservableViewModelCollection<TViewModel, TModel>:

namespace Client.UI
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Diagnostics.Contracts;
    using System.Linq;

    public class ObservableViewModelCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
    {
        private readonly ObservableCollection<TModel> _source;
        private readonly Func<TModel, TViewModel> _viewModelFactory;

        public ObservableViewModelCollection(ObservableCollection<TModel> source, Func<TModel, TViewModel> viewModelFactory)
            : base(source.Select(model => viewModelFactory(model)))
        {
            Contract.Requires(source != null);
            Contract.Requires(viewModelFactory != null);

            this._source = source;
            this._viewModelFactory = viewModelFactory;
            this._source.CollectionChanged += OnSourceCollectionChanged;
        }

        protected virtual TViewModel CreateViewModel(TModel model)
        {
            return _viewModelFactory(model);
        }

        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
            case NotifyCollectionChangedAction.Add:
                for (int i = 0; i < e.NewItems.Count; i++)
                {
                    this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                }
                break;

            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1)
                {
                    this.Move(e.OldStartingIndex, e.NewStartingIndex);
                }
                else
                {
                    List<TViewModel> items = this.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToList();
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAt(e.OldStartingIndex);

                    for (int i = 0; i < items.Count; i++)
                        this.Insert(e.NewStartingIndex + i, items[i]);
                }
                break;

            case NotifyCollectionChangedAction.Remove:
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);
                break;

            case NotifyCollectionChangedAction.Replace:
                // remove
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);

                // add
                goto case NotifyCollectionChangedAction.Add;

            case NotifyCollectionChangedAction.Reset:
                Clear();
                for (int i = 0; i < e.NewItems.Count; i++)
                    this.Add(CreateViewModel((TModel)e.NewItems[i]));
                break;

            default:
                break;
            }
        }
    }
}

Solution 2 - C#

I may not exactly understand your requirements however the way I have handled a similar situation is to use CollectionChanged event on the ObservableCollection and simply create/destroy the view models as required.

void OnApplesCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{    
  // Only add/remove items if already populated. 
  if (!IsPopulated)
    return;

  Apple apple;

  switch (e.Action)
  {
    case NotifyCollectionChangedAction.Add:
      apple = e.NewItems[0] as Apple;
      if (apple != null)
        AddViewModel(asset);
      break;
    case NotifyCollectionChangedAction.Remove:
      apple = e.OldItems[0] as Apple;
      if (apple != null)
        RemoveViewModel(apple);
      break;
  }

}

There can be some performance issues when you add/remove a lot of items in a ListView.

We have solved this by: Extending the ObservableCollection to have an AddRange, RemoveRange, BinaryInsert methods and adding events that notify others the collection is being changed. Together with an extended CollectionViewSource that temporary disconnects the source when the collection is changed it works nicely.

HTH,

Dennis

Solution 3 - C#

Well first of all, I don't think there is a single "right way" to do this. It depends entirely on your application. There are more correct ways and less correct ways.

That much being said, I am wondering why you would need to keep these collections "in sync." What scenario are you considering that would make them go out of sync? If you look at the sample code from Josh Smith's MSDN article on M-V-VM, you will see that the majority of the time, the Models are kept in sync with the ViewModels simply because every time a Model is created, a ViewModel is also created. Like this:

void CreateNewCustomer()
{
    Customer newCustomer = Customer.CreateNewCustomer();
    CustomerViewModel workspace = new CustomerViewModel(newCustomer, _customerRepository);
    this.Workspaces.Add(workspace);
    this.SetActiveWorkspace(workspace);
}

I am wondering, what prevents you from creating an AppleModelView every time you create an Apple? That seems to me to be the easiest way of keeping these collections "in sync," unless I have misunderstood your question.

Solution 4 - C#

Solution 5 - C#

The «Using MVVM to provide undo/redo. Part 2: Viewmodelling lists» article provides the MirrorCollection<V, D> class to achieve the view-model and model collections synchronization.

Additional references

  1. Original link (currently, it is not available): Notify Changed » Blog Archive » Using MVVM to provide undo/redo. Part 2: Viewmodelling lists.

Solution 6 - C#

OK I have a nerd crush on this answer so I had to share this abstract factory I added to it to support my ctor injection.

using System;
using System.Collections.ObjectModel;

namespace MVVM
{
	public class ObservableVMCollectionFactory<TModel, TViewModel>
		: IVMCollectionFactory<TModel, TViewModel>
		where TModel : class
		where TViewModel : class
	{
		private readonly IVMFactory<TModel, TViewModel> _factory;

		public ObservableVMCollectionFactory( IVMFactory<TModel, TViewModel> factory )
		{
			this._factory = factory.CheckForNull();
		}

		public ObservableCollection<TViewModel> CreateVMCollectionFrom( ObservableCollection<TModel> models )
		{
			Func<TModel, TViewModel> viewModelCreator = model => this._factory.CreateVMFrom(model);
			return new ObservableVMCollection<TViewModel, TModel>(models, viewModelCreator);
		}
	}
}

Which builds off of this:

using System.Collections.ObjectModel;

namespace MVVM
{
	public interface IVMCollectionFactory<TModel, TViewModel>
		where TModel : class
		where TViewModel : class
	{
		ObservableCollection<TViewModel> CreateVMCollectionFrom( ObservableCollection<TModel> models );
	}
}

And this:

namespace MVVM
{
	public interface IVMFactory<TModel, TViewModel>
	{
		TViewModel CreateVMFrom( TModel model );
	}
}

And here is the null checker for completeness:

namespace System
{
	public static class Exceptions
	{
		/// <summary>
		/// Checks for null.
		/// </summary>
		/// <param name="thing">The thing.</param>
		/// <param name="message">The message.</param>
		public static T CheckForNull<T>( this T thing, string message )
		{
			if ( thing == null ) throw new NullReferenceException(message);
			return thing;
		}

		/// <summary>
		/// Checks for null.
		/// </summary>
		/// <param name="thing">The thing.</param>
		public static T CheckForNull<T>( this T thing )
		{
			if ( thing == null ) throw new NullReferenceException();
			return thing;
		}
	}
}

Solution 7 - C#

While Sam Harwell's solution is pretty good already, it is subject to two problems:

  1. The event handler that is registered here this._source.CollectionChanged += OnSourceCollectionChanged is never unregistered, i.e. a this._source.CollectionChanged -= OnSourceCollectionChanged is missing.
  2. If event handlers are ever attached to events of view models generated by the viewModelFactory, there is no way of knowing when these event handlers may be detached again. (Or generally speaking: You cannot prepare the generated view models for "destruction".)

Therefore I propose a solution that fixes both (short) shortcomings of Sam Harwell's approach:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.Contracts;
using System.Linq;

namespace Helpers
{
    public class ObservableViewModelCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
    {
        private readonly Func<TModel, TViewModel> _viewModelFactory;
        private readonly Action<TViewModel> _viewModelRemoveHandler;
        private ObservableCollection<TModel> _source;

        public ObservableViewModelCollection(Func<TModel, TViewModel> viewModelFactory, Action<TViewModel> viewModelRemoveHandler = null)
        {
            Contract.Requires(viewModelFactory != null);

            _viewModelFactory = viewModelFactory;
            _viewModelRemoveHandler = viewModelRemoveHandler;
        }

        public ObservableCollection<TModel> Source
        {
            get { return _source; }
            set
            {
                if (_source == value)
                    return;

                this.ClearWithHandling();

                if (_source != null)
                    _source.CollectionChanged -= OnSourceCollectionChanged;

                _source = value;

                if (_source != null)
                {
                    foreach (var model in _source)
                    {
                        this.Add(CreateViewModel(model));
                    }
                    _source.CollectionChanged += OnSourceCollectionChanged;
                }
            }
        }

        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                    }
                    break;

                case NotifyCollectionChangedAction.Move:
                    if (e.OldItems.Count == 1)
                    {
                        this.Move(e.OldStartingIndex, e.NewStartingIndex);
                    }
                    else
                    {
                        List<TViewModel> items = this.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToList();
                        for (int i = 0; i < e.OldItems.Count; i++)
                            this.RemoveAt(e.OldStartingIndex);

                        for (int i = 0; i < items.Count; i++)
                            this.Insert(e.NewStartingIndex + i, items[i]);
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAtWithHandling(e.OldStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Replace:
                    // remove
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAtWithHandling(e.OldStartingIndex);

                    // add
                    goto case NotifyCollectionChangedAction.Add;

                case NotifyCollectionChangedAction.Reset:
                    this.ClearWithHandling();
                    if (e.NewItems == null)
                        break;
                    for (int i = 0; i < e.NewItems.Count; i++)
                        this.Add(CreateViewModel((TModel)e.NewItems[i]));
                    break;

                default:
                    break;
            }
        }

        private void RemoveAtWithHandling(int index)
        {
            _viewModelRemoveHandler?.Invoke(this[index]);
            this.RemoveAt(index);
        }

        private void ClearWithHandling()
        {
            if (_viewModelRemoveHandler != null)
            {
                foreach (var item in this)
                {
                    _viewModelRemoveHandler(item);
                }
            }

            this.Clear();
        }

        private TViewModel CreateViewModel(TModel model)
        {
            return _viewModelFactory(model);
        }
    }
}

To deal with the first of the two problems, you can simply set Source to null in order to get rid of the CollectionChanged event handler.

To deal with the second of the two problems, you can simply add a viewModelRemoveHandler that allows to to "prepare your object for destruction", e.g. by removing any event handlers attached to it.

Solution 8 - C#

I've written some helper classes for wrapping observable collections of business objects in their View Model counterparts here

Solution 9 - C#

I really like 280Z28's solution. Just one remark. Is it necessary to do the loops for each NotifyCollectionChangedAction? I know that the docs for the actions state "one or more items" but since ObservableCollection itself does not support adding or removing ranges, this can never happen I would think.

Solution 10 - C#

Resetting an collection to a default value or to match a target value is something i've hit quite frequently

i Wrote a small helper class of Miscilanious methods that includes

public static class Misc
	{
		public static void SyncCollection<TCol,TEnum>(ICollection<TCol> collection,IEnumerable<TEnum> source, Func<TCol,TEnum,bool> comparer, Func<TEnum, TCol> converter )
		{
			var missing = collection.Where(c => !source.Any(s => comparer(c, s))).ToArray();
			var added = source.Where(s => !collection.Any(c => comparer(c, s))).ToArray();

			foreach (var item in missing)
			{
				collection.Remove(item);
			}
			foreach (var item in added)
			{
				collection.Add(converter(item));
			}
		}
		public static void SyncCollection<T>(ICollection<T> collection, IEnumerable<T> source, EqualityComparer<T> comparer)
		{
			var missing = collection.Where(c=>!source.Any(s=>comparer.Equals(c,s))).ToArray();
			var added = source.Where(s => !collection.Any(c => comparer.Equals(c, s))).ToArray();

			foreach (var item in missing)
			{
				collection.Remove(item);
			}
			foreach (var item in added)
			{
				collection.Add(item);
			}
		}
		public static void SyncCollection<T>(ICollection<T> collection, IEnumerable<T> source)
		{
			SyncCollection(collection,source, EqualityComparer<T>.Default);
		}
	}

which covers most of my needs the first would probably be most applicable as your also converting types

note: this only Syncs the elements in the collection not the values inside them

Solution 11 - C#

This is a slight variation on Sam Harwell's answer, implementing IReadOnlyCollection<> and INotifyCollectionChanged instead of inheriting from ObservableCollection<> directly. This prevents consumers from modifying the collection, which wouldn't generally be desired in this scenario.

This implementation also uses CollectionChangedEventManager to attach the event handler to the source collection to avoid a memory leak if the source collection is not disposed at the same time as the mirrored collection.

/// <summary>
/// A collection that mirrors an <see cref="ObservableCollection{T}"/> source collection 
/// with a transform function to create it's own elements.
/// </summary>
/// <typeparam name="TSource">The type of elements in the source collection.</typeparam>
/// <typeparam name="TDest">The type of elements in this collection.</typeparam>
public class MappedObservableCollection<TSource, TDest>
    : IReadOnlyCollection<TDest>, INotifyCollectionChanged
{
    /// <inheritdoc/>
    public int Count => _mappedCollection.Count;

    /// <inheritdoc/>
    public event NotifyCollectionChangedEventHandler CollectionChanged {
        add { _mappedCollection.CollectionChanged += value; }
        remove { _mappedCollection.CollectionChanged -= value; }
    }

    private readonly Func<TSource, TDest> _elementMapper;
    private readonly ObservableCollection<TDest> _mappedCollection;

    /// <summary>
    /// Initializes a new instance of the <see cref="MappedObservableCollection{TSource, TDest}"/> class.
    /// </summary>
    /// <param name="sourceCollection">The source collection whose elements should be mapped into this collection.</param>
    /// <param name="elementMapper">Function to map elements from the source collection to this collection.</param>
    public MappedObservableCollection(ObservableCollection<TSource> sourceCollection, Func<TSource, TDest> elementMapper)
    {
        if (sourceCollection == null) throw new ArgumentNullException(nameof(sourceCollection));
        _mappedCollection = new ObservableCollection<TDest>(sourceCollection.Select(elementMapper));

        _elementMapper = elementMapper ?? throw new ArgumentNullException(nameof(elementMapper));

        // Update the mapped collection whenever the source collection changes
        // NOTE: Use the weak event pattern here to avoid a memory leak
        // See: https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/weak-event-patterns
        CollectionChangedEventManager.AddHandler(sourceCollection, OnSourceCollectionChanged);
    }

    /// <inheritdoc/>
    IEnumerator<TDest> IEnumerable<TDest>.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <summary>
    /// Mirror a change event in the source collection into the internal mapped collection.
    /// </summary>
    private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action) {
            case NotifyCollectionChangedAction.Add:
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                break;
            case NotifyCollectionChangedAction.Replace:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Reset:
                _mappedCollection.Clear();
                InsertItems(e.NewItems, 0);
                break;
            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1) {
                    _mappedCollection.Move(e.OldStartingIndex, e.NewStartingIndex);
                } else {
                    RemoveItems(e.OldItems, e.OldStartingIndex);

                    var movedItems = _mappedCollection.Skip(e.OldStartingIndex).Take(e.OldItems.Count).GetEnumerator();
                    for (int i = 0; i < e.OldItems.Count; i++) {
                        _mappedCollection.Insert(e.NewStartingIndex + i, movedItems.Current);
                        movedItems.MoveNext();
                    }
                }

                break;
        }
    }

    private void InsertItems(IList newItems, int newStartingIndex)
    {
        for (int i = 0; i < newItems.Count; i++)
            _mappedCollection.Insert(newStartingIndex + i, _elementMapper((TSource)newItems[i]));
    }

    private void RemoveItems(IList oldItems, int oldStartingIndex)
    {
        for (int i = 0; i < oldItems.Count; i++)
            _mappedCollection.RemoveAt(oldStartingIndex);
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJake PearsonView Question on Stackoverflow
Solution 1 - C#Sam HarwellView Answer on Stackoverflow
Solution 2 - C#DennisView Answer on Stackoverflow
Solution 3 - C#CharlieView Answer on Stackoverflow
Solution 4 - C#Jonathan ANTOINEView Answer on Stackoverflow
Solution 5 - C#Sergey Vyacheslavovich BrunovView Answer on Stackoverflow
Solution 6 - C#dFlatView Answer on Stackoverflow
Solution 7 - C#Hauke P.View Answer on Stackoverflow
Solution 8 - C#Aran MulhollandView Answer on Stackoverflow
Solution 9 - C#bertvhView Answer on Stackoverflow
Solution 10 - C#MikeTView Answer on Stackoverflow
Solution 11 - C#mark.monteiroView Answer on Stackoverflow