Excel style undo/redo

Excel style undo/redo

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


oshokodk posted on Wednesday, August 09, 2006

Our app has requirement to support undo/redo functionality the way Word and Excel do it. As I understand, with CSLA is not available out of box. Older version of our app implements undo/redo using stack of commands against Model.

Since CSLA supports undo I am considering building on top of CSLA functionality. Have anybody tried to implement Excel style undo/redo functionality by modifying behavior of BeginEdit (), CancelEdit (), AcceptChanges ()?

Thanks,

Oleg

 

ajj3085 replied on Wednesday, August 09, 2006

I've used excel, but what exactly is 'excel style undo'?

RockfordLhotka replied on Wednesday, August 09, 2006

The n-level undo functionality is not the right match for a command-replay undo model. I wouldn't try to use BeginEdit(), etc. for that purpose - the mismatch is too great I think.

You are probably better off making sure that every property/method on every object has a counter-property and counter-method, and then building a seperate object that maintains a stack of the commands (property sets, method calls) on your object so they can be replayed in reverse for an undo.

Dirk.Rombauts replied on Saturday, August 12, 2006

I'm currently reading the C# 2005 version of the CSLA.NET book, and I just started Chapter 4 about the Data Portal.  So I am quite new to CSLA.NET.

The question of n-level Redo did pop up for me too, though.  My (perhaps naive) assumption is that is should be enough to do this: Whenever an operation is undone, do not discard the undone state but store it in a Queue.  When actions need to be redone, you can load the state that is at the front of the queue.

Is there anything blatantly wrong with this approach?

Dirk

SonOfPirate replied on Wednesday, August 16, 2006

We've implemented this by maintaining dual Stacks.  One for undo and the other for redo.  The key here is to distinguish between the two snapshots:

When BeginEdit is called, you want to take a snapshot of the object before any changes are made so that it can be restored to that point.  Likewise with UndoEdit except that the state you will want to restore when Redo is clicked is the state the object is in at the time Undo is clicked - with all of the changes made.

The process works as follows:

  1. BeginEdit is called to start editing.  A snapshot of the object is stored on the UndoStack.
  2. When CancelEdit is called, a current snapshot is taken of the object and stored on the RedoStack then the most recent snapshot is taken from the UndoStack and the object restored.
  3. Subsequent calls to CancelEdit follow the same behavior until there are no more snapshots on the UndoStack. (CanUndo returns false).
  4. At this point, there will be a number of snapshots on the RedoStack and the changes can all be re-applied by calling the RedoEdit method which does the reverse of CancelEdit by taking a new snapshot of the object without edits and storing it on the UndoStack and restoring the top item from the RedoStack.
  5. Once the RedoStack is emptied again, RedoEdit is disabled. (CanRedo returns false).
  6. At any point during this process if BeginEdit is called, the RedoStack is cleared and the new snapshot is added to the UndoStack on top of whatever items are already there.
  7. When ApplyEdit is called, as mentioned, both stacks are cleared.

Not sure if this is the best way to do this from a memory consumption point-of-view, but it works and behaves just like Excel.  Trick is when you call BeginEdit to take snapshots that can be undone/redone (character-by-character like in Word, for instance). The rest is...automatic.

What we haven't been able to implement yet is a way to limit the size of the stacks from within the objects themselves so only 'x' number of changes can be undone.  It would be nice to be able to remove the "oldest" item from the stack when attempting to add a new one if the size reached a certain limit.  If anyone knows a structure that supports this or a way to do so, I'd appreciate the heads up.

Hope that helps.

 

JHurrell replied on Thursday, August 17, 2006

SonOfPirate:
What we haven't been able to implement yet is a way to limit the size of the stacks from within the objects themselves so only 'x' number of changes can be undone.  It would be nice to be able to remove the "oldest" item from the stack when attempting to add a new one if the size reached a certain limit.  If anyone knows a structure that supports this or a way to do so, I'd appreciate the heads up.


How about rolling your own. Use an ArrayList internally and create your own Pop and Push methods. When you Push, you could do a RemoveAt(0) to get rid of the oldest item if the number of items meets your 'x' number.

- John

SonOfPirate replied on Thursday, August 17, 2006

That's not a bad idea.  I'd actually use the System.Collections.ObjectModel.Collection<T> for the inner list, just cuz I think it's more lightweight than List<T> and we would want to implement using generics.  And, I think implementing an IsFixedSize property (along with the initialCapacity parameter to the constructor), like IList, would provide a consistent way to set this up.  Then, if IsFixedSize = false, it would behave like a normal Stack<T>; if true, we could implement our "drop off" at the end once capacity is reached.

Great idea!  Thanks!

RockfordLhotka replied on Wednesday, August 23, 2006

You are correct, except that instead of a Queue you need to use a Stack. If you look at Chapter 3, you'll see that this is exactly how n-level undo is implemented.
 
The thing is, n-level undo is a relatively heavy-weight approach because it captures the state of the entire object graph. The reason is does this is to ensure complete consistency across the graph in the case of an undo operation (CancelEdit).
 
Replaying commands is an alternative that is potentially lighter weight (in terms of memory consumption). But that approach can't be done from _inside_ the object graph, because all commands against any object in the entire graph, and against any objects outside the graph that the objects rely on (such as other root objects related through a 'using' relationship) need to be captured such that they can be played back (undone) in reverse order.
 
That replay (undo) process requires that every property or method have a "reverse" counterpart. That can be very challenging, because it could mean destroying old objects, recreating destroyed objects, resetting property values, reversing calculations, etc. It is _far_ more difficult than simply resetting the state of the object graph, and requires that your object model be designed to support the concept from day one.
 
Even so, some things probably can't be undone. It is not always possible to create a reverse of every method. This happens in applications from time to time, and the undo mechanism needs to be aware of those operations. The UI often warns the user that "blah can't be undone", and of course Edit|Undo grays out, or simply ignores that particular operation.
 
Rocky


From: Dirk.Rombauts [mailto:cslanet@lhotka.net]
Sent: Saturday, August 12, 2006 4:32 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] Excel style undo/redo

I'm currently reading the C# 2005 version of the CSLA.NET book, and I just started Chapter 4 about the Data Portal.  So I am quite new to CSLA.NET.

The question of n-level Redo did pop up for me too, though.  My (perhaps naive) assumption is that is should be enough to do this: Whenever an operation is undone, do not discard the undone state but store it in a Queue.  When actions need to be redone, you can load the state that is at the front of the queue.

Is there anything blatantly wrong with this approach?

Dirk




Copyright (c) Marimer LLC