An ObservableCollection or BindingList of View Models

An ObservableCollection<T> will notify a view of insertions, deletions, and replacements. Conversely, a BindingList<T> will accept insertions from a view. To be most useful, these collection types should be data bound to a view. In MVVM, that means that these should be properties of a view model. Moreover, it means that these should be collections of view models.

View models are projections of model objects. If an Order has a collection of OrderLines, then an OrderViewModel should have a collection of OrderLineViewModels. In this situation, you need to keep a collection of view models synchronized with a collection of models.

Here are a couple of helper classes that do just that: MappedObservableCollection and MappedBindingList.

MappedObservableCollection takes a mapping function and a source collection. The map creates a view model for each model object. It will produce an observable collection of view models. If the source is observable (i.e. implements INotifyCollectionChanged), then it will mirror any changes in the source collection to the target collection.

public class MappedObservableCollection<TSource, TTarget>
{
    private Func<TSource, TTarget> _map;
    private IEnumerable<TSource> _sourceCollection;
    private ObservableCollection<TTarget> _targetCollection = new ObservableCollection<TTarget>();

    public MappedObservableCollection(Func<TSource, TTarget> map, IEnumerable<TSource> sourceCollection)
    {
        _map = map;
        _sourceCollection = sourceCollection;

        var notifyCollectionChanged = sourceCollection as INotifyCollectionChanged;
        if (notifyCollectionChanged != null)
            notifyCollectionChanged.CollectionChanged += SourceCollectionChanged;
        PopulateTargetCollection();
    }

    public ObservableCollection<TTarget> TargetCollection
    {
        get { return _targetCollection; }
    }

    private void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            // Add the corresponding targets.
            int index = e.NewStartingIndex;
            foreach (TSource item in e.NewItems)
            {
                _targetCollection.Insert(index, _map(item));
                ++index;
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            // Delete the corresponding targets.
            for (int i = 0; i < e.OldItems.Count; i++)
            {
                _targetCollection.RemoveAt(e.OldStartingIndex);
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Replace)
        {
            // Replace the corresponding targets.
            for (int i = 0; i < e.OldItems.Count; i++)
            {
                _targetCollection[i + e.OldStartingIndex] = _map((TSource)e.NewItems[i + e.NewStartingIndex]);
            }
        }
        else
        {
            // Just give up and start over.
            _targetCollection.Clear();
            PopulateTargetCollection();
        }
    }

    private void PopulateTargetCollection()
    {
        foreach (TSource item in _sourceCollection)
        {
            _targetCollection.Add(_map(item));
        }
    }
}

MappedBindingList performs a similar function, but it generates a BindingList<T>. BindingList<T> is useful for DataGrid, which allows the user to add new rows. When the user creates a new row, MappedBindingList calls a factory to get a new model object. Then it maps it into the target collection as a view model.

public class MappedBindingList<TSource, TTarget>
{
    private Func<TSource, TTarget> _map;
    private IEnumerable<TSource> _sourceCollection;
    private BindingList<TTarget> _targetCollection = new BindingList<TTarget>();
    private Func<TSource> _factory;
    
    public MappedBindingList(IEnumerable<TSource> sourceCollection, Func<TSource, TTarget> map, Func<TSource> factory)
    {
        _map = map;
        _sourceCollection = sourceCollection;
        _factory = factory;

        _targetCollection.AllowNew = true;
        _targetCollection.AllowEdit = true;
        _targetCollection.AllowRemove = true;
        _targetCollection.AddingNew += TargetCollection_AddingNew;

        var notifyCollectionChanged = sourceCollection as INotifyCollectionChanged;
        if (notifyCollectionChanged != null)
            notifyCollectionChanged.CollectionChanged += SourceCollectionChanged;
        PopulateTargetCollection();
    }

    private void TargetCollection_AddingNew(object sender, AddingNewEventArgs e)
    {
        e.NewObject = _map(_factory());
    }

    public BindingList<TTarget> TargetCollection
    {
        get { return _targetCollection; }
    }

    private void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            // Add the corresponding targets.
            int index = e.NewStartingIndex;
            foreach (TSource item in e.NewItems)
            {
                _targetCollection.Insert(index, _map(item));
                ++index;
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            // Delete the corresponding targets.
            for (int i = 0; i < e.OldItems.Count; i++)
            {
                _targetCollection.RemoveAt(e.OldStartingIndex);
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Replace)
        {
            // Replace the corresponding targets.
            for (int i = 0; i < e.OldItems.Count; i++)
            {
                _targetCollection[i + e.OldStartingIndex] = _map((TSource)e.NewItems[i + e.NewStartingIndex]);
            }
        }
        else
        {
            // Just give up and start over.
            _targetCollection.Clear();
            PopulateTargetCollection();
        }
    }

    private void PopulateTargetCollection()
    {
        foreach (TSource item in _sourceCollection)
        {
            _targetCollection.Add(_map(item));
        }
    }
}

These mapped collections are useful when using a framework like MVVM Light. But if you are using Update Controls, you’ll just want to use linq. It keeps your view model code cleaner, and also updates your collection when filter or sort properties change.

Reply

By submitting this form, you accept the Mollom privacy policy.