0

I have a 2-levels hierarchy in my model composed of constellations and entities, hosted in a root object, and I want to show them in a TreeView.

Root object L Constellations L Entities

I have my RootObjectViewModel exposing my root object. In that, I fully agree with Bryan Lagunas in MVVM best practices, that the model object should be exposed by the view model instead of doing facade. Excellent video by the way, really worth the time.

I read everywhere that the TreeView.ItemsSource should be mapped to a collection of viewmodels, that is, for my collection of Constellation, my RootObjectViewModel should provide a collection of ConstellationViewModel. My concern is that if my collections of constellations, and entities within, are live, that is, if some items are added, changed (their order) or removed, I have to manually reflect those changes in my ViewModels' collections.

I would find it more elegant to map the ItemsSource to, say, the collection of Constellation itself, so that any change in the model is reflected without duplication, and then have some converter or so to map the TreeViewItems.DataContext to a ConstellationViewModel based on the constellation model.

If this is not clear enough, I can clarify this with some sample code.

Did anyone face the same question and/or try to implement this ?

Thanks you in advance for your help.

Cedric

2 Answers2

0

It depends. If your model has exactly the properties the view needs, and the view can directly alter them when the user clicks around, it's fine to expose the model.

But if your model is, for example, read only and require calls to a service to apply changes, you have to wrap it in a view model to provide the view with writeable properties.

Haukinger
  • 10,420
  • 2
  • 15
  • 28
  • Sure, I don't mean getting completely rid of view models.This is why I mention a converter, let's say a ModelToViewModel converter, that would return a ViewModel for the given model, which would be assigned to the `TreeViewItem` data context, assuming it is possible to provide a converter... – Cédric Bourgeois Nov 11 '16 at 10:03
  • The converter is fine, but if view model creation is that simple (`new ViewModel( model )`), I suppose you don't gain much from it. And if you have lots of dependencies, you get specialized converters for each type, that do the job of the view model. And while it might work in the forward direction, the other way round gets a bit ugly, because you end up with your converter creating/adding new model instances... – Haukinger Nov 11 '16 at 10:57
  • My understanding of MVVM is that GUI is not supposed to manage data, but to inform the view model through commands and events, and then up to the model which would react. Therefore I'm not much afraid of the _other way round_ because the model would generate new items and populate collections that would be, the way forward, displayed appropriately. Now I agree with you that this would mean one converter for each model type to return the appropriate view model. But that's more or less what happens when you generate them in your main view model's collections. – Cédric Bourgeois Nov 11 '16 at 15:36
  • I think I'll start up a POC project and see how it goes – Cédric Bourgeois Nov 11 '16 at 15:39
  • Keep us updated, this view model-creating-converter might be a really cool idea! – Haukinger Nov 11 '16 at 15:40
  • I let you have a look at the POC on github as per my answer below. – Cédric Bourgeois Nov 12 '16 at 22:32
0

Got it working !

It is not possible out-of-the-box, and here's why:


It is possible to use model collections as items source, and to use a converter to get the appropriate view model it the components inside the TreeViewItem. But there isn't any way to interfere with the creation of TreeViewItem to apply the converter to its DataContext. Which means that the TreeViewItem's properties can't be binded to the ViewModel.

In other words :

  • if you want to stick with the standard behavior of the TreeView and don't have to deal with the TreeViewItems properties, if either your collections don't change or can implement ICollectionChanged, and if your models don't change or can implement IPropertyChanged, it is fine to go with the model's collections.
  • If any of these conditions is broken, then you will have to go with building ViewModel's collections and sync them with model's collection.

Now, I implemented a collection type named ConvertingCollection<Tin, Tout> that uses an original collection as an input and syncs its own contents with this input. This is just a basic class with many many ways of improvement, but it works. All you have to do is use this collection as a VM property, set the original collection and converter, and bind the ItemsSource to this collection.

Here's the full code:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Windows.Data;

namespace TreeViewPOC
{
  class ConvertingCollection<Tin, Tout> : ObservableCollection<Tout>
  {
    private IValueConverter _converter;
    private bool _isObservableCollection;
    private IEnumerable<Tin> _originalCollection;
    private Dictionary<Tin, Tout> _mapping = new Dictionary<Tin, Tout>();


    public ConvertingCollection(IValueConverter converter)
    {
      // save parameters
      _converter = converter;
    }

    public ConvertingCollection(IEnumerable<Tin> originalCollection, IValueConverter converter)
    {
      // save parameters
      _converter = converter;
      OriginalCollection = originalCollection;
    }

    #region Properties
    public IEnumerable<Tin> OriginalCollection
    {
      get
      {
        return _originalCollection;
      }
      set
      {
        if (!value.Equals(_originalCollection))
        {
          // manage older collection
          if (_originalCollection != null && _isObservableCollection)
          {
            (_originalCollection as ObservableCollection<Tin>).CollectionChanged -= originalCollection_CollectionChanged;
            this.Clear();
          }

          _originalCollection = value;

          // setup original collection information.
          _isObservableCollection = _originalCollection is INotifyCollectionChanged;
          if (_originalCollection != null && _isObservableCollection)
          {
            (_originalCollection as INotifyCollectionChanged).CollectionChanged += originalCollection_CollectionChanged;
            foreach (Tin item in _originalCollection)
            {
              AddConverted(item);
            }
          }
        }
      }
    }

    #endregion
    /// <summary>
    /// Indicates the time in milliseconds between two refreshes.
    /// </summary>
    /// <notes>
    /// When the original collection isn't observable, it must be explored to reflect changes in the converted collection.
    /// </notes>
    // TODO
    //public int RefreshRate { get; set; } = 1000;

    /// <summary>
    /// Flushes the collection.
    /// </summary>
    public new void Clear()
    {
      _mapping.Clear();
      base.Clear();
    }

    #region Events management

    private void originalCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      switch (e.Action)
      {
        case NotifyCollectionChangedAction.Add:
          foreach (Tin item in e.NewItems)
          {
            AddConverted(item);
          }
          break;

        case NotifyCollectionChangedAction.Remove:
          foreach (Tin item in e.OldItems)
          {
            RemoveConverted(item);
          }
          break;
      }
    }
    #endregion

    #region Helpers
    /// <summary>
    /// Converts an item and adds it to the collection.
    /// </summary>
    /// <param name="item">The original item.</param>
    private void AddConverted(Tin item)
    {
      Tout converted = (Tout) _converter.Convert(item, typeof(Tout), null, CultureInfo.CurrentCulture);
      _mapping.Add(item, converted);
      this.Add(converted);
    }

    /// <summary>
    /// Removes a converted itemfrom the collection based on its original value.
    /// </summary>
    /// <param name="item">The original item.</param>
    private void RemoveConverted(Tin item)
    {
      this.Remove(_mapping[item]);
      _mapping.Remove(item);
    }
    #endregion
  }
}

I created a project on Github named MVVMTreeViewPOC to POC the idea. It is working fine, and it is a good tool to show the limitations.

Cedric.

  • Just to mention another approach of the same kind here : [http://stackoverflow.com/questions/7505524/is-there-any-way-to-convert-the-members-of-a-collection-used-as-an-itemssource#7505785] but it looks like the conversion is performed at the content of the list view item, thus it remains impossible to bind `IsSelected` to the `ViewModel`. – Cédric Bourgeois Nov 14 '16 at 13:28
  • Another great source of information is http://drwpf.com/blog/category/item-containers/ that helps understanding how tree views, list views et.al. actually work. Not yet gone through all articles, but my understanding is that the ItemContainerGenerator, that links an item to a container, should be the right place to use the converter. But the class is sealed... – Cédric Bourgeois Nov 16 '16 at 16:23