validateUserInterfaceItem: and chaining actions

  • I was reading Daniel Jalkut's blog (http://www.red-sweater.com/blog/
    161/the-case-of-the-missing-check) and stumbled on a quote from the
    "NSUserInterfaceValidations Protocol Reference":

    http://developer.apple.com/documentation/Cocoa/Reference/
    ApplicationKit/Protocols/NSUserInterfaceValidations_Protocol/
    Reference/Reference.html

    > To validate a control, the application calls
    > validateUserInterfaceItem: for each item in the responder chain,
    > starting with the first responder. If no responder returns YES, the
    > item is disabled.

    This would suggest, as Daniel points out, that you should return NO
    for unknown actions in validateUserInterfaceItem. Unfortunately the
    quote is WRONG. If you follow the link to the companion guide, you
    find example code that implements the following algorithm: (rewritten
    for simplicity)

    - (BOOL)validate
    {
        id validator = [NSApp targetForAction:[self action] to:[self
    target] from:self];
        if ((validator == nil) || ![validator respondsToSelector:[self
    action]])
            return NO;
        if ([validator respondsToSelector:@selector
    (validateUserInterfaceItem:)])
            return [validator validateUserInterfaceItem:self];
        return YES;
    }

    That is:
    1. Find the target for my action
    2. Validate using that target and nobody else.

    -[NSApp targetForAction:to:from:] walks the responder chain, but only
    calls -[respondsToSelector:]. It doesn't do validation.

    If you read the documentation for NSMenu and NSToolbar, you will see
    that they do something similar. There are some complications with
    different key and main windows, and with application and window
    delegates, but the basic pattern is: Find the target, validate once.

    This is the right approach. You should validate with the target that
    is going to receive the action, not some other random responder up
    the chain that happens to implement the same action, but might never
    see it.

    It follows from the validator code that -[validateUserInterfaceItem:]
    is only called for implemented actions, and that a transparent -
    [validateUserInterfaceItem:] simply returns YES, i.e., implementing

    - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
    {
        return YES;
    }

    has no effect at all. So for actions you don't recognize, simply
    return YES and pretend you're not there:

    - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
    {
        if ([item action]==@selector(foo:))
            return [self canFoo];
        return YES;
    }

    But the quote above actually expresses an interesting idea: If I
    can't perform an action, maybe somebody up the responder chain can?
    This is similar to the way -[keyDown:] can be passed up the responder
    chain until somebody cares.

    Here is a snippet from WebKit's WebFrameView:

    - (void)scrollPageDown:(id)sender
    {
        if (![self _pageVertically:NO]) {
            // If we were already at the bottom, tell the next responder
    to scroll if it can.
            [[self nextResponder] tryToPerform:@selector
    (scrollPageDown:) with:sender];
        }
    }

    That is, when the frame receiving the -[scrollPageDown:] action is
    already at the bottom, it passes the action up the responder chain,
    allowing an outer frame to scroll instead. Very useful. You can see
    the effect when scrolling embedded frames in Safari.

    Now, WebKit doesn't actually validate -[scrollPageDown:], but what if
    you wanted to? If the validation algorithm was as described in the
    quote above, it would just work. But it doesn't work that way, and it
    shouldn't work that way. Not all actions are chained, and the
    validation algorithm can't know if it is.

    Here is one way of doing it, using a category on NSResponder:

    @implementation NSResponder(ChainedValidation)
    - (BOOL)tryToValidateUserInterfaceItem:
    (id<NSValidatedUserInterfaceItem>)item
    {
        if (![self respondsToSelector:[item action]])
            return [[self nextResponder]
    tryToValidateUserInterfaceItem:item];
        if ([self respondsToSelector:@selector
    (validateUserInterfaceItem:)])
            return [self validateUserInterfaceItem:item];
        return YES;
    }
    @end

    Then, for your chained actions:

    - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
    {
        if ([item action]==@selector(foo:))
            return [self canFoo] || [[self nextResponder]
    tryToValidateUserInterfaceItem:item];
        return YES;
    }

    You would have to create categories on NSWindow and NSApplication as
    well since they include their delegate in the responder chain. You
    may want to check if -[self nextResponder] is nil, if you're into
    that sort of thing. (It works as long as you trust that sending a
    message to nil returns nil, and that nil becomes NO when interpreted
    as a BOOL).

    The tricky part is making this work with -[validateMenuItem:] and -
    [validateToolbarItem:]. These protocols fall back to -
    [validateUserInterfaceItem:], so the validation algorithm looks
    something like this:

    - (BOOL)validate
    {
        id validator = [NSApp targetForAction:[self action] to:[self
    target] from:self];
        if ((validator == nil) || ![validator respondsToSelector:[self
    action]])
            return NO;
        if ([validator respondsToSelector:@selector(validateMenuItem:)])
            return [validator validateMenuItem:self];
        if ([validator respondsToSelector:@selector
    (validateUserInterfaceItem:)])
            return [validator validateUserInterfaceItem:self];
        return YES;
    }

    Naïvely, we could extend the category like this:

    @implementation NSResponder(ChainedValidation)
    - (BOOL)tryToValidateMenuItem:(id<NSMenuItem>)item
    {
        if (![self respondsToSelector:[item action]])
            return [[self nextResponder] tryToValidateMenuItem:item];
        if ([self respondsToSelector:@selector(validateMenuItem:)])
            return [self validateMenuItem:item];
        if ([self respondsToSelector:@selector
    (validateUserInterfaceItem:)])
            return [self validateUserInterfaceItem:item];
        return YES;
    }
    @end

    - (BOOL)validateMenuItem:(id<NSMenuItem>)item
    {
        if ([item action]==@selector(foo:))
            return [self canFoo] || [[self nextResponder]
    tryToValidateMenuItem:item];
        return YES;
    }

    This doesn't work perfectly, though. If a responder implements the
    action and -[validateUserInterfaceItem:] as above, but not -
    [validateMenuItem:], the chaining happens through -
    [tryToValidateUserInterfaceItem:], and -[validateMenuItem:] won't be
    called for the following responders.

    Here is another approach:

    @implementation NSResponder(ChainedValidation)
    - (BOOL)tryToValidateUserInterfaceItem:
    (id<NSValidatedUserInterfaceItem>)item
    {
        if (![self respondsToSelector:[item action]])
            return [[self nextResponder]
    tryToValidateUserInterfaceItem:item];
        if ([item conformsToProtocol:@protocol(NSMenuItem)] && [self
    respondsToSelector:@selector(validateMenuItem:)])
            return [self validateMenuItem:(id<NSMenuItem>)item];
        if ([item isKindOfClass:[NSToolbarItem class]] && [self
    respondsToSelector:@selector(validateToolbarItem:)])
            return [self validateToolbarItem:(NSToolbarItem*)item];
        if ([self respondsToSelector:@selector
    (validateUserInterfaceItem:)])
            return [self validateUserInterfaceItem:item];
        return YES;
    }
    @end

    This is dubious since calling -[validateUserInterfaceItem:] could
    cause -[validateMenuItem:] to be called up the responder chain, but
    it would probably do the right thing in most cases. The very odd case
    of an NSToolbarItem subclass adopting the NSMenuItem protocol is left
    as an exercise for the reader.
previous month july 2006 next month
MTWTFSS
          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 26 27 28 29 30
31            
Go to today