How to properly handle Undo and triggered actions
How to properly handle Undo and triggered actions
- Subject: How to properly handle Undo and triggered actions
- From: Jim Thomason <email@hidden>
- Date: Sun, 14 Mar 2010 21:08:48 -0500
I have a coredata document based application, and lately I've been
working on improving undo support. It usually works pretty well, but
there have been some isolated issues that were problematic.
After a lot of investigating over the past few days, I've finally
isolated something and I think I know the root cause - some of my
accessors are triggering KVO responses elsewhere in the app, and those
don't appear to play nicely with undo. A second case is that some
accessors actually create additional objects behind the scenes, and
that also doesn't seem to play very nice. I have solutions for both of
them, and I'm curious if I'm doing it The Right Way, or if I should do
something else instead.
The first issue I don't have a solution for yet. Say the user updates
the dollar amount of a transaction. This causes an update of the
account's balance, which in turn my controller is watching so it loops
through a bunch of details and updates some display elements in the
interface. I don't think the details are important. It's all happening
on the main thread, same context, nothing that I'd consider fancy. And
it all works fine.
The problem is if, for example, the user hits the magic button that
creates a big block of transactions. As far as the user's concerned,
it should just be a single action, so it's all wrappered with the:
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] beginUndoGrouping];
//create a whole mess of stuff
[[[self managedObjectContext] undoManager] endUndoGrouping];
And it all works fine. Click the button, make the big block of new
entries, fire off my note to the controller to update other stuff in
the interface. All is well. Plus, the user can even undo the action
(just once) and it'll make the whole thing vanish and go back to the
way it was.
The problem is with redo - if I try to redo the creation of the big
block, as best as I can tell it triggers the notification to my
controller midway through recreating all of the objects. This in turn
tries to do a calculation on an incomplete set of data, it makes the
assumption that a piece of data is there that has not come back to
life yet, and it crashes and burns. So the question is - how can I fix
this?
I dealt with it by having my controller observe a few notes from the
UndoManager:
NSUndoManagerWillUndoChangeNotification
NSUndoManagerWillRedoChangeNotification
NSUndoManagerDidUndoChangeNotification
NSUndoManagerDidRedoChangeNotification
If a Will(Un|Re)do note is posted, it internally sets a flag telling
the controller that it's during an undo/redo. Then, at the top of my
display updater, I just added the following:
if (duringUndo) {
return;
}
This seems to work fine and take care of the issue.
Incidentally, I observe both the will & did notes and set my own
variable since [undoManager isUndoing] and [undoManager isRedoing]
still return YES when the DidChange notifications fire, so I couldn't
just look at those. But maintaining my own state seems to work fine.
It is pretty slow since I started observing the willChange
notifications, but I haven't a clue why that is.
The second issue is that some objects need to be created behind the
scenes when others are made. So I created custom mutable set methods
to create my additional necessary objects.
-(void) addSubObjects:(id) subObject {
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] beginUndoGrouping];
//add to sub object set
[self createRelatedObjectsFor:subObject];
[[[self managedObjectContext] undoManager] endUndoGrouping];
[[[self managedObjectContext] undoManager]
registerUndoWithTarget:self selector: @selector(removeSubsObject:)
object:subObject];
}
The removeSubsObject looks the same in reverse. Again, this does what
I need to do, except for when I'm undoing/redoing. Since I register my
own undo/redo actions via the reciprocal methods, I end up creating a
new object + re-vivifying the previously created one. To deal with it,
I only create my relatedObjects if I'm not undoing or redoing.
-(void) addSubObjects:(id) subObject {
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] beginUndoGrouping];
//add to sub object set
if (! [[[self managedObjectContext] undoManager] beginUndoGrouping]
&& ! [[[self managedObjectContext] undoManager] beginUndoGrouping]) {
[self createRelatedObjectsFor:subObject];
}
[[[self managedObjectContext] undoManager] endUndoGrouping];
[[[self managedObjectContext] undoManager]
registerUndoWithTarget:self selector: @selector(removeSubsObject:)
object:subObject];
}
That way, if I undo/redo adding a subObject, then I won't create
additional relatedObjects. Again, seems to work fine.
Can anyone comment as to whether either of these are good techniques
or if they're something that'll bite me in the ass? The UndoManager
still seems like black magic to me, so I wasn't sure if I'm doing it
the right way or if there's some other technique I should use instead.
And, of course, if this actually is a decent, proper technique, then
hopefully sharing it will be useful to somebody else.
-Jim....
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden