Learning Cocoa with Objective-C/Document-Based Applications/Multiple Document Architecture

From WikiContent

< Learning Cocoa with Objective-C | Document-Based Applications
Revision as of 12:56, 7 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Learning Cocoa with Objective-C

So far, our examples have centered on applications that have a single GUI. However, in reality many of the applications we use day-in and day-out—such as word processors and web browsers—are based around the idea of a document. They provide a framework for viewing or generating identically-contained, but uniquely-composed, sets of data that can be stored in files.

A document-based application must perform the following tasks:

  • Create new documents
  • Open existing documents stored in files
  • Save documents to user-designated files and locations
  • Revert to previously saved documents
  • Close documents, usually after prompting the user to save changes
  • Print documents and allow the page layout to be modified
  • Monitor and set the document's edited status, as well as reflect that status to the user
  • Manage document windows, including setting window titles

Cocoa provides a multiple-document architecture, helping you take care of these tasks easily. Using this architecture drastically simplifies the work developers must do to implement a multidocument application. Once you understand how this architecture works, you can have a multidocument application up and running in minutes.

This chapter begins with an overview of Cocoa's multiple-document architecture and then presents an in-depth look at the classes that make up this architecture. The final part of the chapter guides you through the process of creating a simple multiple-document text-editing application.

Contents

Architectural Overview

From a user's perspective, a document is a unique body of information contained in its own window. The window gives the user an area in which to edit the document. Users can create an unlimited number of documents and save each to a separate file.

From a Cocoa programming perspective, a document is managed by an instance of the NSDocument class, which, along with NSDocumentController and NSWindowController , provides the functionality for a document-based application. Objects of these classes divide and orchestrate the work of creating, saving, opening, and managing the documents that an application creates. They are tiered in a one-to-many relationship, as depicted in Figure 10-1.

Figure 10-1. Document architecture class relationships

Document architecture class relationships

Document-based applications have one instance of the NSDocumentController class, which creates and manages many potential NSDocument objects (one for each new or open document). In turn, an NSDocument object creates and manages one or more NSWindowController objects, one for each of the windows displayed for a document.

In addition to these three AppKit classes, the multiple-document architecture uses information in the application's info property list (saved as Contents/Info.plist in the application's bundle — we'll discuss bundles more in Chapter 13) to determine the types of data with which the application can work. The information is stored in the property list as an array of document types. Each document-type entry in the array includes the following information:

  • The name of the document type.
  • An array of filename extensions, such as .rtfand .txt, which correspond to a document's data type.
  • An array of Mac OS-style type identifiers, such as TEXT and PICT, which also correspond to a document's data type.
  • A string that determines the role of the application when interacting with data. An application can be an Editor or a View for a given type.
  • The class name of the NSDocument subclass that handles the data type in your application.

Project Builder provides a simple user interface for creating and editing entries in an application's document type array. Even though there's usually no need to modify the property list directly, the document controller uses the information from the info property list to do the following things:

  • Filter out inappropriate file types automatically, allowing users to select only files that the application can handle when an open dialog box is presented
  • Instantiate the appropriate NSDocument subclass for a document's data type when a document is opened

The Document Object

