web-dev-qa-db-ja.com

MVVMとVMのコレクション

一般的なシナリオ:アイテムモデルのコレクションを持つモデル。
例:人々のコレクションがある家。

MVVMでこれを正しく構造化する方法-特に、ModelコレクションとViewModelコレクションの追加と削除による更新に関して

モデルHouseには、モデルPeopleのコレクションが含まれています(通常はList<People>)。
ビューモデルHouseVMには、ラップするHouseオブジェクトとビューモデルのObservableCollection PeopleVMObservableCollection<PeopleVM>)。ここでは、HouseVMが2つのコレクション(同期を必要とする)を保持していることに注意してください。
1。 HouseVM.House.List<People>
2。 HouseVM.ObservableCollection<PeopleVM>

Houseが新しいPeople(追加)またはPeople Leave(削除)で更新されると、そのイベントは両方のコレクションで処理される必要があります。ModelHouse Peopleコレクション[〜#〜] and [〜#〜] the VM HouseVM PeopleVM ObservableCollection。

この構造はMVVMで正しいですか?
追加と削除の二重更新を行わなくても済むようにする方法はありますか?

46
Ricibob

あなたの一般的なアプローチは完全に素晴らしいMVVMです。ViewModelが他のViewModelのコレクションを公開することは、私がいたるところで使用している非常に一般的なシナリオです。 nicodemus13が言ったように、コレクションのアイテムの間にViewModelのないモデルにビューをバインドすることになるため、アイテムをViewModelで直接公開することはお勧めしません。したがって、最初の質問に対する答えは次のとおりです。はい、これは有効なMVVMです。

2番目の質問で対処する問題は、家のモデルの人物モデルのリストと家のViewModelの人物ViewModelのリストの間の同期です。これは手動で行う必要があります。したがって、これを回避する方法はありません。

enter image description here

実行できること:カスタムObservableCollection<T>ViewModelCollection<T>を実装して、その変更を基になるコレクションにプッシュします。双方向の同期を取得するには、モデルのコレクションもObservableCollection <>にして、ViewModelCollectionのCollectionChangedイベントに登録します。

これは私の実装です。 ViewModelFactoryサービスなどを使用しますが、一般的なプリンシパルを見てください。それが役に立てば幸い...

/// <summary>
/// Observable collection of ViewModels that pushes changes to a related collection of models
/// </summary>
/// <typeparam name="TViewModel">Type of ViewModels in collection</typeparam>
/// <typeparam name="TModel">Type of models in underlying collection</typeparam>
public class VmCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
    where TViewModel : class, IViewModel
    where TModel : class

{
    private readonly object _context;
    private readonly ICollection<TModel> _models;
    private bool _synchDisabled;
    private readonly IViewModelProvider _viewModelProvider;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="models">List of models to synch with</param>
    /// <param name="viewModelProvider"></param>
    /// <param name="context"></param>
    /// <param name="autoFetch">
    /// Determines whether the collection of ViewModels should be
    /// fetched from the model collection on construction
    /// </param>
    public VmCollection(ICollection<TModel> models, IViewModelProvider viewModelProvider, object context = null, bool autoFetch = true)
    {
        _models = models;
        _context = context;

        _viewModelProvider = viewModelProvider;

        // Register change handling for synchronization
        // from ViewModels to Models
        CollectionChanged += ViewModelCollectionChanged;

        // If model collection is observable register change
        // handling for synchronization from Models to ViewModels
        if (models is ObservableCollection<TModel>)
        {
            var observableModels = models as ObservableCollection<TModel>;
            observableModels.CollectionChanged += ModelCollectionChanged;
        }


        // Fecth ViewModels
        if (autoFetch) FetchFromModels();
    }

    /// <summary>
    /// CollectionChanged event of the ViewModelCollection
    /// </summary>
    public override sealed event NotifyCollectionChangedEventHandler CollectionChanged
    {
        add { base.CollectionChanged += value; }
        remove { base.CollectionChanged -= value; }
    }

    /// <summary>
    /// Load VM collection from model collection
    /// </summary>
    public void FetchFromModels()
    {
        // Deactivate change pushing
        _synchDisabled = true;

        // Clear collection
        Clear();

        // Create and add new VM for each model
        foreach (var model in _models)
            AddForModel(model);

        // Reactivate change pushing
        _synchDisabled = false;
    }

    private void ViewModelCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Return if synchronization is internally disabled
        if (_synchDisabled) return;

        // Disable synchronization
        _synchDisabled = true;

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                foreach (var m in e.NewItems.OfType<IViewModel>().Select(v => v.Model).OfType<TModel>())
                    _models.Add(m);
                break;

            case NotifyCollectionChangedAction.Remove:
                foreach (var m in e.OldItems.OfType<IViewModel>().Select(v => v.Model).OfType<TModel>())
                    _models.Remove(m);
                break;

            case NotifyCollectionChangedAction.Reset:
                _models.Clear();
                foreach (var m in e.NewItems.OfType<IViewModel>().Select(v => v.Model).OfType<TModel>())
                    _models.Add(m);
                break;
        }

        //Enable synchronization
        _synchDisabled = false;
    }

    private void ModelCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (_synchDisabled) return;
        _synchDisabled = true;

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                foreach (var m in e.NewItems.OfType<TModel>()) 
                    this.AddIfNotNull(CreateViewModel(m));
                break;

            case NotifyCollectionChangedAction.Remove:
                    foreach (var m in e.OldItems.OfType<TModel>()) 
                        this.RemoveIfContains(GetViewModelOfModel(m));
                break;

            case NotifyCollectionChangedAction.Reset:
                Clear();
                FetchFromModels();
                break;
        }

        _synchDisabled = false;
    }

    private TViewModel CreateViewModel(TModel model)
    {
        return _viewModelProvider.GetFor<TViewModel>(model, _context);
    }

    private TViewModel GetViewModelOfModel(TModel model)
    {
        return Items.OfType<IViewModel<TModel>>().FirstOrDefault(v => v.IsViewModelOf(model)) as TViewModel;
    }

    /// <summary>
    /// Adds a new ViewModel for the specified Model instance
    /// </summary>
    /// <param name="model">Model to create ViewModel for</param>
    public void AddForModel(TModel model)
    {
        Add(CreateViewModel(model));
    }

    /// <summary>
    /// Adds a new ViewModel with a new model instance of the specified type,
    /// which is the ModelType or derived from the Model type
    /// </summary>
    /// <typeparam name="TSpecificModel">Type of Model to add ViewModel for</typeparam>
    public void AddNew<TSpecificModel>() where TSpecificModel : TModel, new()
    {
        var m = new TSpecificModel();
        Add(CreateViewModel(m));
    }
}
52
Marc

