Order the book from O'Reilly

Previous PageTable Of ContentsIndexNext Page

In this chapter:

 6.  Databases

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

Top Of Page

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

Top Of Page

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.

Opening a Database

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

Top Of Page

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

Top Of Page

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.


Palm Programming: The Developer's Guide
Copyright © 1999, O'Rielly and Associates, Inc.
Published on the web by permission of O'Rielly and Associates, Inc. Contents modified for web display.

Previous PageTop Of PageTable Of ContentsIndexNext Page