Solution
This section presents the relatively simple MVC MYShapeDraw application example shown in Figure 29.1. The example highlights typical tasks a Controller subsystem needs to perform. Initially, the entire implementation of MYShapeDraw's controller subsystem is in just one class. The example includes the kind of code that has historically been written and rewritten for almost every MVC application. Once the MYShapeDraw application is fully developed, the example's controller is redesigned to make it more general and reusable. By the end of this section, the example's Controller subsystem evolves into a clone of the design used by Cocoa's NSArrayController class. Following the step-by-step reinvention of NSArrayController in this chapter reveals why Cocoa's NSObjectController and its subclasses exist and how they're used in applications.
Figure 29.1 The user interface for application
The MYShapeDraw example application has the following features/requirements above and beyond the features provided by all Cocoa document-based applications:
- Provide a simple Model subsystem: just an array of shape objects.
- Provide a custom graphical view to display shape objects.
- Provide a way to add shape objects to the model.
- Provide a way to select zero, one, or multiple shape objects.
- Provide a way to reposition selected shape objects in the custom view.
- Provide a way to remove selected shape objects from the model.
- Provide a table view to display information about shape objects.
- When either the model or any of the views change, update the others.
There's a lot of code in this section because controllers can't be analyzed in isolation. It's necessary to develop a minimal model and view just to see how the controller interacts. Some of the code for the Model and View subsystems is omitted from this chapter for the sake of brevity and to keep the focus on the Controller subsystem. All of the code is available at www.CocoaDesignPatterns.com.
MYShapeDraw Model Subsystem
The model for this example is just an array of MYShape instances. The MYShape class encapsulates a color and a rectangle that defines the shape's position and size.
A more full-featured Model subsystem might include subclasses of MYShape to represent circles, text, images, and groups of shapes. However, the following base MYShape class is sufficient for this example:
@interface MYShape : NSObject <NSCoding> { NSRect frame; NSColor *color; } @property (readwrite, assign) CGFloat positionX; @property (readwrite, assign) CGFloat positionY; @property (readwrite, copy) NSColor *color; // Returns the receiver's frame - (NSRect)frame; // Moves the receiver's frame by the specified amounts - (void)moveByDeltaX:(float)deltaX deltaY:(float)deltaY; // This is a Template Method to customize selection logic. The default // implementation returns YES if aPoint is within frame. Override this // method to be more selective. The default implementation can be // called from overridden versions. - (BOOL)doesContainPoint:(NSPoint)aPoint; @end
The properties declared for the MYShape class are not identical to the instance variables declared for the class. There's no particular reason for properties and instance variables to coincide, and it's convenient for this example to provide positionX and positionY properties. The Accessor methods (see Chapter 10, "Accessors") for the properties are implemented to calculate values relative to the frame. The implementation of the MYShape class is so simple that it doesn't need to be shown here, but it's available in the example source code.
MYShapeDraw View Subsystem
Based on the requirements for this example, there are at least two different ways to view and interact with the model. A custom NSView subclass is needed to display and select shapes and enable graphical repositioning of selected shapes. An ordinary NSTableView is needed to display information about shapes in a table.
This example doesn't require any code in the View subsystem to use a NSTableView. All of the table configuration is performed in Interface Builder, and the upcoming Controller subsystem provides the data the table needs.
Implementing the custom NSView subclass is almost as straightforward as the model. To start, declare the MYShapeView subclass of NSView as follows:
@interface MYShapeView : NSView { IBOutlet id dataSource; } @property (readwrite, assign) id dataSource; // Don't retain or copy @end
No new methods are needed. The entire functionality of MYShapeView is either inherited from the NSView class, overridden from the NSView class, or provided by the one and only property, dataSource. The dataSource is used to implement the Data Source pattern explained in Chapter 15, "Delegates." MYShapeView instances interrogate their data sources to determine what to draw. The MYShapeView is implemented as follows:
@implementation MYShapeView @synthesize dataSource; - (void)dealloc { [self setDataSource:nil]; [super dealloc]; } // Draw all of the MYShape instances provided by the dataSource // from back to front - (void)drawRect:(NSRect)aRect { [[NSColor whiteColor] set]; NSRectFill(aRect); // Erase the background for(MYShape *currentShape in [[self dataSource] shapesInOrderBackToFront]) { [currentShape drawRect:aRect]; } } @end
That's pretty much all it takes to draw shapes. MYShapeView overrides NSView's –drawRect: Template Method to get an array of MYShape instances from the dataSource and then send a message to each shape requesting that it draw itself. Template Methods are explained in Chapter 4, "Template Method." An interesting question arises at this point: How do MYShape instances know how to draw themselves in MYShapeView instances? Drawing is clearly part of the View subsystem, but the MYShape class is declared in the Model subsystem. The solution used in this example applies the Category pattern from Chapter 6, "Category,'' to extend the MYShape class within the View subsystem using the following declaration and implementation:
// Declare an informal protocol that MYShape instances must implement // in order to be displayed in a MYShapeView. @interface MYShape (MYShapeQuartzDrawing) // This is a Template Method to customize drawing. The default // implementation fills the receiver's frame with the receiver's color. // Override this method to customize drawing. The default // implementation can be called from overridden versions, but it is // not necessary to call the default version. - (void)drawRect:(NSRect)aRect; @end @implementation MYShape (MYShapeQuartzDrawing) // Draw the receiver in the current Quartz graphics context - (void)drawRect:(NSRect)aRect { if(NSIntersectsRect(aRect, [self frame])) { [[self color] set]; NSRectFill([self frame]); } } @end
The MYShapeQuartzDrawing category is implemented right in the same file as the MYShapeView class. Therefore, all of the relevant code for drawing MYShape instances in MYShapeViews is maintained together.
The MYShapeView class provides basic display of the MYShape instances supplied by a dataSource. The code to support graphical editing features could be added to the MYShapeView class, but sometimes it's handy to have a simple display-only class like MYShapeView. The graphical editing support will be added in a subclass of MYShapeView called MYEditorShapeView later in the example, but for now, MYShapeView provides enough capability to move on to the Controller subsystem.
MYShapeEditor Controller Subsystem
So now that the model and view are established, what does the Controller subsystem need to do? The Controller subsystem needs to initialize the model either from scratch or by loading a previously saved model. The Controller subsystem must set up the view. The Controller subsystem must supply an object that will serve as the table view's data source and an object that will serve as the custom view's data source. The Controller subsystem must enable adding shapes to the model. The Controller subsystem needs to keep track of which shapes are selected and enable removal of selected shapes from the model. Finally, the Controller subsystem must keep the model and all views up to date.
The list of controller tasks fall into two general categories, coordinating tasks and mediating tasks. Coordinating tasks include loading the Model and View subsystems and providing data sources. Mediating tasks control the flow of data between view objects and model objects to minimize coupling between the subsystems, while keeping them synchronized.
Coordinating Controller Tasks
The first step in the implementation of MYShapeEditor's Controller subsystem is to tackle the coordinating tasks. Almost every MVC application must set up a view and initialize a model, and the Cocoa framework provides the NSDocument class for just that purpose. NSDocument declares the -windowNibName Template Method, which allows subclasses to identify an Interface Builder file containing the view objects to be loaded. The -dataOfType:error: and -readFromData:ofType:error: Template Methods support saving and loading model data. There are alternative, more sophisticated ways to use NSDocument, but those three methods are a good fit for this example.
Create a MYShapeEditorDocument subclass of NSDocument, provide a pointer to the array of shapes that will comprise the model, and override the necessary NSDocument methods. The following is just the starting point; it will be fleshed out as the example progresses:
@interface MYShapeEditorDocument : NSDocument { NSArray *shapesInOrderBackToFront; // The model } @property (readonly, copy) NSArray *shapesInOrderBackToFront; @end
In the implementation of the MYShapeEditorDocument class, the shapesInOrderBackToFront property is redeclared as readwrite in a class extension also known as an unnamed category so that when the property is synthesized, a "set" Accessor method will be generated.
@interface MYShapeEditorDocument () @property (readwrite, copy) NSArray *shapesInOrderBackToFront; @end
The following implementation of MYShapeEditorDocument takes care of the basic model and view creation:
@implementation MYShapeEditorDocument @synthesize shapesInOrderBackToFront; - (NSString *)windowNibName { // Identify the nib that contains archived View subsystem objects return @"MYShapeEditorDocument"; } - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { // Provide data containing archived model objects for document save NSData *result = [NSKeyedArchiver archivedDataWithRootObject: [self shapesInOrderBackToFront]]; if ((nil == result) && (NULL != outError)) { // Report failure to archive the model data *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL]; } return result; } - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { // Unarchive the model objects from the loaded data NSArray *loadedShapes = [NSKeyedUnarchiver unarchiveObjectWithData:data]; if(nil != loadedShapes) { [self setShapesInOrderBackToFront:loadedShapes]; } else if ( NULL != outError) { // Report failure to unarchive the model from provided data *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL]; } return YES; } @end
The -dataOfType:error: method is called by NSDocument as an intermediate step in the sequence of operations to save the document to a file. MYShapeEditorDocument archives the model, an array of shapes, using the Archiving and Unarchiving pattern from Chapter 11 and then returns the resulting NSData instance to be saved. The -readFromData:ofType:error: method is called by NSDocument when a previously saved document is loaded. MYShapeEditorDocument unarchives an array of shapes from the provided data. The -windowNibName method returns the name of the Interface Builder .nib file that contains an archive of the objects that compose the View subsystem. NSDocument unarchives the user interface objects in the named .nib file so they can be displayed on screen.
That's all it takes to specialize the inherited NSDocument behavior for loading the example's document interface and saving/loading the model. However, it's still necessary to create an array to store shapes when a new empty document is created. It's also necessary to clean up memory when documents are deallocated.
NSDocument's -windowControllerDidLoadNib: Template Method is automatically called after all objects have been unarchived from the document's .nib file but before any of the objects from the .nib are displayed. If no array of shapes has been created by the time -windowControllerDidLoadNib: is called, the following implementation of -windowControllerDidLoadNib: creates an empty array of shapes to use as the model:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController { [super windowControllerDidLoadNib:aController]; if(nil == [self shapesInOrderBackToFront]) { // Create an empty model if there is no other available [self setShapesInOrderBackToFront:[NSArray array]]; } }
MYShapeEditorDocument's -dealloc method sets the array of shapes to nil thus releasing the model when the document is deallocated.
- (void)dealloc { [self setShapesInOrderBackToFront:nil]; [super dealloc]; }
NSDocument is one of the most prominent controller classes in Cocoa. NSDocument provides lots of features that aren't directly relevant to this example including management of the document window's title, access to undo and redo support, periodic auto-save operations, printing, and other standard Cocoa features. NSDocument is straightforward to use, and there are similar document classes in other object-oriented user interface frameworks. NSDocument encapsulates most of the coordinating controller features of any multidocument application and leverages Template Methods extensively to enable customization.
Mediating Controller Tasks (Providing Information to Views)
Cocoa provides several mediating controller classes, and once you understand the roles they can play in your design, they're as easy to reuse as the NSDocument class. However, the reuse opportunities for mediator code aren't always readily apparent. For one thing, every application has a unique model and a different view, so how can the code that glues the different subsystems together be reused in other applications? To answer that question, the example implements specific mediator code to meet the application's requirements and then explores how that code is made reusable.
To get started and keep the design simple, implement all of the custom mediation code for MYShapeEditor's Controller subsystem right in the MYShapeEditorDocument class. Figure 29.2 illustrates the design.
Figure 29.2 The initial design for the MYShapeDraw application.
Each MYShapeEditorDocument instance acts as the data source for an associated custom graphic view and the associated table view. MYEditorShapeView only has one data source method, -shapesInOrderBackToFront, and that's already provided by the @synthesize directive for MYShapeEditorDocument's shapesInOrderBackToFront property. The NSTableView class requires its data source to implement -numberOfRowsInTableView: and -tableView:objectValueForTableColumn:row:, so those methods are added to the implementation of MYShapeEditorDocument as follows:
- (int)numberOfRowsInTableView:(NSTableView *)aTableView { return [[self shapesInOrderBackToFront] count]; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { id shape = [[self shapesInOrderBackToFront] objectAtIndex:rowIndex]; return [shape valueForKey:[aTableColumn identifier]]; }
To enable editing in the table view, MYShapeEditorDocument needs to implement the -tableView:setObjectValue:forTableColumn:row: method.
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [self controllerDidBeginEditing]; id shape = [[self shapesInOrderBackToFront] objectAtIndex:rowIndex]; [shape setValue:anObject forKey:[aTableColumn identifier]]; [self controllerDidEndEditing]; }
The -controllerDidBeginEditing and -controllerDidEndEditing methods (shown in bold within the implementation of -tableView:setObjectValue:forTableColumn:row:) are called before and after a shape is modified. Shapes are part of the model. MYShapeEditorDocument consolidates the code for synchronizing the model, the table view, and the custom view into just the -controllerDidBeginEditing and -controllerDidEndEditing methods so that as long as those methods are called before and after a change to the model, everything is kept updated.
The -controllerDidBeginEditing and -controllerDidEndEditing methods are declared in the following informal protocol, a category of the NSObject base class:
@interface NSObject (MYShapeEditorDocumentEditing) - (void)controllerDidBeginEditing; - (void)controllerDidEndEditing; @end
The informal protocol means that MYShapeEditorDocumentEditing messages can safely be sent to any object descended from NSObject. Informal protocols are explained in Chapter 6.
MYShapeEditorDocument overrides its inherited -controllerDidEndEditing implementation with the following code:
- (void)controllerDidEndEditing { [[self shapeGraphicView] setNeedsDisplay:YES]; [[self shapeTableView] reloadData]; }
MYShapeEditorDocument's -controllerEndEditing method tells shapeGraphicView to redisplay itself at the next opportunity and tells shapeTableView to reload itself from its data source, which indirectly causes shapeTableView to redisplay itself, too. In order for -controllerEndEditing to work, Interface Builder outlets for shapeGraphicView and shapeTableView are needed. Therefore, the MYShapeEditorDocument class interface is updated to the following, and the connections to the outlets are made in Interface Builder to match Figure 29.3.
Figure 29.3 outlets enable update of the views.
@interface MYShapeEditorDocument : NSDocument { NSArray *shapesInOrderBackToFront; // The model IBOutlet NSView *shapeGraphicView; IBOutlet NSTableView *shapeTableView; } @property (readonly, copy) NSArray *shapesInOrderBackToFront; @property (readwrite, retain) NSView *shapeGraphicView; @property (readwrite, retain) NSTableView *shapeTableView; @end
Add the corresponding @synthesize directives to the MYShapeEditorDocument implementation:
@synthesize shapeGraphicView; @synthesize shapeTableView;
At this point, the example has produced a bare-bones shape viewer application with minimal shape editing support provided by the table view. The MYShapeEditor0 folder at www.CocoaDesignPatterns.com contains an Xcode project with all of the code so far. Build the project and run the resulting application. Use the application to open the Sample.shape document provided at the same site. You can double-click the X and Y coordinates displayed in the table view to reposition the shapes in the custom view.
Mediating Controller Tasks (Selection Management)
The next feature to add to the Controller subsystem is the ability to keep track of the selected shapes in each document. One question to ask is whether keeping track of the selection is really a controller task at all, or should views perform that function? Storing selection information in the controller enables designs like the one for MYShapeEditor in which multiple views present information about the same model, and selection changes made in one view are reflected in the other views. The Consequences section of this chapter explains how storing selection information in the controller still makes sense even when multiple views have independent selections. Add an instance variable to store the indexes of the selected shapes and selection methods to produce the following MYShapeEditorDocument interface:
@interface MYShapeEditorDocument : NSDocument { NSArray *shapesInOrderBackToFront;// The model IBOutlet NSView *shapeGraphicView; IBOutlet NSTableView *shapeTableView; NSIndexSet *selectionIndexes; // selection } @property (readonly, copy) NSArray *shapesInOrderBackToFront; @property (readwrite, nonatomic, retain) NSView *shapeGraphicView; @property (readwrite, nonatomic, retain) NSTableView *shapeTableView; // Selection Management - (BOOL)setShapeSelectionIndexes:(NSIndexSet *)indexes; - (NSIndexSet *)shapeSelectionIndexes; - (BOOL)addShapeSelectionIndexes:(NSIndexSet *)indexes; - (BOOL)removeShapeSelectionIndexes:(NSIndexSet *)indexes; - (NSArray *)selectedShapes; @end
The selectionIndexes variable uses an immutable NSIndexSet to efficiently identify which shapes are selected. Each MYShape instance in a document can be uniquely identified by the shape's index (position) within the ordered shapesInOrderBackToFront array. If a shape is selected, add the index of the selected shape to selectionIndexes. To deselect a shape, remove its index from selectionIndexes. To determine whether a shape is selected, check for the shape's index in selectionIndexes. The selection management methods for the following MYShapeEditorDocument class are implemented as follows:
// Selection Management - (BOOL)setControllerSelectionIndexes:(NSIndexSet *)indexes { [self controllerDidBeginEditing]; [indexes retain]; [selectionIndexes release]; selectionIndexes = indexes; [self controllerDidEndEditing]; return YES; } - (NSIndexSet *)controllerSelectionIndexes { if(nil == selectionIndexes) { // Set initially empty selection [self setControllerSelectionIndexes:[NSIndexSet indexSet]]; } return selectionIndexes; } - (BOOL)controllerAddSelectionIndexes:(NSIndexSet *)indexes { NSMutableIndexSet *newIndexSet = [[self controllerSelectionIndexes] mutableCopy]; [newIndexSet addIndexes:indexes]; [self setControllerSelectionIndexes:newIndexSet]; return YES; } - (BOOL)controllerRemoveSelectionIndexes:(NSIndexSet *)indexes { NSMutableIndexSet *newIndexSet = [[self controllerSelectionIndexes] mutableCopy]; [newIndexSet removeIndexes:indexes]; [self setControllerSelectionIndexes:newIndexSet]; return YES; } - (NSArray *)selectedObjects { return [[self shapesInOrderBackToFront] objectsAtIndexes: [self controllerSelectionIndexes]]; }
All changes to the set of selection indexes are funneled through the -setShapeSelectionIndexes: method, which calls [self controllerDidBeginEditing] before updating the selection and [self controllerDidEndEditing] after the update. As a result, changes to the selection cause refresh of both the custom view and the table view. A selection change made from one view is automatically reflected in the other.
When the user changes the selection in the table view, NSTableView informs its delegate and gives the delegate a chance to affect the change via the -tableView:selectionIndexesForProposedSelection: method. In addition to acting as the data source for the table view, each MYShapeEditorDocument instance also acts as the delegate for its table view. The following MYShapeEditorDocument implementation of -tableView:selectionIndexesForProposedSelection: keeps the controller's selection up to date.
// NSTableView delegate methods - (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection: (NSIndexSet *)proposedSelectionIndexes { [self setControllerSelectionIndexes:proposedSelectionIndexes]; return proposedSelectionIndexes; }
Mediating Controller Tasks (Adding and Removing Model Objects)
Adding new shape instances to the model and later removing selected shapes are best performed by Action methods (see Chapter 17, "Outlets, Targets, and Actions"). Add the following two method declarations to the interface for the MYShapeEditorDocument class:
// Actions - (IBAction)addShape:(id)sender; - (IBAction)removeSelectedShapes:(id)sender;
The Action methods are called by buttons in the View subsystem. Implement the Action methods as follows:
- (IBAction)addShape:(id)sender; { [self controllerDidBeginEditing]; [self setShapesInOrderBackToFront:[shapesInOrderBackToFront arrayByAddingObject:[[[MYShape alloc] init] autorelease]]]; [self controllerDidEndEditing]; } - (IBAction)removeSelectedShapes:(id)sender; { [self controllerDidBeginEditing]; NSRange allShapesRange = NSMakeRange(0, [[self shapesInOrderBackToFront] count]); NSMutableIndexSet *indexesToKeep = [NSMutableIndexSet indexSetWithIndexesInRange:allShapesRange]; [indexesToKeep removeIndexes:[self controllerSelectionIndexes]]; [self setShapesInOrderBackToFront:[[self shapesInOrderBackToFront] objectsAtIndexes:indexesToKeep]]; [self setControllerSelectionIndexes:[NSIndexSet indexSet]]; [self controllerDidEndEditing]; }
The next step is to add graphical selection and editing of shapes to the application.
Extending the MYShapeDraw View Subsystem for Editing
Create a subclass of MYShapeView called MYEditorShapeView with the following declaration:
@interface MYEditorShapeView : MYShapeView { NSPoint dragStartPoint; } @end
The dragStartPoint instance variable is just an implementation detail that supports graphical dragging to reposition shapes with the mouse. The partial implementation of MYEditorShapeView that follows is provided to show how the custom view uses its data source to implement selection and editing features, but most of the details aren't important to the Controller subsystem:
@implementation MYEditorShapeView // Overrides the inherited implementation to first draw the shapes and // then draw any selection indications - (void)drawRect:(NSRect)aRect { [super drawRect:aRect]; [NSBezierPath setDefaultLineWidth:MYSelectionIndicatorWidth]; [[NSColor selectedControlColor] set]; // Draw selection indication around each selected shape for(MYShape *currentShape in [[self dataSource] selectedShapes]) { [NSBezierPath strokeRect:[currentShape frame]]; } } // Select or deselect shapes when the mouse button is pressed. // Standard management for multiple selection is provided. A mouse // down without modifier key deselects all previously selected shapes // and selects the shape if any under the mouse. If the Shift modifier // is used and there is a shape under the mouse, toggle the selection // of the shape under the mouse without affecting the selection status // of other shapes. - (void)mouseDown:(NSEvent *)anEvent { NSPoint location = [self convertPoint:[anEvent locationInWindow] fromView:nil]; // Set the drag start location in case the event starts a drag // operation [self setDragStartPoint:location]; // ... The rest of the implementation omitted for brevity ... } // Drag repositions any selected shapes - (void)mouseDragged:(NSEvent *)anEvent { [[self dataSource] controllerDidBeginEditing]; NSPoint location = [self convertPoint: [anEvent locationInWindow] fromView:nil]; NSPoint startPoint = [self dragStartPoint]; float deltaX = location.x - startPoint.x; float deltaY = location.y - startPoint.y; for(MYShape *currentShape in [[self dataSource] selectedShapes]) { [currentShape moveByDeltaX:deltaX deltaY:deltaY]; } [self setDragStartPoint:location]; [self autoscroll:anEvent]; // scroll to keep shapes in view [[self dataSource] controllerDidEndEditing]; } @end
Controllers are responsible for keeping views and models up to date with each other but can't fulfill that role if the model is changed behind the controller's back. Therefore, views must inform the controller about changes made to the model. The two bold lines of code in the implementation of MYEditorShapeView's -mouseDragged: method notify the controller when model objects are modified directly by the view.
You can inspect the full implementation of MYEditorShapeView and the Interface Builder .nib files in the MYShapeEditor1 folder at www.CocoaDesignPatterns.com. Take a little time to explore MYShapeEditor1 application. In spite of the fact that it has taken quite a few pages to describe how it all works, there really isn't very much code. Play with the application.
Redesigning and Generalizing the Solution
MYShapeEditor1 meets all of the example's requirements with straightforward method implementations written from scratch. It might seem like the mediation "glue" code is unique to this example. However, it's pretty common for Model subsystems to store arrays of objects. Certainly, more complex models may use more complex data structures or contain many different arrays of objects, but a class that generalizes the approach used in this example to mediate between any array of arbitrary model objects and multiple views can be reused in a wide variety of applications. So the challenge now is to find and encapsulate the reusable parts of this example to provide that general solution.
Start by creating a new class to implement the general solution and call that class MYMediatingController. Then examine the current implementation of MYShapeEditorDocument and identify features to move to the new class. A general mediating controller must be able to add and remove model objects, so move the –add: and -remove: Action methods to the new class. Selection management is needed in the new class, so move the selectionIndexes instance variable from the MYShapeEditorDocument to the MYMediatingController class. Move all of the selection management methods like - controllerSetSelectionIndexes: and -controllerAddSelectionIndexes: to the new class. Finally, a mediator for an arbitrary array of model objects needs to provide access to that array. Add a method called –arrangedObjects that returns an NSArray pointer. The MYMediatingController declaration should look like the following:
@interface MYMediatingController : NSObject { NSIndexSet *selectionIndexes; // The selection } // arranged content - (NSArray *)arrangedObjects; // Actions - (IBAction)add:(id)sender; - (IBAction)remove:(id)sender; // Selection Management - (BOOL)controllerSetSelectionIndexes:(NSIndexSet *)indexes; - (NSIndexSet *)controllerSelectionIndexes; - (BOOL)controllerAddSelectionIndexes:(NSIndexSet *)indexes; - (BOOL)controllerRemoveSelectionIndexes:(NSIndexSet *)indexes; - (NSArray *)selectedObjects; @end
After the redesign, all that's left in the MYShapeEditorDocument interface is the following:
@interface MYShapeEditorDocument : NSDocument { NSArray *shapesInOrderBackToFront; // The model IBOutlet NSView *shapeGraphicView; IBOutlet NSTableView *shapeTableView; } @property (readonly, copy) NSArray *shapesInOrderBackToFront; @property (readwrite, retain) NSView *shapeGraphicView; @property (readwrite, retain) NSTableView *shapeTableView; @end
As the coordinating controller, MYShapeEditorDocument needs a way to configure the mediating controller. Add an outlet called mediatingController to the interface of MYShapeEditorDocument so that document instances can be connected to a mediating controller via Interface Builder. MYShapeEditorDocument also needs a way to be notified when the model is changed via the Controller subsystem, so add a -mediatingControllerDidDetectChange: method to MYShapeEditorDocument. The MYShapeEditorDocument class is now declared as follows:
@interface MYShapeEditorDocument : NSDocument { NSArray *shapesInOrderBackToFront; // The model IBOutlet NSView *shapeGraphicView; IBOutlet NSTableView *shapeTableView; IBOutlet MYMediatingController *mediatingController; } @property (readonly, copy) NSArray *shapesInOrderBackToFront; @property (readwrite, retain) NSView *shapeGraphicView; @property (readwrite, retain) NSTableView *shapeTableView; @property (readwrite, retain) MYMediatingController *mediatingController; - (void)mediatingControllerDidDetectChange: (NSNotification *)aNotification; @end
Implement MYShapeEditorDocument's -mediatingControllerDidDetectChange: to synchronize the custom shape view and the table view with the model:
- (void)mediatingControllerDidDetectChange: (NSNotification *)aNotification; { [[self shapeGraphicView] setNeedsDisplay:YES]; [[self shapeTableView] reloadData]; [[self shapeTableView] selectRowIndexes: [[self mediatingController] controllerSelectionIndexes] byExtendingSelection:NO]; }
The MYShapeEditor2 folder at www.CocoaDesignPatterns.com contains an Xcode project with the redesign completed. There is an instance of MYMediatingController in the document .nib, and the dataSource outlets of view objects are connected to the mediating controller. The new design is illustrated in Figure 29.4.
Figure 29.4 The new design of
The implementation MYMediatingController shouldn't have any dependencies on other classes in MYShapeEditor, or it won't be reusable in other applications. For example, when MYMediatingController adds new objects to the model, what kind of objects should it add? The class of added objects must be configurable at runtime to keep MYMediatingController general. MYMediatingController also needs a general way to get access to the array of model objects. Add the following instance variable declarations to MYMediatingController:
Class objectClass; // Class of model objects IBOutlet id contentProvider; // Provider of model array NSString *contentProviderKey; // The array property name
At runtime, the contentProvider outlet is connected to whatever application-specific object provides the array of model objects. The contentProviderKey variable contains the name of the array property provided by contentProvider. Setting both the provider and the name of the provider's array property at runtime ensures maximum flexibility.
All that remains is to implement the MYMediatingController without any application-specific dependencies. The implementation of selection management and table view delegate methods are the same in MYMediatingController as they were in MYShapeEditorDocument. The rest of the code in MYMediatingController is similar to the code previously implemented in MYShapeEditorDocument, but the new code can be reused in any application. The following implementation of MYMediatingController shows the changes from MYShapeEditorDocument in bold but omits the implementations of methods that are identical in both classes to keep the listing short.
@implementation MYMediatingController @synthesize objectClass; @synthesize contentProvider; @synthesize contentProviderKey; - (void)dealloc { [self controllerSetSelectionIndexes:nil]; [self setContentProvider:nil]; [self setContentProviderKey:nil]; [super dealloc]; } // arranged content - (NSArray *)arrangedObjects { return [[self contentProvider] valueForKey: [self contentProviderKey]]; } // Actions - (IBAction)add:(id)sender; { [self controllerDidBeginEditing]; NSArray *newContent = [[self arrangedObjects] arrayByAddingObject: [[[[self objectClass] alloc] init] autorelease]]; [[self contentProvider] setValue:newContent forKey: [self contentProviderKey]]; [self controllerDidEndEditing]; } - (IBAction)remove:(id)sender; { [self controllerDidBeginEditing]; NSRange allObjectsRange = NSMakeRange(0, [[self arrangedObjects] count]); NSMutableIndexSet *indexesToKeep = [NSMutableIndexSet indexSetWithIndexesInRange:allObjectsRange]; [indexesToKeep removeIndexes:[self controllerSelectionIndexes]]; NSArray *newContent = [[self arrangedObjects] objectsAtIndexes:indexesToKeep]; [[self contentProvider] setValue:newContent forKey: [self contentProviderKey]]; [self controllerSetSelectionIndexes:[NSIndexSet indexSet]]; [self controllerDidEndEditing]; } // Editing - (void)controllerDidEndEditing { [[NSNotificationCenter defaultCenter] postNotificationName:MYMediatingControllerContentDidChange object:self]; } // NSTableView data source methods - (int)numberOfRowsInTableView:(NSTableView *)aTableView { return [[self arrangedObjects] count]; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { id shape = [[self arrangedObjects] objectAtIndex:rowIndex]; return [shape valueForKey:[aTableColumn identifier]]; } - (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { [self controllerDidBeginEditing]; id shape = [[self arrangedObjects] objectAtIndex:rowIndex]; [shape setValue:anObject forKey:[aTableColumn identifier]]; [self controllerDidEndEditing]; } @end