この状況では、モデルにObservableCollectionsではなくListsを公開させるだけです。すべきでない特別な理由はありません。 ObservableCollectionSystemアセンブリのSystem.Collections.ObjectModel名前空間にあるため、不当な余分な依存関係はなく、ほとんど確実にSystemを持っています。 Listmscorlibにありますが、それは何よりも歴史的な遺物です。

これにより、モデルとビューモデルの相互作用が大幅に簡略化されます。モデルでListsを使用すると、不愉快なボイラープレートコードが大量に作成されるだけなので、理由がわかりません。結局、あなたはイベントに興味があります。

また、HouseVMObservableCollection<PeopleVM>ではなくObservableCollection<People>をラップしているのはなぜですか? VMはビューにバインドするためのものなので、ObservableCollection<PeopleVM>にバインドしているものはすべて実際にPeopleに関心があると思います。それ以外の場合は、バインディング内でバインドしている、または特定のこれが役立つ理由私は一般的にVM他のVMを公開することはありませんが、それは私だけです。

ライブラリ/ WCFについて編集

ライブラリにモデルがあるか、WCFサーバーによって公開された場合でも、イベントを発生させるかどうかに影響を与える理由がわかりません。それは私には完全に有効なようです(明らかに、WCFサービスはイベントを直接公開しません)。 。これが気に入らない場合は、複数の更新をチェーンしなければならないことに悩まされていると思いますが、ObservableCollectionでイベントが行うのと同じ作業を実際に手動で行っているのではないかと思います。私はそれのいくつかを誤解しました。

私が言ったように、個人的には、VMをシンプルに保ち、他のVMを公開せずに最小値を公開するようにします。いくつかの再設計が必要で、特定の部分が少し面倒になる場合があります(ただし、Convertersなどですが、最終的にはシンプルで管理しやすい設計になり、端に処理が​​簡単な刺激が生じます。

あなたの現在のルートはかなり複雑になり、そして最も重要なこととして、追跡するのが面倒になりそうです...しかし、YMMV、それは私の経験です:)

おそらく、一部のロジックを明示的なサービスに移動すると役立つでしょう。

4
nicodemus13