macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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.

  1. 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;
    
    @end

    If 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.

  2. 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 init and dealloc are called each time a document is created or destroyed, respectively. They manage the memory allocation of both the data item array (theItems) and the measureType array. 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.

  3. 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 DataItem object, populate its data and type based on the values in the user interface, and add it to the theItems array. The observant among you will notice that we are actually adding the new data item to theController, which is an array controller that we will create later. This controller will automatically update both the user interface and theItems array.

  4. 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 dataRepresentationOfType method. 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. The while loop writes out the XML for the measureTypes and populates a dictionary from which we will later be able to match an id with a measureType.

    The for loop creates the data items. It looks a bit confusing, but is just pulling the information out of theItems array 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 (self signifies MyDocument.m) and returning YES if 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.

  5. 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 if statements. The first handles type nodes. It grabs the attributes out of the XML node, puts them in a dictionary, and then adds the dictionary to the measureTypes array.

    The second if statement handles item nodes whose id is an empty string. This will cause all of the nodes created from the database to be excluded (because they have ids 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 an id assigned yet.

    If a new data item is found, an instance of the DataItem class is created and populated with the attributes from the XML. Finally, the DataItem object is added to the theItems array.

  6. 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";
    }
    
    @end

    The method windowControllerDidLoadNib gets 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 with measureTypes and to connect the array controller to the theItems array.

  7. 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.

Highlight the EnterData target in the left panel of the Xcode window (you may need to expand Targets to find it)

Pages: 1, 2, 3, 4

Next Pagearrow