Mac OS X Reference Library Apple Developer
Search

Adding a Department Object

The original task specification stated that each document represents an individual department and the employees associated with it. Thus far, however, the only actions that have been taken with respect to departments has been to remove all references to them. In this section you add a Department object to the document—ensuring that only one department is associated with the document—and reconfigure the user interface appropriately.

Creating the Department

When a new document is created, you need to create a Department object, avoiding undo registration (so that a new document does not appear edited when it is first presented). If the user opens a saved document, the Department object should already exist (it is retrieved from the persistent store). NSDocument provides a method—initWithType:error:—that is called only when a new document is created, not when it is subsequently reopened. You can therefore create the Department object in this method, and be assured that when a document is reopened a new Department object will not be created.

Steps

  1. In the MyDocument class header file, add an instance variable, department, of type NSManagedObject, and a corresponding property. The class declaration should now look like this:

    @interface MyDocument : NSPersistentDocument
    {
        NSManagedObject *department;
    }
    @property (nonatomic, assign) NSManagedObject *department;
    @end
  2. In the MyDocument class, synthesize the department property.

    @synthesize department;
  3. In the MyDocument class implementation file, add the instance method -(id)initWithType:error:. The first step is to set self to the result of calling the superclass’s implementation, then check to ensure that self is not nil. The remainder of the implementation described in the following steps is contained within the conditional.

    - (id)initWithType:(NSString *)type error:(NSError **)error {
        self = [super initWithType:type error:error];
        if (self != nil) {
            // implementation continues...
        }
        return self;
    }
  4. To create a new instance of department, it is easiest to use the NSEntityDescription convenience method insertNewObjectForEntityForName:inManagedObjectContext:. The method requires as its second argument a managed object context. You get this from the document itself. The method returns the new object.

    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    self.department = [NSEntityDescription insertNewObjectForEntityForName:@"Department"
                    inManagedObjectContext:managedObjectContext];

    Note that this illustrates the strategy the employees array controller takes to create a new object. You don’t specify the class of the new object, you specify its entity, just as you specify an entity for the array controller.

  5. When you insert the new object into the managed object context it registers the event with its undo manager, so when a new document is created it would appear dirty (edited). To prevent this (and to stop the user from undoing the creation and insertion of the department) you disable undo registration before inserting the new managed object then re-enable it afterwards. Invoke processPendingChanges on the managed object context to ensure changes are propagated.

    After the line NSManagedObjectContext *managedObjectContext = ... disable undo registration:

    [[managedObjectContext undoManager] disableUndoRegistration];

    After the line self.department = ..., process changes and re-enable undo registration:

    [managedObjectContext processPendingChanges];
    [[managedObjectContext undoManager] enableUndoRegistration];

Complete Code Listing

The complete listing for initWithType:error: is shown in Listing 4-1.

Listing 4-1  The complete listing for initWithType:error:

- (id)initWithType:(NSString *)type error:(NSError **)error {
    self = [super initWithType:type error:error];
    if (self != nil) {
        NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
        [[managedObjectContext undoManager] disableUndoRegistration];
        self.department = [NSEntityDescription insertNewObjectForEntityForName:@"Department"
                inManagedObjectContext:managedObjectContext];
        [managedObjectContext processPendingChanges];
        [[managedObjectContext undoManager] enableUndoRegistration];
    }
    return self;
}

Fetching the Department

Note:  This example follows the traditional Cocoa pattern of adding the Department as an instance variable to the document class. Using Core Data, there may be no need to do this‚Äîsee ‚ÄúAdopting the Mediator Pattern.‚Äù The traditional pattern is used here to illustrate aspects of fetching an object. There are also other situations in which this pattern remains valid and useful.

If you need to access the department from within any of your document’s methods, you need to fetch it from the persistent store.

In order to perform a fetch, you need a fetch request and a managed object context. The fetch request specifies what instances of a particular entity it is that you fetch. By implication, therefore, you also need at least an entity description. The managed object context is the gateway to the underlying persistent store coordinator and hence persistent stores.

