Blogroll

Undo support in Cocoa is fantastic but for those who have tried to mix it with Core Data know that it can be a bit frustrating. Generally, undo support can be ignored in most applications and it will “just work”. But when Core Data is added to the recipe then things get a bit confusing and more complicated.

The Conflict With NSUndoManager

For those familiar with the inner workings of Cocoa’s undo support (and if you are not familiar with it, check out the April edition of MacTech ;-) , all of the undo events are registered with an NSUndoManager. In a normal document model application, the window controller (or the window’s delegate if set to something other than the controller) will supply an NSUndoManager for all of the text fields and other editable bits in the window.

This works fine until Core Data comes along. The reason for this is that the NSManagedObjectContext has its own NSUndoManager that is registering those same events and can easily get out of sync.

What I prefer to do is avoid the double NSUndoManager and just use the one included with the NSManagedObjectContext. This can be done by implementing one of the window delegate methods:

Objective-C

1

2

3

4

5

6

7

8

9

-(NSUndoManager*)windowWillReturnUndoManager:(NSWindow*)window

{

return[selfundoManager];

}

-(NSUndoManager*)undoManager

{

return[[selfmanagedObjectContext] undoManager];

}

By utilizing the NSUndoManager that is included in the context, some very powerful features are enabled.

Grouping sheet modifications

A common UI design is to allow the editing of an object in a sheet. The user double clicks on the item or otherwise indicates that they want to edit it and a sheet drops down allowing them to change the values. Naturally, that sheet has an NSUndoManager attached to it (can even use the one from Core Data) and each individual field can be undone. However, how to do we handle the cancel button or the ability to undo the entire sheet?

Both of the above can be handled with groupings inside of the NSManagedObjectContext object’s NSUndoManager. The methods would work as follows:

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

-(void)presentEditSheet:(id)sender

{

[[selfundoManager] beginUndoGrouping];

[NSApp beginSheet:[selfeditSheet]

modalForWindow:[selfwindow]

modalDelegate:nil

didEndSelector:NULL

contextInfo:nil];

}

-(void)acceptChanges:(id)sender

{

[[selfeditSheet] orderOut:sender];

[NSApp endSheet:[selfeditSheet]];

[[selfundoManager] endUndoGrouping];

[[selfundoManager] setActionName:@"Object edit"];

}

-(void)cancelChanges:(id)sender

{

[[selfeditSheet] orderOut:sender];

[NSApp endSheet:[selfeditSheet]];

[[selfundoManager] endUndoGrouping];

[[selfundoManager] undo];

}

In this example I have started an undo group before presenting the sheet. After the sheet is closed I then end the grouping and if the sheet was not cancelled I give the group a name. This name will display in the menu and allow all of the changes inside of the sheet to be undone in one stroke. If the sheet is cancelled then the group is still closed but I immediately call undo instead and everything that occurred within the sheet is wiped clean.

Conclusion

By only using one NSUndoManager it makes life a lot simplier and gives consistent results to the user. This technique can also be used in a document model application but each document will have to reference the appropriate NSUndoManager that is associated with its own NSManagedObjectContext.

Feedback

If you have some other interesting ways to use the NSUndoManager I would love to hear about them either here in the comments or you can email me directly at marcus at cimgf.com.

Comments

When you cancel the sheet using the sample code you provided, that last undo will push the undone changes to the undo manager’s redo stack and thus enable the Redo menu item in the application.

This would allow the user to redo the changes in the sheet without bringing it back up if you are using the same NSManagedObjectContext for the sheet and the document window. Unfortunately, NSUndoManager doesn’t have a method for removing only the top item from either stack.

What I usually end up doing is to create a new NSManagedObjectContext for the sheet (which involves non-trivial copying of the edited managed object to and from the 2 contexts). It’s non-trivial because you have to think about what copying means to relationships in your object model and there’s also the whole thing about NSManagedObjectIDs not being fetchable from another context if they are temporary in the first context.

As a general rule I would not recommend your solution. Also, disabiling undo management for editable sheets kind of defeats the purpose of having an undo manager in the first place. Without an undo manager the developer would need to track all of the changes in the sheet manually so that if the user hits cancel the developer can back everything out.

If the possibility of a redo is that much of an issue I would recommend creating an independent undo manager for the sheet that tracks all of the changes in that sheet. Much cheaper than copying all of the managed objects and since the sheet is destroyed upon completion there is no chance of the redo issue.

As I have said before, just because Apple does it, does not mean it is the best solution or even a good solution. This is especially true of their tutorials as opposed to running code.

There is always more than one way to solve an issue. As I said before I would not recommend yours as an optimal solution. Perhaps if your context is small it would be light enough to use but I would certainly not do it that way.

I did the following: Created a NSWindow subclass that handles all that for me. All I have to do is to call it with a NSWindow*, it will open itself attached to that window, start the undo group and react on its own buttons, which are OK and Cancel in the way mentioned above. This way in the controller I can start editing with just one line: