Microsoft & .NET.NETAdding Records to an Open Database in Windows CE

Adding Records to an Open Database in Windows CE

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Well, after all, what would a database be without some data? It’s time to think about adding records. Before we dig into the specifics of this task, there are a few clarifications we need to make about the relationship between CE database properties, CE database records, and the overall structure of the CE database. (In the discussion that follows, I use the terms “database property” and “field” interchangeably; they are equivalent.)

First, recall I said in the opening paragraphs of the last installment that CE records accommodate any kind of data. This is indeed the case, although perhaps a bit disingenuous. Here’s why.

Basically, a CE database property (field) consists of a CEPROPVAL structure, which provides descriptor information, and optionally of some application allocated space which contains data too large to be directly stored in the CEPROPVAL structure. To better understand this, let’s examine the typedef of the CEPROPVAL structure:

typedef struct _CEPROPVAL
        {
             CEPROPID propid;
             WORD wLenData;
             WORD wFlags;
             CEVALUNION val;
         } CEPROPVAL;
typedef CEPROPVAL *PCEPROPVAL;

When we refer to a specific database property, invariably we do so using its CEPROPID, the first member of the CEPROPVAL structure. The CEPROPID is itself another typedef, which looks like this in the Windows CE header files:

typedef DWORD CEPROPID;

The CEPROPID provides two important pieces of information about a field: the ordinal of the field in the record, and the data type of the field. First, let’s take stock of the possible types for a CE database property.

CE Data Type Constants and Their Meanings

Property Type Constant Data Stored in the Property
CEVT_BLOB A CEBLOB structure
CEVT_FILETIME A FILETIME structure
CEVT_I2 A 16-bit signed integer
CEVT_I4 A 32-bit signed integer
CEVT_LPWSTR A null-terminated string
CEVT_UI2 A 16-bit unsigned integer
CEVT_UI4 A 32-bit unsigned integer

You’ve probably noticed that this is a pretty short list—there are no floating point types, no native date or time types. Basically, the list above is a collection of the things that can either be stored in 32 bits or referenced through a long pointer. What ever goes into a CE database record must be made to fit into one of these types. The relationship between the type of the data and the actual storage of the data will be easier to picture if we skip ahead in the CEPROPVAL structure’s members and examine the CEVALUNION member next. Here’s the typedef of CEVALUNION:

typedef union _CEVALUNION
                     {
                        short iVal;
                        USHORT uiVal;
                        long lVal;
                        ULONG ulVal;
                        FILETIME filetime;
                        LPWSTR lpwstr;
                        CEBLOB blob;
                     } CEVALUNION;

A union, if you aren’t familiar with the idiom, is a single block of space that is large enough to accommodate the storage of its largest member. It can hold only one value, and thus represent only one of its members at any given time. Unions are a useful construct in situations where a variable might assume the value of any of several different types of data. In the case of a CEPROPVAL, its union member allows us to store seven distinct types of data in a generic database property structure.

Here’s a key relationship to visualize: A CEPROPID gives the data type of a specific property. A CEVALUNION stores either the actual data value, or a pointer to the data value. Both are members of the CEPROPVAL structure, which defines the property’s attributes and (directly or indirectly) its value.

Now, let’s revisit the typedef of the CEPROPID:

typedef DWORD CEPROPID;

Recall that we said that a CEPROPID stores two pieces of information: the data type, and the property’s ordinal position in the record. Here’s how we create a CEPROPID for a short integer that’s the third property in the record:

MAKELONG(CEVT_I2, 2)

The MAKELONG macro places the CE data type constant in the LOWORD, and the zero-based index of the property in the HIWORD of the CEPROPID. By storing the property’s index, we are able to implement “sparse records.” In other words, if you have a record type that has 10 properties, and in a particular case you only have data for two of them, them you add the record using an array of only two CEPROPVAL structures—one for each field for which you have data.

The wDataLen and wFlags fields of the CEPROPVAL structure are unused, and by convention always set to 0.

Summing up, here is a litany of the relationships among the structures that constitute a CE database:

  • A database is a group of records owned by a specific database object, and the database object is identified by a unique CEOID.
  • A given database record consists of an array of CEPROPVAL structures, plus application allocated memory for data that is too large to be stored in the members of the CEVALUNION.
  • A record needs only store CEVALPROP structures for fields that contain data.
  • The CEPROPID gives the CE data type of the property, combined with the zero-based index of the property with respect to the complete set of properties in a given record.
  • The CEVALUNION holds either the value of the property, or a pointer to the space that holds the value.

Creating and Populating the CEPROPVAL Array

Whew! Let’s take a look at how the AddBirthdayReminder() function adds records to the CE database. The first part of the AddBirthdayReminder () function is devoted to gathering data from the user interface controls of the dialog box. The one important part of this is where we retrieve the name of the lucky recipient of our largess from the edit control:

