MVVM Sync Коллекции - PullRequest
       5

MVVM Sync Коллекции

43 голосов
/ 10 августа 2009

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

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

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

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

Ответы [ 10 ]

64 голосов
/ 01 февраля 2010

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

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;
            }
        }
    }
}
8 голосов
/ 11 августа 2009

Я не могу точно понять ваши требования, однако способ, которым я справился с подобной ситуацией, состоит в том, чтобы использовать событие 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, который временно отключает источник при изменении коллекции, он работает хорошо.

НТН,

Денис

4 голосов
/ 02 марта 2010

Пример (и пояснения) вы также можете найти здесь: http://blog.lexique -du-net.com / index.php? Post / 2010/03/02 / MV-VM-How-to-keep- коллекций, из-ViewModel-и-модели в синхронизации

Надеюсь, что эта помощь

4 голосов
/ 11 августа 2009

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

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

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

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

1 голос
/ 25 ноября 2011

ОК У меня кретин влюблен в этот ответ , поэтому мне пришлось поделиться этой абстрактной фабрикой, которую я добавил к ней для поддержки инъекции 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;
        }
    }
}
1 голос
/ 13 ноября 2010

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

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

0 голосов
/ 29 февраля 2016

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

  1. Обработчик событий, который зарегистрирован здесь this._source.CollectionChanged += OnSourceCollectionChanged, никогда не бывает незарегистрированным, то есть отсутствует this._source.CollectionChanged -= OnSourceCollectionChanged.
  2. Если обработчики событий когда-либо присоединяются к событиям моделей представлений, генерируемых 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 на ноль, чтобы избавиться от обработчика событий CollectionChanged.

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

0 голосов
/ 02 июня 2015

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

Я написал небольшой вспомогательный класс 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);
        }
    }

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

примечание: только синхронизирует элементы в коллекции, а не значения внутри них

0 голосов
/ 25 августа 2010

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

0 голосов
/ 16 февраля 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...