The World’s Leading Microsoft .NET Magazine
   
 
The .NET Addict's Blog

My Top Tags

                                                           

My RSS Feeds








I heart FeedBurner

Latest Diggs - Programming

Computers Blogs - Blog Top Sites

Site Hits

Total: 3,824,578
since: 19 Jan 2005

How I learned to love NSUndoManager

posted Mon 19 Mar 07

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.

tags:                    

links: digg this    del.icio.us    technorati    reddit

AddThis Social Bookmark Button




1. Mike Abdullah left...
Mon 19 Mar 07 2:23 pm :: http://mikeabdullah.net

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:

-addObserver:toObjectsAtIndexes:forKeyPath:options:context:


2. Kevin Hoffman left...
Mon 19 Mar 07 2:26 pm

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?


3. Mike Abdullah left...
Mon 19 Mar 07 6:05 pm :: http://mikeabdullah.net

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:

[employees addObserver:self

  • toObjectsAtIndexes:allIndexes

  • forKeyPath:@"personName"

  • options:options

  • context:context];

And then repeat that with @"estimatedRaise"


4. John C. Randolph left...
Mon 19 Mar 07 8:32 pm

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.

-jcr


5. John Timmer left...

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.


6. Kevin Hoffman left...
Tue 20 Mar 07 9:13 am

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!!


7. Kevin Hoffman left...
Tue 20 Mar 07 9:15 am

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.


8. Mike Abdullah left...
Tue 20 Mar 07 10:25 am :: http://mikeabdullah.net

I admit that bit is a bit convoluted, try something like this:

NSIndexSet *indexes = )];

I'd suggest perhaps using the power of categories to add one onto NSArray that looks something like:

- (void)addObserver:(id)observer

  • toAllObjectsForKeyPaths:(NSArray *)keyPaths

  • options:(NSKeyValueObservingOptions)options

  • context:(void *)context;

That could simplify life a bit :) The options bit is just either NSKeyValueObservingOptionNew or NSKeyValueObservingOptionOld (or you can bitwise OR them together). Just say if you want a better explanation of them.


9. Kevin Hoffman left...
Tue 20 Mar 07 10:27 am

Using a category to extend the NSMutableArray sounds like an awesome idea, to simplify the syntax some. However, can you explain the syntax behind:

NSIndexSet *indices = )];

either that's weird syntax, or your comment got munged by the blog software :)


10. Mike Abdullah left...
Tue 20 Mar 07 10:27 am :: http://mikeabdullah.net

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?


11. Kevin Hoffman left...
Tue 20 Mar 07 10:31 am

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 =

MACRONOTFOUND:test
;


12. Kevin Hoffman left...
Tue 20 Mar 07 10:33 am

Well, I know that it uses wiki syntax but can't get it to escape the square brackets.


13. Mike Abdullah left...
Tue 20 Mar 07 10:35 am :: http://mikeabdullah.net

<pre>NSIndexSet *indexes = )];</pre>

You say it uses wiki syntax, so hopefully my "pre" tag works!


14. Kevin Hoffman left...
Tue 20 Mar 07 10:37 am

bah. Piece of junk. It uses Wiki syntax but doesn't allow Raw HTML, so the <pre>won't work.

Testing *testing = [ test ];

Ok, if you put the square brackets on their own line, it works.


15. Mike Abdullah left...
Tue 20 Mar 07 10:38 am :: http://mikeabdullah.net

NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:

  • NSMakeRange(0, employees.count)];

Sorry for cluttering up your blog here so much, I've resorted to dot syntax since double square brackets causes some other kind of weirdness. Please fix it! :)


16. Kevin Hoffman left...
Tue 20 Mar 07 10:40 am

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.


17. John C. Randolph left...
Tue 20 Mar 07 5:31 pm

Kevin,

The great thing for me about these articles is that it helps me to appreciate just how good I've got it. ;-)

So, just to give you a bit of implementation detail about the undo stack... The undo manager keeps a list of what amount to trampoline objects whose only purpose is to bounce a message to their target. Marcel Weiher has taken this concept rather further with what he calls "High Order Messaging", and you can check out his HOM code at metaobject.com. Using HOM, for example, you can send a message to those members of an array that fit some criterion such as: [[ isKindOfClass:] doSomething], and only those objects in myArray that are Foo or Foo derivatives are sent the -doSomething message.

Another thing you might do in an HOM would be (say) [[self onMainThread] doSomething], where -onMainThread would return a proxy object that would tell self -performSelectorOnMainThread: and pass it @selector(doSomething).

Marcel's example has methods for arrays including -each, -select, -filter, and I forget what others. Just another example of what can be done by message forwarding and recording.

-jcr


18. Kevin Hoffman left...
Wed 21 Mar 07 7:25 am

