We are going to continue in our exploration of CE native database functionality by learning how to manipulate specific individual records. To this end, we’ll be looking at a bit more of the code from “BetterBirthdays,” the example program provided with the last lesson.
Editor’s note: Check out Nancy’s last article, Flexible Data Access, Powerful Subtleties, for more on the “BetterBirthdays” example.
Deleting a Record from the Database
To begin, let’s examine the ways in which we can use a record’s CEOID, which is returned by a successful call to CeSeekDatabase(). (Note that in the example code, we saved this value in the global variable globalRecOID.)
Deleting a record is a straightforward operation, so we will start there. Below is a fragment of code from the message switch of the BirthdayDlgProc() function:
case IDC_DELETE: //open the database globalHDB = CeOpenDatabase (&globalCEOID, NULL, 0, 0, NULL); //delete the record CeDeleteRecord(globalHDB, globalRecOID ); //close the database CloseHandle( globalHDB ); globalHDB = 0; //re-init dialog ctrls InitDlgCtrls(hDlg); break;
To delete a record, we use athe simplest form of the CeOpenDatabase() function, identifying the database with the previously saved CEOID, globalCEOID, and setting all remaining parameters to zero. Using the handle to the database returned by CeOpenDatabase(), and the record CEOID saved after the prior call to FindBirthday(), we delete the record with a call to FindBirthday(), we delete the record with a call to CeDeleteRecord(), and then close the database.
Modifying an Existing Record
We also can access a record by CEOID in order to make modifications. Implementing this capability requires only a few enhancements to the AddBirthdayReminder() function. We used AddBirthdayReminder() in the first example program, BirthdayReminder. Because this new version of the function is similar in many ways to the original version, we’ll just highlight the changes here.
First, notice that we have added an item to the original function’s parameter list. The new parameter, an int, is used as a flag. It can assume one of two values: ADD_REC or UPDATE_REC. The value of this flag determines whether the function adds a new record or updates an existing record.
BOOL AddBirthdayReminder( HWND hDlg, int iAddUpdateFlag ){
Another change is that in this version of the program, we no longer store the birth date as the SYSTEMTIME structure that is returned by the dialog’s Date Picker control:
//retrieve birthdate from the date time control hwndWhen = GetDlgItem(hDlg,IDC_WHEN); memset( &SysTime, 0x0, sizeof(SysTime)); DateTime_GetSystemtime(hwndWhen,&SysTime); //translate to filetime format SystemTimeToFileTime( &SysTime, &ftBirthday );
Rather, we convert the SYSTEMTIME value to a FILETIME. The FILETIME format is much more efficient, for two reasons. First, it encodes all the information in the SYSTEMTIME structure in a single DWORD, saving a great deal of storage space. Second, the CE native database type CEVT_FILETIME directly supports FILETIME values, so its translated value can be stored directly in the CEVALUNION member val.filetime. Here’s the new CEPROPVAL structure initialization for this property:
//next, we write the "when" value into a property //write the prop id : ce type + index pRecord[1].propid = MAKELONG(CEVT_FILETIME , 1) ; //now put the FILETIME structure in the value //union for this property. pRecord[1].val.filetime = ftBirthday;
When we write the record to the database, one of two circumstances will prevail: Either we will be creating a new record at the end of the database, or we will be updating an existing record that we can precisely and immediately locate by CEOID. For this reason, we can do a simple database open operation:
//open the database
globalHDB = CeOpenDatabase (&globalCEOID, NULL, 0, 0, NULL);
rc = GetLastError();
If we detect that this is a record addition, we seek to the end of the database and write the record:
if( iAddUpdateFlag == ADD_REC )
{
//set oid parm to 0 to create a new rec
CeWriteRecordProps(globalHDB, 0, NUMBER_BIRTHDAY_PROPS,
pRecord);
}
If, instead, this is an update, we don’t have to seek at all. We simply reference the existing record through its previously retrieved and saved CEOID, and write the new values directly into it:
else
{
//set oid parm to value set by "Find"
CeWriteRecordProps(globalHDB, globalRecOID,
NUMBER_BIRTHDAY_PROPS, pRecord);
}
Iterating Records
Iterative reading of CE database records is a pretty slick situation, as you probably guessed, based on the fact there is a special database open mode for doing it. This technique is demonstrated in the BirthdayListDlgProc(), which is invoked when the user clicks the BirthdayReminder Dialog’s Show All button.
The work of creating the list of birthday reminder records is all done in response to the WM_INITDIALOG message. Let’s examine the database access-related parts of the BirthdayListDlgProc().
case WM_INITDIALOG: //get a handle to the list control hwndList = GetDlgItem(hDlg,IDC_BIRTHDAY_LIST); //open the database in AUTOINCREMENT mode, no sort //order active globalHDB = CeOpenDatabase (&globalCEOID, NULL, 0, CEDB_AUTOINCREMENT , NULL);
First, we open the database using its CEOID and setting the CEDB_AUTOINCREMENT flag. This flag causes the current record location to be incremented with each read operation.
//seek to the beginning of the database
oid = CeSeekDatabase (globalHDB, CEDB_SEEK_BEGINNING, 0, &dwIndex);
Next, we seek to the beginning of the database to establish a current record.
// Read all properties for the record. Have the system
// allocate the buffer containing the data.
oid = CeReadRecordProps (globalHDB, CEDB_ALLOWREALLOC,
&wProps, NULL,
&(LPBYTE )pRecord, &dwRecSize);
Now, we call CeReadRecordProps() The parameters, in the order shown, are:
- The handle to the database.
- A flag that allows the function to allocate a correctly sized record buffer in our behalf.
- The address of the null initialized variable wProps. On return, wProps will contain a count of the record’s properties.
- A pointer to an array of CEPROPIDs for which to retireve values. In this case, it is set to NULL, meaning all properties should be returned.
- The address of a pointer to the buffer (a pointer to a pointer) containing the retrieved record, set to NULL to indicate that the system must allocate the record buffer.
- The address of a variable that will contain the size in bytes of the returned buffer on a successful return.
This all sounds fairly complicated, and it could turn out that way in certain circumstances. However, most of the time, the best and easiest way to make this call is just as you see it above. Let the system allocate the record buffer, and retrieve all the properties at one time, because this is much more efficient than any other scheme.
while( oid != 0 )
{
lpRecord = (PCEPROPVAL)pRecord;
//get the Name string
memset( szBirthday, 0x0, sizeof(szBirthday));
We cast the pointer to the buffer returned by CeReadRecordProps() to a PCEPROPVAL so that you may use it to access the individual CEPROPVAL structures. Now, we are ready to create the listbox strings:
//get the date FileTimeToSystemTime( &lpRecord[1].val.filetime, &stBirthDate); //what gift did we have in mind? memset(szWhat, 0x0, sizeof(szWhat)); if(lpRecord[2].val.iVal == IDC_CANDY ) { wcscpy( szWhat, TEXT("Candy"));}; if(lpRecord[2].val.iVal == IDC_FLOWERS ) { wcscpy( szWhat, TEXT("Flowers"));}; if(lpRecord[2].val.iVal == IDC_ANT_FARM ) { wcscpy( szWhat, TEXT("Ant Farm"));}; //format the string wsprintf((TCHAR*)&szBirthday, TEXT("%s %i/%i/%i %s"), lpRecord[0].val.lpwstr, stBirthDate.wDay , stBirthDate.wMonth , stBirthDate.wYear, szWhat); //insert the string SendMessage(hwndList, LB_INSERTSTRING, -1, (long)&szBirthday );
To make listbox strings out of the returned property values, we convert the birth date stored as a file as a FILETIME back to a SYSTEMTIME, select a gift item based on the stored integer value, and use the CEVALUNION member val.lpwstr to pass the name string to wsprintf(). Presto, formatted birthday reminder record data in a Unicode string.
A Few Final Words on CE Databases
We’ve touched on a great many things you can do with the CE database API, a toolset that is both flexible and powerful. However, there are some inherent limitations of which to be aware. The first is that a database may contain only one kind of record. The second, which is actually a corollary to the first, is that you can’t explicitly create one-to-many or many-to-many relationships. In a strict sense, it’s also true that the Windows CE database is really an object store, not a true database. It shares no relationship with standard relational database tools such as SQL and the like. Personally, I don’t see any of these facts as particularly troubling or intrusive, for the reason that the CEBLOB type allows you to store sophisticated, application specific data structures inside a record.
Looking Ahead
One of the very great strengths of the integrated nature of the CE database API is that it allows you to find other databases that exist on a device, query their structures, and dynamically incorporate their data. In upcoming examples, we’ll build on the skills you’ve learned up to this point. You’ll see how to connect to a Windows CE database from the desktop, using the remote database API. You’ll learn how to remotely enumerate databases, find out what kind of information they contain, and manipulate those databases in your applications. We’ll also look at power conservation strategies for programming CE.
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.
# # #