代码之家  ›  专栏  ›  技术社区  ›  Jake Pearson

MVVM同步集合

  •  43
  • Jake Pearson  · 技术社区  · 15 年前

    是否有一种标准化的方法可以将模型对象集合与C和WPF中匹配的ModelView对象集合同步?我正在寻找一种类,如果我只有几个苹果,并且我可以将它们全部保存在内存中,那么可以保持下面两个集合的同步。

    另一种说法是,我想确保如果我将Apple添加到Apples集合中,我想将AppleModelView添加到AppleModelView集合中。我可以通过聆听每个收藏的CollectionChanged事件来撰写自己的作品。这似乎是一个常见的场景,一个比我聪明的人已经定义了“正确的方法”来做这件事。

    public class BasketModel
    {
        public ObservableCollection<Apple> Apples { get; }
    }
    
    public class BasketModelView
    {
        public ObservableCollection<AppleModelView> AppleModelViews { get; }
    }
    
    10 回复  |  直到 8 年前
        1
  •  8
  •   Dennis    15 年前

    我可能不会 确切地 了解您的需求,但是我处理类似情况的方法是在ObservableCollection上使用CollectionChanged事件,并根据需要简单地创建/销毁视图模型。

    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一起使用,当集合更改时,它会临时断开源连接,工作得很好。

    HTH

    丹尼斯

        2
  •  64
  •   Sam Harwell    15 年前

    我使用懒散构造的自动更新集合:

    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
  •  4
  •   Charlie    15 年前

    首先,我认为没有一个“正确的方法”可以做到这一点。这完全取决于您的应用程序。有更多正确的方法和更少正确的方法。

    尽管如此,我想知道你为什么要保留这些收藏品。” 同步中 “你认为什么情况会使他们失去同步?如果你看看Josh Smith的示例代码 MSDN article on M-V-VM ,您将看到,大多数情况下,模型与视图模型保持同步,因为每次创建模型时,都会创建一个视图模型。这样地:

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

    我想知道,是什么阻止你创建一个 AppleModelView 每次创建 Apple ?在我看来,这是保持这些收藏“同步”的最简单方法,除非我误解了你的问题。

        4
  •  4
  •   Jonathan ANTOINE    14 年前
        5
  •  1
  •   Sergey Vyacheslavovich Brunov prodigitalson    14 年前

    本文使用mvvm提供undo/redo,提供mirrorcollection类来实现视图模型和模型集合的同步。

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

        6
  •  1
  •   Community c0D3l0g1c    7 年前

    好吧,我很喜欢书呆子。 this answer 所以我不得不分享这个抽象工厂,我添加到它来支持我的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
  •  0
  •   Aran Mulholland JohnnyAce    14 年前

    我已经编写了一些助手类,用于将可观察的业务对象集合包装在它们的视图模型对应物中。 here

        8
  •  0
  •   bertvh    14 年前

    我很喜欢280Z28的解决方案。只是一句话。是否需要为每个NotifyCollectionChangedAction执行循环?我知道操作的文档状态为“一个或多个项目”,但是由于ObservableCollection本身不支持添加或删除范围,我认为这永远不会发生。

        9
  •  0
  •   MikeT    9 年前

    将集合重置为默认值或匹配目标值是我经常遇到的问题

    我编写了一个包含错误方法的小助手类,其中包括

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

    它满足了我的大部分需求 第一个可能最适用,因为您也在转换类型

    注意:这只同步集合中的元素,而不同步其中的值

        10
  •  0
  •   Community c0D3l0g1c    7 年前

    同时 Sam Harwell's solution 已经很好了,有两个问题:

    1. 在此注册的事件处理程序 this._source.CollectionChanged += OnSourceCollectionChanged 从未注册,即 this._source.CollectionChanged -= OnSourceCollectionChanged 遗失了。
    2. 如果事件处理程序曾经附加到由 viewModelFactory ,无法知道何时可以再次分离这些事件处理程序。(或者一般来说,您不能为“销毁”准备生成的视图模型。)

    因此,我提出了一个解决方案,解决Sam Harwell方法的两个(短)缺点:

    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 它允许“准备要销毁的对象”,例如通过删除附加到对象上的任何事件处理程序。