MVVM和VM集合

52

一个常见的场景:一个具有多个项目模型的模型。
例如:一个房子包含多个人。

如何正确为MVVM结构化-特别是关于如何更新添加和删除的Model和ViewModel集合?

Model House 包含一个 Model People 集合(通常是 List<People>)。
ViewModel HouseVM 包含其封装的 House 对象和 ViewModel PeopleVM 的 ObservableCollection (ObservableCollection<PeopleVM>)。请注意,这里 HouseVM 拥有两个需要同步的集合:
1. HouseVM.House.List<People>
2. HouseVM.ObservableCollection<PeopleVM>

当 House 更新(添加或移除)新的 People 时,该事件必须在 Model House People 集合 VM HouseVM PeopleVM ObservableCollection 中处理。

这种结构是否正确实现了 MVVM?
是否有什么方法可以避免对添加和删除进行双重更新?


4
我非常感兴趣听到答案,因为这是一个我从未真正摆脱过的问题。 - Ucodia
2个回答

61

您的一般方法是完全正确的MVVM,拥有一个公开其他ViewModel集合的ViewModel是非常常见的情况,我在很多地方都使用它。像nicodemus13所说的那样,我不建议直接在ViewModel中公开项,因为这样你的视图会绑定到没有ViewModel的模型,而在你的集合项之间没有ViewModel。所以,对于你的第一个问题的答案是:是的,这是有效的MVVM。

你在第二个问题中解决的问题是房子模型中的人员模型列表和房屋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));
    }
}

1
@Marc:你真的做得非常出色。这是我在StackOverflow上见过的最好的答案之一。干得好。 - Chris Pratt
2
@Marc 是 StackOverflow 最优秀的之一! - MoonKnight
@Marc,这种解决方案唯一的问题是内存使用和可能的内存泄漏。内存使用:我的应用程序有10000+个集合计数。因此,如果不是更多,数据的初始加载需要两倍的时间,并且需要两倍甚至更多的RAM内存。内存泄漏:您的实现中可能存在的内存泄漏来自CollectionChanged订阅。您是否知道其他方法可以在不重复集合的情况下获得相同的结果? - Stefan Vasiljevic
@StefanVasiljevic 确实,对于每个项模型进行包装并保持集合同步会带来性能损失。内存泄漏并不是真正的问题,因为您只需订阅一次初始模型集合,通常不会在之后交换集合。我脑海中想到的最清晰的解决方案是 ViewModel 上的数据虚拟化:无论如何,您都不希望在 UI 上绑定 10000 个项,因此请在 VM 上创建原始项的筛选集合,并从此创建 ViewModel 集合,而不是完整列表。这有助于您的情况吗? - Marc
2
@VijayChavda 这有点学术性,但事件被重写和密封以避免在构造函数中调用虚成员。关于此的更多信息,请参见:https://dev59.com/V3VD5IYBdhLWcg3wAWoO。重写只是确保ViewModelCollection的任何继承实现都不会破坏事件,从而确保整个功能正常运行。正如所说,这几乎没有实际意义,但从学术上讲,密封事件是有意义的,而且不会有任何损害。 - Marc
显示剩余7条评论

4
在这种情况下,我只是让模型暴露ObservableCollection而不是List。没有特别的原因不能这样做。ObservableCollection在System.Collections.ObjectModel命名空间中,所以没有不合理的额外依赖,你几乎肯定已经有System了。List在mscorlib中,但那只是历史遗留问题。
这大大简化了模型-视图模型交互,我认为没有理由不这样做,在模型上使用List只会创建许多令人不愉快的样板代码。毕竟,你对事件感兴趣。
此外,为什么你的HouseVM包装了一个ObservableCollection,而不是ObservableCollection? VM用于绑定到视图,因此我认为任何绑定到ObservableCollection的内容实际上都与People有关,否则你就是在内部进行绑定,或者有没有特殊的原因使这个功能更有用?我通常不会让VM公开其他VM,但也许这只是我的想法。
编辑有关库/WCF
我不明白为什么将模型放在库中,甚至由WCF服务器公开是否会影响它们是否引发事件,这对我来说似乎完全有效(显然WCF服务不会直接公开事件)。如果您不喜欢这个,我认为您必须链接多个更新,虽然我想知道是否实际上只是手动执行事件在ObservableCollection中所做的工作,除非我误解了其中的一些内容。
就我个人而言,像我说的那样,我会让VM保持简单,并让它们公开最少的内容而不公开其他VM。这可能需要进行一些重新设计,使某些部分有点痛苦(例如Converter),但是您最终将获得一个简单、易于管理的设计,并且边缘的问题也很容易处理。
对我来说,你现在走的路线很快就会变得非常复杂,最重要的是难以跟踪...然而,你的经验可能有所不同:)
也许将一些逻辑移动到明确的服务中有助于解决问题?

2
给模型一个ObservableCollection<Items>而不是List<Item>,仍然不能得到一个ObservableCollection<ItemsVM> - 或者我理解错了吗?并且模型不应该知道ItemVMs,只需要知道Items。 - Ricibob
3
关于虚拟机暴露虚拟机的问题是有效的。我这样做是因为我有一个关于我的房子的视图——与HouseVM绑定,但我也有关于房屋居民的视图,这些视图需要在仅使用People提供的属性之上拓展属性——因此需要PeopleVM。 - Ricibob
3
人可能具有“种族”属性,但是“PeopleVM”可能将其映射到一个“颜色”来绑定到UI元素的“颜色”属性。这可以通过对人物对象使用转换器来完成,但我认为使用转换器会使代码冗长。通常更好的做法是通过VM属性进行转换。 - Ricibob
1
@nicodemus13 如果您认为一个VM持有模型集合及其VM集合的计数器不是一个好的设计,那么当这些子项必须呈现命令或验证时,您会如何处理?我一直在面对这个问题,将所有模型放在视图模型中从来没有让人满意过。 - Ucodia
不客气。转换器很麻烦,但我找到了一个使用“属性”看起来很优雅的解决方案。 - nicodemus13
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接