52

A common senario: A model with a collection of item models.
E.g a House with a collection of People.

How to structure this correctly for MVVM - particulary with regard to updating the Model and ViewModel collections with additions and deletes?

Model House contains a collection of model People (normally a List<People>).
View model HouseVM contains the House object that it wraps and an ObservableCollection of view model PeopleVM (ObservableCollection<PeopleVM>). Note that we end up here with the HouseVM holding two collections (that require syncing):
1. HouseVM.House.List<People>
2. HouseVM.ObservableCollection<PeopleVM>

When the House is updated with new People (add) or People leave (remove) that event must now be handled in both collections the Model House People collection AND the the VM HouseVM PeopleVM ObservableCollection.

Is this structure correct MVVM?
Is there anyway to avoid having to do the double update for Adds and Removes?

Ricibob
  • 7,505
  • 5
  • 46
  • 65
  • 4
    I am really interested in hearing the answers because it is an issue I have never really able to get rid of. – Ucodia Apr 05 '13 at 09:50

2 Answers2

59

Your general approach is perfectly fine MVVM, having a ViewModel exposing a collection of other ViewModels is a very common scenario, which I use all over the place. I would not recommend exposing items directly in a ViewModel, like nicodemus13 said, as you end up with your view binding to models without ViewModels in between for your collection's items. So, the answer to your first question is: Yes, this is valid MVVM.

The problem you are addressing in your second question is the synchronization between the list of people models in your house model and the list of people ViewModels in your house ViewModel. You have to do this manually. So, no there is no way to avoid this.

enter image description here

What you can do: Implement a custom ObservableCollection<T>, ViewModelCollection<T>, which pushes it's changes to an underlying collection. To get two way synching, make the model's collection an ObservableCollection<> too and register to the CollectionChanged event in your ViewModelCollection.

This is my implementation. It uses a ViewModelFactory service and so on, but just have a look at the general principal. I hope it helps...

/// <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));
    }
}
Marc
  • 12,706
  • 7
  • 61
  • 97
  • This looks a good approach - I think it what I'm after - let me ingest it. – Ricibob Apr 05 '13 at 10:13
  • I have been trying to implement a similar pattern but I never got satisfied with result. You gave us a wonderful implementation here. Thanks a lot! – Ucodia Apr 05 '13 at 12:32
  • 1
    @Marc: you really went above and beyond here. One of the best answers I've ever seen on StackOverflow. Good job. – Chris Pratt Apr 05 '13 at 15:34
  • @Marc, you're right about not exposing the items in the model. Only you can then easily end up with view-models that simply re-expose the same properties of models where you have simple models, which can get a bit absurd if you're wrapping data items just for the sake of it. – nicodemus13 Apr 09 '13 at 09:31
  • @nicodemus13: True, that's a general MVVM thing. Though my experience is that the ViewModels tend to become quite complex while the models remain simple, which is better than the other way around ;) Cheers! – Marc Apr 09 '13 at 11:46
  • 2
    @Marc is one of StackOverflows finest! – MoonKnight Oct 09 '13 at 11:11
  • @Marc the only problem with this solution is memory usage and possible memory leaks. Memory usage: My applications has a collection counts in 10000+. So initial loading of data takes twice as long, and takes twice as much RAM memory, if not even more. Memory Leaks: Possible memory leaks in your implementation are from CollectionChanged subscriptions. Do you know any other way of having the same result without duplicating the collection? – Stefan Vasiljevic Feb 16 '16 at 20:08
  • @StefanVasiljevic True, there is a performance penalty for wrapping each of the item models and keeping the collections in synch. Memory leaks are not really an issue, as you subscribe to the initial model collection once and usually don't exchange the collection afterwards. The cleanest solution that comes to my mind would be data virtualization on the ViewModel: You don't want to bind to 10000 items on your UI anyways, so create a filtered collection of original items on the VM and create a ViewModel collection from this, instead of the full list. Does this help in your situation? – Marc Feb 18 '16 at 14:49
  • I have a doubt - Why override `CollectionChanged`? Wouldn't base `ObservableCollection`'s implementation work? – Vijay Chavda Jun 14 '17 at 18:24
  • 2
    @VijayChavda It is kind of academic but the event is overridden and sealed to avoid calling a virtual member in the constructor. More info on this here: https://stackoverflow.com/questions/119506/virtual-member-call-in-a-constructor. Overriding just makes sure that no inherited implementation of ViewModelCollection breaks the event and thus the entire functionality. As said, it's hardly practically relevant, but academically it makes sense to seal the event and it doesn't hurt. – Marc Jun 15 '17 at 16:06
  • Wow, that taught me something today. Thanks! :) – Vijay Chavda Jun 15 '17 at 16:14
  • @Marc Do you have an example of your ViewModelFactory? Especially `IViewModelProvider` and `IViewModel` – j.t.h. Aug 26 '20 at 18:01
4

In this situation I simply make the model expose ObservableCollections rather than Lists. There's no particular reason why it shouldn't. The ObservableCollection is in the System.Collections.ObjectModel namespace of the System assembly, so there's no unreasonable extra dependencies, you almost certainly have System anyway. List is in mscorlib, but that's as much a historical artefact as anything.

This simplifies the model-viewmodel interactions massively, I can't see a reason not to do it, using Lists on the model just creates lots of unpleasant boiler-plate code. You are interested in the events, after all.

