I wrote in my recent post on Cappuccino about the OK but not great
state of documentation. My learning process has been smooth though. I
have always had a sense of moving forward with little frustration. But
I should attempt to fill some of the documentation gaps myself.
I recently wanted to understand how to program outline views, but ran
into some of those gaps. And so my second blog post of the day begins
like so.
CPOutlineView
is a subclass of CPTableView
however from the code using an outline, the requirements are
different. Rather than providing a two-dimensional array of data, an
application has to provide more of a hierarchy of two-dimensional
data. The way I achieve this in my first example is through defining a
new class, an SDOutlineController
.
(Don't worry about "SD" - that's just the class prefix I am
using. Class prefixes are a convention going way back with NeXTStep
and before that Smalltalk. Presumably Javascript itself will adopt a
conventional if not standard namespace mechanism, and Objective-J can
piggyback on that. But I digress...)
This example outline has two groups at the top level: "my stuff" and
"other's stuff". Each top-level group has three second-level groups of
priorities: "high", "medium", and "low". The third level of the
outline are the lists of stuff, themselves. These are arrays of
instances of SDStuff
.
Here's the class definition for stuff with instance variables for a
date, a topic, and notes:
@implementation SDStuff : CPObject
{
CPDate date @accessors(readonly);
CPString topic @accessors(readonly);
CPString notes @accessors(readonly);
}
This is Objective-J, a superset of Javascript. I'm assuming that's
fairly readable even if you've not programmed in Objective-C and
Cocoa. Objective-J can be compiled to Javascript up-front on the
server, or in the browser.
The SDOutlineController is the C in MVC for this outline. The V is
Cappuccino's CPOutlineView class (and some others). The controller's
job is to work with an application's model (the M) in order to present
it in the view for user interaction. The model in this case is a
little controved, but boils down the essence of a hierarchical model.
I have defined the top-level of the model hierarchy to be a simple
class with a label and references to it's children, which group stuff
into high, medium, and low priorities:
@implementation SDStuffTopLevel : CPObject
{
CPString label @accessors(readonly);
SDStuffParent highPriorityStuffParent @accessors(readonly);
SDStuffParent mediumPriorityStuffParent @accessors(readonly);
SDStuffParent lowPriorityStuffParent @accessors(readonly);
}
The middle of the hierarchy is similarly defined, but the children are
maintained as an array of stuff:
@implementation SDStuffParent : CPObject
{
CPString label @accessors(readonly);
CPArray stuff @accessors(readonly);
}
The model classes above organize data into a fairly simple
hierarchical structure. However this structure does not suit all
imaginable hierarchies. And so SDOutlineController
is
designed to avoid having the view depend on any given structure, and
vice-versa. The controller has a reference to the top-level for my
stuff and the top-level for other's stuff:
@implementation SDOutlineController : CPObject
{
SDStuffTopLevel myStuffTopLevel;
SDStuffTopLevel othersStuffTopLevel;
}
The controller has methods expected by CPOutlineView
for
mediating between the model and the view. The messages are:
outlineView:numberOfChildrenOfItem:
is kind of obvious.
outlineView:isItemExpandable:
should be YES (true) if the item has children.
outlineView:child:ofItem:
should access a hierarchical node's children given an index.
outlineView:objectValueForTableColumn:byItem:
should provide an object for representing the given model element in the view.
In this example, the top-level of the model always has two
elements. The protocol for CPOutlineView
uses the
convention of passing nil
(i.e. the Objective-J
equivalent of Javascript's null
.) to indicate the root of
the model. And so I have not implemented the root as a class.
The following method provides the number of children for any given
model element because I have implemented methods for
the count
message for each model class.
- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
{
return (item == nil) ? 2 : [item count];
}
Expandability of a hierarchical element is based on the number of
children being greater than zero:
- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
{
return 0 < [self outlineView: outlineView numberOfChildrenOfItem: item];
}
Indexing the child of a hierarchical element is a little more
complicated, again just because the root is represented
as nil
. Otherwise I have
implemented objectAtIndex:
for the model classes:
- (id)outlineView:(CPOutlineView)outlineView child:(int)index ofItem:(id)item
{
var result = nil;
if (nil == item) {
switch (index) {
case 0:
result = myStuffTopLevel;
break;
case 1:
result = othersStuffTopLevel;
break;
}
} else {
result = [item objectAtIndex: index];
}
return result;
}
The remaining method determines the the presentation object for a
given model element. The root is never displayed, but to be compelete,
this method returns the empty string. Every model class
implements objectValueForOutlineColumn:
in order to convert itself to its
presentation object (which is always a string in this example).
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
return (nil == item) ? @"" : [item objectValueForOutlineColumn: tableColumn];
}
I will use a follow-up blog post to discuss the implementation
of objectValueForOutlineColumn:
. Perhaps this post
provides a little more clarity than other information I've found on
the web for programming with CPOutlineView
. Let me know
if you have a correction or something to add.