In this chapter:
As we described earlier, permanent data resides in memory. This memory is divided into two sections: the dynamic and storage heaps. Permanent data resides in the storage heap and is controlled by the Data Manager (the dynamic heap is managed strictly by the Memory Manager).
Data is organized into two components: databases and records. The relationship between the two is straightforward. A database is a related collection of records. Records are relocatable blocks of memory (handles). An individual record can't exceed 64KB in size.
Overview of Databases and Records |
A database, as a collection of records, maintains certain key information about each record (see Figure 6-1):
In the Palm 3.0 OS, there is one large storage heap; in previous versions, there were many small ones. A database resides in a storage heap, but its records need not be in the same heap (see Figure 6-1).
Figure 6- 1.
Database with two records in a database in persistent memory
Databases also contain the following other types of information:
An application info block
This usually contains category names as well as any other database-wide information.
A sort info block
This is where you store a list of record numbers in a variant order. For example, address book entries might be sorted by company rather than by a person's name. Most applications don't use a sort info block.
Name, type, and creator
Databases are created with a name (which must be unique), a type, and a creator. When a user deletes an application, the Palm OS automatically deletes all databases that share the same creator. The preferences record is removed at the same time. So that this cleanup can happen correctly, it's important that your databases use the creator of their application.
Write-Protected Memory
In order to maintain the integrity of the storage heap, it is hardware write-protected. This ensures that a rogue application referencing a stray pointer can't accidentally destroy important data or applications. Therefore, changes to the databases can only be made through the Data Manager API. These APIs check that writes are made only within an allocated chunk of memory-writes to random areas or past the end of an allocated chunk are not allowed.
Palm 3.0 OS Heap Changes
In pre-3.0 versions of the OS, a database heap is limited to at most 64KB. The persistent area of memory is therefore divided into many different database heaps. In this pre-3.0 world, it is much harder to manage memory, since each allocated record must fit within one heap. The 3.0 OS does not have that 64KB limit on database heaps. Instead, the persistent memory area contains just one large database heap.
NOTE: The multiple database heaps lead to a problem: although there is free memory available, there might not be enough available for a record. The situation occurs when you have, for example, 10 databases heaps, each of size 64KB, and each half full. Although there is 320KB memory available, a record of size 40KB can't be allocated (because no single heap can hold it). The original 1.0 OS exacerbated this problem with an ill-chosen strategy for database allocations: records were allocated by attempting to keep heaps equally full. This made large record allocations more and more difficult as previous allocations were made. |
NOTE: The 2.0 OS switched to a first-fit strategy (a record is allocated in the first heap in which it will fit). A change to the 2.0 OS (found in the System Update 2.0.4) modified the strategy (if there isn't room in an existing heap for a chunk, chunks from the most empty heap are moved out of that heap until there is enough space). It isn't until 3.0, however, that a full fix (one large heap) is in place. |
Where Databases Are Stored
Although all current Palm OS devices have only one memory card, the Palm OS supports multiple cards. Cards are numbered, starting with the internal card, which is 0. When you create a database, you specify the card on which it is created. If and when multiple cards are supported, there will need to be some user interface to decide default locations. Until that time, you create your databases on card 0.
NOTE: While creating databases on card 0 is fine, other code in the application shouldn't rely on the value of the card being 0. By not hardcoding this value, the application will work with multiple card devices. |
How Records Are Stored Versus How They
Are Referenced
While your application is running, reference database records using handles. Database records are not stored this way, however. Within the database heap they are stored as local IDs. A local ID is an offset from the beginning of the card on which it is located. Because items are stored this way, the base address of the card can change without requiring any changes in the database heap.
A future Palm device OS with multiple card slots would have separate base addresses for each slot. Thus, the memory address for a chunk on a memory card would depend on what slot it was in (and thus what its base address was).
This is relevant to your job as an application writer when you get to the application info block in the Database. Application info (and sort info) blocks are stored as local IDs. Also, if you need to store a reference to a memory chunk within a record, you can't store a handle (because they are valid only while your application is running). Instead, you'd need to convert the handle to a local ID (using a Memory Manager function) and store the local ID.
Creating, Opening, and Closing Databases |
You handle these standard operations in a straightforward manner in Palm applications.
Creating a Database
To create a database, you normally use DmCreateDatabase:
Err DmCreateDatabase(UInt cardNo, CharPtr nameP, ULong creator, ULong type, Boolean resDB)
The creator is the unique creator you've registered at the Palm developer web site (http://www.palm.com/devzone). You use the type to distinguish between multiple databases with different types of information in them. The nameP is the name of the database, and it must be unique.
NOTE: Until Palm Developer Support issues guidelines on how to use multiple card numbers, just use 0 as your card number when creating databases. |
In order to guarantee that your database name is unique, you need to include your creator as part of your database name. Developer Support recommends that you name your database with two parts, the database name followed by a hyphen (-) and your creator code. An application with a creator of "Neil" that created two databases might name them:
Database1-Neil Database2-Neil
Create your database in your StartApplication routine
You normally create your database from within your StartApplication routine. This is in cases where the database does not yet exist. Here is a typical code sequence to do that:
// Find the Customer database. If it doesn't exist, create it. gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode); if (! gDB) { err = DmCreateDatabase(0, kCustName, kSalesCreator, kCustType, false); if (err) return err; gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode); if (!gDB) return DmGetLastErr(); // code to initialize records and application info }
Creating a database from an image
If your application has a database that should be initialized with a predefined set of records, it may make sense to "freeze-dry" a database as a resource in your application. Thus, when you build your application, add an existing database image to it. Then, when your application's StartApplication routine is called, if the database doesn't exist, you can create it and initialize it from this freeze-dried image.
Of course, you could just provide your user with a Palm database (PDB) file to download. The advantage is that your application is smaller; the disadvantage is that the user might not download the file. In this case, you'd still need to check for the existence of your databases.
Here's an example:
gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode); if (!gDB) { VoidHand imageHandle = DmGetResource('DBIM', 1000); err = DmCreateDatabaseFromImage(MemHandleLock(imageHandle)); MemHandleUnlock(imageHandle); DmReleaseResource(imageHandle); if (err) return err; gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode); if (!gDB) return DmGetLastErr(); }
This code assumes that there's a resource of type DBIM with ID 1000 in your application's resource database that contains an appropriate image.
You can create the database on a Palm OS device, and then do a HotSync to back up the database. The file that Palm Desktop creates is a database image (a PDB file).
The exercise of getting a data file into a resource in your application is not covered here. Your development environment determines how you do this.
You usually open your database by type and creator with a call like this:
gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode);
In your application, use a mode of dmModeReadWrite, since you may be modifying records in the database. If you know that you aren't making any modifications, then use a dmModeReadOnly mode.
If your application supports private records, then you should honor the user's preference of whether to show private records by setting dmModeShowSecret to the mode, as necessary. Here's code that adds the dmModeShowSecret appropriately:
SystemPreferencesType sysPrefs; // Determine if secret records should be shown. PrefGetPreferences(&sysPrefs); if (!sysPrefs.hideSecretRecords) mode |= dmModeShowSecret; gDB = DmOpenDatabaseByTypeCreator(kCustType, kSalesCreator, mode);
Closing a Database
When you are finished with a database, call DmCloseDatabase:
err = DmCloseDatabase(gDB);
Don't leave databases open unnecessarily, because each open database takes approximately 100 bytes of room in the dynamic heap. A good rule of thumb might be that if the user isn't in a view that has access to the data in that database, it shouldn't be open.
Note that when you close a database, records in that database that are locked or busy remain that way. If for some reason your code must close a database while you have locked or busy records, call DmResetRecordStates before calling DmCloseDatabase:
err = DmResetRecordStates(gDB); err = DmCloseDatabase(gDB);
The Data Manager doesn't do this resetting automatically from DmCloseDatabase because of the performance penalty. The philosophy is that you shouldn't penalize the vast majority of applications that have released and unlocked all their records. Instead, force the minority of applications to make the extra call and incur the speed penalty in these rare cases.
Creating the Application Info Block in a Database
The application info block is a block of memory that is associated with your database as a whole. You can use it for database-wide information. For example, you might have a database of checks and want to keep the total value of all the checks. Or you might allow the user to choose from among more than one sort order and need to keep track of the current sort order. Or you might need to keep track of category names. In each of these cases, the application info block is an appropriate place to keep this information. Here's a snippet of code (normally used when you create your database) to allocate and initialize the application info block:
UInt cardNo; LocalID dbID; LocalID appInfoID; MyAppInfoType *appInfoP; if (DmOpenDatabaseInfo(gDB, &dbID, NULL, NULL, &cardNo, NULL)) return dmErrInvalidParam; h = DmNewHandle(gDB, sizeof(MyAppInfoType)); if (!h) return dmErrMemError; appInfoID = MemHandleToLocalID(h); DmSetDatabaseInfo(cardNo, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL); appInfoP = (MyAppInfoType *) MemHandleLock(h); DmSet(appInfoP, 0, sizeof(MyAppInfoType), 0); // Code deleted to initialize fields in appInfoP // Unlock MemPtrUnlock(appInfoP);
Note that you can't use MemHandleNew to allocate the block, because you want the block to be in the same heap as the database and not in the dynamic heap. Therefore, use DmNewHandle. Also, you can't directly store the handle in the database. Instead, you must convert it to a local ID.
NOTE: Remember that a local ID is an offset from the beginning of the card. This is necessary for the future in case multiple cards are supported. In such a case, the memory addresses would be dependent on the slot in which the card was placed. |
If you use the Category Manager to manage your database, you need to make sure the first field in your application info block is of type AppInfoType (this stores the mapping from category number to category name). To initialize this field, call CategoryInitialize:
CategoryInitialize(&appInfoP->appInfo, LocalizedAppInfoStr);
The second parameter is the resource ID of an application string list (resource type tAIS) that contains the initial category names. You need to add one of these to your resource file (it's common to initialize it with "Unfiled", "Business", and "Personal").
Working with Records |
Now that you know how to set up databases, you need to populate them with records. How you sort and therefore find a record is usually determined when you create it. Let's first look at the mechanics of finding a record. After that, we'll create a new record.
Finding a Record
If your records are sorted based on the value of a field (or fields) within the record, you can do a binary search to find a particular record. If your records aren't sorted (or you are looking for a record based on the value of an unsorted field), you need to iterate through all the records, testing each record to see whether it is the one you want. "Iterating Through the Records in a Database or Category" later in this chapter shows how to iterate through all records. If you are looking for a unique ID, there's a call to find a record.
Finding a record given a unique ID
If you've got the unique ID, you get the record number using DmFindRecordByID:
UInt recordNumber; err = DmFindRecordByID(gDB, uniqueID, &recordNumber);
Note that this search starts at the first record and keeps looking until it finds the one with a matching unique ID.
Finding a record given a key
If you have records sorted by some criterion (see "Sorting the Records in a Database" later in this chapter), you can do a binary search to find a specific record. First, you need to define a comparison routine that compares two records and determines the ordering between the two. Here are the possible orderings:
The comparison routine takes six parameters:
The extra parameters (beyond just the records) are there to allow sorting based on further information. This is information found outside the record and includes such things as attributes (its category, for instance), a unique ID, and a specified sort order. The "other" integer parameter is necessary whenever you call a routine that requires a comparison routine; it is then passed on to your comparison routine. This parameter is commonly used to pass a sort order to your sorting routine. Note that the application info block is rarely used as part of a comparison routine-perhaps to sort by alphabetized categories (Business first, then Personal, then Unfiled). Since the category names are stored in the application info block, it's needed by a comparison routine that wants to take into account category names.
Here's an example comparison function that compares first by lastName field and then by firstName field. The attributes, unique ID, application info block, and extra integer parameter are not used:
static Int CompareRecordFunc(MyRecordStruct *rec1, MyRecordStruct *rec2, Int unusedInt, SortRecordInfoPtr unused1, SortRecordInfoPtr unused2, VoidHand appInfoH) { Int result; result = StrCompare(rec1->lastName, rec2->lastName); if (result == 0) result = StrCompare(rec1->firstName, rec2->firstName); return result; }
The DmFindSortPosition is used to find a record (or to find where a record would be placed if it were in the database). It takes five parameters:
Here's a search for a specific record. Note that DmFindSortPosition returns a number in the range 0..numberOfRecords. A return result of 0 signifies that the passed-in record is less than any existing records. A return result equal to the number of records signifies that the passed-in record is _ the last record. A return result, i, in the range 1..numberOfRecords-1 signifies that record i -1 _ passed-in record < record i. Here's a use of DmFindSortPosition that finds the record, if present:
Boolean foundIt = false; MyRecordStruct findRecord; UInt recordNumber; findRecord.lastName = "Rhodes"; findRecord.firstName = "Neil"; recordNumber = DmFindSortPosition(gDB, &findRecord, 0, (DmComparF *) CompareRecordFunc, 0); if (recordNumber > 0) { MyRecordStruct *record; Handle theRecordHandle; theRecordHandle = DmQueryRecord(gDB, recordNumber - 1); record = MemHandleLock(theRecordHandle); foundIt = StrCompare(findRecord.lastName, record->lastName) == 0 && StrCompare(findRecord.firstName, record->firstName); MemHandleUnlock(theOrderHandle); } if (foundIt) { // recordNumber - 1 is the matching record } else { // record at recordNumber < findRecord < record at recordNumber+1 }
Creating a New Record
You create a new record with DmNewRecord:
myRecordHandle = DmNewRecord(gDB, &recordIndex, recordSize)
The recordSize is the initial record size; you can change it later with MemHandleSetSize, just as you would with any handle. Make sure you specify a positive record size; zero-size records are not valid.
You'll notice that you need to specify the index number of the record as the second parameter. You initialize it with the desired record index; when DmNewRecord returns, it contains the actual record index.
Record indexes are zero-based; they range from 0 to one less than the number of records. If your desired record index is in this range, the new record will be created with your desired record index. All the records with that index and above are shifted up (their record indexes are increased by one). If your desired record index is _ the number of records, your new record will be created after the last record, and the actual record index will be returned.
Adding at the beginning of the database
To add to the beginning of the database, use 0 as a desired record index:
UInt recordIndex = 0; myRecordHandle = DmNewRecord(gDB, &recordIndex, recordSize)
Adding at the end of the database
To add to the end of the database, use dmMaxRecordIndex as your desired record index:
UInt recordIndex = dmMaxRecordIndex; myRecordHandle = DmNewRecord(gDB, &recordIndex, recordSize) // now recordIndex contains the actual index
You should rarely add to the end of the database, because archived and deleted records are kept at the end.
Adding in sort order
Use DmFindSortPosition to determine where to insert the record:
UInt recordIndex; MyRecordStruct newRecord; VoidHand myRecordHandle; MyRecordStruct *newRecordPtr; // initialize fields of newRecord recordIndex = DmFindSortPosition(gDB, &newRecord, 0, (DmComparF *) CompareRecordFunc, 0); myRecordHandle = DmNewRecord(gDB, &recordIndex, sizeof(MyRecordStruct)); newRecordPtr = MemHandleLock(myRecordHandle); DmWrite(newRecordPtr, 0, &newRecord, sizeof(newRecord)); MemHandleUnlock(myRecordHandle);
The recordNumber returned by DmFindSortPosition is the record number you use with DmNewRecord.
Reading from a Record
Reading from a record is very simple. Although records are write-protected, they are still in RAM; thus you can just get a record from a database, lock it, and then read from it. Here's an example:
VoidHand myRecord = DmQueryRecord(gDB, recordNumber); StructType *s = MemHandleLock(myRecord); DoSomethingReadOnly(s->field); MemHandleUnlock(myRecord);
The DmQueryRecord call returns a record that is read-only; it can't be written to, as it doesn't mark the record as busy.
Modifying a Record
In order to modify a record, you must use DmGetRecord, which marks the record busy. Call DmReleaseRecord when you're finished with it. Because you can't just write to the pointer (the storage area is write-protected), you must use either DmSet (to set a range to a particular character value) or DmWrite.
Often, a record has a structure associated with it. You usually read and write the entire structure:
VoidHand myRecord = DmGetRecord(gDB, recordNumber); StructType *s = MemHandleLock(myRecord); StructType theStructure; theStructure = *s; theStructure.field = newValue; DmWrite(gDB, s, 0, &theStructure, sizeof(theStructure)); MemHandleUnlock(myRecord); DmReleaseRecord(gDB, recordNumber, true);
Another alternative is to write specific fields in the structure. A very handy thing to use in this case is the standard C offsetof macro (offsetof returns the offset of a field within a structure):
VoidHand myRecord = DmGetRecord(gDB, recordNumber); StructType *s = MemHandleLock(myRecord); DmWrite(s, offsetof(StructType, field), &newValue, sizeof(newValue)); MemHandleUnlock(myRecord); DmReleaseRecord(gDB, recordNumber, true);
The second approach has the advantage of writing less data; it writes only the data that needs to change.
The third parameter to DmReleaseRecord tells whether the record was actually modified or not. Passing the value true causes the record to be marked as modified. If you modify a record but don't tell DmReleaseRecord that you changed it, during a HotSync the database's conduit may not realize the record has been changed.
Handling Secret Records
In order for a Palm OS user to feel comfortable maintaining sensitive information on his device, the Palm OS supports secret (also called private) records. In the Security application, the user can specify whether to show or hide private records. The user can specify a password that is required before private records are shown.
Each record has a bit associated with it (in the record attributes) that indicates whether it is secret. As part of the mode you use when opening a database, you can request that secret records be skipped. "Opening a Database" on page 143 shows the code you need. Once you make that request, some of the database operations on that open database completely ignore secret records. The routines that take index numbers (like DmGetRecord or DmQueryRecord) don't ignore secret records, nor does DmNumRecords. DmNumRecordsInCategory and DmSeekRecordInCategory do ignore secret records, though. You can use these to find a correct index number.
The user sets the secret bit of a record in a Details dialog for that record. Here is some code that handles that request:
ControlPtr privateCheckbox; UInt attributes; Boolean isSecret; DmRecordInfo(CustomerDB, recordNumber, &attributes, NULL, NULL); isSecret = (attributes & dmRecAttrSecret) == dmRecAttrSecret; privateCheckbox = GetObjectFromActiveForm(DetailsPrivateCheckbox); CtlSetValue(privateCheckbox, isSecret); hitButton = FrmDoDialog(frm); if (hitButton == DetailsOKButton) { if (CtlGetValue(privateCheckbox) != isSecret) { if (CtlGetValue(privateCheckbox)) { attributes |= dmRecAttrSecret; // tell user how to hide private records if (!gHideSecretRecords) FrmAlert(privateRecordInfoAlert); } else attributes &= ~dmRecAttrSecret; DmSetRecordInfo(CustomerDB, recordNumber, &attributes, NULL); } }
Note that we must put up an alert (see Figure 6-2) if the user marks a record as private while show all records is enabled. As we are still showing private records, this might be confusing for a new user, who sees this private checkbox, marks something as private, and expects something to happen as a result.
-Figure 6- 2.
Alert shown when user marks a record as private while showing private records
Iterating Through the Records in a Database
or Category
Whether you want only the items in a particular category or all the records, you still need to use category calls. These calls skip over deleted or archived (but still present) and private records (if the database is not opened with dmModeShowSecret).
Here's some code to visit every record:
UInt theCategory = dmAllCategories; // could be a specific category UInt totalItems = DmNumRecordsInCategory(gDB, theCategory); UInt i; UInt recordNum = 0; for (i = 0; i < totalItems; i++) { VoidHand recordH = DmQueryNextInCategory (gDB, &recordNum, theCategory); // at this point, recordNum contains the desired record number. // You could use DmGetRecord to get write-access, and then // DmReleaseRecord when finished // do something with recordH }
Sorting the Records in a Database
Just as finding an item in a sorted database requires a comparison routine, sorting a database requires a similar routine. There are two different sort routines you can use. The first, DmInsertionSort, uses an insertion sort (similar to the way most people sort a hand of cards, placing each card in its proper location one by one). The insertion sort works very quickly on an almost-sorted database. For example, if you change one record in a sorted database it may now be out of place while all the other records are still in sorted order. Use the insertion sort to put it back in order.
The second routine, DmQuickSort, uses a quicksort (it successively partitions the records). If you don't know anything about the sort state of the database, use the quicksort. Changing the sort order (for instance, by name instead of by creation date) causes all records to be out of order. This is an excellent time to use the quicksort.
Insertion sort
err = DmInsertionSort(gDB, (DmComparF *) CompareRecordFunc, 0);
Quicksort
err = DmQuickSort(gDB, (DmComparF *) CompareRecordFunc, 0);
Both sorting routines put deleted and archived records at the end of the database (deleted records aren't passed to the comparison routine, since there's no record data). Keeping deleted and archived records at the end of the database isn't required, but it is a widely followed convention used by the sorting routines and by DmFindSortPosition.
One other difference between the two sorting routines is that DmInsertionSort is a stable sort, while DmQuickSort is not. That is, two records that compare the same will remain in the same relative order after DmInsertionSort but might switch positions after DmQuickSort.
Deleting a Record
Deleting a record is slightly complicated because of the interaction with conduits and the data on the desktop. The simplest record deletion is to completely remove the record from the database (using DmRemoveRecord). This is used when the user creates a record but then immediately decides to delete it. Since there's no corresponding record on the desktop, there's no information that needs to be maintained in the database so that synchronization can occur.
When a preexisting record is deleted, it also needs to be deleted on the desktop during the next Hotsync. To handle this deletion from the desktop, the unique ID and attributes are still maintained in the database (but the record's memory chunk is freed). The deleted attribute of the record is set. The conduit looks for this bit setting and then deletes such records from the desktop and from the handheld on the next HotSync. DmDeleteRecord does this kind of deletion, leaving the record's unique ID and attributes in the database.
The final possibility is that the user requests that a deleted record be archived on the desktop (see Figure 6-3). In this case, the memory chunk can't be freed (because the data must be copied to the desktop to be archived). Instead, the archived bit of the record is set, and it is treated on the handheld as if it were deleted. Once a HotSync occurs, the conduit copies the record to the desktop and then deletes it from the handheld database. DmArchiveRecord does this archiving.
Figure 6- 3.
Dialog allowing the user to archive a record on the desktop (it shows up after the user asks to delete a record)
Newly archived and deleted records should be moved to the end of the database (the sorting routines and DmFindSortPosition rely on archived and deleted records being only at the end of the database). Here's the logic you'll probably want to use when the user deletes a record:
if (isNew && !gSaveBackup) DmRemoveRecord(gDB, recordNumber); // remove all traces else { if (gSaveBackup) //need to archive it on PC DmArchiveRecord(gDB, recordNumber); else DmDeleteRecord(gDB, recordNumber); // leave the unique ID and attrs // Deleted records are stored at the end of the database DmMoveRecord (gDB, recordNumber, DmNumRecords(gDB)); }
If the user doesn't explicitly request that a record be deleted, but implicitly requests it by deleting necessary data (for instance, ending up with an empty memo in the Memo Pad), you don't need to archive the record. Here's the code you use:
if (recordIsEmpty) { if (isNew) DmRemoveRecord(gDB, recordNumber); // remove all traces else { DmDeleteRecord(gDB, recordNumber); // leave the unique ID and attrs // Deleted records are stored at the end of the database DmMoveRecord (gDB, recordNumber, DmNumRecords(gDB)); } }
Dealing with Large Records
The maximum amount of data a record can hold is slightly less than 64KB of data. If you've got larger amounts of data to deal with, there are a couple of ways to tackle the problem.
File streaming
If you're using Palm OS 3.0, you can use the File Streaming Manager. The File Streaming Manager provides a file-based API (currently implemented as separate chunks within a database heap). You create a uniquely named file and a small record that stores only that filename. We suggest you use as a filename the database creator followed by the database type, followed by the record's unique ID. Use FileOpen to create a file:
FileHand fileHandle; UInt cardNo = 0; fileHandle = FileOpen(cardNo, uniqueFileName, kCustType, kSalesCreator, fileModeReadWrite, &err);
Store the filename as the contents of the record. Read and write with FileRead and FileWrite. When you are done reading and writing, close the file with FileClose. When you delete the record, you can delete the file with FileDelete.
NOTE: One disadvantage of file streams is that your conduit has no access to these files. |
Multiple chunks in a separate database
If you are running Palm OS 2.0 or earlier, the File Stream Manager isn't available. Therefore, you need to allocate multiple chunks in a separate database yourself. The record stores the unique IDs of each of the chunks in the separate chunk database. Here's a rough idea of how you might support a record of up to 180KB (we'll have 18 records of 10KB each-we don't want each record to be too big, since it's easier to pack smaller objects into the many 64KB heaps than it is to pack fewer larger ones). We assume we've got two open databases: gDB, where our "large" records are, and gChunkDB, which contains our chunks:
#define kNumChunks 18 #define kChunkSize (10 * 1024) typedef struct { ULong uniqueIDs[kNumChunks]; } MyRecordType; MyRecordType newRecord; MyRecordType *newRecordPtr = 0; Handle h; int i; UInt numRecordsInChunkDatabase; // keep track of original number of records // so in case a problem occurs we can delete // any we've added numRecordsInChunkDatabase = DmNumRecords(gChunkDB); for (i = 0; i < kNumChunks; i++) { UInt chunkRecordNumber = dmMaxRecordIndex; h = DmNewRecord(gChunkDB, &chunkRecordNumber, kChunkSize); if (!h) break; if (DmRecordInfo(gChunkDB, chunkRecordNumber, NULL, &newRecord.uniqueIDs[i], NULL) != 0) break; DmReleaseRecord(gChunkDB, chunkRecordNumber, true); } if (i >= kNumChunks) { // we were able to allocate all the chunks UInt recordNumber = 0; h = DmNewRecord(gDB, &recordNumber, sizeof(MyRecordType)); if (h) { newRecordPtr = MemHandleLock(h); DmWrite(newRecordPtr, 0, &newRecord, sizeof(newRecord)); DmReleaseRecord(gDB, recordNumber, true); } } if (!newRecordPtr) { // unable to allocate all chunks and record // delete all the chunks we allocated UInt recordNumToDelete; recordNumToDelete = DmNumRecords(gChunkDB) - 1; while (recordNumToDelete >= numRecordsInChunkDatabase) DmRemoveRecord(gChunkDB, recordNumToDelete--); }
Now that you've allocated the record (and the chunks it points to), it's fairly straightforward to edit any of the 180KB bytes of data. You use the unique ID to go into the appropriate chunk (reading it from the chunk database after finding the index with DmFindRecordByID).
Editing a Record in Place
The Field Manager can be set to edit a string field in place. The string need not take up the entire record; you specify the starting offset of the string and the current string length. The Field Manager resizes the handle as necessary while the string is edited.
This mechanism is a great way to handle editing a single string in a record. However, you can't have multiple fields simultaneously editing multiple strings in a record. For example, if you have a record containing both last name and first name, you can't create two fields in a single form to edit both the last name and first name in place. (This makes sense, because each of the fields may want to resize the handle.)
The following sections explain this mechanism.
Initialize the field with the handle
This code shows how to initialize the field with the handle:
typedef struct { int field; // other fields char textField[1]; // may actually be longer, null-terminated } MyRecType; Handle theRecordHandle; Handle oldTextHandle = FldGetTextHandle(fld); if (fld) { // must dispose of the old handle, or we'll leak memory MemHandleFree(oldTextHandle); } theRecordHandle = DmGetRecord(gDB, recordNumber); recPtr = MemHandleLock(theRecordHandle); FldSetText(fld, theRecordHandle, offsetof(MyRecType, textField), StrLen(theRecordHandle.textField) + 1);
Cleanup once the editing is finished
When the editing is done (this usually occurs when the form is closing), three things need to be done:
Here's the code:
Boolean dirty = FldDirty(fld); if (dirty) FldCompactText(fld); FldSetTextHandle(fld, NULL); DmReleaseRecord(gDB, recordNumber, dirty);
Examining Databases in the Sales Sample |
Now that you understand how databases and records function within the storage heap space, let's look at how we use them in our Sales application.
Defining the Sales Databases
The Sales application has three different databases. The first holds customers, the second orders (one record for each order), and the third items. Here are the constant definitions for the names and types:
#define kCustomerDBType 'Cust' #define kCustomerDBName "Customers-Sles" #define kOrderDBType 'Ordr' #define kOrderDBName "Orders-Sles" #define kProductDBType 'Prod' #define kProductDBName "Products-Sles"
Reading and Writing the Customer
The customer is stored as the customer ID followed by four null-terminated strings back to back (it's "packed," so to speak). Here's a structure we use for the customer record (there's no way to represent the four strings, so we just specify the first one):
typedef struct { SDWord customerID; char name[1]; // actually may be longer than 1 } PackedCustomer;
When we're working with a customer and need to access each of the fields, we use a different structure:
typedef struct { SDWord customerID; const char *name; const char *address; const char *city; const char *phone; } Customer;
Here's a routine that takes a locked PackedCustomer and fills out a customer-it unpacks the customer. Note that each field points into the PackedCustomer (to avoid allocating additional memory). The customer is valid only while the PackedCustomer remains locked (otherwise, the pointers are not valid):
// packedCustomer must remain locked while customer is in use static void UnpackCustomer(Customer *customer, const PackedCustomer *packedCustomer) { const char *s = packedCustomer->name; customer->customerID = packedCustomer->customerID; customer->name = s; s += StrLen(s) + 1; customer->address = s; s += StrLen(s) + 1; customer->city = s; s += StrLen(s) + 1; customer->phone = s; s += StrLen(s) + 1; }
We have an inverse routine that packs a customer:
static void PackCustomer(Customer *customer, VoidHand customerDBEntry) { // figure out necessary size UInt length = 0; CharPtr s; UInt offset = 0; length = sizeof(customer->customerID) + StrLen(customer->name) + StrLen(customer->address) + StrLen(customer->city) + StrLen(customer->phone) + 4; // 4 for string terminators // resize the VoidHand if (MemHandleResize(customerDBEntry, length) == 0) { // copy the fields s = MemHandleLock(customerDBEntry); offset = 0; DmWrite(s, offset, (CharPtr) &customer->customerID, sizeof(customer->customerID)); offset += sizeof(customer->customerID); DmStrCopy(s, offset, (CharPtr) customer->name); offset += StrLen(customer->name) + 1; DmStrCopy(s, offset, (CharPtr) customer->address); offset += StrLen(customer->address) + 1; DmStrCopy(s, offset, (CharPtr) customer->city); offset += StrLen(customer->city) + 1; DmStrCopy(s, offset, (CharPtr) customer->phone); MemHandleUnlock(customerDBEntry); } }
Reading and Writing Products
Similarly, we have structures for packed and unpacked products:
typedef struct { ULong productID; ULong price; // in cents const char *name; } Product; typedef struct { DWord productID; DWord price; // in cents char name[1]; // actually may be longer than 1 } PackedProduct;
Since the structure for packed and unpacked products is so similar, we could write our code to not distinguish between the two. However, in the future, we may want to represent the data in records differently from the data in memory. By separating the two now, we prepare for possible changes in the future.
The productID is unique within the database. We keep the price in cents so we don't have to deal with floating-point numbers.
We have routines that pack and unpack:
static void PackProduct(Product *product, VoidHand productDBEntry) { // figure out necessary size UInt length = 0; CharPtr s; UInt offset = 0; length = sizeof(product->productID) + sizeof(product->price) + StrLen(product->name) + 1; // resize the VoidHand if (MemHandleResize(productDBEntry, length) == 0) { // copy the fields s = MemHandleLock(productDBEntry); DmWrite(s, offsetof(PackedProduct, productID), &product->productID, sizeof(product->productID)); DmWrite(s, offsetof(PackedProduct, price), &product->price, sizeof(product->price)); DmStrCopy(s, offsetof(PackedProduct, name), (CharPtr) product->name); MemHandleUnlock(productDBEntry); } } // packedProduct must remain locked while product is in use static void UnpackProduct(Product *product, const PackedProduct *packedProduct) { product->productID = packedProduct->productID; product->price = packedProduct->price; product->name = packedProduct->name; }
Working with Orders
Orders have a variable number of items:
typedef struct { DWord productID; DWord quantity; } Item; typedef struct { SDWord customerID; Word numItems; Item items[1]; // this array will actually be numItems long. } Order;
There is zero or one order per customer. An order is matched to its customer via the customerUniqueID.
We have variables for the open databases:
static DmOpenRef gCustomerDB; static DmOpenRef gOrderDB; static DmOpenRef gProductDB;
Opening, Creating, and Closing the Sales Databases
Here's our StartApplication that opens the databases (after creating each one, if necessary):
static Err StartApplication(void) { UInt prefsSize; UInt mode = dmModeReadWrite; Err err = 0; CategoriesStruct *c; Boolean created; // code that reads preferences deleted // Determime if secret records should be shown. gHideSecretRecords = PrefGetPreference(prefHidePrivateRecords); if (!gHideSecretRecords) mode |= dmModeShowSecret; // Find the Customer database. If it doesn't exist, create it. OpenOrCreateDB(&gCustomerDB, kCustomerDBType, kSalesCreator, mode, 0, kCustomerDBName, &created); if (created) InitializeCustomers(); // Find the Order database. If it doesn't exist, create it. OpenOrCreateDB(&gOrderDB, kOrderDBType, kSalesCreator, mode, 0, kOrderDBName, &created); if (created) InitializeOrders(); // Find the Product database. If it doesn't exist, create it. OpenOrCreateDB(&gProductDB, kProductDBType, kSalesCreator, mode, 0, kProductDBName, &created); if (created) InitializeProducts(); c = GetLockedAppInfo(); gNumCategories = c->numCategories; MemPtrUnlock(c); return err; }
It uses a utility routine to open (and create, if necessary) each database:
// open a database. If it doesn't exist, create it. static Err OpenOrCreateDB(DmOpenRef *dbP, ULong type, ULong creator, ULong mode, UInt cardNo, char *name, Boolean *created) { Err err; *created = false; *dbP = DmOpenDatabaseByTypeCreator(type, creator, mode); err = DmGetLastErr(); if (! *dbP) { err = DmCreateDatabase(0, name, creator, type, false); if (err) return err; *created = true; *dbP = DmOpenDatabaseByTypeCreator(type, creator, mode); if (! *dbP) return DmGetLastErr(); } return err; }
It uses another utility routine to read the categories from the application info block for the product database:
static CategoriesStruct * GetLockedAppInfo() { UInt cardNo; LocalID dbID; LocalID appInfoID; Err err; if ((err = DmOpenDatabaseInfo(gProductDB, &dbID, NULL, NULL, &cardNo, NULL)) != 0) return NULL; if ((err = DmDatabaseInfo(cardNo, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL)) != 0) return NULL; return MemLocalIDToLockedPtr(appInfoID, cardNo); }
When the application closes, it has to close the databases:
static void StopApplication(void) { // code that saves preferences deleted // Close all open forms, this will force any unsaved data to // be written to the database. FrmCloseAllForms(); // Close the databases. DmCloseDatabase(gCustomerDB); DmCloseDatabase(gOrderDB); DmCloseDatabase(gProductDB); }
Initializing the Sales Databases
We have routines to initialize each of the databases. At some point, these routines could be removed (instead, our conduit would initialize the database during a HotSync).
Initializing the customer database
Here's the initialization routine for customers:
static void InitializeCustomers(void) { Customer c1 = {1, "Joe's toys-1", "123 Main St." ,"Anytown", "(123) 456-7890"}; Customer c2 = {2, "Bucket of Toys-2", "" ,"", ""}; Customer c3 = {3, "Toys we be-3", "" ,"", ""}; Customer c4 = {4, "a", "" ,"", ""}; Customer c5 = {5, "b", "" ,"", ""}; Customer c6 = {6, "c", "" ,"", ""}; Customer c7 = {7, "d", "" ,"", ""}; Customer *customers[7]; UInt numCustomers = sizeof(customers) / sizeof(customers[0]); UInt i; customers[0] = &c1; customers[1] = &c2; customers[2] = &c3; customers[3] = &c4; customers[4] = &c5; customers[5] = &c6; customers[6] = &c7; for (i = 0; i < numCustomers; i++) { UInt index = dmMaxRecordIndex; VoidHand h = DmNewRecord(gCustomerDB, &index, 1); if (h) { PackCustomer(customers[i], h); DmReleaseRecord(gCustomerDB, index, true); } } }
Initializing the product database
Here's the routine to initialize products:
static void InitializeProducts(void) { #define kMaxPerCategory 4 #define kNumCategories 3 Product prod1 = {125, 253 ,"GI-Joe"}; Product prod2 = {135, 1122 ,"Barbie"}; Product prod3 = {145, 752 ,"Ken"}; Product prod4 = {9, 852 ,"Skipper"}; Product prod5 = {126, 253 ,"Kite"}; Product prod6 = {127, 350 , "Silly-Putty"}; Product prod7 = {138, 650 ,"Yo-yo"}; Product prod8 = {199, 950 ,"Legos"}; Product prod9 = {120, 999 ,"Monopoly"}; Product prod10= {129, 888 , "Yahtzee"}; Product prod11= {10, 899 , "Life"}; Product prod12= {20, 1199 ,"Battleship"}; Product *products[kNumCategories][kMaxPerCategory]; UInt i; UInt j; VoidHand h; products[0][0] = &prod1; products[0][1] = &prod2; products[0][2] = &prod3; products[0][3] = &prod4; products[1][0] = &prod5; products[1][1] = &prod6; products[1][2] = &prod7; products[1][3] = &prod8; products[2][0] = &prod9; products[2][1] = &prod10; products[2][2] = &prod11; products[2][3] = &prod12; for (i = 0; i < kNumCategories; i++) { for (j = 0; j < kMaxPerCategory && products[i][j]->name; j++) { UInt index; PackedProduct findRecord; VoidHand h; findRecord.productID = products[i][j]->productID; index = DmFindSortPosition(gProductDB, &findRecord, 0, (DmComparF* ) CompareIDFunc, 0); h = DmNewRecord(gProductDB, &index, 1); if (h) { UInt attr; // Set the category of the new record to the category it // belongs in. DmRecordInfo(gProductDB, index, &attr, NULL, NULL); attr &= ~dmRecAttrCategoryMask; attr |= i; // category is kept in low bits of attr DmSetRecordInfo(gProductDB, index, &attr, NULL); PackProduct(products[i][j], h); DmReleaseRecord(gProductDB, index, true); } } } h = DmNewHandle(gProductDB, offsetof(CategoriesStruct, names[kNumCategories])); if (h) { char *categories[] = {"Dolls", "Toys", "Games"}; CategoriesStruct *c = MemHandleLock(h); LocalID dbID; LocalID appInfoID; UInt cardNo; UInt num = kNumCategories; Err err; DmWrite(c, offsetof(CategoriesStruct, numCategories), &num, sizeof(num)); for (i = 0; i < kNumCategories; i++) DmStrCopy(c, offsetof(CategoriesStruct, names[i]), categories[i]); MemHandleUnlock(h); appInfoID = MemHandleToLocalID( h); err = DmOpenDatabaseInfo(gProductDB, &dbID, NULL, NULL, &cardNo, NULL); if (err == 0) { err = DmSetDatabaseInfo(cardNo, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL); ErrNonFatalDisplayIf(err, "DmSetDatabaseInfo failed"); } } }
The code inserts the products sorted by product ID (an alternative would be to create the products in unsorted order and then sort them afterward). Note also that the attributes of each record are modified to set the category of the product.
The comparison routine for sorting
Here's the comparison routine used for sorting products, companies, and orders:
static Int CompareIDFunc(SDWord *p1, SDWord *p2, Int i, SortRecordInfoPtr s1, SortRecordInfoPtr s2, VoidHand appInfoH) { // can't just return *p1 - *p2 because that's a long that may overflow // our return type of Int. Therefore, we do the subtraction ourself // and check long difference = *p1 - *p2; if (difference < 0) return -1; else if (difference > 0) return 1; else return 0; return (*p1 - *p2); }
Initializing the orders database
Finally, the orders must be initialized:
static void InitializeOrders(void) { Item item1 = {125, 253}; Item item2 = {126, 999}; Item item3 = {127, 888}; Item item4 = {138, 777}; Item item5 = {125, 6}; Item item6 = {120, 5}; Item item7 = {129, 5}; Item item8 = {10, 3}; Item item9 = {20, 45}; Item item10 = {125, 66}; Item item11 = {125, 75}; Item item12 = {125, 23}; Item item13 = {125, 55}; Item item14 = {125, 888}; Item item15 = {125, 456}; Item items[15]; VoidHand h; Order *order; UInt recordNum; UInt numItems = sizeof(items) / sizeof(items[0]); items[0] = item1; items[1] = item2; items[2] = item3; items[3] = item4; items[4] = item5; items[5] = item6; items[6] = item7; items[7] = item8; items[8] = item9; items[9] = item10; items[10] = item11; items[11] = item12; items[12] = item13; items[13] = item14; items[14] = item15; order= GetOrCreateOrderForCustomer(1, &recordNum); // write numItems DmWrite(order, offsetof(Order, numItems), &numItems, sizeof(numItems)); // resize to hold more items h = MemPtrRecoverHandle(order); MemHandleUnlock(h); MemHandleResize(h, offsetof(Order, items) + sizeof(Item) * numItems); order = MemHandleLock(h); // write new items DmWrite(order, offsetof(Order, items), items, sizeof(items)); // done with it MemHandleUnlock(h); DmReleaseRecord(gOrderDB, recordNum, true); }
Adding Records
All we do is add some items to the first customer. The remaining customers we treat as still needing an order. (We do this primarily to test later code that shows which customers do and do not have orders.) We use a routine that takes a customer ID and returns the corresponding order (or creates it as necessary). This routine is used not only for initializing the database, but also at other points in the program:
static Order * GetOrCreateOrderForCustomer(Long customerID, UInt *recordNumPtr) { VoidHand theOrderHandle; Order *order; Boolean exists; *recordNumPtr = OrderRecordNumber(customerID, &exists); if (exists) { theOrderHandle = DmGetRecord(gOrderDB, *recordNumPtr); ErrNonFatalDisplayIf(!theOrderHandle, "DMGetRecord failed!"); order = MemHandleLock(theOrderHandle); } else { Order o; theOrderHandle = DmNewRecord(gOrderDB, recordNumPtr, sizeof(Order)); if (!theOrderHandle) { FrmAlert(DeviceFullAlert); return NULL; } o.numItems = 0; o.customerID = customerID; order = MemHandleLock(theOrderHandle); DmWrite(order, 0, &o, sizeof(o)); } return order; }
OrderRecordNumber returns the record number of a customer's order or the location at which the order should be inserted, if no such order exists:
// returns record number for order, if it exists, or where it // should be inserted static UInt OrderRecordNumber(Long customerID, Boolean *orderExists) { Order findRecord; UInt recordNumber; *orderExists = false; findRecord.customerID = customerID; recordNumber = DmFindSortPosition(gOrderDB, &findRecord, 0, (DmComparF *) CompareIDFunc, 0); if (recordNumber > 0) { Order *order; VoidHand theOrderHandle; Boolean foundIt; theOrderHandle = DmQueryRecord(gOrderDB, recordNumber - 1); ErrNonFatalDisplayIf(!theOrderHandle, "DMGetRecord failed!"); order = MemHandleLock(theOrderHandle); foundIt = order->customerID == customerID; MemHandleUnlock(theOrderHandle); if (foundIt) { *orderExists = true; return recordNumber - 1; } } return recordNumber; }
The Customers Form
Let's now look at how the customers are displayed in the Customers form. Customers are displayed in a list that has a drawing callback function that displays the customer for a particular row (since it's called by the system, it must have the CALLBACK macros for GCC). The customers that already have an order are shown in bold, to distinguish them from the others. The text pointer is unused, since we don't store our customer names in the list but obtain them from the database. Here's the routine:
static void DrawOneCustomerInListWithFont(UInt itemNumber, RectanglePtr bounds, CharPtr *text) { VoidHand h; Int seekAmount = itemNumber; UInt index = 0; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif // must do seek to skip over secret records DmSeekRecordInCategory(gCustomerDB, &index, seekAmount, dmSeekForward, dmAllCategories); h = DmQueryRecord(gCustomerDB, index); if (h) { FontID curFont; Boolean setFont = false; PackedCustomer *packedCustomer = MemHandleLock(h); if (!OrderExistsForCustomer(packedCustomer->customerID)) { setFont = true; curFont = FntSetFont(boldFont); } DrawCharsToFitWidth(packedCustomer->name, bounds); MemHandleUnlock(h); if (setFont) FntSetFont(curFont); } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif }
The routine uses two other routines: one that finds the unique ID for a specific row number and one that tells whether an order exists. Here's the routine that returns a unique ID:
static ULong GetCustomerIDForNthCustomer(UInt itemNumber) { Long customerID; UInt index = 0; Int seekAmount = itemNumber; VoidHand h; PackedCustomer *packedCustomer; // must do seek to skip over secret records DmSeekRecordInCategory(gCustomerDB, &index, seekAmount, dmSeekForward, dmAllCategories); h = DmQueryRecord(gCustomerDB, index); ErrNonFatalDisplayIf(!h, "can't get customer in GetCustomerIDForNthCustomer"); packedCustomer = MemHandleLock(h); customerID = packedCustomer->customerID; MemHandleUnlock(h); return customerID; }
Note the use of DmSeekRecordInCategory, which skips over any secret records. Here's the code that calls OrderRecordNumber to figure out whether an order exists (so that the customer name can be bolded or not):
static Boolean OrderExistsForCustomer(Long customerID) { Boolean orderExists; OrderRecordNumber(customerID, &orderExists); return orderExists; }
Editing Customers
Here's the EditCustomerWithSelection routine that handles editing customers, deleting customers, and setting/clearing the private record attribute. The gotoData parameter is used to preselect some text in a field (used for displaying the results of a Find):
static void EditCustomerWithSelection(UInt recordNumber, Boolean isNew, Boolean *deleted, Boolean *hidden, struct frmGoto *gotoData) { FormPtr previousForm = FrmGetActiveForm(); FormPtr frm; UInt hitButton; Boolean dirty = false; ControlPtr privateCheckbox; UInt attributes; Boolean isSecret; FieldPtr nameField; FieldPtr addressField; FieldPtr cityField; FieldPtr phoneField; Customer theCustomer; UInt offset = offsetof(PackedCustomer, name); VoidHand customerHandle = DmGetRecord(gCustomerDB, recordNumber); *hidden = *deleted = false; DmRecordInfo(gCustomerDB, recordNumber, &attributes, NULL, NULL); isSecret = (attributes & dmRecAttrSecret) == dmRecAttrSecret; frm = FrmInitForm(CustomerForm); FrmSetEventHandler(frm, CustomerHandleEvent); FrmSetActiveForm(frm); UnpackCustomer(&theCustomer, MemHandleLock(customerHandle)); // code deleted that initializes the fields // unlock the customer MemHandleUnlock(customerHandle); privateCheckbox = GetObjectFromActiveForm(CustomerPrivateCheckbox); CtlSetValue(privateCheckbox, isSecret); hitButton = FrmDoDialog(frm); if (hitButton == CustomerOKButton) { dirty = FldDirty(nameField) || FldDirty(addressField) || FldDirty(cityField) || FldDirty(phoneField); if (dirty) { // code deleted that reads the fields into theCustomer } PackCustomer(&theCustomer, customerHandle); if (CtlGetValue(privateCheckbox) != isSecret) { dirty = true; if (CtlGetValue(privateCheckbox)) { attributes |= dmRecAttrSecret; // tell user how to hide private records if (gHideSecretRecords) *hidden = true; else FrmAlert(privateRecordInfoAlert); } else attributes &= ~dmRecAttrSecret; DmSetRecordInfo(gCustomerDB, recordNumber, &attributes, NULL); } } DmReleaseRecord(gCustomerDB, recordNumber, dirty); if (hitButton == CustomerDeleteButton) { *deleted = true; if (isNew && !gSaveBackup) DmRemoveRecord(gCustomerDB, recordNumber); else { if (gSaveBackup) // Need to archive it on PC DmArchiveRecord(gCustomerDB, recordNumber); else DmDeleteRecord(gCustomerDB, recordNumber); // Deleted records are stored at the end of the database DmMoveRecord(gCustomerDB, recordNumber, DmNumRecords(gCustomerDB)); } } else if (hitButton == CustomerOKButton && isNew && !(StrLen(theCustomer.name) || StrLen(theCustomer.address) || StrLen(theCustomer.city) || StrLen(theCustomer.phone))) { *deleted = true; // delete Customer if it is new & empty DmRemoveRecord(gCustomerDB, recordNumber); } else if (hitButton == CustomerCancelButton && isNew) { *deleted = true; DmRemoveRecord(gCustomerDB, recordNumber); } if (previousForm) FrmSetActiveForm(previousForm); FrmDeleteForm(frm); }
We have a utility routine we use that doesn't require a gotoData parameter:
static void EditCustomer(UInt recordNumber, Boolean isNew, Boolean *deleted, Boolean *hidden) { EditCustomerWithSelection(recordNumber, isNew, deleted, hidden, NULL); }
The Order Form
Most of the functionality in this form is provided in a table (see "Tables" on page 204). We won't look at the table parts specifically, but it's worth knowing that each visible row of the table has an item index number associated with it (this is retrieved with TblGetRowID). Here's the code that draws a product name for a particular row:
static void OrderDrawProductName(VoidPtr table, Word row, Word column, RectanglePtr bounds) { VoidHand h = NULL; Product p; UInt itemNumber; ULong productID; CharPtr toDraw; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif toDraw = "-Product-"; itemNumber = TblGetRowID(table, row); productID = gCurrentOrder->items[itemNumber].productID; if (productID) { h = GetProductFromProductID(productID, &p, NULL); if (h) toDraw = (CharPtr) p.name; } DrawCharsToFitWidth(toDraw, bounds); if (h) MemHandleUnlock(h); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif }
Looking up a product
GetProductFromProductId looks up a product given a product ID. Here's the code for that:
// if successful, returns the product, and the locked VoidHand static VoidHand GetProductFromProductID(ULong productID, Product *theProduct, UInt *indexPtr) { UInt index; PackedProduct findRecord; VoidHand foundHandle = 0; findRecord.productID = productID; index = DmFindSortPosition(gProductDB, &findRecord, 0, (DmComparF *) CompareIDFunc, 0); if (index > 0) { PackedProduct *p; VoidHand h; index--; h = DmQueryRecord(gProductDB, index); p = MemHandleLock(h); if (p->productID == productID) { if (theProduct) UnpackProduct(theProduct, p); else MemHandleUnlock(h); if (indexPtr) *indexPtr = index; return h; } MemHandleUnlock(h); } return NULL; }
Editing an item
The code to display the product ID and quantity doesn't use the Database Manager (so we don't show that code).
Here's a snippet of code from OrderSaveAmount that modifies the quantity, if it has been edited:
CharPtr textP = FldGetTextPtr(fld); Item oldItem = gCurrentOrder->items[gCurrentSelectedItemIndex]; if (table->currentColumn == kQuantityColumn) { if (textP) oldItem.quantity = StrAToI(textP); else oldItem.quantity = 0; } DmWrite(gCurrentOrder, offsetof(Order, items[gCurrentSelectedItemIndex]), &oldItem, sizeof(oldItem));
Note that DmWrite is used to modify gCurrentOrder, since gCurrentOrder is a record in the order database and can't be written to directly.
Deleting an item
We need to delete an item in certain circumstances (if the user explicitly chooses to delete an item, or sets the quantity to 0, and then stops editing that item). Here's the code that does that (note that it uses DmWrite to move succeeding items forward and uses MemPtrResize to make the record smaller):
// gCurrentOrder changes after this routine. // gCurrentItem is no longer valid static void DeleteNthItem(UInt itemNumber) { UInt newNumItems; ErrNonFatalDisplayIf(itemNumber >= gCurrentOrder->numItems, "bad itemNumber"); // move items from itemNumber+1..numItems down 1 to // itemNumber .. numItems - 1 if (itemNumber < gCurrentOrder->numItems - 1) DmWrite(gCurrentOrder, offsetof(Order, items[itemNumber]), &gCurrentOrder->items[itemNumber+1], (gCurrentOrder->numItems - itemNumber - 1) * sizeof(Item)); // decrement numItems; newNumItems = gCurrentOrder->numItems - 1; DmWrite(gCurrentOrder, offsetof(Order, numItems), &newNumItems, sizeof(newNumItems)); // resize the pointer smaller. We could use MemPtrRecoverHandle, // MemHandleUnlock, MemHandleResize, MemHandleLock. // However, MemPtrResize will always work // as long as your are making a chunk smaller. Thanks, Bob! MemPtrResize(gCurrentOrder, offsetof(Order, items[gCurrentOrder->numItems])); }
Adding a new item
Similarly, we must have a routine to add a new item:
// returns true if successfull. itemNumber is location at which it was // added static Boolean AddNewItem(UInt *itemNumber) { VoidHand theOrderHandle; Err err; UInt numItems; Item newItem = {0, 0}; ErrNonFatalDisplayIf(!gCurrentOrder, "no current order"); theOrderHandle = MemPtrRecoverHandle(gCurrentOrder); MemHandleUnlock(theOrderHandle); err = MemHandleResize(theOrderHandle, MemHandleSize(theOrderHandle) + sizeof(Item)); gCurrentOrder = MemHandleLock(theOrderHandle); if (err != 0) { FrmAlert(DeviceFullAlert); return false; } numItems = gCurrentOrder->numItems + 1; DmWrite(gCurrentOrder, offsetof(Order, numItems), &numItems, sizeof(numItems)); *itemNumber = gCurrentOrder->numItems - 1; DmWrite(gCurrentOrder, offsetof(Order, items[*itemNumber]), &newItem, sizeof(newItem)); gCurrentOrderChanged = true; return true; }
Note that if we can't resize the handle, we display the system alert telling the user that the device is full.
Finishing an order record
When the Order form is closed, the records in the order database must be updated. If there are no items, the entire order is deleted:
static void OrderFormClose(void) { VoidHand theOrderHandle; UInt numItems; OrderDeselectRowAndDeleteIfEmpty(); numItems = gCurrentOrder->numItems; // unlock the order theOrderHandle = MemPtrRecoverHandle(gCurrentOrder); MemHandleUnlock(theOrderHandle); // delete Order if it is empty; release it back to the database otherwise if (numItems == 0) DmRemoveRecord(gOrderDB, gCurrentOrderIndex); else DmReleaseRecord(gOrderDB, gCurrentOrderIndex, gCurrentOrderChanged); }
The Item Form
Once the form is initialized, the user interacts with it until a button is tapped. The event handler for the form handles the button tap:
static Boolean ItemHandleEvent(EventPtr event) { Boolean handled = false; FieldPtr fld; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif switch (event->eType) { case ctlSelectEvent: switch (event->data.ctlSelect.controlID) { case ItemOKButton: { char *textPtr; ULong quantity; fld = GetObjectFromActiveForm(ItemQuantityField); textPtr = FldGetTextPtr(fld); ErrNonFatalDisplayIf(!textPtr, "No quantity text"); quantity = StrAToI(textPtr); DmWrite(gCurrentOrder, offsetof(Order, items[gCurrentItemNumber].quantity), &quantity, sizeof(quantity)); if (gHaveProductIndex) { VoidHand h; PackedProduct *p; h = DmQueryRecord(gProductDB, gCurrentProductIndex); ErrNonFatalDisplayIf(!h, "Can't find the record"); p = MemHandleLock(h); DmWrite(gCurrentOrder, offsetof(Order, items[gCurrentItemNumber].productID), &p->productID, sizeof(p->productID)); MemHandleUnlock(h); } } break; case ItemCancelButton: break; case ItemDeleteButton: if (FrmAlert(DeleteItemAlert) == DeleteItemOK) DeleteNthItem(gCurrentItemNumber); else handled = true; break; } break; // code for other events deleted } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return handled; }
If the user taps OK, the code updates the quantity and product ID of the current item (if the user has edited it). If the user taps Delete, the code calls DeleteNthItem (which we've already seen). On a Cancel, the code doesn't modify the current order.