Using Tree Controllers With NSXML Objects

An NSXMLDocument object represents a hierarchical tree structure composed of NSXMLNode objects. In the Model-View-Controller design pattern, an NSXMLDocument is a model object because it represents application data in XML format. It makes sense then that an NSXMLDocument object would be ideally matched with a view object and a controller object that are designed to handle data arranged in tree structures. For this purpose, Cocoa provides the NSTreeController class for the controller layer; for hierarchy-displaying views, there are the NSOutlineView and NSBrowser classes.

The following sections walk you through the steps for using an NSTreeController object paired with an NSOutlineView object in an NSXML application. For communication of data between view, controller, and model objects, the example uses bindings, and does not rely on any data-source, delegation, or action methods.

Creating the Document-Based Application Project

The project example is an application that displays the structure of an XML file in an outline view; when the user selects a node in the outline view, the application shows the textual content of that node (and its children) in a text view. Figure 1 shows the outline-view part of the application at runtime.

Figure 1  Outline view displaying XML data
Outline view displaying XML data

For this example, the application is document-based—a likely scenario for an application whose document data is XML markup. An important requirement for Cocoa bindings is that each instance of the NSDocument subclass holds a reference (as an instance variable) to the NSXMLDocument object representing an XML file or other source. (In the object modeling terminology of bindings, the NSXMLDocument object is a property of the document instance.) The instance of the NSDocument subclass is the File's Owner for the document's nib file, which contains the NSOutlineView object and the NSTreeController used for establishing bindings.

Listing 1 shows the germane part of the NSDocument subclass implementation. In readFromData:ofType:error:, the application creates an NSXMLDocument instance from the XML file selected by the user and calls the accessor method setTheDocument: to assign and retain the instance variable. This "setter" method and its complementary "getter" method not only ensure proper memory management of the instance variable, but help to make the document subclass compliant with the requirements of key-value coding.

Listing 1  Setting the NSXMLDocument object as a property of File's Owner

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
    NSXMLDocument *newDocument = [[NSXMLDocument alloc] initWithData:data
        options:NSXMLNodeOptionsNone error:nil];
    [self setTheDocument:newDocument];
    [newDocument release];
    return YES;
}
 
- (NSXMLDocument *)theDocument
{
    return [[theDocument retain] autorelease];
}
 
- (void)setTheDocument:(NSXMLDocument *)newTheDocument
{
    if (theDocument != newTheDocument)
    {
        [newTheDocument retain];
        [theDocument release];
        theDocument = newTheDocument;
    }
}

Adding Methods to NSXMLNode

Categories are a powerful feature of Objective-C. The let you add methods to a class without having to make a subclass. To make the NSXML objects in our sample application work together with the NSTreeController object, it is necessary to add a couple methods to the NSXMLNode class through a category. Listing 2 shows the implementation of the methods in the category.

In its configuration, the NSTreeController object requires some way to know whether any given node in a tree structure is a leaf node—that is, a node with no children. The category's isLeaf method serves this purpose by returning YES if the node is a text node. In addition, each node in the outline view must have some text to represent it. For elements (NSXMLElement objects), that text could be the element name. However, other XML nodes objects, such as text nodes, don't have names but do have string values. Thus a method is needed to unify the textual representation returned for a given node; the category method displayName does this.

Listing 2  Adding a category to NSXMLNode

@implementation NSXMLNode (NSXMLNodeAdditions)
 
- (NSString *)displayName {
 
    NSString *displayName = [self name];
    if (!displayName) {
        displayName = [self stringValue];
    }
    return displayName;
}
 
- (BOOL)isLeaf {
 
    return [self kind] == NSXMLTextKind ? YES : NO;
}
 
@end

Establishing the Bindings

To complete this application in Interface Builder, you must create the user interface and establish bindings between the tree controller and the NSXMLDocument object and the outline view and the tree controller. Drag the outline view object from the Cocoa-Data palette and place it in the document window; make it a single column, size it, and set any other attributes needed to configure it. Next, drag the tree-controller object on the Cocoa-Controllers palette into the nib file window. Figure 2 identifies this controller object.

Figure 2  The Cocoa-Controllers palette
The Cocoa-Controllers palette

With the tree controller selected, open the Attributes pane of the inspector window (Command-1). First, set the value of the Object Class Name field to NSXMLNode. This is the underlying class type of the objects that compose the tree structure.

The Attributes pane has several "key path" attributes that are specific to the NSTreeController object. Enter key paths in the first and third of these text fields:

Finally, add to this pane's Keys table the name of the other method in the category on NSXMLNode: displayName. When you are finished, the Attributes pane for the tree controller should look like the one in Figure 3.

Figure 3  Setting the model keys
Setting the model keys

Once the tree controller is configured with the relevant key paths of the model object, you can establish a binding between the tree controller and the NSXMLDocument instance held as a property of the document object. With the NSTreeController object still selected, open the Bindings pane of the inspector (Command-4). Expose the contentObject property and set the values of the Bind To pop-up menu and Model Key Path text field as shown in Figure 4. This step makes the NSXMLDocument object the source of content for the tree controller.

Figure 4  Setting the tree controller's content object
Setting the tree controller's content object

Here the Bind To value is set to File's Owner, which is the instance of the NSDocument subclass that holds the NSXMLDocument object as a property. Also note that the key path entered in the Model Key Path field includes not only the theDocument property of the document but the rootElement property, which corresponds to the rootElement method of NSXMLDocument. The content object of the tree controller must be the object at the root of the tree hierarchy, which for an XML document is its root element.

The final part of the procedure requires you to establish bindings between the outline view and the tree controller. Open the Bindings pane of the inspector and repeatedly click the top of the outline view until the inspector shows Table Column. Then expose the value property in the Bindings pane of the inspector and establish a binding through the tree controller's arrangedObjects property to the displayName attribute of each node in the model object (the NSXMLDocument instance). The bindings setting should look like the example in Figure 5.

Figure 5  Setting the displayed value for items in the outline view
Setting the displayed value for items in the outline view

Compile the project, run it, and observe how the outline view now reflects the structure of the XML document.