Welcome to the "Digital Hub" concepts article series, whether you have just joined us or have been following along since the beginning. So far, we have covered an enterprise software architecture based on a flexible "hub" that stores and vends data to a number of cross-platform tools and applications using a standard XML "spoke." If you have been doing all of the tutorials, you have built a database along with a WebObjects application that transforms the data back and forth between the database and XML.
In this article, we are going to build the first client module that will work with the XML spoke data. We are going to using my favorite development environment on the Macintosh, which is Cocoa. If you have not programmed in Cocoa before, this will be a whirlwind tour that will whet your appetite to the potential of this great resource. However, no matter what you experience, you will learn how to quickly create an independent application that participates and adapts to a larger enterprise software system.
The client module we are going to build works with an XML file created and consumed by other components in the larger enterprise system. While I encourage you to start at the beginning and go through each article of this series in order, you may choose to complete this one on its own by using this XML file. If you have completed the series to date, you can generate your own XML using the WeatherHub application that we built in part four.
Here is the XML template from part four that we are going to use for our design:
<?xml version="1.0" encoding="UTF-8"?>
<weather>
<measureTypes>
<type id="0" name="Temperature" dataType="number" />
<type id="1" name="Wind Speed" dataType="number" />
</measureTypes>
<data>
<item id="0" time="2004-04-17 00:00:00 -0700" typeId="0" data="60" />
<item id="1" time="2004-04-17 00:00:00 -0700" typeId="1" data="3.2" />
</data>
</weather>
As a quick review, the measureTypes represent types of weather data that can be measured. In this case, Temperature and Wind Speed can be measured. New measureTypes could be added by other components in the system (such as an administration tool). The data node contains specifically measured instances of weather data. Each data item stores the time at which the data was measured, a typeId to match the data to a measureType, and the actual data itself. Note that every element extracted from the database has an id attribute that maps to its database id.
What we need now is an intuitive application for field scientists to use to record the data from their gauges and thermometers. After the data is recorded, it can be uploaded to the central hub. For now, we will assume that each scientist has been issued an Apple iBook and has Internet connectivity at his or her office, but may not have connectivity in the field when they are taking their readings. It goes without saying that you do not want to deploy a new version of the application every time a new measureType is added, so the user interface of the application should update dynamically based on the XML. Finally, the application is not for viewing old data or for modifying data that has already been uploaded to the database. We will only allow the creation of new data and the editing of that new data.
Here is what the finished application will look like:
![]() |
We are going to use the Cocoa developing environment because it is the coolest language for creating Macintosh applications. Our user interface will consist of a drop-down menu to select a measureType and a text box in which to type the data. As new data is added, it will be displayed in a table view so that it can be modified or deleted. The application will read XML that has been downloaded through the WeatherHub web interface and will also save XML that can be uploaded through the WeatherHub web interface.
Launch Xcode and choose New Project... from the File menu.
Choose Cocoa Document-based Application as the project type and click Next.
![]() |
Name the project EnterData and click Finish.
|
Related Reading Cocoa in a Nutshell |
Applying "Digital Hub" Concepts to Enterprise Software Design, Part 4
Applying "Digital Hub" Concepts to Enterprise Software Design, Part 3
Applying "Digital Hub" Concepts to Enterprise Software Design, Part 2
Applying "Digital Hub" Concepts to Enterprise Software Design
|
DataItem ClassNext, we are going to create a class to store a weather data item. The class will not have any methods but will have instance variables for the data, the measureType, and the date that the measurement was taken.
Right-click (or Control-click) the Classes folder in the left panel of your project (you may need to expand the EnterData group) and choose Add -> New File....
![]() |
Under the Cocoa section, choose the Objective-C Class type and click Next.
Name the file DataItem.m and click Finish.
![]() |
Find the DataItem.h file that was just created in Xcode, and modify it to match the following:
/*
Contains the data associated with a measurement item.
*/
#import <Cocoa/Cocoa.h>
@interface DataItem : NSObject {
NSString *theData; // Value of measurement data
NSString *typeName; // Type of the data
NSCalendarDate *timeTaken; // Timestamp
}
@end
The above header should be pretty self-explanatory, even if the Objective-C syntax is new to you. We are simply creating string variables for the data and type names that we will store, and a date variable for the time the data was taken.
The class file will be pretty simple, too. There will be an init method that allocates memory for the timeTaken variable when the DataItem object is created, and a dealloc method that releases the memory when the object is destroyed.
Replace the contents of DataItem.m with the following:
#import "DataItem.h"
@implementation DataItem
- (id)init
{
if (self = [super init]) {
timeTaken = [[NSCalendarDate alloc] init];
}
return self;
}
- (void)dealloc
{
[timeTaken release];
[super dealloc];
}
@end
If you would prefer to download the code, here are the links: DataItem.h and DataItem.m.
Find and double-click MyDocument.nib in Xcode. You can find it in the Resources group or use search to find it. Double-clicking the .nib file will automatically launch Interface Builder. To avoid a cluttered screen, I recommend that you hide all of the applications on your screen except Interface Builder. You can do this by holding down the Option key together with the Apple key and then clicking the Interface Builder icon.
Select the text that says "Your document contents here" and press delete to remove it.
Choose Show Info in the Tools menu do display the attributes of the selected object. The info panel should be labeled "NSWindow," as the window is the only item that can currently be modified.
Check the "Has texture" checkbox to give the window a metal appearance.
![]() |
Interface Builder has a palette full of user interface elements that can be dragged onto your window. Find the palette containing Cocoa Text Controls and drag a text field to your textured window.
Switch to the Cocoa Controls and Indicators palette and drag a pop-up menu and an aqua button over the interface. Arrange the user interface elements to look like the following screen shot. Notice that as you are moving elements around, blue guides with help you align the elements to match Apple's standards.
![]() |
Double-click the button and change its text to "Add".
While the button is selected, notice that the information panel has changed to show the attributes for the NSButton class. Use the drop-down menu next to "Key Equiv:" to set Return as the keyboard shortcut for your Add button. Notice that this causes the button to appear blue instead of clear.
Using the mouse, select all three of our user interface elements by dragging a box around them. Then group the elements by choosing "Make subviews of > Box" in the Layout menu. You will probably need to move things around to make them look good. Double-click the box title (or use the information panel) and change it to Enter new measurement then click "Add". Your window should now look something like this:
![]() |
Next, change the palette to Cocoa Data Views and drag a table over your window. Adjust its size so that it fills the middle portion of the window.
Select the table and use the information panel to specify three columns. Then edit the names and widths of the columns so that they look like the screenshot below. In order to edit a column, you may need to click the column header several times to make sure that you have the column selected, as opposed to the table as a whole.
![]() |
Find the icon labeled PopUp in the palette and drag it onto the column header for Type. A small up-and-down arrow icon should appear in every table cell that is in that column.
Finally, drag one more button onto the bottom of the window and rename it Delete Selected. I chose a metal look for this button, but it does not really matter which one your choose. Adjust your layout so that everything looks nice and matches the Apple layout standards.
![]() |
Once upon a time, I developed a Windows application that was similar to the tool we are building today. It took about three weeks for my team to build the first version. The users of the tool, who did not have much sympathy for developers, wanted an application that was resizable like all of the other applications that they were used to. Well, it took about another month to write the complicated code to handle resizing properly. I would rather have spent that month solving world peace, or at least doing something a bit more exciting than working on window resizing.
When I later came across the way that Cocoa handles resizing, I just about fell out of my chair. It is great and it does not require a graphing calculator to figure out! Let me show you how to do it:
First select the box that contains the three elements at the top of the window. In the information pane, select Size in the drop-down menu at the top of the pane. In the picture of the square, you can toggle straight lines and springs by clicking on them. Create three springs outside of the center square; left, right, and bottom. See the screenshot if you need help. Since there are no springs inside of the square, our user interface box will not change size as the window is resized. It will stay attached to the top of the window, will stay centered, and will not get dragged downward as the window gets taller.
![]() |
Next, let's do the Delete Selected button. Make sure the button is selected, choose Size in the information page, then create springs above and to the sides of the square. This will emulate the behavior of the box except that the button will stay anchored to the bottom of the window instead of the top of the window.
![]() |
Finally, we are going to set up the table. Because the table could contain quite a bit of data, we want to let it take most of the screen real estate. So select the table, make sure Size is selected in the information pane, and create springs inside of the square. This will cause the table to expand to fill the space available, keeping the margin between the table and the window constant on all sides.
![]() |
To test your interface, choose Test Interface from the File menu. Play around with the resizing and confirm that it works that way you expect. Click Apple-Q to return to Interface Builder. Celebrate good times, come on!
|
MyDocument and the Objective-C CodeSave 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;
@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.
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.
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.
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.
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.
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.
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.
![]() |
|
If it is not already open, open MyDocument.nib in Interface Builder by double-clicking it.
Drag MyDocument.h from Xcode to the Instances tab of the window titled "MyDocument.nib" in Interface builder. This will cause the .nib file to be aware of the outlets you added in the code. You will have successfully completed this step if the "MyDocument.nib" window switches to the Classes tab and the MyDocument class is visible. At this point, you may want to hide all of the applications on your screen except for Interface Builder.
Switch back to the Instances tab in the "MyDocument.nib" window.
While holding down the Control key on the keyboard, use the mouse to drag a line from the File's Owner icon in the "MyDocument.nib" window to the pop-up menu in the user interface window. Then, using the connections options in the information pane, select the typeMenu outlet and press the Connect button. See the screenshot below:
![]() |
The connection that you just made allows your code to access the pop-up menu using the typeMenu variable that you declared in the MyDocument.h header file.
In a similar way, drag a line from File's Owner to the text box and connect it to the "dataField" outlet.
Now we are going to make a connect the other direction. While holding down the Control key, drag a line from the Add button to the File's Owner icon. In the information pane, choose the target "createNewItem:" and click the connect button. If you do not see the "createNewItem:" target, then you may need to change the second pop-up menu in the information pane to Outlets. See the screenshot if you need help.
![]() |
When we drag a connection from the user interface to File's Owner, we are telling the application to call the createNewItem method when the user clicks the button.
Notice that we have not connected the table or the delete button yet. That is because Xcode has recently added the concept of bindings. We can (and will) create a controller object that will manage a lot of the busywork in synchronizing the user interface elements and the dataItems array.
Switch to the Controllers palette in Interface Builder and drag the array controller from the palette to the "MyDocument.nib" window. The array controller is the one that looks like three green cubes. Rename the controller that you just created to DataItem Controller.
Drag a second array controller to the "MyDocument.nib" window and rename it MeasureType Controller.
Hold down the Control key and drag a line from the File's Owner icon to the DataItem Controller icon that you created. Choose "theController" in the information pane and click the connect button. This will allow our createNewItem method to send messages to the array controller.
![]() |
The array controllers can stay synchronized with our array instance variables, but we need to tell them to do so. Click the DataItem Controller icon and choose the Bindings option from the pop-up menu in the information pane.
Expand the "contentArray" option in the information page.
Set the Bind to: option to File's Owner (MyDocument) and type theItems for the Model Key Path option. Then make sure that the Bind checkbox is checked.
![]() |
Next, choose Attributes from the information pane pop-up menu. We are going to add three keys to the NSArrayController, one for each instance variable in DataItem (since DataItems are the objects stored in the array). Click the Add button three times and rename the keys to theData, typeName, and timeTaken
![]() |
Repeat steps 4 through 6, but this time bind the MeasureTypes Controller to the measureTypes array. Remember to check the Bind checkbox!
With MeasureTypes Controller still selected, choose Attributes from the information page pop-up menu. Add a key labeled "name".
We are almost done! All that remains is binding the array controllers to the user interface elements with Interface Builder. Here is how to do that:
Select the Date column in the table. It may take several clicks. You will know you have it selected when the column header is blue and the information pane is labeled "NSTableColumn". In the pop-up menu at the top of the information pane, select Bindings. Expand the options for value. Select DataItem Controller as the Bind to: value, arrangedObjects as the controller key, and timeTaken as the model key path. Make sure that the Bind checkbox is checked. See the following screenshot:
![]() |
Use the same method to bind the Data column. The model key path will be theData.
Bind the Type column as well. Because this column is displayed with pop-up menus, you will bind typeName to the selectedValue attribute rather than value. Otherwise, the steps are the same.
The Type column as needs to know the values with which to populate its pop-up menus. Select the small triangle on the right side of the column header. The information pane should now be labeled "NSPopUpButtonCell".
Expand the "content" options in the bindings pane. Set Bind to: to MeasureType Controller, the controller key to "arrangedObjects", and the modal key path to name.
Finally, connect the Delete Selected button to the DataItem Controller by holding Control and dragging a line from the button to the DataItem Controller icon. Select the "remove:" target (you may have to set the pop-up menu to Outlets first) and click Connect.
![]() |
Congratulations, you are done! Save the work you did in Interface Builder,
then switch back to Xcode and run your application by pressing Apple-R. If an
element of the user interface is not working, be sure to check its connections
and bindings. Open the
sample XML file, add some new data, and save it. Check out the column sorting
too. If you built the WeatherHub application in the previous article, try exporting
XML, adding some new data, and then importing the data back into the hub.
Now, I know that you are probably having a lot of fun creating this Cocoa application. Cocoa is pretty cool, particularly with the new bindings technology. However, I do not want you to miss this the real beauty of what is going on here.
Because we laid the foundation of flexible enterprise hub architecture based on XML, we were able, in a short time, to design and build a powerful data-entry tool that plugs right into the greater system and can contribute valuable new features without changing any of the other components. It does not matter that most of the system so far was written in Java and the new piece was written in Cocoa. We can use the best tool for the job without restriction (as long as it can parse XML).
Let's say that after creating this application, all of the iBook-toting scientists are happy, but those with Windows machines feel left out. No problem; just whip up a similar application in Visual Basic .NET that parses and creates XML in the same way.
As a final example, imagine that a Linux machine is controlling a thermometer and should be able to submit the data without human interaction. Again, no problem, because you can quickly create a script in your favorite language that can take the data from the thermometer, package it in XML, and send it to the hub via a file upload or a web service.
In fact, in the next article, we will build another module in another language to prove this point. It will be a Perl tool that will retrieve data from the WeatherHub application using web services rather than requiring the user to manually download the XML from the web interface. Then the tool will do some analysis of the data. Stay Tuned!
When we open the XML in our Cocoa application and then save it, we are tossing out the old data and only preserving the data we need to make updates. This is fine if someone is downloading the data only for the purpose of adding updates, but if they want to use it for other purposes--while they have it stored locally--it might be better to preserve the old data. As extra credit, save all of the data that is read from the XML file and write it back out again. Remember that the old data should not be displayed in the user interface of the EnterData application.
Another thing that would be great is data validation based on the dataType attribute of each measurement type. In this tutorial, we are not doing any validation of the data that is entered. If this was a real-world project, the validation should be dynamic based on the values found in the XML.
Adam Behringer works as the CEO of Bee Documents, which strives to help legal and medical firms efficiently manage their documents.
Return to MacDevCenter.com.
Copyright © 2009 O'Reilly Media, Inc.