MOViewController


Base class for a controller that manages a hunk of view hierarchy.
Superclass: NSWindowController





Introduction



Generally a ViewController is a controller class, in the MVC sense, that owns, and is responsible for controlling, a self-contained hunk of interface functionality. MOViewController is an abstract base class which implements the basic functionality and which is subclassed to form specific kinds of ViewControllers. MOViewController is a subclass of NSWindowController, but instead of owning a whole window, it owns a chunk of view hierarchy. Some subclasses will be domain-generic, some will be domain-specific.

Examples of domain-generic ViewControllers would include: MOSplitViewController, MOTabViewController, MOViewListViewController. Usually, as is evident from the examples, generic ViewControllers manage some sort of container view.

MOViewControllers have a number of primary properties:



MOViewController is a base class for a controller that manages a view. It inherits nib loading and managing from NSWindowController, but instead of owning and controlling a whole window, a MOViewController owns and controls a hunk of view hierarchy.

A MOViewController creates/loads its interface



MOViewControllers are responsible for creating or loading their interfaces at runtime (and destroying them when they deallocate). It is possible to instantiate any number of the same type of MOViewController and thus each instance must create or load a new copy of its view.

MOViewController usually loads its interface from a nib file that contains (at least) a top-level "disembodied" (i.e. window-less) NSView. The base MOViewController class provides general facilities for locating and loading the nib file associated with a MOViewController subclass (by default a MOViewController subclass looks for a nib file with the same name as the class). When a MOViewController loads its interface from a nib the MOViewController itself is the "File's Owner" of the nib and therefore may have outlet and action connections established during nib loading to and from the interface elements being loaded. Some subclasses of MOViewController may choose to simply create their interfaces programmatically, often because they are so simple (e.g. a MOSplitViewController really only needs to alloc/init an NSSplitView).

MOViewControllers are lazy. They create or load their interfaces only when they are first actually needed (which should usually be right before they are about to actually be shown on the screen for the first time). Subclasses should take care to maintain this laziness and should avoid asking a child MOViewController for its view until it is really needed. This is especially true for the generic "container"-style subclasses that can break the laziness for almost everything. For example, MOTabViewController takes care not to ask for the views of its children that are not the active tab until the user actually selects the tab and the view needs to be shown.

The difference between contentView and view



MOViewController has methods -contentView and -view. These are different views. The -contentView is the view that is connected to the "contentView" outlet in your nib file or the view that you create programmatically and set using -setContentView:.

The -view is a special view that MOViewController creates. Once the -contentView is loaded, this special view is created and the -contentView is made a subview of it. Here are some guidelines on which one to use in different circumstances.

In your -viewDidLoad, you generally will want to talk to the -contentView. Remember that the -contentView is the one that you created (in the nib file or programmatically) and if you need to do further configuration on it, you access it through the -contentView method.

A parent controller should use the -view of its children. In particular, when a prent controller is going to install one of its child controllers. It should install the -view of its child.

If you need to programmatically change the autosizing behavior of a controller's view, you should talk to its -view. When the -contentView is first loaded, and the -view is created, the -view is given the same autosizing settings as the -contentView, by default. But if they need to change after that you must change the settings directly on the -view.

The reason that MOViewController has these two different views is to support minimum and maximum siuzes for controllers' views. You can use the -setMinContentSize: and -setMaxContentSize: API to set miminum and maximum sizes for your controller's -contentView. MOViewController uses its special -view to make sure that the minimum and maximum sizes get respected without confusing whatever view your controllers' view may be installed in. The special view of a controller does this by letting the -contentView inside it start to crop if the -view is made smaller than the minimum size and letting the -contentView be smaller than the -view if the -view is made larger than the maximum size. This provides a safe "backstop" behavior which prevents cases which could cause the interface to get into sizing states that it could not get back out of again.

The ViewController hierarchy



MOViewControllers are typically aggregated together into a hierarchy. Each MOViewController can have any number of child MOViewControllers. The hierarchy of MOViewControllers implies/requires an analogous (intended) hierarchy of the views that the ViewControllers own. The parent MOViewController is responsible for installing and uninstalling the views of its child MOViewControllers somewhere in its own view hierarchy.