The primary job of a document object—an instance of an NSDocument subclass that you provide as part of your application—is to represent, manipulate, store, and load the data associated with a document. Based on the document types it claims to understand (as specified in the application's info property list), a document object must be prepared to do the following things:

  • Provide other objects in the application that the data displayed in its window(s). The document object must provide the data in any of the formats supported by the application.
  • Load data into internal data structures and display it in windows. The document object must accept the data in any format supported by the application.
  • Store document data in a file at a specified location in the filesystem.
  • Read document data stored in a file.

With the assistance of its window controllers, a document-object instance manages the display and capture of the data in its windows. The document-object instance associated with the key window is made the first responder to action messages indicating that a user wants to save, print, revert, or close a document. A fully implemented document object knows how to track its edited status, print document data, and perform undo and redo operations. As you'll see in the examples in this and later chapters, these behaviors aren't provided completely by default, but the NSDocument class goes a long way to assist you in implementing each.

For edited-status tracking, the NSDocument class provides an API for updating a document change counter. For undo/redo operations, NSDocument creates an NSUndoManager when one is requested, which responds appropriately to the Edit → Undo and Edit → Redo menu commands, updating the change counter when undo and redo operations are invoked.

Every application that takes advantage of the AppKit's document-based application architecture must create at least one subclass of NSDocument. The architecture requires that you override some methods of the NSDocument class. These methods must be implemented:

- (NSString *)windowNibName;
Called by the document controller to determine the name of the nib file that contains the user interface to view and edit the document.
- (void)windowControllerDidLoadNib:(NSWindowController *)aController;
Called once the window controller has loaded the nib file and all of the user interface connections have been made. This provides an opportunity for any initialization that needs to be performed.
- (NSData *)dataRepresentationOfType:(NSString *)aType;
Must be implemented to create and return document data of a supported type, usually in preparation for writing that data to a file as an NSData object.
- (BOOL)loadDataRepresentation:(NSData *)data:(NSString *)aType;
Must be implemented to convert an NSData object (that contains the document data of a particular type) into the document's internal data structures so that the document is ready to display its contents. The NSData object usually results from the document reading a document file.

Tip

A common mistake made by novice Cocoa programmers is to treat the document object as a model, though it's really a controller object that adapts between the view of the document itself and whatever model is being used to hold the representation.

The Document Controller

The primary job of an application's document-controller object (NSDocumentController ) is to create and open documents, as well as to track and manage these documents. The document controller maintains a list of document objects and tracks the current document (the document whose window is currently key). It is hardwired to respond appropriately to certain application events, such as when the application starts up, when it terminates, when the system powers off, and when documents are opened or printed from the Finder. For example, when a user chooses New from the File menu, the document controller does the following things:

  1. Allocates an instance of the NSDocument subclass specified in the first entry of the application's document type array
  2. Initializes the instance by invoking the subclass's init method

When the user chooses Open from the File menu, the document controller does the following things:

  1. Displays the Open panel, filtering the file list using the data type(s) from the application's info property list, and gets the user's selection
  2. Uses the type information from the file and data to allocate an instance of the appropriate NSDocument subclass
  3. Initializes the object by invoking its initWithContentsOfFile:ofType: method, which loads the contents of the file into the document instance

When the user chooses Save or Save As from the File menu, the document controller does the following things:

  1. If needed (if the document has not been saved before, or if the user chooses Save As), displays the Save panel and gets the user's selection
  2. Uses the type information from the filename that the user gave and requests the data from the application using the dataRepresentationOfType: method
  3. Stores the data in the returned data object into the filesystem

In a document-based application, many of the application's menu items are already connected to the document controller. These methods are implemented by the NSDocumentController class and are listed in Table 10-1.

Table 10-1. Target/action configuration for default multidocument application

File menu command First responder action implemented by NSDocumentController
New
newDocument:
Open
openDocument:
Save
saveDocument:
Save As
saveDocumentAs:
Save To
saveDocumentTo:
Save All
saveAllDocuments:
Close
closeDocument:
Revert
revertDocumentToSaved:
Print
printDocument:
Page Layout
runPageLayout:


The default document-controller behavior provided by the NSDocumentController class is usually sufficient for most situations; you shouldn't need to subclass it unless you need to provide alternative functionality for the methods listed earlier.

The Window Controller

A window controller, an instance of the NSWindowController class, manages one window associated with a document. If a document has multiple open windows, each window has its own instance of NSWindowController. For example, a document might have a main data-entry window and a window that lists records for selection. Each window would have its own window controller. When a document has multiple window controllers, only one of them is considered the primary window controller. When the primary window is closed, the document and all other windows are closed.

When requested by the NSDocument class, a window controller loads the nib file containing a window and displays it. The window controller assumes responsibility for managing the nib file.

When a document is closed, the window controller is responsible for properly closing windows, as well as freeing any top-level objects instantiated by the nib file. This includes the window itself and any additional objects added to the nib.

Most of the time, you can use the default window controller provided by the AppKit. Some applications may want to subclass NSWindowController to move the user-interface-specific logic out of the NSDocument subclass. The Sketch sample application in /Developer/Examples/AppKit uses this technique. Another situation that would make subclassing desirable is if you wanted to support multiple views onto a document; for example, in a 3D modeling application you would want to present various views of the model.

Memory Management

The multiple-document architecture automates much of the memory management for documents and their associated window and document controllers. One of the document controller's responsibilities is to ensure that a document is open and using memory only if it has a window open on the screen. When a window closes, it tells its window controller that it is closing. The window controller, in turn, tells its document that it is closing. The document notes that the window controller is closing, removes the window controller from its list of window controllers, and releases it. As this is the only place the window controller is retained, the window controller gets released and deallocated as a result.

Building a Document-Based Application

It is possible to put together a document-based application without writing very much code. If your requirements are minimal, the AppKit provides you with default window-controller and document-controller instances. You are left with the task of composing the document interface, implementing a subclass of NSDocument, and adding any other custom classes or behavior required by your application.

To show how the pieces of the document-based architecture fit together in practice, we will create a very simple text editor. By the time we're finished with this example, which consists of a relatively small amount of code, we'll have created an application that—without Cocoa's help—might have taken days or weeks to construct and debug.

Document-Based Application Template

Project Builder provides a template named "Document-based Application" to expedite the development of these kinds of applications. This project type provides the following things:

The application's main nib file
This nib contains a standard Cocoa application menu bar. The menu items in the File and Edit menus are already connected to the appropriate first responder action methods in the document controller.
A nib file for the application's document
This nib file contains a single window to which other UI elements can be added. A subclass of NSDocument, named MyDocument , has been created, has an outlet to the document window, and has been made File's Owner of the nib file.
A skeletal NSDocument subclass implementation
The project includes MyDocument.h and MyDocument.mfiles, matching the definition of the NSDocument subclass in the document's nib file. The MyDocument.mfile contains commented starter implementations of important methods (called "stubbed-out" methods) that will help you implement the functionality needed.
A document-type entry in the application's info property list
In the Application Settings pane of the Targets display is a simple user interface for modifying the application's Info.plistfile. The provided file contains placeholder values for global application keys, as well as the document type array.

Create the Project

To get started working on building our text editor:

  1. Launch Project Builder, and choose New Project from the File menu (File → New Project).
  2. Select Cocoa Document-based Application from the application type dialog box, as shown in Figure 10-2.

    Figure 10-2. Creating a document-based application

    Creating a document-based application

  3. Name the project "Simple Text Edit", and save it into your ~/LearningCocoafolder.

Examine the Document Interface

Double-click on the MyDocument.nibfile (located in the Resources folder of the Groups & Files panel in Project Builder), so you can examine the interface in Interface Builder. The nib file is quite simple, as shown in Figure 10-3. There is only a single window with a default text string.

If you select the File's Owner instance and bring up the Inspector (Tools → Show Info, or Shift-[[Image:Learning Cocoa with Objective-C_I_1_tt372.png|]]-I), you'll notice in the Attributes pane that File's Owner is set to correspond to an instance of MyDocument. Also, in the Connections pane, you'll see an outlet with a connection to the window.

Switch back to Project Builder, and double-click on the MainMenu.nibfile to open it in Interface Builder. Click through the menu items with the Connections inspector open, as shown in Figure 10-4, and notice how many of the application's menu items have already been connected to appropriate first responder action methods. These methods are implemented by the application's document controller (an NSDocumentController instance).

Figure 10-3. MyDocument.nib in Interface Builder

MyDocument.nib in Interface Builder

Figure 10-4. Examining prebuilt connections

Examining prebuilt connections

Examine the Document Implementation

Return to Project Builder, and open MyDocument.m, located in the Classes folder of the Groups & Files pane. Examine the skeletal implementation of this NSDocument subclass, and you'll see that the four methods that must be implemented already have a skeletal implementation, as shown in Example 10-1.

Example 10-1. Skeletal NSDocument subclass implementation

#import "MyDocument.h"

@implementation MyDocument

- (id)init
{
    [super init];
    if (self) {
    
        // Add your subclass-specific initialization here.
        // If an error occurs here, send a [self dealloc] message and return nil.
    
    }
    return self;
}

- (NSString *)windowNibName
{
    // Override returning the nib file name of the document
    // If you need to use a subclass of NSWindowController or if your 
    // document supports multiple NSWindowControllers, you should remove
    // this method and override -makeWindowControllers instead.
    return @"MyDocument";
}

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    // Add any code here that need to be executed once the windowController 
    // has loaded the document's window.
}

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    // Insert code here to write your document from the given data.
    // You can also choose to override -fileWrapperRepresentationOfType: 
    // or -writeToFile:ofType: instead.
    return nil;
}

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    // Insert code here to read your document from the given data.
    // You can also choose to override -loadFileWrapperRepresentation:ofType: 
    // or -readFromFile:ofType: instead.
    return YES;
}

