The majority of Core Data sample code is for either single-window applications or single-document-window applications. There are, however, times when an application or document requires more than one window on its data. When starting out learning to write code with Core Data, I spent a great deal of time trying to find a tutorial that covered multi-window applications; aside from Apple’s CoreRecipes application (which I found rather frightening to disassemble), there is very little reference information ‘out there’.
In this article I aim to show the way I code multi-window Cocoa data apps. I am by no means trying to say how all such code should be written and I definitely don’t claim that what I am currently doing is absolutely the best way to do things. Hopefully the article may help someone else who is beginning to write a multi-window Core Data application. Please accept my apologies if it is written at a level below your own knowledge; on the other hand if it assumes too much or you don’t understand something, feel free to contact me and ask.
- Edit – The example project for this post is available to download from here: coredatamultiwin.zip.
Before we start with the theory, let’s have a look at what we’re going to create. This sample application is a very simple example of a company ‘people records’ database application.
The main screen displays a list of departments alongside a list of the people in a selected department; you can also click a button to view the details of a selected person in a new window. Clearly this particular application would be perfectly fine as a single-window application and I have deliberately made it as simple as possible. It doesn’t take much imagination, though, to see how this could be extended, making multi-window display essential. I have spent a large amount of time with commercial Fundraising database products like The Raiser’s Edge, and in these it is often necessary to view multiple records at the same time. You might, for example, need to look at individual screens for a husband and wife to see how much money they have donated as a couple, or perhaps have two different company records on screen to compare their history.
When working with multiple windows and Core Data, the key is to understand the primary concepts of the framework. When I started trying to write a Core Data application, it was great to see the walk-through tutorials that involve hardly any coding but this did lead to a slight frustration that I hadn’t got quite the right understanding of the underlying foundation to expand the given examples in the way I wanted. Understanding exactly what a Managed Object Context is and how it deals with the data held in a Persistent Store is very important to using multiple windows with Core Data. I will try to give more information about the core concepts as we move through the article.
I’ll assume that if you’re reading this, you know at least the basics of creating Cocoa and specifically Core Data apps so I won’t provide too many screenshots of that sort of task! Using Xcode to generate a new Core Data Document-based application produces a project with, amongst other things, a MyDocument
object inheriting from NSPersistentDocument
. NSPersistentDocument
is similar to NSDocument
in that it handles opening and saving files but adds built-in Core Data functionality for working with data in XML, Binary and SQLite formats. It is this document object (as with a traditional NSDocument
object) that keeps track of the windows specific to an open document, keeping them separate from any general application-related windows that are handled by the application object or its delegate. To work with multiple windows in one document, we need to change the standard document object to work with multiple window controllers.
A window controller instance (of NSWindowController
) acts as the control code for a window that is displayed on screen. Typically, you subclass NSWindowController
for each different type of window that you display (in our example application, these would be the window that displays the lists of departments and people and the window that displays details of a single person). You then need to create an actual instance of an NSWindowController
or its subclass for every single window that is displayed on screen.
If you haven’t already, go ahead and create a new ‘Core Data Document-based application.’ By default, its NSPersistentDocument
object called MyDocument
is set to work with only one window that is taken from a nib file called MyDocument.nib. If you open the ‘MyDocument.m’ file, you will see that it uses the method windowNibName:
to get the name of the nib file for the window and then opens the window contained within the nib, after which it calls windowControllerDidLoadNib:
.
An NSDocument
/NSPersistentDocument
has two possible ways to open its document windows. When a new document object is created, if implementation code exists for the windowNibName:
method, that method will be called and a single window will be created for use as the document’s window. If this code is not implemented, the document object will instead try to call its makeWindowControllers:
method.
A document object maintains its own array of NSWindowController
instances — ie an array of single NSWindowController
or subclass instances for each window that is currently open for the document. When using the makeWindowControllers:
method, opening a new window for a document requires that you allocate some memory to a window controller instance, initialize it as with any other object, and then add it to the document object’s array of window controllers. Each window can also be specified to be important enough to close the document object (and therefore any other open document windows) if it is itself closed. In our example application, the main document should be closed when the window that displays the list of people is closed but not when any individual windows of a person’s details are closed.
For now, we are going to change the MyDocument.m file to use makeWindowControllers:
rather than windowNibName:
.
Delete the windowNibName:
method and replace it with this:
- (void)makeWindowControllers { NSWindowController *mainWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self]; [mainWindowController setShouldCloseDocument:YES]; [self addWindowController:mainWindowController]; }
The code is fairly self-explanatory. It creates a generic NSWindowController
instance using the MyDocument.nib file, specifies that it should close the document when the window is closed, and then adds the controller instance to the document object’s window controller array.
Build and run the application and as the default behaviour is to open a new, untitled document, our standard document window will open in exactly the same way as if you’d left the original windowNibName:
code in MyDocument.m as it was when created. Choosing ‘File -> New’ will show a new window with a title to match that of the new and unsaved document (‘Untitled 2′, for example).
To create the data model for our sample application, open up MyDocument.xcdatamodel in Xcode.
Department
and give it a string attribute called name
.Person
and give it string attributes for firstName
and lastName
. Department
entity, create a to-many relationship employees
with destination Person
.Person
entity, create a to-one relationship to the Department
entity and call it department
.department
relationship the inverse of the employees
relationship.My data model looks like this:
We’re going to set up the interface manually rather than use the ‘option-drag from model to window’ approach so go ahead and open the MyDocument.nib file in Interface Builder.
NSArrayController
, specify its Mode as ‘Entity’ and set the Entity Name to ‘Department’. It’s helpful to name array controllers to avoid confusion when using Cocoa Bindings so use the Identity Inspector tab to set the name to ‘Departments’.Open the Window
and delete the existing content label. Add two NSTableViews
(one with one column and one with two) and some buttons to look like the interface below:
Go back to XCode and build and run the project. Everything should function as expected — you should be able to add and remove departments as expected, with employees being inserted into the currently selected department (and if no department is selected, the ‘+’ button under the employees table does nothing), and also Undo your actions.
The interface might not be terribly pleasing but it is reasonable enough for this example application. If you wish, disable the ‘+’ and ‘-’ buttons when these actions are disabled by their NSArrayController
— easily accomplished by binding the ‘Enabled’ attribute on each button to the relevant array controller’s ‘canAdd’ and ‘canRemove’ keys.
Now for a little theory. When you were making the array controllers in our sample application, you bound their Managed Object Context to the File’s Owner. It’s worth at this point saying a little about Managed Object Contexts before we continue with developing our application. If you are happy that you understand how Managed Object Contexts relate to Core Data, feel free to skip the rest of this section.
Apple documentation says you should think of a Managed Object Context as a ‘scratchpad’ for data. It also helps me to think of a Managed Object Context behaving like a cloud, hovering over the ‘persistent’ stored data. In a Core Data application, persistent data is stored in a ‘Persistent Store’. For this article it’s not necessary to go to much into Persistent Stores or their Co-ordinators but it is important to understand the keyword persistent. In Core Data, the data that is ‘persistent’ is the actual data which is physically stored on disc — for example when you choose to save a document in our sample application, the entries you have made in the Departments and People tableviews are made persistent and stored to disc. What then, are you seeing on screen before you save to a persistent store? Well, the data you are editing is being held in a ‘temporary’ place — in our Managed Object Context (MOC for short from now on…).
When you open a document in our sample application, the NSPersistentDocument
object MyDocument
automatically creates an empty NSManagedObjectContext
object. As the main window is instantiated from the nib file, the Departments NSArrayController
asks its MOC to load any persistent data for the Entity that you have specified, in this case the ‘Department’ records. Core Data is all about Managed Objects (hence the Managed Object Context) and these ‘records’, or instances of the ‘Department’ entity, are instances of NSManagedObject
(or a custom subclass of NSManagedObject
). If the current document is a new document, there are obviously no pre-existing persistent managed objects for the MOC to load.
Once the window appears on screen, you might add a department to the Departments array controller; the array controller tells its MOC to create a new managed object using the ‘Department’ entity specification, which you are then able to edit. It’s only when you Save the document that the MOC ‘rains down’ its data into the Persistent Store. You are therefore able to make multiple changes to managed objects, add new objects or remove existing ones, but nothing affects the data stored on disc until you actually tell the MOC to persist its data by telling the document to Save.
Let’s consider a case where you have previously saved a document with two Department records. When you open the document, the Departments array controller asks the MOC for any managed objects with the ‘Department’ entity specification. The MOC then asks the Persistent Store for these objects and retrieves them into its memory (in fact, Core Data uses a system of ‘faults’ to load data only when needed but I won’t go into this here). It remembers which objects in its memory relate to existing persistent objects such that when you tell the MOC to persist any changes, it updates the values in the persistent store. Imagine that you delete one of the two existing departments and add a new one — until you save, the data on screen at this point is that held only in the MOC. If you close the document without saving (or your application crashes…), the MOC will be deallocated from memory without updating the persistent store and your original document will remain unaffected.
The MOC is also what provides Undo support ‘for free’ in a Core Data application. When you choose ‘Edit -> Undo’, the MOC for that window is asked to step back through its recent activity. Again, nothing changes in the persistent store until you Save.
As a trick, go back and change the MyDocument
makeWindowControllers:
method to open a second window thus:
- (void)makeWindowControllers { NSWindowController *mainWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self]; [mainWindowController setShouldCloseDocument:TRUE]; [self addWindowController:mainWindowController]; NSWindowController *secondWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self]; [self addWindowController:secondWindowController]; }
When you run the application, two windows will appear. Both will have the same MOC so if you add a Department in one window, it will also appear in the list in the other and vice-versa. And, at the risk of being punched in the face for repetition, the persistent store isn’t affected until you Save the document.
Make sure you change the makeWindowControllers:
method back to its original ‘one window’ implementation before proceeding.
Let’s move on now to create the window that will display the information for individual employees in our application. In the existing nib file, the File’s Owner is set to be the MyDocument
object (by the use of [...initWithWindowNibName: owner:self]
in the makeWindowControllers:
method) and it is this that provides the array controllers with their Managed Object Context (remember an NSPersistentDocument
object automatically creates one managedObjectContext
for your use). But, for our new person window, in order to keep track of which person that window represents we’re going to have to subclass NSWindowController
; the File’s Owner of this new window nib will no longer be MyDocument
but our new subclass. In order to be able to provide a MOC to the objects in the new nib file, we will have to keep a reference to it in our subclass, and be able to specify this when the window controller instance is created.
NSManagedObjectContext
pointer called _moc
.managedObjectContext:
and setManagedObjectContext:
, together with an initialization method.Your code should look like this:
@interface PersonWindowController : NSWindowController { NSManagedObjectContext *_moc; } - (void)setManagedObjectContext:(NSManagedObjectContext *)value; - (NSManagedObjectContext *)managedObjectContext; - (PersonWindowController *)initWithManagedObjectContext:(NSManagedObjectContext *)inMoc; @end
The initialization method will be used to set up a PersonWindowController
instance with the specified MOC.
In the PersonWindowController.m file, add implementations for the methods thus:
@implementation PersonWindowController - (PersonWindowController *)initWithManagedObjectContext:(NSManagedObjectContext *)inMoc { self = [super initWithWindowNibName:@"PersonWindow"]; [self setManagedObjectContext:inMoc]; return self; } - (void)setManagedObjectContext:(NSManagedObjectContext *)value { // keep only weak ref _moc = value; } - (NSManagedObjectContext *)managedObjectContext { return _moc; } @end
Next we need to have code to open our new PersonWindows on demand:
- (IBAction)openPersonWindow:(id)sender;
openPersonWindow:
thus:
- (IBAction)openPersonWindow:(id)sender { PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithManagedObjectContext:[self managedObjectContext]]; [newWindowController setShouldCloseDocument:NO]; [self addWindowController:newWindowController]; [newWindowController showWindow:sender]; }
Now that we have a suitable window controller for our new window, we need to create a nib file for it to use:
Next we need to make a button to open the new window:
NSButton
to the window called ‘Open Employee Window’.openPersonWindow:
method of File’s Owner.Build and run the application. Each time you click on the ‘Open Employee Window’ button, a new, blank window will open. This window might not be particularly impressive as it doesn’t display any data, but at least it’s a start! Just to reassure yourself that the new window is a document window, make sure that closing the main document window will also close any associated employee windows.
- Edit – Handling enabling and disabling of the ‘Open Employee Window’ button is a little more complicated than the ‘+’ and ‘-’ buttons. The hack way would be to bind its ‘Enabled’ attribute to the People array controller’s ‘canRemove’ key since canRemove is only true if a person is currently selected. The better way to do this, however, is described in a great post on this article at: http://www.passingcuriosity.com/.
In order to display our employee data in the new window, we obviously need to be able to tell our personWindowController
which employee it should display.
So, in the PersonWindowController.h file, add an NSManagedObject
pointer called _person
to the interface. Then add KVC methods to access it called person
and setPerson:
and change the initialization method so your code looks like this:
@interface PersonWindowController : NSWindowController { NSManagedObjectContext *_moc; NSManagedObject *_person; } - (void)setManagedObjectContext:(NSManagedObjectContext *)value; - (NSManagedObjectContext *)managedObjectContext; - (void)setPerson:(NSManagedObject *)value; - (NSManagedObject *)person; - (PersonWindowController *)initWithPerson:(NSManagedObject *)inPerson inManagedObjectContext:(NSManagedObjectContext *)inMoc; @end
The initialization method has been modified to allow us to initialize with a specified person object. Change the method implementation in PersonWindowController.m and add the _person
getter and setter:
- (PersonWindowController *)initWithPerson:(NSManagedObject *)inPerson inManagedObjectContext:(NSManagedObjectContext *)inMoc { self = [super initWithWindowNibName:@"PersonWindow"]; [self setManagedObjectContext:inMoc]; [self setPerson:inPerson]; return self; } - (void)setPerson:(NSManagedObject *)value { // keep only weak ref _person = value; } - (NSManagedObject *)person { return _person; }
To know which Person object we need to pass to the window we’re opening, we need to be able to ask the ‘People’ array controller which object is currently selected. So, in MyDocument.h, add to the MyDocument
interface an IBOutlet
for the NSArrayController
:
@interface MyDocument : NSPersistentDocument { IBOutlet NSArrayController *peopleArrayController; }
Return to Interface Builder and connect this new outlet to the People array controller.
In MyDocument.m, change openPersonWindow:
to the following:
- (IBAction)openPersonWindow:(id)sender { NSArray *selectedPeople = [peopleArrayController selectedObjects]; NSManagedObject *selectedPerson; int i; for( i = 0; i < [selectedPeople count]; i++ ) { selectedPerson = [[peopleArrayController selectedObjects] objectAtIndex:i]; PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithPerson:selectedPerson inManagedObjectContext:[self managedObjectContext]]; [newWindowController setShouldCloseDocument:NO]; [self addWindowController:newWindowController]; [newWindowController showWindow:sender]; } }
This code asks the peopleArrayController
for the currently selected objects. It then moves through these opening a new PersonWindowController
instance for each selected person.
In order to display the Person data from the NSManagedObject
a particular window represents, we can use an NSObjectController
that is tied to the window controller’s person
key.
NSObjectController
called ‘Person’.NSArrayController
called ‘Departments’, set its Mode to ‘Entity’ its Entity Name to ‘Department’, and specify that it Prepares Content.Next add labels, text fields and a popup menu to the person window so it looks like this:
To get the values to display in the window setup the following:
After you’ve built and run the project, create a department and a person. Clicking the Open Employee Window button will now open a window displaying the current person with their details. When you change their first or last name (and either press Enter or tab out of the field to ‘End Editing’ on that field), notice that the entries in the main window change to match those in the person window. If you move a person into a different department, notice that they disappear from the list in the other window (assuming the old department is still selected, of course). Notice also that Undo automatically works for changes made in the employee window and if there are multiple people selected when you press the button, the right number of windows should open.
It would be nice if the person window that was displayed had a title relevant to the person it represents. When a window controller builds a window from a nib file, it calls the windowTitleForDocumentDisplayName:
method which by default simply returns the supplied displayName
attribute. To change this behaviour, override the method by using the following declaration in PersonWindowController.m:
- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName { if( [self person] ) return [NSString stringWithFormat:@"Employee: %@ %@", [[self person] valueForKey:@"firstName"], [[self person] valueForKey:@"lastName"]]; else return displayName; // shouldn't happen but you never know... }
Now whenever you open an employee window, it will be appropriately titled.
It would also be nice if the list of departments and employees were both sorted alphabetically. By default, Core Data displays objects in the order in which they were loaded which is arbitrary. To sort the two array controllers, add an IBOutlet
NSArrayController
called departmentsArrayController
to match the peopleArrayController
in MyDocument.h and connect it in Interface Builder to the Departments array controller. Also in Interface Builder, set both array controllers to ‘Auto Rearrange Content’. This ensures that the array controller will rearrange itself whenever any changes are made to its objects.
Next add the following code at the end of windowControllerDidLoadNib:
in MyDocument.m:
NSSortDescriptor *departmentSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES] autorelease]; NSArray *departmentSortDescArray = [NSArray arrayWithObject:departmentSortDesc]; [departmentsArrayController setSortDescriptors:departmentSortDescArray]; NSSortDescriptor *personLastNameSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES] autorelease]; NSSortDescriptor *personFirstNameSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"firstName" ascending:YES] autorelease]; NSArray *personSortDescArray = [NSArray arrayWithObjects:personLastNameSortDesc, personFirstNameSortDesc, nil]; [peopleArrayController setSortDescriptors:personSortDescArray];
Now the two array controllers will sort the displayed data in alphabetical order.
There is still the slight problem that we are able to open multiple windows for one person. To correct this, change the openPersonWindow:
method declaration to the following:
- (IBAction)openPersonWindow:(id)sender { NSArray *selectedPeople = [peopleArrayController selectedObjects]; NSArray *openPeopleWindows = [self windowControllers]; NSManagedObject *selectedPerson; if( [selectedPeople count] > 0 ) { int i; for( i = 0; i < [selectedPeople count]; i++ ) { selectedPerson = [[peopleArrayController selectedObjects] objectAtIndex:i]; BOOL personIsAlreadyOpen = NO; // check to see if person window is already open int j; for( j = 0; j < [openPeopleWindows count]; j++ ) { NSWindowController *eachWindowController = [openPeopleWindows objectAtIndex:j]; // only ask PeopleWindowControllers if they are open for our person if( !personIsAlreadyOpen && [eachWindowController respondsToSelector:@selector(person)] ) { if( [(PersonWindowController *)eachWindowController person] == selectedPerson ) { [eachWindowController showWindow:sender]; personIsAlreadyOpen = YES; } } } if( !personIsAlreadyOpen ) { PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithPerson:selectedPerson inManagedObjectContext:[self managedObjectContext]]; [newWindowController setShouldCloseDocument:NO]; [self addWindowController:newWindowController]; [newWindowController showWindow:sender]; } } } }
There are various ways to accomplish this task; the above code checks through each of the document object’s window controllers to see if it responds to the person:
method. If it does, it asks whether the selectedPerson
is the person displayed by that window controller. If it is, showWindow:
is called to bring it to the front. Otherwise, a new window controller is created and added to the document object’s window controllers.
At this point, we have a very simple but hopefully functional multiple window Core Data application. Making multi-window applications using the Core Data framework is actually not that difficult, it simply requires that you (understand and) keep hold of references to managed object contexts and pass around references to the NSManagedObjects held within them.
You might wish to keep a copy of the project in its current state as in the next article I’ll be expanding it by working with multiple managed object contexts.
If you’ve found this article useful, please let me know. If you think it could be made better (and I’m sure it can), please tell me and I will make the requisite changes!
转自Tim Isted的博客:
http://www.timisted.net/blog/archive/multiple-windows-with-core-data/