MVVM和VM集合

时间:2013-04-05 09:12:53

标签: wpf mvvm collections viewmodel

常见的场景:具有项目模型集合的模型 例如,一个有人的集合的房子。

如何正确构建MVVM - 特别是在使用添加和删除更新Model和ViewModel集合时?

模型House包含模型People的集合(通常为List<People>)。
视图模型HouseVM包含它包装的House对象以及视图模型PeopleVMObservableCollection<PeopleVM>)的ObservableCollection。请注意,我们最终会在HouseVM中保存两个集合(需要同步):
1. HouseVM.House.List<People>
2. HouseVM.ObservableCollection<PeopleVM>

当House更新为新人(添加)或人员离开(删除)时,现在必须在两个集合中处理该事件,Model House People集合 AND VM HouseVM PeopleVM ObservableCollection。 / p>

这个结构是否正确MVVM?
反正是否有必要对添加和删除进行双重更新?

2 个答案:

答案 0 :(得分:51)

你的一般方法是完美的MVVM,让ViewModel公开其他ViewModel的集合是一个非常常见的场景,我在这里使用它。我不建议直接在ViewModel中公开项目,比如nicodemus13说,因为你最终将视图绑定到没有ViewModel的模型中,用于你的集合的项目。所以,你的第一个问题的答案是:是的,这是有效的MVVM。

您在第二个问题中解决的问题是您的房屋模型中的人员模型列表与您的房屋ViewModel中的ViewModel人员列表之间的同步。您必须手动执行此操作。所以,没有办法避免这种情况。

enter image description here

您可以做什么:实施自定义ObservableCollection<T>ViewModelCollection<T>,将其更改推送到基础集合。要进行双向同步,请将模型的集合设为ObservableCollection&lt;&gt;也注册到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));
    }
}

答案 1 :(得分:4)

在这种情况下,我只是让模型公开ObservableCollection而不是List s。它不应该没有特别的原因。 ObservableCollection位于System.Collections.ObjectModel程序集的System命名空间中,因此没有不合理的额外依赖关系,您几乎肯定会有SystemList位于mscorlib,但这与任何事物一样都是历史人工制品。

这大大简化了模型 - 视图模型的交互,我看不出不这样做的理由,在模型上使用List只会创建大量令人不快的样板代码。毕竟,你对这些活动感兴趣。

另外,为什么HouseVM包裹ObservableCollection<PeopleVM>而不是ObservableCollection<People>?虚拟机用于绑定到视图,所以我认为绑定到ObservableCollection<PeopleVM>的任何内容实际上都对People感兴趣,否则你绑定绑定内部,或者是否有特定原因为什么这有用?我通常不会有虚拟机暴露其他虚拟机,但也许这只是我。

编辑关于库/ WCF

我不明白为什么在库中使用模型,或者甚至由WCF服务器公开会影响它们是否引发事件,这对我来说似乎完全有效(显然WCF服务不会暴露事件直接)。如果您不喜欢这样,我认为您不得不连接多个更新,但我想知道您是否真的只是手动执行与事件在ObservableCollection中相同的工作,除非我已经误解了其中一些。

就个人而言,就像我说的那样,我会让VM保持简单,让它们暴露最小值而不暴露其他虚拟机。它可能需要一些重新设计并使某些部件有点痛苦(例如Converter s,但是,最终会得到一个简单,易于管理的设计,边缘会有一些易于处理的烦恼。

在我看来,你现在的路线会很快变得非常复杂,最重要的是,很难跟上......但是,YMMV,这只是我的经历:)

也许将一些逻辑转移到显式服务可能有帮助吗?