views:

179

answers:

2

In the model, I have:

public ObservableCollection<Item> Items { get; private set; }

In the ViewModel, I have a corresponding list of ItemViewModels. I would like this list to be two-way bound to the model's list:

public ObservableCollection<ItemViewModel> ItemViewModels ...

In the XAML, I will bind (in this case a TreeView) to the ItemViewModels property.

My question is, what goes in the "..." in the ViewModel shown above? I am hoping for a line or two of code to binds these two ObservableCollections (providing the type of the ViewModel to construct for each model object). However, what I'm fearing is necessary is a bunch of code to handle the Items.CollectionChanged event and manually updates the ItemViewModels list by constructing ViewModels as necessary, and the corresponding opposite that will update the Items collection based on changes to ItemViewModels.

Thanks!

Eric

+1  A: 

Yes, your fears are true, you'd have to wrap all ObservableCollection functionality.

My return question is though, why would you want to have view-model wrapper around already what seems to be nice model? View model is useful if your data model is based on some unbindable business logic. Normally this business/data layer has one or two ways of retrieving data and notifying external observers of its changes which are easily handled by view model and converted into changes to ObservableCollection. In fact in .NET 3.5 ObservableCollection was part of WindowsBase.dll, so normally it wouldn't be used in data models in the first place.

My suggestion is either the logic which populates/modifies ObservableCollection should be moved from your data model into view model, or you should just bind directly to the layer you currently call data model and just call it what it is. A view model.

You can obviously write a helper class which will be syncing two collections using some converter lambdas (from Item to ItemViewModel and backward) and use it all over places like this (make sure you handle item uniqueness properly though), however IMHO this approach spawns redundant amount of wrapper classes, and each layer reduces functionality and adds complexity. Which is exactly the opposite of MVVM goals.

repka
A: 

You can use the following class :

public class BoundObservableCollection<T, TSource> : ObservableCollection<T>
{
    private ObservableCollection<TSource> _source;
    private Func<TSource, T> _converter;
    private Func<T, TSource, bool> _isSameSource;

    public BoundObservableCollection(
        ObservableCollection<TSource> source,
        Func<TSource, T> converter,
        Func<T, TSource, bool> isSameSource)
        : base()
    {
        _source = source;
        _converter = converter;
        _isSameSource = isSameSource;

        // Copy items
        AddItems(_source);

        // Subscribe to the source's CollectionChanged event
        _source.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_source_CollectionChanged);
    }

    private void AddItems(IEnumerable<TSource> items)
    {
        foreach (var sourceItem in items)
        {
            Add(_converter(sourceItem));
        }
    }

    void _source_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                AddItems(e.NewItems.Cast<TSource>());
                break;
            case NotifyCollectionChangedAction.Move:
                // Not sure what to do here...
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (var sourceItem in e.OldItems.Cast<TSource>())
                {
                    var toRemove = this.First(item => _isSameSource(item, sourceItem));
                    this.Remove(toRemove);
                }
                break;
            case NotifyCollectionChangedAction.Replace:
                for (int i = e.NewStartingIndex; i < e.NewItems.Count; i++)
                {
                    this[i] = _converter((TSource)e.NewItems[i]);
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                this.Clear();
                this.AddItems(_source);
                break;
            default:
                break;
        }
    }
}

Use it as follows :

var models = new ObservableCollection<Model>();
var viewModels =
    new BoundObservableCollection<ViewModel, Model>(
        models,
        m => new ViewModel(m), // creates a ViewModel from a Model
        (vm, m) => vm.Model.Equals(m)); // checks if the ViewModel corresponds to the specified model

The BoundObservableCollection will be updated when the ObservableCollection will change, but not the other way around (you would have to override a few methods to do that)

Thomas Levesque
Thanks! I knew someone out there had already done the work to encapsulate this functionality. A question: I see you use the isSameSource when the change action is Remove. Why not just use the e.OldStartingIndex and e.OldItems.Count: for (int i = 0; i < e.OldItems.Count; i++) this.RemoveAt(e.OldStartingIndex);
Eric
@Eric, yes, it could work, and it would actually be better... if you're certain that both collections are in the same order. Since I'm not handling the `NotifyCollectionChangedAction.Move` case, that's not guaranteed...
Thomas Levesque