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 (Cocoa-dev@lists.apple.com) 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: http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com This email sent to arch...@mail-archive.com