//Find out, from the edit control, who gets the loot
hwndWho = GetDlgItem(hDlg,IDC_WHO);
iTextLen = SendMessage( hwndWho, WM_GETTEXTLENGTH, 0, 0);

//require user input
if( !iTextLen )
   {return FALSE;}

//allocate for wide character count plus terminal null
lpwszWho = (TCHAR* )LocalAlloc(LPTR, (iTextLen + 1)
         * sizeof(WCHAR) );
//get the input string
SendMessage( hwndWho, WM_GETTEXT, (iTextLen + 1),
                       (LPARAM)(LPSTR)lpwszWho);

The code for interrogating the dialog controls is fairly self explanatory, but notice two things: First, we have found the exact length of the name string, and allocated space to hold it, plus the terminating NULL. Second, the birth date that we retrieve from the DateTime control is returned to us in a SYSTEMTIME structure, which is not among the supported CE data types.

Now, we’ll skip ahead to the real work of structuring and populating the CE database record.

There are three items of data in a BirthdayReminder record: a name, a birth date, and an integer that indicates the type of gift we’ll be sending. In this example, we require the input of all three for each record. Therefore, to create a record, we need to allocate enough space to store three CEPROPVAL structures, plus enough additional space to store the name string and a SYSTEMTIME structure. Here’s how we do this:

//allocate an array of CEPROPVAL structures,
//plus storage space for strings
pRecord = (PCEPROPVAL)LocalAlloc( LPTR, 
                                (sizeof(CEPROPVAL)
                                * NUMBER_BIRTHDAY_PROPS)
                                + ((iTextLen + 1) * sizeof(WCHAR)
                                + sizeof(SysTime)));
if(!pRecord )
    {goto FAIL;}

We now have a single block of space large enough to hold the CEPROPVAL descriptor structures and all of the string and SYSTEMTIME data. The CEPROPVAL structures occupy the beginning of this data block; the name string and the SYSTEMTIME are stored in the remainder of the block. In order to write them into the proper location, we initialize a pointer to the byte immediately following the CEPROPVAL array.

//init pointer to record's string & SYSTEMTIME storage
pStringStore = (PBYTE)pRecord + (sizeof(CEPROPVAL)
                              * NUMBER_BIRTHDAY_PROPS);

Now, we begin populating the array of CEPROPVAL structures. The wLenData and wFlags fields are unused, and by convention always set to zero.

      //populate record
      //init the unused prop val members
for( i = 0; i < NUMBER_BIRTHDAY_PROPS; i++ )
   {
       pRecord[i].wLenData = 0;
       pRecord[i].wFlags = 0;
    }

We step through the array of property value structures by indexing the CEPRPOVAL pointer, pRecord.

//first we write the "who" string into a property
//write the prop id : ce type + index
pRecord[0].propid = MAKELONG(CEVT_LPWSTR, 0) ;
//copy the string to the data buffer at the end of the propval array
wcscpy((TCHAR*)pStringStore, lpwszWho );
//set the property union member to this ptr
pRecord[0].val.lpwstr = (LPWSTR)pStringStore;

To populate the name property, first we set its CEPROPID. The data type is CEVT_LPWSTR, “pointer to Unicode string”, and its index in the array of record properties is 0. Next, we copy the data from the buffer we allocated when we got the edit control input to the data storage area allocated for this record. Finally, we set the CEVALUNION member to the offset in the record’s data storage space where we copied the string. It is absolutely critical that you copy string data into the record allocation, and set the CEVALUNION member “lpwstr” to the correct offset in the record’s data storage area. If you set “lpwstr” to an address outside the record allocation, (the buffer from which we copied the string, for example) when the application exits, that memory will be freed and the database will be corrupted.

Next, we’ll populate the birthdate property, which is a SYSTEMTIME value. Because this type isn’t directly supported by CE, we must save it as a byte string, or BLOB (Binary Large OBject).

//next we write the "when" value into a property
//write the prop id : ce type + index
pRecord[1].propid = MAKELONG(CEVT_BLOB, 1) ;

First, we synthesize its CEPROPID, by combining its type and the index of this property in the record’s property array. A CEBLOB is yet another structure, typedef’ed like this:

typedef struct _CEBLOB {DWORD dwCount;
                        LPBYTE lpb;
                       } CEBLOB;

The size in bytes of the object is specified by dwCount, and the address of the first byte of the string is given by lpb. As with the name string, we must copy the actual data for this property into the data area of the record allocation.

//move the data buffer pointer past the "who" string
   blobSystime.lpb = pStringStore + (iTextLen + 1) * sizeof(WCHAR);

To do this, we move the pointer past the region into which we copied the name string. Notice that we don’t use LocalSize(lpwszWho) to advance the pointer, but instead, calculate the new location in the record’s data storage area using the string length returned by WM_GETTEXTLENGTH. LocalSize() returns the size of the allocation that was actually made in our behalf, which may have been larger than our request. Moving the buffer pointer using LocalSize() could cause us to write beyond the end of the record allocation, corrupting the database, and possibly crashing the application as well.

