MVVM Sync Collections

2019-01-10 02:12发布

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# wpf mvvm
10条回答
▲ chillily
2楼-- · 2019-01-10 02:37

The article Using MVVM to provide undo/redo provides MirrorCollection class to achieve the view-model and model collections synchronization.

http://blog.notifychanged.com/2009/01/30/viewmodelling-lists/

查看更多
Bombasti
3楼-- · 2019-01-10 02:39

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.

查看更多
Juvenile、少年°
5楼-- · 2019-01-10 02:40

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;
        }
    }
}
查看更多
何必那么认真
6楼-- · 2019-01-10 02:40

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

查看更多
▲ chillily
7楼-- · 2019-01-10 02:42

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

查看更多
登录 后发表回答