views:

609

answers:

5

I am getting my knickers in a twist recently about View Models (VM).

Just like this guy I have come to the conclusion that the collections I need to expose on my VM typically contain a different type to the collections exposed on my business objects.

Hence there must be a bi-directional mapping or transformation between these two types. (Just to complicate things, on my project this data is "Live" such that as soon as you change a property it gets transmitted to other computers)

I can just about cope with that concept, using a framework like Truss, although I suspect there will be a nasty surprise somewhere within.

Not only must objects be transformed but a synchronization between these two collections is required. (Just to complicate things I can think of cases where the VM collection might be a subset or union of business object collections, not simply a 1:1 synchronization).

I can see how to do a one-way "live" sync, using a replicating ObservableCollection or something like CLINQ.

The problem then becomes: What is the best way to create/delete items?

Bi-directinal sync does not seem to be on the cards - I have found no such examples, and the only class that supports anything remotely like that is the ListCollectionView. Would bi-directional sync even be a sensible way to add back into the business object collection?

All the samples I have seen never seem to tackle anything this "complex".

So my question is: How do you solve this? Is there some technique to update the model collections from the VM? What is the best general approach to this?

+1  A: 

Personally, I use an ObservableCollection in my model and my viewmodel.

class Model
{
    public ObservableCollection<Foo> Foos;
}

class ViewModel
{
    public Model Model;
    public ObservableCollection<FooView> Foos;

    public ViewModel()
    {
        Model.Foos.CollectionChanged += OnModelFoosCollection_CollectionChanged;
    }

    void OnModelFoosCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
       Foo re;

       switch (e.Action)
       {
          case NotifyCollectionChangedAction.Add:
             re = e.NewItems[0] as Foo;
             if (re != null)
                AddFoo(re);  //For other logic that may need to be applied
             break;
          case NotifyCollectionChangedAction.Remove:
             re = e.OldItems[0] as Foo;
             if (re != null)
                RemoveFoo(re); 
             break;
          case NotifyCollectionChangedAction.Reset:
             Foos.Clear();
             /* I have an AddRange in an ObservableCollection-derived class
                You could do Model.Foo.ForEach(ree => AddFoo(ree));
             */
             var converter = 
                from ree in Model.Foo
                select new FooView(ree);
             Reports.AddRange(converter); 
             break;
          default:
             //exercise for the reader :)
             s_ILog.Error("OnModelFoosCollection_CollectionChangedDid not deal with " + e.Action.ToString()); 
             break;
       }
    }   

    void AddFoo(Foo f)
    {
        Foos.Add(new FooView(f));
    }

    void RemoveFoo(Foo f)
    {
       var match = from f in Foos
          where f.Model == f  //if you have a unique id, that might be a faster comparison
          select f;
       if(match.Any())
          Foos.Remove(match.First());
    }
}

Now, when you remove something from the Model's Foo collection, it will automatically remove the corresponding FooView. This corresponds with how I think about this sort of thing. If I want to delete something, the Model is where is really needs to be deleted.

It feels like a lot of code, but it isn't that much, really. I'm sure one could build a generic version of this, but IMO you always end up wanting custom logic dealing with addition/removal of elements.

Tom
OK this makes sense and was the conclusion that I came to. I still want to see more evidence of other peoples approaches to give me confidence this is "the best way". For example I have never seen this approach in a sample or blog post :-/
Schneider
What if the model only exposes an IList or something? Turning every list in your program into an ObservableCollection is not a solution. What if your model doesn't implement INotifyPropertyChanged? Listening to an ObservableCollection in the ViewModel and then pushing the changes back to the model works fine, but you can't expect the model to offer you such amenities, and in many cases ObservableCollection might not provide the functionality you need in your model.
Benny Jobigan
I tend to do more of the Sacha Barber style MVVM. I'm writing new code, I don't have legacy Models, so I rarely even have a model at all. In general, if I want INPC, I write it. If I need ObservableCollection, I use it.
Tom
+1  A: 
Alex_P
can you give examples of such controls that do/do not notify?
Schneider
See edit in the post.
Alex_P
+2  A: 

I too am struggling with the bi-directional sync of two collections for use with WPF via MVVM. I blogged MVVM: To Wrap or Not to Wrap? How much should the ViewModel wrap the Model? (Part 1) and MVVM: To Wrap or Not to Wrap? Should ViewModels wrap collections too? (Part 2) regarding the question, including some sample code that shows a two way sync. However, as noted in the posts, the implementation is not ideal. I would qualify it as a proof of concept.

I like the BLINQ, CLINQ, and Obtics frameworks that Alex_P posted about. These are a very nice way to get one side of the sync behvaior. Maybe the other side (from VM to Model) can be implemented via an alternate path? I just posted part 3 on my blog that discusses some of this.

From what I can see, bi-directional via BLINQ and CLINQ is not supported in cases where the LINQ statement projects the data to a new structure.

However, it does look like CLINQ may support Bi-Directional syncing in cases where the LINQ query returns the same datatype as the underlying collection. This is more of a filtering scenario, which doesn't match the use case of a ViewModel wrapping the data in the Model.

NathanAW
glad to see I am not the only person wrestling with this issue :) I am coming to the conclusion that VM collections should be readonly, and then use another mechanism such as command to add/remove. I have yet to POC this though
Schneider
+1  A: 

I've written some helper classes for wrapping observable collections of business objects in their View Model counterparts here, maybe it should be extended to go the other way. always looking for contributions...

Aran Mulholland
+1  A: 

I have proposed a general Undo / Redo framework based in MVVM, that uses some techniques related to the problem you describe. It uses collections implementing this interface:

public interface MirrorCollectionConversor<V, D>
{
   V GetViewItem(D modelItem, int index);
   D GetModelItem(V viewItem, int index);
}

(V is for the ViewModel items, D for the model items)

Using this interface it automatically synchronizes the viewmodel collection when the model collection changes. If you change the viewmodel the change is simply redirected to the model collection.

The GetViewItem function gives you some flexibility in how the viewmodel objects are related to his model counterparts.

You can find the details here.

(I admit that the construction is quite complex and I will be very happy to listen to suggestions).

DaniCE