You can define an accessor method for the department. The first thing it should do is check whether or not the department has already been fetched. If it has, return it immediately. If it has not already been fetched, create a fetch request for the Department entity and fetch from the document’s managed object context.

Steps

  1. In the MyDocument class implementation file, add the instance method -(NSManagedObject *)department. The first step is to check whether department is not nil. If it is not, return it.

    - (NSManagedObject *)department {
        if (department != nil) {
            return department;
        }
        // implementation continues...
        return department;
    }

  2. To use a fetch request, you need a managed object context, an NSError variable to pass as an argument to the fetch method, and an array variable to which the returned value is assigned. Given these, you can create the fetch request.

    NSManagedObjectContext *moc = [self managedObjectContext];
    NSError *fetchError = nil;
    NSArray *fetchResults;
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  3. As a minimum for the fetch request, you must specify the entity description for the entity that is to be fetched. You may also provide a predicate and an array of sort descriptors. In this case there is (or should be!) only one department to fetch, so neither a predicate nor sort orderings are required.

    You get the entity description using a convenience method—entityForName:inManagedObjectContext:—of NSEntityDescription. It takes as its arguments the name of an entity and a managed object context. It uses the context to find the persistent store coordinator, and from the model associated with the coordinator, the entity description with the specified name.

    You set the entity for the fetch request, then use the context to execute the fetch.

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Department"
                inManagedObjectContext:moc];
    [fetchRequest setEntity:entity];
    fetchResults = [moc executeFetchRequest:fetchRequest error:&fetchError];
  4. If there is one object in the returned array, and there is no fetch error, the object in the array is the Department object. If these conditions are not satisfied, then something has gone wrong. If there is an error, you can display it most easily using NSDocument's presentError: method. This, however, creates an application-modal window, so ideally you should use presentError:modalForWindow:delegate:didPresentSelector:contextInfo: to present a panel modal just for the window, but showing how to do that lies outside the scope of this example (it requires significant additional code and explanation that is not directly related to understanding Core Data and NSPersistentDocument). If there is no error, but either the result is nil or the count of the results array is not 1, then something has gone wrong and the user should be alerted. Again how to do this is not shown here.

    if ((fetchResults != nil) && ([fetchResults count] == 1) && (fetchError == nil)) {
        self.department = [fetchResults objectAtIndex:0];
        return department;
    }
    if (fetchError != nil) {
        [self presentError:fetchError];
    }
    else {
        // should present custom error message...
    }
    return nil;

Complete Code Listing

The complete listing for the department method is given in Listing 4-2.

Listing 4-2  The complete listing for the department method

- (NSManagedObject *)department
{
    if (department != nil) {
        return department;
    }
 
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSError *fetchError = nil;
    NSArray *fetchResults;
 
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Department"
                                              inManagedObjectContext:moc];
 
    [fetchRequest setEntity:entity];
    fetchResults = [moc executeFetchRequest:fetchRequest error:&fetchError];
 
    if ((fetchResults != nil) && ([fetchResults count] == 1) && (fetchError == nil)) {
        self.department = [fetchResults objectAtIndex:0];
        return department;
    }
 
    if (fetchError != nil) {
        [self presentError:fetchError];
    }
    else {
        // should present custom error message...
    }
    return nil;
}

Custom Department Methods

In this tutorial, you do not create a custom Department class. There is no custom logic and there’s no need to implement custom accessor methods—Core Data generates efficient accessors for you at runtime (see Managed Object Accessor Methods). Later in the tutorial (“Setting Metadata for a Store”), however, you need to access the document’s department’s name. You can do this using key-value coding:

NSString *departmentName = [[self department] valueForKey:@"name"];

but it is more efficient to use accessor methods (this example shows using the dot syntax):

NSString *departmentName = self.department.name;

(The efficiency gain in the situation described in “Setting Metadata for a Store” is minimal, however the principle here is the important thing.) To avoid compiler warnings, though, you need to declare the accessor methods. You can do this using properties in a category of NSManagedObject.

