Коллекции Sync MVVM

Существует ли стандартизованный способ синхронизации коллекции объектов Model с коллекцией совпадающих объектов ModelView в С# и WPF? Я ищу какой-то класс, который сохранит следующие две коллекции, синхронизированные, предполагая, что у меня есть только несколько яблок, и я могу сохранить их все в памяти.

Еще один способ сказать это, я хочу убедиться, что если я добавлю Apple в коллекцию яблок, я бы хотел добавить AppleModelView в коллекцию AppleModelViews. Я мог бы написать свой собственный, прослушивая каждое событие CollectionChanged в каждой коллекции. Это похоже на общий сценарий, когда кто-то умнее меня определил "правильный путь", чтобы сделать это.

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

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

Ответ 1

Я, возможно, не совсем понимаю ваши требования, но так, как я справился с аналогичной ситуацией, это использовать событие CollectionChanged в ObservableCollection и просто создавать/уничтожать модели представления по мере необходимости.

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;
  }

}

При добавлении/удалении большого количества элементов в ListView могут возникать проблемы с производительностью.

Мы решили это: Расширение ObservableCollection с помощью методов AddRange, RemoveRange, BinaryInsert и добавления событий, которые уведомили других о том, что коллекция изменяется. Вместе с расширенным CollectionViewSource, который временно отключает источник при смене коллекции, он работает хорошо.

НТН,

Dennis

Ответ 2

Я использую лениво построенные, автоматически обновляющие коллекции:

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;
        }
    }
}

Используя следующий 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;
            }
        }
    }
}

Ответ 3

Ну, во-первых, я не думаю, что для этого есть один "правильный путь". Это полностью зависит от вашего приложения. Есть более правильные пути и менее правильные пути.

Что-то говорят, мне интересно, почему вам нужно будет хранить эти коллекции "синхронно". Какой сценарий вы планируете сделать, чтобы заставить их выйти из синхронизации? Если вы посмотрите на пример кода из статьи Josh Smith MSDN на MV-VM, вы увидите, что большую часть времени Модели синхронизируется с ViewModels просто потому, что каждый раз, когда создается Модель, создается ViewModel. Вот так:

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

Мне интересно, что мешает вам создавать AppleModelView каждый раз, когда вы создаете Apple? Мне кажется, что это самый простой способ сохранить эти коллекции "синхронно", если я не понял ваш вопрос.

Ответ 5

Статья Использование MVVM для обеспечения отмены/повтора предоставляет класс MirrorCollection для достижения синхронизации модели представления и модели.

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

Ответ 6

OK У меня есть ботаник в этом ответе, поэтому мне пришлось поделиться этим абстрактом factory, который я добавил к нему для поддержки моей инъекции ctor.

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);
        }
    }
}

Что из этого выйдет:

using System.Collections.ObjectModel;

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

И это:

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

И вот нулевая проверка для полноты:

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;
        }
    }
}

Ответ 7

Я написал несколько вспомогательных классов для обертки наблюдаемых коллекций бизнес-объектов в их образцах Model Model здесь

Ответ 8

Мне очень нравится решение 280Z28. Только одно замечание. Нужно ли делать петли для каждого NotifyCollectionChangedAction? Я знаю, что в документах для действий указано "один или несколько элементов", но поскольку ObservableCollection сам не поддерживает добавление или удаление диапазонов, этого никогда не произойдет, я бы подумал.

Ответ 9

Сброс коллекции до значения по умолчанию или для соответствия целевому значению - это то, что я часто встречался

i Написал небольшой вспомогательный класс Miscilanious методов, который включает

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);
        }
    }

который охватывает большинство моих потребностей первый, вероятно, будет наиболее применимым, так как ваши типы конвертирования

note: this only Синхронизирует элементы в коллекции, а не значения внутри них

Ответ 10

В то время как решение Сэма Харуэлла уже довольно хорошо, оно подвержено двум проблемам:

  • Обработчик событий, зарегистрированный здесь this._source.CollectionChanged += OnSourceCollectionChanged, никогда не регистрируется, т.е. отсутствует this._source.CollectionChanged -= OnSourceCollectionChanged.
  • Если обработчики событий всегда привязаны к событиям моделей просмотра, сгенерированным viewModelFactory, невозможно узнать, когда эти обработчики событий могут быть снова отсоединены. (Или вообще говоря: вы не можете подготовить созданные модели просмотра для "уничтожения".)

Поэтому я предлагаю решение, которое фиксирует оба (коротких) недостатка подхода Сэма Харвелла:

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);
        }
    }
}

Чтобы справиться с первой из двух проблем, вы можете просто установить Source в значение null, чтобы избавиться от обработчика события CollectionChanged.

Чтобы справиться со второй из двух проблем, вы можете просто добавить viewModelRemoveHandler, который позволяет "подготовить ваш объект к уничтожению", например. удалив любые связанные с ним обработчики событий.