//copy the system time structure to the buffer as a byte string
memcpy(blobSystime.lpb, (PBYTE)&SysTime,
                   sizeof(SysTime) );
//set the size of the object in the CEBLOB structure
blobSystime.dwCount = sizeof(SysTime);

//put the CEBLOB structure in the value union for this property.
pRecord[1].val.blob = blobSystime;

We use memcpy() to move the SYSTEMTIME structure into the record allocation as a string of bytes. (This is also a good approach for storing floating point numbers in native format.) Finally, we initialize the dwCount member of the CEBLOB structure with the size of the SYSTEMTIME structure, and set the CEVALUNION member “val.blob” to the initialized blob structure.

Saving the best for last, we initialize the final CEPROPVAL structure for this record.

//copy the "what" data to a CEPROPVAL structure
//write the prop id : ce type + index
pRecord[2].propid = MAKELONG(CEVT_I2, 2) ;

//convert the data to int
pRecord[2].val.iVal = iWhat;

Because the data for this field is a short integer, we can store the property value directly in the “val.iVal” member of the CEVALUNION. Now we have a completely initialized data structure for the record.

Positioning for Data Access

With the exception of iterative reading through a database opened with the CEDB_AUTOINCREMENT flag, there isn’t really a “current record” construct that automatically operates on CE record access. As is the case with most of the rest of life, if you want to be somewhere, you have to exert a bit of effort to get there. Specifically, with respect CE databases, you must seek to a record before you can operate on it using CeSeekDatabase().

//seek to the end & write 
// the database PROPVAL array
CeSeekDatabase(globalHDB, 
               CEDB_SEEK_END,
               1, 
               &dwIndex );

The parameters to CeSeekDatabase(), in the order shown, are the handle to the database; a flag indicating the type of seek to perform; a value whose meaning depends on the seek type (in this case it is the number of records to seek beyond the end of the database); and the address of a variable that holds the index of the new position on a successful return. Seeking is fairly simple if you want to add a record to the beginning or end of the database, and more complex if you want to find a specific record based on the value of a sorted field. For now, we’ll let this example stand on its own. We investigate the use of CeSeekDatabase() in greater detail in the next example program, BetterBirthdays.

All that remains is to write the record to the database, clean up after ourselves, and go home.

//set oid parm to 0 to create a new rec
CeWriteRecordProps(globalHDB, 0,
                   NUMBER_BIRTHDAY_PROPS, 
                   pRecord);
if( GetLastError() == ERROR_INVALID_PARAMETER )
     { goto FAIL;}
if( GetLastError() == ERROR_DISK_FULL )
     { goto FAIL;}

First, we write the record, using a call to CeWriteRecordProps(). The parameters are, in the order shown, the handle to the database, the CEOID of the record to write, the number of CEPROPVAL structures in the array we are passing, and a pointer to the block we allocated to hold the array plus additional data storage. Notice that in this case we pass a value of 0 for the record’s CEOID. This causes a new record to be created and added to the database. CeWriteRecordProps() returns the CEOID for the new record if the call is successful. To test for success or failure, we call GetLastError(). While you are learning the CE database moves, routinely testing for a status of ERROR_INVALID_PARAMETER is a useful debugging tool, so we show that here.

Finally, we free all the allocations we made to build the record data structures:

//free the cepropval struct array
LocalFree( pRecord );

//free the name buffer
if(lpwszWho)
{
LocalFree( lpwszWho );
}

When the dialog box receives an IDOK or IDCANCEL message, we close the database like this:

if ((LOWORD(wParam) == IDOK) || (LOWORD(wParam) == IDCANCEL))
{
    CloseHandle(globalHDB);
    EndDialog(hDlg, LOWORD(wParam));
    return TRUE;
}

Notice that we close the database by using its handle, not its CEOID.

Reviewing the Birthday Reminder Example

At this point, you’ve seen how to:

  • Create a simple a database using CeCreateDatabase()
  • Open a database without using an active sort order using CeOpenDatabase()
  • Allocate and initialize an array of CEPROPVAL structures to create a record
  • Seek to the end of the database using CeSeekDatabase()
  • Add a record using CeWriteRecordProps()
  • Close a database using CloseHandle()
  • Test for error status using GetLastError()

But wait, there’s more…,and we’ll see about that in the next example program, BetterBirthdays.

About the Author

Nancy Nicolaisen is a software engineer who has designed and implemented highly modular Windows CE products that include features such as full remote diagnostics, CE-side data compression, dynamically constructed user interface, automatic screen size detection, and entry time data validation.

In addition to writing for Developer.com, she has written several books, including Making Win 32 Applications Mobile.

# # #

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories