【问题标题】:MVVM Sync CollectionsMVVM 同步集合
【发布时间】:2010-11-18 09:54:12
【问题描述】:

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

换一种说法,我想确定如果我将 Apple 添加到 Apples 集合中,我希望将 AppleModelView 添加到 AppleModelViews 集合中。我可以通过收听每个集合的 CollectionChanged 事件来编写自己的代码。这似乎是一个常见的场景,比我聪明的人已经定义了“正确的方法”来做到这一点。

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

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

【问题讨论】:

  • 我完全不明白你的问题。我今天可能有点慢,但你可能需要重申一下。
  • 我在上面添加了另一段,希望对您有所帮助。
  • 为什么有单独的集合? Apple 可以是 AppleModelView 的子集,然后取决于您的 Apple 是如何仅填充 AppleModelView 的相关部分的。一般来说,我将我的模型完全排除在 WPF 之外,并且只有 ViewModel。模型是数据库上的实体或其他任何东西。
  • 你也可以在绑定过程中使用值转换器将Apple转换为AppleModelView,这取决于你是否想在其他地方重用AppleModelView

标签: c# wpf mvvm


【解决方案1】:

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

说了这么多,我想知道为什么您需要保持这些集合“同步”。您认为哪种情况会使它们不同步?如果您查看来自 Josh Smith 的MSDN article on M-V-VM 的示例代码,您会发现在大多数情况下,模型与 ViewModel 保持同步,因为每次创建模型时,也会创建一个 ViewModel。像这样:

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

我想知道,是什么阻止您在每次创建 Apple 时创建 AppleModelView?在我看来,除非我误解了您的问题,否则这似乎是使这些集合“同步”的最简单方法。

【讨论】:

  • 感谢您的帖子。我可能在我的脑海里让这太难了。我会回去工作的。
【解决方案2】:

我可能完全理解您的要求,但是我处理类似情况的方式是在 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,

丹尼斯

【讨论】:

    【解决方案3】:

    我使用延迟构建的自动更新集合:

    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&lt;TViewModel, TModel&gt;

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

    【讨论】:

    • 对于使用 Silverlight 的任何人,您需要使用 #if !SILVERLIGHT 之类的内容注释掉 NotifyCollectionChangedAction.Move 案例
    • 在您的NotifyCollectionChangedAction.Move 情况下在else 情况下,如果NewStartingIndex 为> OldStartingIndex,则可能存在错误,您可以通过在删除和插入之间添加以下代码来修复它:if(newIndex &gt; e.OldStartingIndex) newIndex-=e.OldItems.Count; .
    • @JoãoPortela - 你能发布更多你的实际变化吗?原代码中没有newIndex变量。
    • @grantnz 看看this,这清楚了吗?
    • 应该从这段代码中删除ContractsLazy 的使用。它给这个答案带来了不必要的复杂性..
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      【讨论】:

        【解决方案6】:

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

        【讨论】:

        • 完全跑题了,但你是 Bert Vermeire 吗?
        • 不,我不是,'h' 没有意义 :)
        • ObservableCollection 确实支持多个项目,我相信是 WPF 基本上不支持单个更改事件中的多个项目。
        • @jpierson:ObservableCollection 确实支持多个删除(即清除)但不添加我承认 AddRange 方法会很有用,但通过强制它一次添加一个,它们会强制缩短响应时间监控 GUI 元素。减少锁定的可能性,因为有人一次向列表视图添加了 10000 个元素
        【解决方案7】:

        «Using MVVM to provide undo/redo. Part 2: Viewmodelling lists» article 提供了MirrorCollection&lt;V, D&gt; 类来实现视图模型和模型集合的同步。

        其他参考

        1. 原文链接(目前不可用):Notify Changed » Blog Archive » Using MVVM to provide undo/redo. Part 2: Viewmodelling lists

        【讨论】:

        【解决方案8】:

        好的,我对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;
                }
            }
        }
        

        【讨论】:

          【解决方案9】:

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

          我编写了一个小辅助类 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);
                  }
              }
          

          满足了我的大部分需求 第一个可能最适用于您的转换类型

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

          【讨论】:

            【解决方案10】:

            虽然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 设置为null 以摆脱CollectionChanged 事件处理程序。

            要处理这两个问题中的第二个,您可以简单地添加一个viewModelRemoveHandler,它允许“准备您的对象以进行销毁”,例如通过删除任何附加到它的事件处理程序。

            【讨论】:

            • 我还注意到,在获取Reset 时,e.NewItems 有时可能为空。我刚刚也添加了处理这种情况的代码。
            • 你能提供一个简单的例子来说明你将如何实现viewModelRemoveHandler 吗?
            • 唷,在很短的时间跨度内生成一个工作示例已经太久了。如果我没记错的话,我将一个函数作为第二个参数传递给了构造函数。此函数删除了附加到视图模型的所有事件。这有帮助吗?
            • 是的 - 我猜是这样,但我希望你有一个很好的例子。对不起,四年的墓志铭!还是谢谢!
            【解决方案11】:

            这是 Sam Harwell 的 answer 的一个细微变化,实现了 IReadOnlyCollection&lt;&gt;INotifyCollectionChanged,而不是直接从 ObservableCollection&lt;&gt; 继承。这可以防止消费者修改集合,这在这种情况下通常是不需要的。

            此实现还使用CollectionChangedEventManager 将事件处理程序附加到源集合,以避免在源集合未与镜像集合同时释放的情况下发生内存泄漏。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-03-04
              • 1970-01-01
              • 2019-10-13
              • 2020-03-23
              • 1970-01-01
              • 1970-01-01
              • 2013-03-27
              相关资源
              最近更新 更多