I’m currently implementing an undo/redo mechanism using the Command Pattern. Everything works so far. My problem now is to implement the undo/redo functionality in a way that it is bound to a given context.
Assume the following situation:
You have win1 and win2. In win1 you execute action1; then you switch to win2 and you execute action2 and action3. You “undo-stack” would look as follows
- action3
- action2
- action1
Now what happens if win1 gets closed/removed and you start performing undos. At action1 the program would probably crash as the context is has been executed against previously (i.e. win1) doesn’t exist any more. As such, when win1 gets closed, all the corresponding actions should be removed from the “undo-stack”.
My question is on whether there exist already implementations/best practices for implementing such scenario properly? Or do you have any ideas?
What happens if win1 gets closed/removed? To answer this question, ask you another one: is the action reversible?
1. Case where the closing/removal action is reversible:
The closing/removal event is recorded in the undo stack, and when the user is performing undos, it un-does the closing/removal.
For example, when a user removes, through your application, a file win1, the app should not remove the file, but only mark it as a candidate for removal; then, a potential undo will un-mark the file.
2. Case of irreversible actions:
If you’re talking about irreversible actions, given that the reversibility is not depending of your application, then deal with it, for example by disabling the corresponding undo elements.
For example, you’re writing a tool which manages server farms, and at a given moment, the server win1 is materially unplugged from the network. In this case, your application can’t replug the server, since it requires human intervention.
The application should then adapt its behavior in a more intuitive way: cancel or undos anterior to the irreversible one, skip the irreversible action, or something radically different and innovative. It’s up to your interaction designer to come with the most intuitive way.
This is not much different from a case such as when your application tries to access a database, but the database is offline: you can’t do anything about it, and the way you behave depends on the precise context.
Example:
Let’s take an example from your comment: the removal of an Excel sheet. The successive actions are:
- Change the cell A1 in sheet 1,
- Change the cell A1 in sheet 2,
- Remove the sheet 1.
Two possible behaviors would be:
-
The reversibility of sheet removal. Strangely, Excel team have chosen to not make it reversible. While this is probably motivated by some technical aspects I ignore, this behavior is totally wrong UX-wise. As a user, when by mistake I destroy a sheet, I expect to be able to get it back by pressing Ctrl+Z.
Not being able to do that makes it risky to use Excel, since I can never know what could be reverted, and what could result in a loss of work. That’s not nice at all.
-
The impossibility to remove the sheet with the loss of “Change the cell A1 in sheet 1” step in the undo stack after the removal. Technically, it’s not so difficult. Imagine the following structure:
class undoEntry { string text; // Text which is displayed to the user in the undo history. sheet? correspondingSheet; // The sheet where the action happened, or null. undoAction action; // The action to undo. }
With
correspondingSheet
property, you can then walk though the undo stack when a sheet is removed, and delete the entries you shouldn’t display any longer.
3
Apologies for necromancy but I’ve actually encountered teams scratching their heads over this a lot. Usually I find the solution very simple which is just use more than one stack: done, ship it, collect money.
That was a tad crude but that’s usually sufficient and it makes sense from a UX perspective. Take this example of me using StackExchange right now. Say I was simultaneously answering a separate SE question and multi-tasking in a separate tab in my browser.
I don’t want to ctrl-Z undo and start invisibly undoing changes I made in my other tabs (I’d hate to switch back to the other tab and realize I accidentally reverted all my work while working in this one), or have ctrl-Z make my browser switch tabs to on me without me explicitly saying so to make those changes visible. My current tab is my “current universe”, and it has its own local data which is invalidated when I close the tab. So when I undo, I only want to undo changes in my current tab and not the other tabs. And the simple answer to allowing that is to have a separate undo stack for each tab (or even more granular context; in this case I suspect the text control itself has its own context and local undo stack) in the browser, not one undo stack to rule them all with entries that get invalidated. That also keeps the memory management tight and clean and so on without these invalidation concerns.
You’ll have to forgive me if I’m a little bit passionate about this one since the team I worked with before started getting tempted to do all these fancy things to detect invalidation of undo entries and when I pointed out the UX concerns above, they were contemplating having multiple “stack pointers” into the stack which can be moved separately based on which tab/window we were in. I was like, “Nooooo! Just use more than one undo stack! Geeeeeeez!!!” 😀 I think I got some gray hairs just specifically from that one.
Having a separate undo stack for each window should be enough in your case given what you said here:
At action1 the program would probably crash as the context is has been executed against previously (i.e. win1) doesn’t exist any more. As such, when win1 gets closed, all the corresponding actions should be removed from the “undo-stack”.
If that data is invalidated when win1
is closed, that implies the data belongs to the window and doesn’t make sense outside of it, just as with my browser tab analogy. So that should go into a different undo/command stack as I see it — don’t jumble them all up in one stack.
If these windows share state and there are changes to the underlying shared business logic side of the data along with data local and specific only to a particular window being modified simultaneously with one user input action, then that gets a lot more tricky and we have to rethink a lot more. But given what I usually encounter and the specific problem you are facing now, I suspect just using multiple undo stacks should be enough, and the undo stack local to win1
would be tossed away when you close that window.
Context is important from a programming/technical standpoint, as well as, usability. Your Excel comment is a good example. Several documents and then worksheets could be in the same instance of Excel and your undo would go across them (assuming you made recent actions on different ones), but this wouldn’t happen in another instance of Excel or from Excel to Word.
Users may be able to provide the context where they see the line drawn with this functionality. Microsoft could have built this functionality in the context of all open Office applications. I wouldn’t like it as a user eventhough it is technically possible.