I posted a follow-up blog post showing how I implemented the category and the batch-observe stuff that Mike showed me:

http://dotnetaddict.dotnetdev elopersjournal.com/batch_observing_an_entire_array_of_objects_in_cocoa_or__ ho.htm


19. Kurt Wiesel left...
Wed 21 Mar 07 3:17 pm

Here's a Cocoa example how to use HOM in: Making a "Do not show this warning again" alert

http://gigliwood.com/weblog/Cocoa/Making_a__Do_not_sh.html


20. Tim Bedford left...
Mon 03 Dec 07 8:42 am

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.

In your example you have 3 message passes. In the first ; the undo manager stores the invocation target and answers self. And so the second is ; but the undo object does not understand the removeObjectFromEmployeesAtIndex message and so the runtime generates a ;

Usually passing a message to an object that it doesn't understand defaults to NSObject's implementation of forwardInvocation which throws an exception. However the NSUndoManager takes the NSInvocation, changes the target to the one it was just told and adds it to the undo/redo stack.

Note that in your code you are not sending a selector as a parameter in a message. You can do that too in certain places that require callbacks like sheets with @selector(my:selector:name:).

I too code in ObjC/Cocoa and C#/.Net but in the opposite direction so I'm finding your posts very interesting.


21. Kevin Hoffman left...

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.

One day, when I'm not spending 18 hours a day doing C# stuff and 10 minutes per week doing Cocoa, I'll get to learn details like this :)


Tag Related Posts

Upgrading your Leopard install to Java SE 6 64-Bit

Mon 12 Jan 09 1:38 P GMT-05
tags:            

Apple drops the iPhone NDA for Released Software

Wed 01 Oct 08 3:54 P GMT-05
tags:          

Cappuccino, Objective-J, and You

Wed 10 Sep 08 6:14 P GMT-05

So I'm in the LA Times ;)

Wed 27 Aug 08 2:51 P GMT-05
tags:                  

MobileMe vs. Live Mesh Throwdown - Round 1

Wed 16 Jul 08 10:33 A GMT-05

Building Model Classes in C# and Cocoa

Sun 15 Jun 08 3:13 P GMT-05
tags:            

MobileMe vs. Live Mesh - Round 1

Wed 11 Jun 08 12:20 A GMT-05

NYC SharePoint Developer Needed

Mon 12 May 08 12:09 P GMT-05

My Macbook Air is masculine, dammit!

Mon 17 Mar 08 6:59 P GMT-05
tags:          

iPhone Underrated as a Gaming Device?

Fri 14 Mar 08 1:50 P GMT-05
tags:        

My take on the iPhone SDK

Sat 08 Mar 08 1:39 P GMT-05

Jobs says "not likely" to Flash on the iPhone

Thu 06 Mar 08 1:39 A GMT-05
tags:          

My Macbook Air Review

Sun 02 Mar 08 4:20 P GMT-05

iPhone Roadmap March 6th

Fri 29 Feb 08 10:41 P GMT-05
tags:        

Video of the Macbook Air in Action

Wed 20 Feb 08 3:04 P GMT-05

Macbook Airはきれいですよ!

Sun 17 Feb 08 2:38 A GMT-05

Why is O'Reilly Condoning iPhone Hacking?

Mon 11 Feb 08 3:55 P GMT-05

Evaluating my next laptop purchase

Wed 06 Feb 08 8:40 P GMT-05

The iPhone SDK key has been leaked! Oh Noez!!!1

Tue 29 Jan 08 11:36 A GMT-05
tags:        

Why Geeks just don't "get" the Macbook Air

Thu 17 Jan 08 2:30 P GMT-05

Popcorn + TiVo + Macbook Pro + iPhone == Hell Yeah!

Tue 15 Jan 08 3:11 P GMT-05
tags:          

How my ADC membership changed my life

Mon 31 Dec 07 3:57 P GMT-05
tags:      

Leopard Code Sample : Sprinkling in some Bonjour

Tue 27 Nov 07 2:32 P GMT-05
tags:        

Leopard Sample: A Bound NSCollectionView

Mon 29 Oct 07 1:41 A GMT-05

Leopard is out - let the code samples begin!

Fri 26 Oct 07 10:09 A GMT-05
tags:          

My life is complete : iPhone SDK is CONFIRMED.

Wed 17 Oct 07 6:38 P GMT-05
tags:          

Leopard Shipping October 26th!!

Tue 16 Oct 07 4:59 P GMT-05
tags:        

My iPhone Review

Mon 23 Jul 07 1:09 P GMT-05
tags:        

Microsoft Codename Acropolis - Unwrapped

Wed 20 Jun 07 3:22 P GMT-05
tags:              

The dreaded language bleed-over has begun

Tue 19 Jun 07 6:23 P GMT-05
tags: