N-Level Undo & State Mgmt for Collections

N-Level Undo & State Mgmt for Collections

Old forum URL: forums.lhotka.net/forums/t/195.aspx


SonOfPirate posted on Wednesday, May 24, 2006

An issue that came up while reading CSLA.NET 1.x that has resurfaced reading 2.0 has to do with the way the n-level undo features are implemented for collections.  In particular, the fact that the current process does not accurately return the collection to its previous state when CancelEdit/UndoChanges is called.  Perhaps an example will illustrate...

Assume there is a collection that is pre-populated (possibly from a database) with 3 objects, say an invoice object's LineItems collection.  During editing, the second of the items is deleted (removed) then the change is cancelled as follows:

LineItems.BeginEdit();
LineItems.RemoveAt(1);
LineItems.CancelEdit();

It is my belief that the collection should be returned to the exact same state as before this code executed; however, this is not the case. If we execute the same code with two additional statements as shown here:

myItem = LineItems[2];
LineItems.BeginEdit();
LineItems.RemoveAt(1);
LineItems.CancelEdit();
myItem = LineItems[2];

you will find that myItem is set to different objects because we do not restore the collection completely - the relative indexing of the items in the collection has changed. What you will find is that the first time we set myItem the collection returns the third item (as expected). But, the second time, we find that myItem is the second item (the restored item)!

After reviewing the code, it is obvious that this is happening because the UndeleteChild method simply re-Adds the item to the collection - to the END of the collection.

This violates the tenet that the collection's state would be restored as it was before the edit.  If this were the case, then myItem would reference the same object both times it is set in the above code.

I think I see the head-aches involved, but why not serialize the collection and have the collection maintain its own snapshots just like business objects? We would have to eliminate the cascading calls, obviously, and may result in much 'heavier' applications, but at least this would ensure that the indexing/positioning of items within the collection was restored. I'm throwing this out for what I hope to be a productive discussion.

BTW - this is most noticable when you bind the collection to a grid-type control, remove an item from anywhere in the middle of the list then undo the change. You'll see the removed item reappear at the bottom. Not exactly what was in mind or expected.

Thanks in advance to all who contribute on this.

hurcane replied on Wednesday, May 24, 2006

From a business data perspective, I'm not sure the order of objects in a collection is considered critical to the functionality of an object. I worked with somebody who had fits when SQL returned data out of order. Every one of his fetch queries had an ORDER BY clause on it, including summation queries. We tried to stress that the business logic worked the same regardless of the order of the list. Order is something that human beings desire, so we shouldn't sort the data until the user is going to see it.

Are you using the sorted binding list that wraps the business list? I haven't used this, but I would think the sorted binding list should refresh itself when the list changes, and the refresh should sort the data. Or else, the UI control (grid, listbox, etc.) should handle the sorting and should be refreshed (and resorted). Even though the order inside the business object has changed, the sorted view of the data should revert to the look and feel that you desire.

Igor replied on Thursday, May 25, 2006

The fact that you can use collection items indexes in your code does not mean that you should. Very often you cannot or do not want to control the indexes (because any code that uses your collection can add/remove items at will).

------

BTW, in pre-.Net times many VB developers (myself included) learned not to trust collection items indexes; for example, this is what VB6 Component Tools Guide says in Object Model Creation Guidelines\The Visual Basic Collection Object :

 

Important:   Collection objects maintain their numeric index numbers automatically as you add and delete elements. The numeric index of a given element will thus change over time. Do not save a numeric index value and expect it to retrieve the same element later in your program. Use keys for this purpose.

------

Writing code without using indexes may not be convenient, but it is definitely possible.

 

pfeds replied on Thursday, May 25, 2006

I'm currently suffering a problem where I have a detail view with a child collection displayed in a grid view.  It's similar to the ProjectTracker example.

The problem I seem to have at the moment is such:

 

I start with two items in the grid.

I unassign both.

Then I press Cancel which will call CancelEdit(); This works and both items are replaced.

I then unassign one item and press Cancel.

This item does not get replaced.

 

Any ideas?

 

ajj3085 replied on Thursday, May 25, 2006

You've already canceled the edit, so you no longer have anything to undo.  The second call does nothing because you've already called CancelEdit once.

Cancel should probably close the window; if not, after CancelEdit, you'll have to call BeginEdit immediately after so that your next call to CancelEdit will work.

HTH
Andy

RockfordLhotka replied on Thursday, May 25, 2006

This initial post got me thinking. It probably would be possible to maintain the original order on an undo - but there'd be some serious overhead to make it happen. On the surface it seems like you could just keep the original position of an item when it was "removed" so you could put it back in that spot - but that ignores the fact that adds could happen in there too. So then you'd have to track both adds and removes and the order in which they occur so you could undo them in order. Obviously that's not realistic.

So instead, you could have BeginEdit() create an array/list that mirrors the actual list and deletedList contents - referencing the same objects. These lists (main and deleted) would be the "state" of the collection. On an ApplyEdit() you'd discard these lists, but on a CancelEdit() you'd use these lists to restore the actual list and deletedList to match. The deletedList is easy - you'd simply replace the current one with the previous one. But you can't do that for the actual list itself, so this means you have to walk through the list item-by-item to make sure it matches.

Of course you could Clear() the current list and just reload it - but for a lot of items that could be quite slow, so I think you'd have to go item-by-item and resolve whether they match, and if not how to make them match in a relatively efficient manner.

In the end, this adds up to a lot of overhead - but it seems (conceptually) like it would be possible. It isn't something I anticipate looking at in the near future, because I think a simpler answer is to use SortedBindingList to provide a view over the real collection instead.

Copyright (c) Marimer LLC