Applying "Digital Hub" Concepts to Enterprise Software Design, Part 5
Pages: 1, 2, 3, 4
MyDocument and the Objective-C Code
Save your work in Interface Builder and switch back to Xcode. We are going to write some more code.
Find the MyDocument.h file in Xcode and modify it to match the following:
/* Manages the opening, saving, and editing of an XML file containing weather measurements. */ #import <Cocoa/Cocoa.h> @interface MyDocument : NSDocument { NSMutableArray *theItems; // Array of DataItem objects NSMutableArray *measureTypes; // for pop-up menu and XML IBOutlet NSArrayController *theController; // Link to ArrayController IBOutlet NSPopUpButton *typeMenu; // pop-up menu IBOutlet NSTextField *dataField; // text field for data } /* Creates a new DataItem object, populates it from the UI and adds it to theItems array. */ - (IBAction)createNewItem:(id)sender; @endIf you would prefer to download the code, here is a link (MyDocument.h).
In the above header, we created two arrays: one to hold all of the data items, and one to hold the measurement types. Then we created three outlets that will allow our code to access elements of the user interface. We will connect the code to the interface later. Finally, we declared a method for creating a new data item.
Next, we are going to edit MyDocument.m. We will go through one piece at a time so that I can explain each section that we are adding. First, find MyDocument.m in Xcode and modify it to match the following:
#import "MyDocument.h" #import "DataItem.h" @implementation MyDocument - (id)init { self = [super init]; if (self) { theItems = [[NSMutableArray alloc] init]; measureTypes = [[NSMutableArray alloc] init]; } return self; } - (void)dealloc { [measureTypes release]; [theItems release]; [super dealloc]; }The
initanddeallocare called each time a document is created or destroyed, respectively. They manage the memory allocation of both the data item array (theItems) and themeasureTypearray. These arrays will be populated based on the XML and user input. For now, we are just making sure that they are ready to go when they are needed.Next, add the following method to MyDocument.m:
- (IBAction)createNewItem:(id)sender { DataItem *newItem = [[DataItem alloc] init]; [newItem takeValue:[dataField stringValue] forKey:@"theData"]; [newItem takeValue:[typeMenu titleOfSelectedItem] forKey:@"typeName"]; [theController addObject:newItem]; [newItem release]; [dataField setStringValue:@""]; }Later, we are going to link this method to the Add button in the user interface. When a user presses this button, the client module will initialize a new
DataItemobject, populate its data and type based on the values in the user interface, and add it to thetheItemsarray. The observant among you will notice that we are actually adding the new data item totheController, which is an array controller that we will create later. This controller will automatically update both the user interface andtheItemsarray.Add the following methods to MyDocument.m, which will handle saving and loading the XML.
- (NSData *)dataRepresentationOfType:(NSString *)aType { int i; NSMutableString *output = [[NSMutableString alloc] init]; NSMutableDictionary *typeByName = [[NSMutableDictionary alloc] init]; [output appendString:@"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"]; [output appendString:@"<weather><measureTypes>"]; NSEnumerator *enumerator = [measureTypes objectEnumerator]; id key; while ((key = [enumerator nextObject])) { [output appendFormat:@"<type id=\"%@\" name=\"%@\" dataType=\"%@\" />", [key valueForKey:@"id"], [key valueForKey:@"name"], [key valueForKey:@"type"]]; [typeByName setObject:[key valueForKey:@"id"] forKey:[key valueForKey:@"name"]]; } [output appendString: @"</measureTypes><data>"]; for (i=0; i < [theItems count]; i++) { [output appendFormat:@ "<item id=\"\" time=\"%@\" typeId=\"%@\" data=\"%@\" />", [[[theItems objectAtIndex:i] valueForKey:@"timeTaken"] descriptionWithCalendarFormat:@"%Y-%m-%d %H:%M:%S %z"], [typeByName valueForKey: [[theItems objectAtIndex:i] valueForKey:@"typeName"]], [[theItems objectAtIndex:i] valueForKey:@"theData"]]; } [typeByName release]; [output appendString:@"</data></weather>"]; [output autorelease]; return [output dataUsingEncoding:NSASCIIStringEncoding]; } - (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType { BOOL success; NSXMLParser *addressParser = [[NSXMLParser alloc] initWithData:data]; [addressParser setDelegate:self]; success = [addressParser parse]; return YES; }Study the above code for the
dataRepresentationOfTypemethod. Implementing this method in a document-based Cocoa application enables saving. In our case, it does this by transforming the data stored in out application into an XML string. We are creating the XML by appending strings together; nothing fancy. Thewhileloop writes out the XML for themeasureTypesand populates a dictionary from which we will later be able to match anidwith ameasureType.The
forloop creates the data items. It looks a bit confusing, but is just pulling the information out oftheItemsarray and formatting according to our XML specifications.Now take a look at
loadDataRepresentation. This is a method that you can override to handle file loading in a document-based Cocoa project. We are creating an XML parser, choosing a delegate class that implements some parsing methods (selfsignifies MyDocument.m) and returningYESif it parses without an error. All of the logic for parsing with XML is in a delegate method, which we will write next. If you have been following this series, you will recognize this because it is very similar to the way that we parsed XML using WebObjects when a file is uploaded to the hub.Add the following method to MyDocument.m, which is for parsing the XML.
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { if ( [elementName isEqualToString:@"type"]) { // this part will happen with the XML is opened----------- NSMutableDictionary *newType = [[NSMutableDictionary alloc] init]; [newType setObject:[attributeDict valueForKey:@"name"] forKey:@"name"]; [newType setObject:[attributeDict valueForKey:@"dataType"] forKey:@"type"]; [newType setObject:[attributeDict valueForKey:@"id"] forKey:@"id"]; [measureTypes addObject:newType]; [newType release]; } // only add if ID is null!!!! if ( [elementName isEqualToString:@"item"] && [[attributeDict valueForKey:@"id"] isEqualToString:@""]) { DataItem *newItem = [[DataItem alloc] init]; [newItem takeValue:[attributeDict valueForKey:@"data"] forKey:@"theData"]; NSEnumerator *enumerator = [measureTypes objectEnumerator]; id key; while ((key = [enumerator nextObject])) { if ([[attributeDict valueForKey:@"typeId"] isEqualToString:[key valueForKey:@"id"]]) { [newItem takeValue:[key valueForKey:@"name"] forKey:@"typeName"]; } } [newItem takeValue: [NSCalendarDate dateWithString:[attributeDict valueForKey:@"time"]] forKey:@"timeTaken"]; [theItems addObject:newItem]; NSLog([theItems description]); [newItem release]; return; } }Because we set MyDocument.m as the XML parser delegate, the above method will be called each time the XML parser finds an XML node. We want it to extract the information from the XML and put in into our local variables. There are two
ifstatements. The first handlestypenodes. It grabs the attributes out of the XML node, puts them in a dictionary, and then adds the dictionary to themeasureTypesarray.The second
ifstatement handlesitemnodes whoseidis an empty string. This will cause all of the nodes created from the database to be excluded (because they haveids assigned). Why do we want to do this? Because our tool is for creating new data, not for editing old data. If a user creates some new data, saves the file, and then opens it again later, they will be able to edit the data that they created because it will not have anidassigned yet.If a new data item is found, an instance of the
DataItemclass is created and populated with the attributes from the XML. Finally, theDataItemobject is added to thetheItemsarray.Finally, add the following code to MyDocument.m.
- (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; [typeMenu removeAllItems]; NSEnumerator *enumerator = [measureTypes objectEnumerator]; id key; while ((key = [enumerator nextObject])) { [typeMenu addItemWithTitle:[key valueForKey:@"name"]]; } [theController setContent:theItems]; } - (NSString *)windowNibName { return @"MyDocument"; } @endThe method
windowControllerDidLoadNibgets called when the document user interface has been loaded. This is essentially the last thing that happens before the user gets to play with the data. We are using the method to populate the drop-down menu withmeasureTypesand to connect the array controller to thetheItemsarray.-
If you would prefer to download the code, here is a link to the complete MyDocument.m file.
Before we leave Xcode, there is one more thing that we need to do. We need to tell MyDocument that it can save and write XML files.
Highlight the EnterData target in the left panel of the Xcode window (you may need to expand Targets to find it). Press Apple-I to bring up the Target EnterData Info window. In the Document Types table, set the name of the first item to XML and the extensions to xml. Close the Info window and save your project.
![]() |