Note that there is not one MOViewController per view. ViewControllers almost always have a coarser granularity than views. Consider the following window:



There might be three MOViewControllers here: a ParentViewController that owns the content view of the whole window, and two instances of ChildViewController that own the content view of each box. The ParentViewController really owns a view with two boxes in it. The ChildViewController really owns a view with a scroll view and a button inside of it. (And the scroll view contains a clip view, scroller, and header view. And the clip view contains a table view.) The ParentViewController either creates the ChildViewControllers itself (if they are always the same) or it might be given them through API it presents (-setSlot1Controller:/-setSlot2Controller: for instance). Once the ParentViewController has its ChildViewControllers for each slot, it asks them for their views and installs the views inside the boxes that are inside the view it owns.

As the above example shows, the parent MOViewController need not install the view of a child MOViewController directly in the view that it owns. It might be installed into one of its subviews. In fact, while a MOViewController technically owns a single view, it should be thought of as owning the entire view hierarchy under its view (up to but not including the views owned by any child ViewControllers).

Combined ViewController and view hierarchy



It is useful to think about ViewControllers and views as forming a combined hierarchy (although there need not be a representation of that combined hierarchy in the program). This combined hierarchy is formed by taking the view hierarchy and inserting each ViewController into it in between the view that ViewController owns and that view's superview. In the above example, the combined hierarchy (using Cocoa widget structure) looks like this:



This is more detail than most folks would care about (especially the innards of the scroll views), but it is provided to make it clear that a ViewController can own an entire view hierarchy, not just a single view.

With this combined hierarchy it is easier to see the ownership details. The two ChildViewControllers own every view that is nested under them in this example since they themselves have no child MOViewControllers. The ParentViewController owns the views nested under it except that the (view) ownership stops when a new ViewController occurs. So, in this example, the ParentViewController owns the window content view and the two NSBoxes.

