web-dev-qa-db-ja.com

MVVM同期コレクション

ModelオブジェクトのコレクションをC#およびWPFで一致するModelViewオブジェクトのコレクションと同期する標準化された方法はありますか?リンゴが数個しかなく、すべてをメモリに保持できると仮定して、次の2つのコレクションの同期を維持するようなクラスを探しています。

別の言い方をすれば、ApplesコレクションにAppleを追加するかどうかを確認したいのですが、AppleModelViewsコレクションにAppleModelViewを追加したいと思います。各コレクションのCollectionChangedイベント。これは、私より賢い誰かがそれを行うための「正しい方法」を定義した一般的なシナリオのようです。

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

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

私は正確にあなたの要件を理解していないかもしれませんが、私が同様の状況を処理した方法は、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、

デニス

9
Dennis

私は怠惰に構築された自動更新コレクションを使用します:

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;
            }
        }
    }
}
64
Sam Harwell

ここにも例(および説明)があります: http://blog.lexique-du-net.com/index.php?post/2010/03/02/MV-VM-How-to- keep-collections-of-ViewModel-and-Model-in-sync

この助けを願っています

4
Jonatha ANTOINE

まず第一に、これを行うための単一の「正しい方法」はないと思います。それは完全にあなたのアプリケーションに依存します。より正しい方法とあまり正しくない方法があります。

そうは言っても、なぜこれらのコレクションを「同期」に保つ必要があるのだろうかと思います。それらが同期しなくなると考えているシナリオは何ですか? Josh Smithの MV-VMに関するMSDNの記事 のサンプルコードを見ると、ほとんどの場合、モデルがViewModelと同期していることがわかります。これは、モデルが作成されると、ViewModelも作成されます。このような:

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

AppleModelViewを作成するたびにAppleを作成できないのはなぜですか?あなたの質問を誤解しない限り、これらのコレクションを「同期」させる最も簡単な方法のように思えます。

4
Charlie

記事「MVVMを使用して元に戻す/やり直しを提供する」では、ビューモデルとモデルコレクションの同期を実現するためのMirrorCollectionクラスを提供しています。

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

1
Sergey Brunov

OK私はオタクに恋をしています この答え それで私は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
dFlat

コレクションをデフォルト値にリセットしたり、ターゲット値に一致させたりすることは、私が頻繁にヒットすることです。

私は以下を含む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
MikeT

観察可能なビジネスオブジェクトのコレクションを対応するビューモデルでラップするためのヘルパークラスをいくつか作成しました ここ

0
Aran Mulholland

Sam Harwellのソリューション はすでにかなり良いですが、2つの問題があります。

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

2つの問題の最初の問題に対処するには、Sourceをnullに設定して、CollectionChangedイベントハンドラーを取り除くことができます。

2つの問題の2番目に対処するには、「オブジェクトを破壊する準備をする」ことを可能にするviewModelRemoveHandlerを追加するだけです。それに接続されているイベントハンドラーを削除します。

0
Hauke P.

私は280Z28のソリューションが本当に好きです。ただ一つの発言。 NotifyCollectionChangedActionごとにループを実行する必要がありますか?アクションのドキュメントに「1つ以上のアイテム」と記載されていることは知っていますが、ObservableCollection自体は範囲の追加または削除をサポートしていないため、これは決して起こり得ないと思います。

0
bertvh