Bug Responder

When the user takes an action in a Cocoa app the framework looks for the first object in the responder chain that can handle that action. Basically each object in the responder chain is asked if it implements the given action selector and if not passes the buck on to the next object in the chain. This works great since you can put code to handle user actions in the higher levels of your applications controller classes without having to worry about calling into them yourself from button handlers. The trade off here is that the further removed from the action you put the code the less contextual information you may have with which to make your decisions.

Generally this isn’t a problem. Your NSWindowController will handle an action for the relevant document or you handle it higher up for the application in general. Things become a little more complicated though when you have view controllers. These are very much like window controllers except they are responsible for only a sub-portion of the view hierarchy. As an example consider a view controller in charge of a date picker. The date picker may have several subviews, one for the day, month, year, maybe some more for the time. The idea of a view controller is that it can create a view hierarchy and manage it in a way thats opaque to the rest of the app. The date picker is a trivial example – you can imagine view controllers that manage a complex amount of state and touch their model area in a non-trivial way.

Often you’ll want to give the user the ability to report bugs in your software, sort of like what Safari does with it’s Bug Button. Ideally we’d click the bug button or chose an item from a menu and we’d get a nice bug report with contextual information as to what the user was looking at. There were a couple of ways to approach this. First is to add code in each controller that’ll handle a ‘reportBug:’ action, gather it’s information and send it to the centralized bug reporting class. This fails though because if I implement ‘reportBug:’ in my date picker then I lose the context in which I was trying to pick a date – and the bug report loses some of it’s usefulness. If I implement ‘reportBug:’ in my window controller then I can know the big picture but I don’t know what’s going on with the view controllers below me – they may have pertinent information that hasn’t been committed to the model layer yet. So it looks like we’re screwed either way.

My solution to the problem was to implement my ‘reportBug:’ action at the highest level in my application. It looks like this:


- (IBAction) reportBug: (id) sender
{
    KBBugReportController *bugReport = [KBBugReportController sharedBugReportController];
    [bugReport gatherBugReportInformation];
    [bugReport sendBugReport];
}

Nice and simple, isn’t it? Except how can ‘gatherBugReportInformation’ work? The trick is we walk the responder chain ourselves and ask each responder to contribute some context information. By the time we’ve walked the entire chain we’ll have travelled from the most specific information to the most general.

Here’s what that’d look like:

- (void) gatherBugReportInformation
{
    KBCoalescingDictionary *bugReportInformation = [KBCoalescingDictionary dictionary];
    NSResponder *responder = [[NSApp mainWindow] firstResponder];
    while ( responder != nil ) 
    {
        if ( [responder respondsToSelector: @selector( submitBugReportInformation: )] )
        {
            [(id)responder submitBugReportInformation: bugReportInformation];
        }
        responder = [responder nextResponder];
    }

    NSArray *titles = [bugReportInformation objectsForKey: kKBBugReportTitleKey];
    [self setTitle: [titles componentsJoinedByString: @" "]];

    NSArray *contents = [bugReportInformation objectsForKey: kKBBugReportContentKey];
    [self setContent: [contents componentsJoinedByString: @" "]];

    [bugReportInformation setObject: [NSDate date] forKey: kKBBugReportTimeStampKey];

    [self setBugReportInformation: bugReportInformation];
}

The method I use here is that responders implement an informal protocol or one method – ‘submitBugReportInformation:’. The method takes one argument a special NSMutableDictionary into which they can write whatever bug report info they want. I’ve got a few special keys defined for the bug report title, contents and time stamp. If you send the bug report in an email the title and contents come in handy. There is one trick here and that’s using a coalescing dictionary. All that does is that when it gets asked to set and object for a key that already exists it instead makes and array and sticks both objects into the key slot. Asking it objectForKey will return the last object set and asking it for objectsForKey (note the ‘s’) will return the array of all objects set for that key. This makes it easy to just write whatever you want into it without worrying about blowing away other objects data.

By the end of ‘gatherBugReportInformation’ we’ve travelled the chain and given each responder the opportunity to help us out with some context info. All we need to do now is send our dictionary off to our server somehow. We can either upload it with an HTTP POST request or send it off in an email. Either way I suggest letting the user have a look at what you’re going to send; you can get quite a backlash for not being polite about this kind of thing.