Modernizing the Responder Chain 2009/12/08

Cocoa’s responder chain has served us over the decades, but it is time to modernize and make it more flexible.

New Cocoa developers have a hard time learning how the responder chain works, where they can or should place their actions and validation methods, and how to debug things when they go wrong. But even experienced developers can run into difficulty with even modestly interesting interfaces. Say you have a window with a toolbar, a sidebar view and a content view. Further, say you have your code nicely factored into controllers for the sidebar and content. Now, if the first responder is in a search field in the toolbar, your sidebar and content views and controllers aren’t in the responder chain. If they have actions they can perform any time (ex: “New Collection”), you may end up having to put version of each of those actions, and menu validation, on your window controller to forward to the appropriate pane’s controller.

Here I’ll propose some possible solutions to these problems, though I’m sure the AppKit folks could improve on them.

The current state of affairs is that NSApplication has -targetForAction:to:from: which does a pre-defined scan of the responder chain, attempting to find a target that responds to the action’s selector. If there is a target found, callers might then perform some validation via one of many schemes that have been piled on over the years. Some targets might call out to delegates with things like -textView:doCommandBySelector:. Also, some delegates (but only those blessed by AppKit) are given a chance to participate directly in the responder search, even though they are not NSResponders! For extra fun, NSError presentation has a different responder chain than normal target/action, though I’m going to ignore that here.

This has left us with a bunch of concepts intermingled in ways that aren’t terribly flexible. Instead, let’s break down the target/action responder chain into more orthogonal pieces:

  • Walking the responder chain in an extensible fashion
  • Querying targets for whether they want responsibility for an action
  • Validating the action
  • Performing the action


Iterating the Responder Chain

Imagine the following API:

 typedef BOOL (^OAResponderChainApplier)(id target);

 // ... on NSApplication
 - (BOOL)applyToFullResponderChain:(OAResponderChainApplier)applier;

 // ... on NSObject
 - (BOOL)applyToResponderChain:(OAResponderChainApplier)applier;

The NSObject method would default to simply calling the applier block and returning its result, with YES signaling to continue the traversal. Subclasses could then extend this to consider extra objects. For example, NSApplication would first call super and then if that returns YES, it would call it on its delegate if it had one. NSResponder would consider its nextResponder, NSWindow its delegate/windowController, and so on. This allows each object to decide who else it will include in the responder chain (and in what order).

The NSApplication method would perform the entry points for the full search of the responder chain (keyWindow’s first responder, mainWindow’s, …)

Given these few changes, you can very simply log every object that will be considered:

[NSApp applyToFullResponderChain:^BOOL(id target) {
    NSLog(@"%@ %p", NSStringFromClass([target class]), target);
    return YES; // continue;
}];


Determining Responsibility

Now, once we have a nice way to iterate the responder chain, lets look at the issue of picking the responsible target. -targetForAction:to:from:, when passed a nil target, will search until it finds something in the responder chain that implements the action. That is, responsibility is defined as -respondsToSelector: — not entirely unreasonable, but not very flexible.

Once the first implementor is found, the search is called off, and validation begins. There is no provision for distinguishing between implementing a selector and taking responsibility for it, given the situation at hand (selection, availability of some resource, etc). Stated another way, the -validate* methods only have a YES and NO response — there is no WhatAreYouTalkingAboutThatIsNotMyProblem response.

Let’s define another new method on NSObject:

  - (id)responsibleTargetForAction:(SEL)action sender:(id)sender;

This returns nil if it doesn’t want to declare the responsible target, or it returns non-nil if it does (probably self). -targetForAction:to:from:, for the nil candidate target case, then has an applier block that looks like:

__block id target = nil;

OAResponderChainApplier applier = ^(id object){
    id responsible = [object responsibleTargetForAction:theAction sender:sender];
    if (responsible) {
        // Someone claimed to be responsible for the action.  The sender will
        // re-validate with any appropriate means and might still get refused,
        // but we should stop iterating.
        target = responsible;
        return NO;
    }
    return YES;
};

In the sample project, I define this is a semi-ugly way: check -respondsToSelector: and then based on the sender call the appropriate -validate* method. This means that a NO response from -validateMenuItem: takes on the “not my problem” behavior by default. After we return the object, the validation will be reapplied by AppKit (which is ugly but shouldn’t really hurt anything).

This still doesn’t quite split up determining responsibility from validation, but I’m sure Apple could do better with deeper hooks into AppKit. The interesting bit here is that this gives us all three choices we are looking for:

  • I’m responsible and the action can proceed
  • I’m responsible and the action is blocked
  • That’s not my problem. Go find someone else to bother.


Sample Code

Here is a project that implements some of these ideas: OATargetSelection

Obviously this implementation is a hack, overriding AppKit methods and re-implementing the responder chain. This also doesn’t handle the NSViewController absence in the responder chain, though between this and ObjC 2.0 associated storage, it might be possible to pile another hack atop this to let views know their view controllers and thus include them in the search as if they were a delegate. Really, I’d rather Apple solved both these problems.


Further Crazy Ideas

Finally, another big problem with the current target/action scheme is the implementation distance between the action’s code and its validation. Both bits of code typically start start out the same, computing some information about the selection or other state. A trivial case might look like:

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
{
    SEL action = [menuItem action];

    if (action == @selector(delete:)) {
        NSArray *selection = [self selectedItems];
        return [selection count] > 0;
    }

    ...
}

- (IBAction)delete:(id)sender;
{
    NSArray *selection = [self selectedItems];

    ...
}


But imagine that we made the selector just a key used to find an “action”, and that an action could validate itself. Instead of the -responsibleTargetForAction:sender: I proposed above, maybe we’d instead have this:

typedef BOOL (^IBAction)(id sender, BOOL justValidate);

- (IBAction)actionForSelector:(SEL)sel sender:(id)sender;
{
    // iterate the responder chain for someone claiming responsibility for the action or nil if none is found.
}
...

- (IBAction)delete:(id)sender;
{
    return ^BOOL(id sender, BOOL justValidate) {
        NSArray *selection = [self selectedItems];
        NSUInteger selectionCount = [selection count];

        if (selectionCount == 0 || justValidate)
            return (selectionCount > 0);

        // perform the action
    }
}

Now the formulation above has many, many flaws, but I think there is some promise in the general idea. Allowing the validation and the implementation to defined more closely together would be really nice:

  • Less chance of the two prefix snippets getting out of sync, resulting in incorrect validation
  • Less chance of dangling validation checks handing out when the an action gets renamed or removed
  • Possibly less code overall just be getting rid of duplicated lines.


Closing

If you’d find something like this useful too, feel free to dup my Radar, #7455939. Thanks!



blog comments powered by Disqus