In my previous post, I asked the question “Do I expose the model directly, or do I wrap each item in its own view model?” We looked at two plausible options for handling this and saw some code examples of each.

Now it’s time for a more interesting and challenging scenario: Do I expose the model’s collections directly, or do I wrap each collection? Let me paint this picture a bit so that we can see the scenario and the challenges.

MVVM says that a ViewModel sits between the View (XAML) and the Model. The ViewModel (VM) exposes data from the model plus additional view-specific details that the view (V) can easily bind to. Ideally, the view can be constructed with zero code-behind. Pure XAML with bindings to manage data and UI state. This is all well and good.

Now, lets say that our model is reasonably real-world and has a entities that contain collections of other entities, which in turn contain collections of other entities. Maybe some of these even have references back to other trees of data. The classic example is customers and orders.

The Model:

Customer
    |_ Shipping Addresses (Collection)
    |_ Billing Address
    |_ Orders (Collection)
    |_ First Name
    |_ Last Name
    |_ Etc…

Step 2: Shipping Addresses (Collection)

We want to bind the shipping addresses into the view. This is a collection of shipping address objects, exposed by the model’s customer object. Previously, we created the ViewModel to expose the customer to the view. One option fully wrapped the customer object and the other exposed portions of the model directly to the view.

Disclaimer:
The code below shows an intermediate solution on the road to something reasonable. Please see future posts in this series for improved options.

In the previous post, we left off with the following class:

using System.ComponentModel;
using System.Collections.ObjectModel;
 
namespace MvvmWrappers.ViewModels
{
    public class CustomerVM : INotifyPropertyChanged
    {
        private Models.Customer _Customer = null;
 
        /// <summary>
        /// Constructor - Add an event handler for PropertyChanged.
        /// </summary>
        public CustomerVM(Models.Customer customer)
        {
            _Customer = customer;
            _Customer.PropertyChanged += new PropertyChangedEventHandler(_Customer_PropertyChanged);
        }
 
        /// <summary>
        /// Watch for changes in the underlying model and propagate those that
        /// are exposed by this ViewModel.
        /// </summary>
        void _Customer_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "FirstName":
                case "LastName":
                    this.OnPropertyChanged(e.PropertyName);
                    break;
            }
        }
 
        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string FirstName
        {
            get { return _Customer.FirstName; }
            set { _Customer.FirstName = value; }
        }
 
        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string LastName
        {
            get { return _Customer.LastName; }
            set { _Customer.LastName = value; }
        }
        
        #region INotifyPropertyChanged
        ...
        #endregion
 
    }
 
}
 

Now we need to add a collection that exposes the Shipping Addresses, but not directly. We want to expose each shipping address through a ViewModel. This means that we need to somehow wrap each item, and put these into a collection. Additionally, we are good binding citizens and expose the data as a collection that supports INotifyCollectionChanged, such as ObservableCollection.

Assuming that we previously created a ViewModel for shipping address, our first attempt at modifying the CustomerVM might look like this:

using System.ComponentModel;
using System.Collections.ObjectModel;
using System;

namespace MvvmWrappers.ViewModels
{
    public class CustomerVM : INotifyPropertyChanged
    {
        private Models.Customer _Customer = null;

        /// <summary>
        /// Constructor - Add an event handler for PropertyChanged.
        /// </summary>
        public CustomerVM(Models.Customer customer)
        {
            _Customer = customer;
            _Customer.PropertyChanged += 
                new PropertyChangedEventHandler(_Customer_PropertyChanged);

            // Wrap each ShippingAddress in a ViewModel.
            _ShippingAddresses = new ObservableCollection<ShippingAddressVM>();
            foreach (var sa in customer.ShippingAddresses)
                _ShippingAddresses.Add(new ShippingAddressVM(sa));
        }

        /// <summary>
        /// Watch for changes in the underlying model and propagate those that
        /// are exposed by this ViewModel.
        /// </summary>
        void _Customer_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "FirstName":
                case "LastName":
                    this.OnPropertyChanged(e.PropertyName);
                    break;
            }
        }


        private ObservableCollection<ShippingAddressVM> _ShippingAddresses;
        public ObservableCollection<ShippingAddressVM> ShippingAddresses
        {
            get { return _ShippingAddresses; }
            set
            {
                if (value == _ShippingAddresses)
                    return;

                _ShippingAddresses = value;
                OnPropertyChanged("ShippingAddresses");
            }
        }
        


        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string FirstName
        {
            get { return _Customer.FirstName; }
            set { _Customer.FirstName = value; }
        }

        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string LastName
        {
            get { return _Customer.LastName; }
            set { _Customer.LastName = value; }
        }
        
        #region INotifyPropertyChanged
        ...
        #endregion

    }
}

 

This is a good start, but lacking a couple things. First off, the model’s collections support INotifyCollectionChanged (via ObservableCollection). We’d like the UI to automatically respond if an item is added or removed from the model.

Likewise, we’d like any changes to the ViewModel collections to be propagated to the Model.

As in the previous case, this probably means that we need to intercept the CollectionChanged event and handle it accordingly. Here’s a brute-force approach, albeit a quite painful and ugly:

using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;

namespace MvvmWrappers.ViewModels
{
    public class CustomerVM : INotifyPropertyChanged
    {
        private Models.Customer _Customer = null;
        private bool _IgnoreChanges = false;



        /// <summary>
        /// Constructor - Add an event handler for PropertyChanged.
        /// </summary>
        public CustomerVM(Models.Customer customer)
        {
            _Customer = customer;
            _Customer.PropertyChanged += new PropertyChangedEventHandler(_Customer_PropertyChanged);

            // Wrap each ShippingAddress in a ViewModel.
            _ShippingAddresses = new ObservableCollection<ShippingAddressVM>();
            foreach (var sa in customer.ShippingAddresses)
                _ShippingAddresses.Add(new ShippingAddressVM(sa));


            _ShippingAddresses.CollectionChanged += 
                new NotifyCollectionChangedEventHandler(ShippingAddressVMs_CollectionChanged);
            _Customer.ShippingAddresses.CollectionChanged += 
                new NotifyCollectionChangedEventHandler(ShippingAddresses_CollectionChanged);
        }

        void ShippingAddressVMs_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_IgnoreChanges)
                return;

            _IgnoreChanges = true;

            // If a reset, then e.OldItems is empty. Just clear and reload.
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                _Customer.ShippingAddresses.Clear();

                foreach (var sa in ShippingAddresses)
                    _Customer.ShippingAddresses.Add(sa.GetUnderlyingObject());
            }
            else
            {
                // Remove items from collection.
                var toRemove = new List<Models.ShippingAddress>();

                if (null != e.OldItems && e.OldItems.Count > 0)
                    foreach (var item in e.OldItems)
                        foreach (var existingItem in _Customer.ShippingAddresses)
                            if (((ShippingAddressVM)item).IsViewFor(existingItem))
                                toRemove.Add(existingItem);

                foreach (var item in toRemove)
                    _Customer.ShippingAddresses.Remove(item);

                // Add new items to the collection.
                if (null != e.NewItems && e.NewItems.Count > 0)
                    foreach (var item in e.NewItems)
                        _Customer.ShippingAddresses.Add(((ShippingAddressVM)item).GetUnderlyingObject());
            }

            _IgnoreChanges = false;
        }

        void ShippingAddresses_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_IgnoreChanges)
                return;

            _IgnoreChanges = true;
            
            // If a reset, then e.OldItems is emtpy. Just clear and reload.
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                ShippingAddresses.Clear();

                foreach (var sa in _Customer.ShippingAddresses)
                    _ShippingAddresses.Add(new ShippingAddressVM(sa));
            }
            else
            {
                // Remove items from collection.
                var toRemove = new List<ShippingAddressVM>();

                if (null != e.OldItems && e.OldItems.Count > 0)
                    foreach (var item in e.OldItems)
                        foreach (var existingItem in ShippingAddresses)
                            if (existingItem.IsViewFor((Models.ShippingAddress)item))
                                toRemove.Add(existingItem);

                foreach (var item in toRemove)
                    ShippingAddresses.Remove(item);

                // Add new items to the collection.
                if (null != e.NewItems && e.NewItems.Count > 0)
                    foreach (var item in e.NewItems)
                        ShippingAddresses.Add(new ShippingAddressVM((Models.ShippingAddress)item));
            }

            _IgnoreChanges = false;
        }

        /// <summary>
        /// Watch for changes in the underlying model and propagate those that
        /// are exposed by this ViewModel.
        /// </summary>
        void _Customer_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "FirstName":
                case "LastName":
                    this.OnPropertyChanged(e.PropertyName);
                    break;

                case "ShippingAddresses":
                    // Underlying collection was rebuilt.
                    this.ShippingAddresses_CollectionChanged(
                        this,
                        new NotifyCollectionChangedEventArgs(
                            NotifyCollectionChangedAction.Reset
                        )
                    );

                    break;
            }
        }

        /// <summary>
        /// A collection of shipping addresses, wrapped in a ShippingAddressVM ViewModel
        /// </summary>
        private ObservableCollection<ShippingAddressVM> _ShippingAddresses;
        public ObservableCollection<ShippingAddressVM> ShippingAddresses
        {
            get { return _ShippingAddresses; }
            set
            {
                if (value == _ShippingAddresses)
                    return;

                _ShippingAddresses.CollectionChanged -= ShippingAddressVMs_CollectionChanged;
                _ShippingAddresses = value;
                _ShippingAddresses.CollectionChanged += ShippingAddressVMs_CollectionChanged;

                // Underlying collection was rebuilt.
                this.ShippingAddressVMs_CollectionChanged(
                    this,
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Reset
                    )
                );

                OnPropertyChanged("ShippingAddresses");
            }
        }

        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string FirstName
        {
            get { return _Customer.FirstName; }
            set { _Customer.FirstName = value; }
        }

        /// <summary>
        /// Delegate the storage to the model.
        /// Also delegate the INotifyPropertyChanged handling to the model.
        /// </summary>
        public string LastName
        {
            get { return _Customer.LastName; }
            set { _Customer.LastName = value; }
        }

        #region INotifyPropertyChanged
        ...
        #endregion

    }
}

 

 

 

Now we see that the collections stay in sync when items are added to the Model. The Model also stays in sync with changes to the ViewModel (I wrote Unit Tests to prove it ;-). This implementation works, but is less than ideal.

What to do? Is there a simple way to achieve MVVMs goals of wrapping the Model in VMs, including collections?

There may be hope! See part 3 for a review of some interesting frameworks that help with objects that support change notification. We’ll look into using something like BLINQCLINQ, or Obtics. These three frameworks offer LINQ based implementations that can expose an observable collection as a new, synchronized collection that is based on a LINQ query against the source.

Cheers!