Also, why is your HouseVM wrapping an ObservableCollection<PeopleVM>, rather than ObservableCollection<People>? VMs are for binding to views, so I would think that whatever is binding to your ObservableCollection<PeopleVM> is actually interested in People, otherwise you're binding-within-a-binding, or is there a specific reason why this is useful? I wouldn't generally have a VM expose other VMs, but maybe that's just me.

Edit about libraries/WCF

I don't see why having a model in a library, or even exposed by a WCF-server should affect whether they raise events or not, it seems perfectly valid to me (obviously the WCF-service won't expose the events directly). If you don't like this, I think you're stuck with having to chain multiple updates, though I wonder if you're actually just manually doing the same work as the event would do in an ObservableCollection, unless I've misunderstood some of it.

Personally, like I said, I'd keep the VMs simple, and have them expose the minimum and not expose other VMs. It can take some redesign and make certain parts a bit of a pain (e.g. Converters, however, you end up with a simple, easy-to-manage design with some simple-to-handle irritations on the edges.

It seems to me that your current route is going to end up very complex rather quickly and, most importantly, awkward to follow... However, YMMV, it's just my experience :)

Perhaps moving some of the logic to explicit services might help?

nicodemus13
  • 2,258
  • 2
  • 19
  • 31
  • 1
    Giving the Model an ObserbableCollection instead of Lists still doesnt get me an ObserbableCollection - or have I misunderstood? And the Model shouldn't know about ItemVMs just about Items. – Ricibob Apr 05 '13 at 09:26
  • Plus my models are in libraries and run in WCF servers etc - this is no place for ObservableCollections. – Ricibob Apr 05 '13 at 09:29
  • 3
    The question about VMs exposing VMs is valid. I do this because I have a view on my House - with bindings to HouseVM but I also have views on the House occupants - which require properties over the top of those exposed by just People - hence the need for PeopleVM. – Ricibob Apr 05 '13 at 09:34
  • 3
    E.g People might have a Ethnicity property but the PeopleVM might map that to a Color for binding to a UI element Color property. This could be done with a converter on a People object but I find the use of converters heavy. It generally works much better to do conversion via VM properties. – Ricibob Apr 05 '13 at 09:37
  • Ok, I'd probably stick with the converter here, as it seems to be a specific UI thing and I'd rather keep the VM simpler and make the UI converter-heavy, but I accept it can be horses-for-courses. – nicodemus13 Apr 05 '13 at 09:40
  • Im thinking of a UI situation where you have House view that has a list of People (occupants) on one side and when you click on the list you get a UI view on the selected Person. That Person view requires a PersonVM for additional view property bindings. – Ricibob Apr 05 '13 at 09:41
  • So, I think I'd split it into two views, with the `HouseView` creating creating the `PersonView`. You can follow my suggestion and then you also have a view-separation. Should not the HouseView manage HouseVMs and Houses, and a PersonView manage PersonVms and Persons? (This potentially raises other difficulties as MVVM doesn't address views themselves and the controller aspects.) – nicodemus13 Apr 05 '13 at 09:51
  • 1
    @nicodemus13 Then if you say it is not a good design to have a VM holding a collection of models and its VM collection counterparty, how would you handle the case where those child items must present commands or validation? I have been facing this issue a lot and going all models in view models has never been satisfying. – Ucodia Apr 05 '13 at 09:57
  • Its true that if only one Person is displayed as a view at a time then you suggestion works. If however we want to display People details in the list using a DataTemplate e.g have a SkinColor indicator in the DataTemplate bound to a PersonVM.SkinColor property that maps from a Person.Ethnicity property then we run into the same issue as before. – Ricibob Apr 05 '13 at 10:00
  • @Ucodia I don't quite follow, could you post a new question with an example? Comments don't seem the best place :) Also... I don't mean to say my suggestions are necessarily 'good' design, I may misunderstand the problem entirely or miss important details. – nicodemus13 Apr 05 '13 at 10:01
  • @Ricibob: This could still be handled with, say a `Converter`? I'm not meaning to argue, just trying to see the differences in approach and where my suggestion creates unreasonable problems. If you prefer the route laid out in your question, I'm not sure I can see a way of making what you want to do easier. Perhaps someone else has more experience and can offer better help! – nicodemus13 Apr 05 '13 at 10:05
  • No problem nicodemus13 I appreciate your input. I see your point and Im just trying to thrash out whether its really a feasable option or not. I think for me the issue is as the UI expands you end up with a mess of Converters (which I really hate) - the sub VM just seems to handle this a lot tighter IMO. Thanks again for input. – Ricibob Apr 05 '13 at 10:21
  • You're welcome. The converters are a pain, but I found what I think to be an elegant solution using `Attributes`. – nicodemus13 Apr 05 '13 at 10:24
  • @nicodemus13 I was also really interested in this question because I encountered this issue countless time. I do think that having a collection of model alongside with the collection of VM for these models is a valid because those models probably need presentation and commands too. Maybe I just missunderstood you but you seemd to say it was too much complexity (which is totally true) and thus that it was better to avoid it by exposing models (which is conceptually broken). All I want is to be able to find a pattern for this kind of situation instead of avoiding it! Hope I am clear now ;) – Ucodia Apr 05 '13 at 12:26