Update the User Interface

You can now update the user interface to include the Department object. Figure 4-1 provides an example of how the user interface might look when you have finished.

  1. Open the MyDocument nib file in Interface Builder. Add an NSObjectController instance. Its entity is Department. Bind its managedObjectContext to File’s Owner’s managedObjectContext and its contentObject to File’s Owner’s department.

  2. Add two text fields to the window. Bind the value of one to the department’s name (bind to the Department object controller’s selection.name), the other to the department’s budget—the latter requires a number formatter (so that the input string is converted into a number object). (To set a formatter in Interface Builder, drag a number formatter from the Cocoa library onto the text field.)

Figure 4-1  User interface with department

User interface with departmentUser interface with department

You can now specify that the employee array controllers retrieve their employees not directly from the managed object context, but from the department’s employees relationship. This is important to ensure that when a new employee is added, it is properly added to the department’s employees relationship, and the employee’s department relationship is set.

Remember to bind the array controller’s contentSet, not its contentArray. Managed objects represent to-many relationships using a set, not an array. Remember also that by default, removing an object from an array controller whose content set is bound to a relationship simply removes the object from the relationship, not from the object graph. If (as is the case) you want the remove operation to act as a delete, you must enable the Deletes Objects On Remove option for the contentSet binding.

  1. For each array controller, bind the contentSet to the department controller’s selection.employees.

  2. Inspect the bindings for the Employees array controller that manages the content of the table view. For the contentSet binding, enable the Deletes Objects On Remove option.

Build and Test

Build and run the application again. You should find that if you set the department name and then save the document, when you reopen the document, its department’s name is properly reconstituted.

Supporting Document Revert

Since the document maintains a strong reference to the department object, you should make sure that it is let go in the case of the document being reverted. You need to implement a revertToContentsOfURL:ofType:error: method, as follows:

- (BOOL)revertToContentsOfURL:(NSURL *)inAbsoluteURL ofType:(NSString *)inTypeName error:(NSError **)outError {
    BOOL result = [super revertToContentsOfURL:inAbsoluteURL ofType:inTypeName error:outError];
    if (result) {
        self.department = nil;
    }
    return result;
}

Adopting the Mediator Pattern

In the Cocoa document architecture, an NSDocument instance serves primarily as a model controller and one or more instances of NSWindowController serve as view controllers (see The Roles of Key Objects in Document-Based Applications). The mediator pattern extends this concept of distributed control—mediating controllers mediate the flow of data between view objects and model objects in an application (for a general discussion, see Cocoa Design Patterns).

In the current application, the Department instance serves as a “root” object for the graph of model objects. This is a common pattern in traditional Cocoa programming, and you would typically access other model objects via relationships from this root object through the document instance. Many developers may be more used to and more comfortable with the idea of keeping a reference to a root model object or collection. If you are using Core Data, however, the recommendation is not to do that (unless it is necessary or useful), but instead to hand responsibility for model object graph to the managed object context. If you need a reference to a particular instance, you either execute a fetch request, or (more commonly) retrieve it from the relevant object controller.

In this example, there is actually no need to keep an explicit reference to the department in the document instance, although it is convenient for an implementation of the paste method (see “Paste”) and—to reiterate—here it served the useful purpose of illustrating how to fetch objects from the document's managed object context. Since there is only ever one Department record, however, the NSObjectController object can simply fetch it directly. Thus, instead of binding the department object controller’s contentObject to the File’s Owner, you bind only its managedObjectContext and configure it to automatically prepare content (see setAutomaticallyPreparesContent:)—this means that at runtime, the controller automatically executes a fetch to fill its content. Since there is one and only one Department instance, the correct Department instance is retrieved. If you do need a reference to the instance, you can either fetch it or retrieve it from the object controller. You then dispense with the department and setDepartment: methods, and the explicit support for the revert method described in “Supporting Document Revert.”

What Happened?

More of the task goals have now been met through adding an instance of Department to the document.




Last updated: 2009-02-04

Did this document help you? Yes It's good, but... Not helpful...