@end

Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_1_tt375.png|]]-S), and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_1_tt376.png|]]-R).

Now you can experiment with the document-based application.

  1. Create new document windows (File → New, or [[Image:Learning Cocoa with Objective-C_I_1_tt377.png|]]-N), and close them (File → Close, or [[Image:Learning Cocoa with Objective-C_I_1_tt378.png|]]-W).
  2. Next, try saving a document window (File → Save, or [[Image:Learning Cocoa with Objective-C_I_1_tt379.png|]]-S). Notice that a dialog box asks you to select a location in which to save the document. Choose a location and hit OK. Another dialog box says that the file could not be saved. This is because the default dataRepresentationOfType: method returns nil instead of a valid NSData object, because no default file type has been specified.
  3. Now quit the application (NewApplication → Quit, or [[Image:Learning Cocoa with Objective-C_I_1_tt380.png|]]-Q).

Next, we'll implement the functionality needed to turn this skeleton into a full-blown text editor that allows us to save and open text files.

Compose the Interface

In this section, you'll define the look and feel of the application's document. Just modify the default nib file (created by Project Builder's template) by adding a text view that will allow the user to view and edit text.

  1. Open MyDocument.nib in Interface Builder, if it isn't already open.
  2. Remove the default text object that says "Your document contents here."
  3. Drag an NSTextView to the window from the Cocoa-Data views pane of the palette, as shown in Figure 10-5.

    Figure 10-5. Dragging a text view onto the document window

    Dragging a text view onto the document window

  4. Move and resize the text view so that it occupies the entire window, as shown in Figure 10-6.

    Figure 10-6. Resizing and setting the attributes of the text view

    Resizing and setting the attributes of the text view

  5. With the text view selected, bring up the Size pane in the Inspector. Change the Autosizing options so that the view will follow changes in the windows size.
  6. Switch back to Project Builder, open MyDocument.h, and add a declaration for the text view's outlet by inserting the boldface text shown in Example 10-2.

    Example 10-2. Adding the textView outlet to the NSDocument subclass

    #import <Cocoa/Cocoa.h>
    
    @interface MyDocument : NSDocument
    {
        IBOutlet NSTextView * textView;
    }
    @end
    
  7. Save ([[Image:Learning Cocoa with Objective-C_I_1_tt383.png|]]-S) MyDocument.h.
  8. Bring Interface Builder to the front, and drag MyDocument.hfrom Project Builder's Group & Files listing into the Instances panel of Interface Builder's MyDocument.nibwindow. This gives Interface Builder the opportunity to parse the outlet, so you can use it for connections.
  9. In Interface Builder's Instances pane, Control-drag a connection from the File's Owner instance (this is a proxy for a MyDocument instance) to the text view.
  10. Connect the textView outlet to the view by clicking on the Connect button in the Info window.

    Warning

    Do not generate an instance of MyDocument to make this connection. The document-based application framework makes an instance automatically, which is assigned to the File's Owner object. At runtime, the File's Owner will be an instance of MyDocument.

  11. Save ([[Image:Learning Cocoa with Objective-C_I_1_tt384.png|]] -S) the nib file.

Modify the Info Property List

The Applications Settings pane of the target window allows you to create and modify a variety of application-wide properties. Critical values, like the name of the executable and the name of the main Cocoa class, are provided by default. Many of the other properties are important for a full-fledged application, but they can remain unset for this simple example. You'll learn more about these properties later in the book. For now, don't worry about them.

Our Simple Text Edit application will handle only one kind of data: text. It's very simple to modify the application's info property list to add support for this document type.

  1. In Project Builder, select the Targets pane in the main window.
  2. Select the default (and only) target named Simple Text Edit.
  3. Select the Info.plist Entries → Simple View → Document Types in the outline, as shown in Figure 10-7.
  4. Modify the default document type entry. Rename DocumentType to Text, and replace the quoted question marks with txt in the Extensions field and with TXTin the OS types field. Once you've entered this information, click on the Change button.

    Figure 10-7. Editing a document type

    Editing a document type


These settings allow the document architecture to recognize .txt files as files that can be opened by our application, instructing the system to use an instance of the MyDocument class to open those files. In addition, the system will allow only files saved from a MyDocument instance to have the extension .txt.

Implement the MyDocument Class

Now, we implement the MyDocument class to support reading and writing text data.

  1. In Project Builder, click vertical Files tab, then select the MyDocument.h file from the Classes folder in the Groups & Files panel.
  2. Add the dataFromFile instance variable as shown:
    #import <Cocoa/Cocoa.h>
    
    @interface MyDocument : NSDocument
    {
        IBOutlet NSTextView * textView;
    
        NSData * dataFromFile;
    }
    @end
    

    This variable will hold a reference to the raw data loaded from a file.


Open MyDocument.m. The following steps will fill in the methods of the skeleton source file from Example 10-1. We'll fill in the stubbed methods in a different order than they appear in the file so that we can have each step build on top of the previous one. In addition, we'll show the code without the comments—it's your choice whether to leave them in your application.

  1. Implement the loadDataRepresentation: method so that text data can be loaded from the filesystem into the document. When a new document is created, this method is called before the nib is fully loaded and all of the connections have been made. Because of this, the connection to the text view won't be made yet. In this method, we are just going to store the data object into the dataFromFile variable.
    - (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
    {
        dataFromFile = [data retain];
        return YES;
    }
    
  2. Implement the dataRepresentationOfType: method so that the document can save its contents. The NSTextView class can present its data as a string that we can encode into a data object.
    - (NSData *)dataRepresentationOfType:(NSString *)aType
    {
        NSString * text = [textView string];
                                return [text dataUsingEncoding:NSUTF8StringEncoding];
    }
    
  3. Implement the windowControllerDidLoadNib: method so that text data can be loaded into the text view.
    - (void)windowControllerDidLoadNib:(NSWindowController *) aController
    {
        [super windowControllerDidLoadNib:aController];
        if (dataFromFile){
            NSString * text = [[NSString alloc]initWithData:dataFromFile    // a
                                                                           encoding:NSUTF8StringEncoding];
                                    [textView setString:text];                                      // b
                                    [text release];
                                }
                                [textView setAllowsUndo:YES];                                       // c
    }
    

    The code we added does the following things:

    1. Creates a string from the dataFromFile object.
    2. Sets the string that serves as the textView's model to the string that we just created for the dataFromFile object.
    3. Enables Undo and Redo functionality that is already built into the NSTextView class. With this enabled, text changes can be undone and redone. The Undo Manager can keep an unlimited number of undos in its stack. As well, the document can keep track of the edited status of the application.
  4. Add a dealloc method at the end of the MyDocument.mfile (before the @end statement) to clean up the dataFromFile object.
                            - (void)dealloc
                            {
                                [dataFromFile release];
                                [super dealloc];
                            }
                         
    
  5. Save the project ([[Image:Learning Cocoa with Objective-C_I_1_tt391.png|]]-S), clean it (Build → Clean),[1] and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_1_tt392.png|]]-R). Try the following:
    1. Type some text into the running application. Use Cut and Paste to edit the text.
    2. Save the document. Note the filename appears in the window's titlebar. Make sure that the "Hide Extension" checkbox is not clicked so that you can see the extension of the file in the Finder and other applications.
    3. Play with the spell checker.
    4. Close the document window (File → Close, or [[Image:Learning Cocoa with Objective-C_I_1_tt393.png|]]-W).
    5. Open the document you saved in step 2 in TextEdit (/Applications) to see how Mac OS X's default text editor handles the data created by the Simple Text Editor application.
    6. Quit TextEdit.

Cocoa's multiple-document architecture, as well as the capabilities built into the NSTextView class, provides the functionality that users expect Cfrom a text editing application. We've simply glued these features together by adding just a few lines of code.

Exercises

  1. Read the Apple developer documentation on the NSDocumentController and NSDocument classes.
  2. Add the ability for the editor to read and write Property List (plist) files.
  3. Try to revert (File → Revert) functionality. Can you explain why it doesn't seem to work?

Notes

  1. A bug in Project Builder (up to and including version 2.0.1) requires you to clean the project so that the new Info.plist settings can be incorporated into the application.
Personal tools