|
The other night I was again sifting through Hillegass' book, entering the code in such a way that it worked on Leopard and potentially took advantage of some of the new features of Objective-C 2.0 such as garbage collection, fast enumerators, property syntax, and much more.
This is where I found the NSUndoManager class. Basically the way this works is, just before you do something that you want to be able to undo, you pass the undo manager a selector containing values that will reverse the action about to be taken. Then, the undo manager places that selector on a stack. When someone selects "undo" from the GUI, the undo manager just fires the selector and performs the action that will reverse the most recently taken action. Actions that are undone are then pushed onto the top of the redo stack. This way, when someone selects "Redo" from the GUI (either through menu or through shift-apple-Z), the action on the redo stack is popped off and performed.
How is this all made possible? It's possible through the beauty of Objective-C and selectors. Selectors are one of the areas in which Objective-C differs vastly from counterparts like C#. In Objective-C, I can send a selector as a parameter to a message. This allows me to essentially take a canned invocation (represented by the NSInvocation class) and send it to a method. Canned invocations can then be invoked later by whatever infrastructure requires it. Take a look at this code (from Aaron's book) that tells the undo manager how to undo the action about to take place:
[[undo prepareWithInvocationTarget:self]
removeObjectFromEmployeesAtIndex:index];
[employees insertObject:p atIndex:index];
Basically what's going on here is the undo manager (the undo object) has a method called prepareWithInvocationTarget: that takes a target for the invocation (in this case self). The return value of that method can take, as a message, the selector that follows (in this case removeObjectFromEmployeesAtIndex:). What this is doing is saying, "When you need to undo what I'm about to do, here's the method you call, and the arguments you supply."
As a result of this pattern, you end up with pairs of methods that undo each other (providing the undo/redo pairs). I looked through some of my old WPF samples, code I'd written, and some books that I've got lying around, and I couldn't find any really good implementations of undo/redo. While it is possible, through the use of Commands, to get undo/redo functionality within a single control (e.g. you can undo/redo text that is modified within a single text box), but using the Cocoa NSUndoManager, I can actually do things like undo inserting a person, or undo/redo the change of a person's name from Bob to Steve, etc. Not only that, but you can change the appearance of the Undo menu item relative to whatever item is on the stack. For example, if you use the following lines of code:
if (![undo isUndoing])
[undo setActionName:@"Insert Person"];
This changes the text of the Undo menu item from "Undo" to "Undo Insert Person". This is ridiculously powerful and unbelievably useful. Granted, I am a Cocoa newbie, and this might be old news to everyone else, but this is awesome for me, especially given the fact that there is nothing equivalent to this within WPF/.NET Framework 3.0/Vista.
And the icing on the cake - I was able to upgrade the slow enumerators used in Aaron's code to the Objective-C 2.0 fast-enumeration syntax so that I could call the method for attaching a Key-Value-Observer:
for ( Person *p in employees)
{
[self startObservingPerson:p];
}
In the words of Lord Vader himself: Impressive. Most impressive.
p.s. For those of you aware of C# 3.5 and LINQ, you might find some comparisons possible between Objective-C selectors and the use of lambda functions, which turn method calls into serializable expression trees that can be evaluated and parsed for later use, or even stored. These are used extensively for the ADO.NET Entity Framework. The reason I didn't bring them up is because WPF doesn't natively have C# 3.5 support (and won't until Orcas), and nothing within Orcas suggests that Microsoft will be utilizing these to create a native, built-in undo/redo framework.
I notice you're iterating though objects with that new ObjC 2.0 code. Not
sure if you're aware of it, but NSArray has a convenience method that is
faster than enumeration for observing lots of objects:
Yeah, but that's if you're going to directly observe the elements within
the array. The code inside 'startObservingPerson' adds an observer to the
'personName' ivar and to the 'estimatedRaise' ivar. I'm not sure if your
NSArray shortcut will do that, I'm thinking that would be OK if I just
wanted to observe the person object itself and not individual fields within
it.. or am I not understanding fully what's going on here?
Kevin, that's exactly what this method is for :) It lets you observe
properties of objects within the array, so you'd do something like this:
toObjectsAtIndexes:allIndexes
forKeyPath:@"personName"
options:options
context:context];
So, how do people implement undo in .NET apps today? I must say I'm a bit
surprised, I would have expected MSFT to have provided undo in their
framework years ago.
If you do need everything in the array to perform that method, there's
NSArray's
- (void)makeObjectsPerformSelector:(SEL)aSelector
withObject:(id)anObject
Which should accomplish more or less the same
thing.
Mike, after I went home last night I poked around with that method and yes,
it does do exactly what I want - rig an entire array to be observed by a
single object for a single key path. What I couldn't figure out was how to
build the parameters for the NSIndexSet and the NSRange. The documentation
I found seemed a good enough reference, but perhaps my Cocoa newbieness is
preventing me from figuring it out. Think you could post the syntax for
how you instantiated and prepared the "allIndexes" and "options" objects?
Thanks!!
John - at the moment there's nothing built into either Windows Forms or WPF
to facilitate undo/redo functionality. The best I've been able to manage in
WPF is hacking around with the page navigation history and storing
arbitrary data in the page navigations so that when you hit "back" you
essentially undo the last action.. but that's still a hack and nothing
close to the elegance of NSUndoManager.
I admit that bit is a bit convoluted, try something like this:
toAllObjectsForKeyPaths:(NSArray *)keyPaths
options:(NSKeyValueObservingOptions)options
context:(void *)context;
Using a category to extend the NSMutableArray sounds like an awesome idea,
to simplify the syntax some. However, can you explain the syntax behind:
Um, somehow your comments system has eaten my NSIndexSet stuff. Can you
rescue it somehow, or is there something special I need to put when writing
code on this blog?
The blog comments use square-bracket wiki syntax, so Cocoa is going to hose
it. Try using two square open brackets and two square close brackets when
entering Objective-C in a comment. Testing =
Well, I know that it uses wiki syntax but can't get it to escape the square
brackets.
<pre>NSIndexSet *indexes = )];</pre>
bah. Piece of junk. It uses Wiki syntax but doesn't allow Raw HTML, so the
<pre>won't work.
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(0, employees.count)];
Mike - thanks for the syntax! I apologize for the unfriendliness of the
comment system. I have no control over the comment system or the software
used for the blog.
Kevin,
I posted a follow-up blog post showing how I implemented the category and
the batch-observe stuff that Mike showed me:
Here's a Cocoa example how to use HOM in: Making a "Do not show this
warning again" alert
Just discovered this blog and am reading through old posts. This one on
NSUndoManager, I'm wondering if you understood all the magic behind it? You
have not completely explained it. You mention selectors but you have not
mentioned forwardInvocation which the undo manager implementation relies
upon.
Obviously you know a truckload more about Cocoa than I do. I know that my
sample worked, and I've spent a little bit of time realizing that there's a
lot of magic involved with selectors and that the whole NSUndoManager thing
is made possible through the dynamic dispatch nature of Objective-C, but
beyond that, it still has the label of "magic" because I don't yet
understand the internals.