Friday, January 30th, 2009...1:26 am

Using MVVM to provide undo/redo. Part 2: Viewmodelling lists

In the previous part we use the viewmodel pattern to made views where all the editions of properties of the “data” viewmodel objects could be undo/redoed.

But, what happens with lists? or, if we have that the datamodel objects are represented by (data) viewmodel objects… how we have to present a list of data objects?

In this article I will give a possible solution to this more complex situation and how it could work with undo/redo.

To answer this questions I will try to follow the preceding case. In that case we have that the viewmodel shows to the view his own version of the data objects and his properties. The view was binded to this properties, but when some “set” method was called, the viewmodel submitted an undoitem to the project that finally modify the data when executed. After that the viewmodel is notified of the change of the data object and throws an notify change to the view.

Now we need a list that shows a viewmodel version of the model data. To construct this list we will use an new class that I will call MirrorCollection.

This list gets his data from some list of model ojects and will redirect all the changes to that list, like in the previous case.

The MirrorCollection<V,D> class

These are the more important class requirements:

1) every one of his items must be the viewmodel version of  the corresponding item form the datamodel list

2) modifications in the mirror lista are redirected to modifications in the data model list using an undoitem

3) any modification in the datamodel list is notified to the mirror list with an INotifyCollectionChanged event

Ok, so lets go to implement it!

The mirror list constructor takes as parameters the datamodel list<D>, the project class to send the undoitems, and some class that implement the following interface that has to allow constructing the viewmodel V items form de dataobjects D:

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

Usually the viewmodel class will implement that.

The mirror collection class construction is as follows

public MirroredList(IList baseList,
    MirrorCollectionConversor mirrorItemConversor,
    IProject proj)
{
    if (baseList == null)
        throw new ArgumentNullException("baseList");
    this._MirrorItemConversor = mirrorItemConversor;
    this._proj = proj;
    this._BaseList = baseList;
    ICollection collection = _BaseList as ICollection;
    INotifyCollectionChanged changeable = _BaseList as INotifyCollectionChanged;

    if (changeable == null)
        throw new ArgumentException("List must support "
        + "INotifyCollectionChanged", "baseList");

    if (collection != null)
        Monitor.Enter(collection.SyncRoot);
    try
    {
        ResetList();
        changeable.CollectionChanged += new NotifyCollectionChangedEventHandler(changeable_CollectionChanged);
    }
    finally
    {
        if (collection != null)
        Monitor.Exit(collection.SyncRoot);
    }
}

private void ResetList()
{
    _MirrorList = new List();
    int count = 0;

    foreach (D res in _BaseList)
    {
        V viewItem = _MirrorItemConversor.GetViewItem(res,count);
        count++;
        _MirrorList.Add(viewItem);
    }
}

Now we can implement the Insert, Delete actions of our mirror list in the following, quite elegant way

public void IList.Insert(int index, D modelItem)
{
    if (_SubmitCollectionChangedCommand == null)
        ThrowReadOnly();
    NotifyCollectionChangedEventArgs info =
        new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, modelItem, index);
    _SubmitCollectionChangedCommand(info);
}

public void IList.RemoveAt(int index)
{
    if (_SubmitCollectionChangedCommand == null)
        ThrowReadOnly();
    D modelItem = _MirrorItemConversor.GetModelItem(this[index], index);
    NotifyCollectionChangedEventArgs info =
        new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, modelItem, index);
    _SubmitCollectionChangedCommand(info);
}

_SubmitCollecionChangedCommand is a delegate implemented by the viewmodel object who owns the mirrored list in the following way:

public void SubmitCollectionChangedCommand(
    NotifyCollectionChangedEventArgs info)
{
    UIEditList uiEditList = new UIEditList();
    uiEditList.DataObject = ModelObject;
    uiEditList.Info = info;
    uiEditList.Items = ModelObject.ModelList;
    _proj.Submit(uiEditList);
}

And here is the internal implementation of de UIListChange .DoCommand

public bool DoCommand(NotifyCollectionChangedEventArgs infoComm)
{
    switch (infoComm.Action)
    {
        case NotifyCollectionChangedAction.Add:
        if (infoComm.OldItems != null)
            throw new ArgumentException("Old items present in Add?!", "info");
        if (infoComm.NewItems == null)
            throw new ArgumentException("New items not present in Add?!", "info");
        ItemsResource.Insert(infoComm.NewStartingIndex, infoComm.NewItems[0]);
.....

The only important point that we have not covered is how our mirror list will process the NotifyCollectionChangedEvent that it receives from his model list. But the implementation is clear it has to replication all the insertions and deletes in his list.

An interesting point here is that Silverlight2 has a reduced version of  NotifyCollectionChangedEventArgs action types: it does not include moves, only Add, Remove, Replace and Reset. I think that this is the only change required between the WPF and the Silverlight implementation.

Conclusion

Well, this is a quite long post and I have not commented nothing about how you will use this, so les quickly see how a viewmodel object will construct a mirror list:

public MirrorList SomeList
{
    get  {
        if (  _someList == null)
            _someList = new MirrorList(modelObjet.list, this, _proy);
        return _someList;
    }
}

and that’s all! You don’t have to worry about undos and redos… just do your inserts and deletes in you mirror list and everything will be done. Of course, your view will be also updated if the model list is updated from any other source, for example if your server notifies you that someone else has modified your object!

Note: I will be very pleased to receive your opinions about this, and possible ways to get it better.

Note: the ideas for the mirrorcollection class are taken form here and here, althoug the problen that they focus is quite different: in short it is that WPF does not react to changes in the data binded objects if the change is made by a different thread than the UI thread. In those articles they show ways to implement lists based in other list that could be modified by others thread.

Update: you can find the source code here.

Leave a Reply