This combined hierarchy is actually present at runtime in the form of the responder chain. Like an NSWindowController is the nextResponder of its window, a MOViewController is the nextResponder of its view when that view is installed in a view hierarchy (and the controllers own nextResponder is the view's superview). In other words, a MOViewController inserts itself into the responder chain in between its own view and the superview of its view. This allows it to receive focus-dispatched (nil-target) action messages and even to be in line for event handling (although the uses for a MOViewController directly implementing event-handling methods are rare).

Knowing about child and parent ViewControllers



A MOViewController knows in the abstract sense that it may have a parent and children.

A specific MOViewController subclass may or may not know specific details about its children (such as how many there will be and of what specific subclass(es) of MOViewController they are).

A MOViewController subclass, however, should never make assumptions about its parent.

It can sometimes be necessary for a MOViewController to know about other specific MOViewControllers (for the purposes of setting up relationships between what is shown in one and what is shown in the other, for example), but these relationships should be separated from the MOViewController hierarchy. In other words a FooViewController may need to know about some associated BarViewController, but it should not accomplish this by assuming its parent ViewController is the BarViewController (or its parent's second child, etc...). It should have other API or mechanism for finding its associated BarViewController. Such relationships can often be accomplished through notification protocols, or, sometimes, a more direct knowledge is necessary. But in any case, the relative path through the ViewController hierarchy is not the right way to set up such relationships.

Installing and uninstalling a ViewController's view is dynamic



"Installing" and "uninstalling" refer to putting a MOViewController's view into a view hierarchy or taking it back out again. The example above shows a situation where the views of all the child MOViewControllers are always installed. This is sometimes the case in real MOViewControllers, but not always.

A MOSplitViewController manages a variable number of child MOViewControllers. Each child MOViewController represents one split of the NSSplitView and the views of all the child MOViewControllers are always installed.

On the other hand, a MOTabViewController also manages a variable number of child MOViewControllers. Each child MOViewController represents one tab in the NSTabView, and at any given time only the child for the currently selected tab is installed. In effect the MOTabViewController swaps one child at a time into a single area of its UI.

ViewController Hierarchy is Dynamic



The hierarchy of MOViewControllers can change at runtime. This is where a lot of the potential for user-driven interface configurability comes from. For example MOTabViewController might be implemented to support dragging an individual tab out into a separate floating window or into some other MOTabViewController. Doing this would result in runtime rearrangement of the actual MOViewController hierarchy.

There are many implications to such "tear-off" functionality that are not discussed in this document and are ultimately a matter of higher level policy, but the dynamic nature of the MOViewController hierarchy provides the basis for being able to support such features.

ViewController and Focus-dispatched actions



User interface initiated commands and actions typically come in two varieties.

Some actions are always performed by the same receiver. In the example above, the two buttons labeled "Remove Row" are examples. Each one is wired directly into the ChildViewController that owns it and each one acts on the table within the same ChildViewController. This type of action is not ambiguous and ViewController need not provide any mechanism or policy at all (except inasmuch as it may be the explicit target of the action).

However, other actions are dispatched based on the current focus within the application. Menu and toolbar items often cause this type of action and the Cut/Copy/Paste commands are canonical examples. There is one Copy menu item, but there are any number of things that a user might want to copy. What gets copied typically depends on what UI element in the active window has "focus". Typically, this same "focus" concept is used to determine who gets typing events. There's a dynamic chain of objects, the responder chain, starting at the UI control that currently has the "focus" that are all able to get a crack at handling such actions and the first one in the chain who can handle an action is the one that gets it. The responder chain is formed by -nextResponder links. Normally a view's -nextResponder is its superview, the -nextResponder of the content view of a window is the window, and the window's -nextResponder is its NSWindowController (if it has one).

When using MOViewControllers, the controllers are in this chain. A MOViewController subclass should be able to implement methods to handle actions and have those methods called when some view within its owned view hierarchy has focus (and the view(s) themselves do not handle the action directly). In order to allow a MOViewController to insert itself into the responder chain, a parent MOViewController is responsible for informing its child MOViewController whenever it has installed that child's view or is about to uninstall the child's view and the base MOViewController class handles managing the changes to the responder chain that need to be done as it is installed and uninstalled. When a MOViewController's view is installed, the controller inserts itself into the responder chain in between its view and the superview in which its view was installed. When a MOViewController's view is about to be uninstalled, it removes itself from the responder chain and restores the original chain (ie makes its view's next responder by its view's superview).

Top-level ViewControllers and owned windows



A MOViewController that has no parent MOViewController can be asked to live in its own window. If this happens, the MOViewController will own a window that it lives in and will manage that window. The MOViewController's view will be the -contentView of the window. Whether a MOViewController lives in its own window can change over time. A MOViewController might live in a MOViewController hierarchy for a while and then be removed from the hierarchy (torn off) and put into its own window, then later be put back into a hierarchy, discarding the window.

As an extension of the rule that a MOViewController should not assume specifics about its parent MOViewController, it is usually not a good idea for a MOViewController to decide for itself to live in a separate window. Rather, this should be left up to the object that created the MOViewController or to a higher level mechanism.

Other MOViewController services



The MOViewController base class defines a number of other services that can be assumed to be common among all MOViewControllers.

Labels and icons



Each ViewController can have a label and an icon. These can be the same for all instances of a subclass or they can be per-instance. They can change over time. Special support is provided for when the label/icon are a file system path and file icon. A parent ViewController can access its children's labels and icons. For example, a MOTabViewController uses the labels of its subcontrollers as the actual tab labels. A MOViewController that owns its own window uses its label and icon for the window's title. Label changes cause a notification to be propagated up the MOViewController hierarchy so parents can be aware when a child's label changes.

Controller state saving and restoring



A mechanism is provided for saving the state of a MOViewController and later restoring it. A MOViewController can provide a "state dictionary" for itself and can restore its state from a dictionary that it is given. State typically includes information about the geometry and other visual configuration of the MOViewController and its view.

MOViewController provides an API to get a state dictionary out of a MOViewController hierarchy and to apply it back again later. Subclasses are responsible for implementing the details of what gets saved for their specific content and how to apply it back. MOViewController manages traversing the hierarchy (and, notably, making sure that restoring a saved configuration does not cause immediate non-lazy interface loading).

The MOViewController base class automatically stores its class name, its label and icon, its view's frame information, and the state dictionaries of its subcontrollers. A MOViewController that should not store and restore the state of its subcontrollers can override -savesSubcontrollerState to return NO.

Here are some further examples of state saved by different kinds of controllers in MOKit. MOTabViewController stores the selected tab, the tab font, the tab view style, and various other settable attributes of the NSTabView it manages. MOSplitViewController stores whether it is horizontal or vertical and whether it uses a pane splitter or a normal splitter. MOViewListView stores the expansion state of its subcontrollers as well as various attributes of the MOViewListView.

Custom MOViewControllers should store anything that is needed to restore themselves to their current visual appearance and internal state.

A distinction is made between state that is independent of the specific content the MOViewController might be showing and state depends on the content. For example, in a subclass that manages a UI with an outline view, the column widths and order might be independent of its content, while the selection and the expansion state of the items might be dependent on the content. A MOViewController can be asked to provide a state dictionary with or without the content-dependent state (the content-independent state is always included). Similarly, when being given a dictionary to restore its state from, the MOViewController may be asked to ignore any content-dependent state, if it is present. When writing a subclass of MOViewController, you should think carefully about which category each piece of state falls into.

It is also wise, when implementing a custom MOViewController, to make your override of -takeStateDictionary:ignoringContentState: forgiving. You should be prepared for incomplete or incorrect information and handle those cases gracefully. This is especially true for content-dependent state since you never know, when restoring state at a later date, if the content has changed since the state was saved amking the content-dependent state stale or inappropriate.

In addition to the basic API that allows getting and resotring state in a pre-built MOViewController hierarchy, MOViewController provides an API to create a MOViewController hierarchy from a state dictionary. With this API, MOViewController uses the class information recorded in the state dictionary to actually create the hierarchy (which then resotres its state from the rest of the information in the dictionary.)

Keyboard UI loop support



MOViewController provides a mechanism for allowing reasonable handling of key-loops for keyboard controlled UI access. A MOViewController has a -firstKeyView outlet. Usually this is connected in Interface Builder to the first view that should receive focus in the MOViewController's interface. It can also be set programmatically. The -firstKeyView of a MOViewController should be a member of a complete key loop. When the -firstKeyView is set, MOViewController computes the -lastKeyView by traversing to the -firstKeyView's -previousKeyView. Once MOViewController knows the first and last key view it can manage the key loop for the whole hierarchy as subcontrollers are installed and uninstalled.

This mechanism is designed and implemented but has not been tested fully and is likely to need some further tweaks.

Writing a MOViewController subclass



This section discusses the actual steps involved in creating a new MOViewController subclass. There are only a few things you really need to do. And there are a number of things you may want to do in addition to the required steps.

First steps



The first thing you need to do to is make a subclass and create the interface. A minimal subclass does not actually need to implement any methods. MOViewController, by default, will look for a nib file named the same as the subclass to load as its interface.

Here is a minimal subclass:

        @interface MyCustomController : MOViewController {
            @private
            // instance variables (should be private)
        }
        
        @end
        
        @implementation MyCustomController
        
        @end


Add any instance variables you need, including outlets that will be connected to your interface. Add any methods you need, including actions that will be connected from your interface.

Once you have the class, create a new empty nib file. Drag the MOViewController.h header into the nib so that IB will know about the class. Then drag your MyCustomController.h header into the nib.

Select the File's Owner icon in the nib file, go to the Class inspector pane (Cmd-5 will bring up the Class inspector). Set the File's Onwer's class to MyCustomController.

Drag a Custom View off the "Cocoa Container Views" palette into your nib window. When you drop it, a View icon should appear. Double-clicking this icon will show your view in a window (but the window is not part of the nib file.) You can tell the difference between a view and a window in IB because the IB window holding a view has a dark gray border around its edge.

Connect the "contentView" outlet of the File's Owner to the view you just added. To do this Control-drag a connection line from File's Owner to the View icon in the nib window. Then select the "contentView" outlet in the inspector and click the Connect button.

Now you can define the interface for your controller within the custom view. Create the interface, making any connections you need. Remember to connect the outlets and actions for your controller to the File's Owner icon in the nib file.

Trying it out



This is all that is needed to make a simple controller. You can try it out by simply creating an instance of your controller and giving it a window to live in like this:

            MyCustomController *controller = [[MyCustomController alloc] init];
            [controller setWantsOwnWindow:YES];
            [controller showWindow:nil];


(This code could be run from anywhere. A good place to test it out might be in the -applicationDidFinishLaunching: method of your application delegate.)

Commonly overridden methods

There are a number of MOViewController methods that are commonly overridden for various purposes.

The designated initializer for MOViewController is -init. If you need to do initialization when an instance of your subclass is created, override that method. The common idiom for overriding a designated initializer looks like this:

        - (id)init {
            self = [super init];
            if (self) {
                // Your init code here.
            }
            return self;
        }


Init methods like this are not special to MOViewController. All Cocoa init methods should follow this pattern. The first thing you should do is call the superclass' designated initializer. Note that the return value is assigned to self. This is because it is permissable for an init method to replace the receiving instance with a new instance if it needs to. When init methods fail they return nil, so the next line makes sure the superclass did not fail to do its initialization. If the superclass failed, we do not do anything. Finally, we return self.

Be careful what you do in your controller's init method. The controller's interface is not loaded yet, and the init method should never do anything to cause the interface to be loaded (such as sending a [self view] message). Any initialization that can be put off until the view gets loaded should be. Only do what is absolutely required in the init method.

Even more common than overriding -init is overriding -viewDidLoad. This message will be sent to the controller right after it loads its nib file. At this time, the controller's outlets and actions will have been set up and you can do any further initialization of the interface that might be needed. In general, you should set up everything you can in the nib file, but some things must be done in code, and -viewDidLoad gives you a place to do them.

        - (void)viewDidLoad {
            [super viewDidLoad];
            // Your setup code here
        }


Always call super's implementation of viewDidLoad when you override it. There is a similar method, -viewWillLoad, that can be overridden if you need to do any work prior to the interface being loaded.

As with many Objective-C classes, you may need to override -dealloc to release any objects that your controller has references to. Note that the contents of the nib file is taken care of by the MOViewController base class, so you do NOT need to release objects that were loaded from your nib file.

Sometimes a controller may need to know when the controller hierarchy it is part of changes. MOViewController has two methods that can be overridden for this. -controller:didInsertSubcontroller:atIndex: and -controller:willRemoveSubcontroller:atIndex:. When a subcontroller is being added or removed, the controller itself, all its ancestors, and all its descendants are sent these messages. When overriding these, be sure to call super and be sure to keep in mind that these messages are sent to many objects. It is common to examine the controller or subcontroller arguments to these methods to be sure that the change is one that you are interested in before doing any work.

Similarly, a controller may need to know when it is installed or uninstalled in a view hierarchy. MOViewController has two methods that can be overridden for this. -controllerViewWasInstalled: and -controllerViewWillBeUninstalled:. When a controller's view is installed or uninstalled, the controller itself, all its ancestors, and all its descendants are sent these messages. When overriding these, be sure to call super and be sure to keep in mind that these messages are sent to many objects. It is common to examine the controller argument to these methods to be sure that the change is one that you are interested in before doing any work.

If you need to do any validation or other work every time through the event loop when your controller's UI is visible, you can override the -update method. This method will be called at the end of each event for all controllers that are currently installed and whose ancestors are all currently installed (ie whose UI is currently installed in some window). Note that this -update mechanism only works if the top-level controller of your controller's hierarchy is installed in its own window using the -setWantsOwnWindow: API.

If, for some reason the name of the nib file for a MOViewController subclass is not the same as the class' name, you can override +defaultViewNibName. to return the actual name of the nib file to load for that class. Note that overriding this method will affect any subclasses of your custom controller class as well as your class itself.

Nib-less controllers



Sometimes a controller does not want to get its interface from a nib file. A MOViewController subclass can create its own interface programmatically. To do this, it should override -loadView. Within the override of loadView, the subclass should call [self setContentView:theView] once it has created its view.

Controller labels



It is a good idea to give your controller a meaningful label. That may be a constant label that is the same for all instances or it may be instance-specific. It may change over time. I may include an icon and it may or may not represent a file path.

Use the setLabel: API and related methods to set a label for your controller. If the label is the same for all instances, then doing this from an -init override may be the easiest way.

If you need to know when your controller or one of its descendants changes its label you can override -controllerDidChangeLabel:. For example, a MOTabViewController overrides this to watch its children's labels, and if they change it updates the labels on its tabs.

State saving



If you wish to be able to save and restore the state of your controller, you should override -stateDictionaryIgnoringContentState: and -takeStateDictionary:ignoringContentState:.

Both methods should always call super.

-stateDictionaryIgnoringContentState: should call super and then add its own specific state to the mutable dictionary returned by the superclass implementation.

-takeStateDictionary:ignoringContentState: should call super and then restore any of its own specific state it finds in the dictionary.

MOViewController and NSController



This section discusses how the new NSController facility that has been introduced in Panther fits in with MOViewController. First, NSController is kind of an unfortunate class name since it is so general and the term "controller" is so overloaded. It is important to note that NSController and MOViewController are not filling the same roles. In fact they can be extremely powerful when used in combination.

A MOViewController may optionally own one or more NSControllers. Typically these NSControllers would be loaded from the MOViewController's nib file along with the MOViewController's interface. NSControllers are really useful for binding data to UI elements. The trick is how that binding happens. In a demo, it is common to see NSControllers used in a simplistic way. The NSController is bound to UI elements, but the data that is being bound is created and managed automatically by the controller. This makes for nice demos, but it is not a complete picture of how a real application would use NSController.

In a real application, the NSController will have bindings at both "ends". On the "front end" it will be bound to UI elements the same as in the demo case. But instead of owning and managing its own data, it will instead be bound to external data on the "back end". These bindings will be through the File's Owner of the nib file containing the NSController.

For applications that use MOViewController, the File's Owner of the nbib file is going to be a MOViewController subclass. This means that the NSController bindings to data will go through the MOViewController.

Let's look at a specific example. Imagine you are building a rolodex application. The model objects are Person objects with the obvious collection of keys (data members) like name, address, etc... The application has a document class that manages a list of Person objects. It also has a MOViewController subclass (PersonListController) that shows a list of Person objects and allows editing, inserting, removing. The PersonListController class knows about the document whose Person objects it will be showing. Both the PersonListController and the document class are Key-value Coding compliant (which simply means that the methods for accessing the document of the PersonListController and the Person array of the document follow the KVC naming conventions).

The PersonListController's nib file has a table view and maybe some individual text fields and things. It also has an NSArrayController. The table and fields and other UI elements are bound to the NSArrayController just like you may have seen in NSController demos. But, in addition, the NSArrayController is bound to the controller's document's array of Person objects. This is done, in the bindings inspector for the NSArrayController, by binding the "contentArray" of the NSArrayController to the File's Owner with the key path "document.people" (assuming that the PersonListController's key for getting to its document is "document" and the document class' key for getting to the array of Person objects is "people".)

Binding through the File's Owner gives a couple of benefits:



In a simple case, the PersonListController might not need a single line of actual code (since it inherits the ability to have an associated document from MOViewController (which inherits it from NSWindowController). By using NSController, the following features are automatically supported:



Future feature ideas



This is a working list of future directions for MOViewController:







(Last Updated 3/20/2005)
HTML documentation generated by HeaderDoc