Order the book from O'Reilly

Previous PageTable Of ContentsIndexNext Page

In this chapter:

 13.  Two-Way Syncing

You can implement two-way syncing using two different methods. While both methods rely on Palm sample code, that is where the similarity ends. The first is based on the conduit classes (commonly referred to as basemon and basetabl) and the second on new code called Generic Conduit. Before delving into either approach, however, we need to discuss the logic involved in two-way, mirror image syncing.

The Logic of Syncing

Top Of Page

There are two forms of syncing that occur between the desktop and the handheld. The quicker method is appropriately named "fast sync" and the other is likewise aptly named "slow sync." A fast sync occurs when the handheld is being synced to the same desktop machine that it was synced to the previous time. Because handhelds can be synced to multiple desktops, this is not the only possibility. As a result, there are quite a few logic puzzles that need sorting out when records don't match. Let's start with the easier, fast scenario.

Fast Sync

A fast sync occurs when a handheld syncs to the same desktop as it last did, so you can be assured that the delete/archive/modify bits from the handheld are accurate. In such cases, the conduit needs to do the following:

Examine the desktop data

The conduit reads the current desktop data into a local database.

Examine the handheld data

For each changed record on the handheld, the conduit does the following:

Examine the local data

It is necessary to handle modified records in the local database by comparing them to the handheld records:

Dispose of the old data

Now the conduit deletes all records in the local database that are marked for deletion. At this point, all the records in the local database should match those on the handheld.

Write local database to desktop database

Finally, all the data is moved from the temporary local database back to permanent storage; the archive database is written out first and then the local database. A copy of the local database is also saved as a backup database-you will use this for slow sync.

Thorny Comparisons-Changes to the Same Records on Both Platforms

There are some very thorny cases of record conflicts we have to consider. When you give users the capability of modifying a record in more than one location, some twists result. The problem occurs when you have a record that can be changed simultaneously on the handheld and on the local database, but in different ways. For example, a customer record in our Sales application has its address changed on the handheld database and its name changed on the local database. Or a record was deleted on one platform and changed on another. The number of scenarios is so great that we require some formal rules to govern cases of conflict.

The Palm philosophy concerning such problems is that no data loss should occur, even at the price of a possible proliferation of records with only minor differences. Thus, in the case of a record with a name field of "Smith" on the handheld and "Smithy" on the local database, the end result is two records, each present in both databases. Here are the various possibilities and how this philosophy plays out into rules for actual conflicts.

A record is deleted on one database and modified on the other

The deleted version of the record is done away with, and the changed version is copied to the other platform.

A record is archived on one database and changed on the other

The archived version of the record is put in the archive database, and the changed version is copied to the other platform. Exception: if the archived version has been changed in exactly the same way, we do the right thing and end up with only one archived record.

A record is archived on one database and deleted on the other

The record is put in the archive database.

A record is changed on one database and changed differently on the other

The result is two records. This is true for records with the same field change, such as our case of "Smith" and "Smithy." It is also true for a record where the name field is changed on one record and the address field on the other. In this case, you also end up with two records. Thus these initial records:

Handheld Database

Local Database

Name: Smith

Name: Smithy

Address: 120 Park Street

Address 100 East Street

City: River City

City: River City

yield the following records in fully synced mirror image databases:

Handheld Database

Local Database

Name: Smith

Name: Smith

Address: 120 Park Street

Address: 120 Park Street

City: River City

City: River City

   

Name: Smithy

Name: Smithy

Address 100 East Street

Address 100 East Street

City: River City

City: River City

A record is changed on one database and changed identically
on the other

If a record is changed in both places in the same way (the same field contains the same new value in both places), the result is one record in both places.

This can get tricky, however. While it may be clear that "Smith" is not "Smithy", it is not so obvious that "Smith" is not "smith". Depending on the nature of your record fields, you may need to make case-by-case decisions about the meaning of identical.

Slow Sync

A slow sync takes place when the last sync of the handheld was not with this desktop. Commonly, this occurs when the user has more recently synced to another desktop machine. Less frequently, this happens because this is the first sync of a handheld. If the last sync of the handheld was not with this desktop, the modify/archive/delete bits are not accurate with respect to this desktop. They may be accurate with the desktop last synced to, but this doesn't help with the current sync scenario.

Since the modify/archive/delete bits aren't accurate, we've got to figure out how the handheld database has changed from the desktop database since the last sync. In order to do this, we need an accurate copy of the local database at the time of the last sync. This is complicated by the possibility that the local database may have changed since the last sync. The solution to this problem is to use the backup copy that we made after the last sync between these two machines-the last point at which these two matched.

Since this backup, both the handheld and the desktop database may have diverged. While it is true that all the changes to the desktop have been marked (changes, new, deleted, archived), it is not true for the handheld. Some or all of the changes to the handheld data were lost when the intervening sync took place; the deleted/archived records were removed, and the modified records were unmarked.

To deal with this problem, we need to use a slow sync. As the name implies, a slow sync looks at every record from the handheld. It copies them to an in-memory database on the desktop called the remote database and compares them to the backup database records. Here are the possibilities that need to be taken into account:

At this point, we've got a copy of the remote database where each record has been left alone, marked as modified (due to being new or changed), or marked as deleted. Now the conduit can carry out the rest of the sync. Thus, the difference between the two syncs is the initial time required to mark records so that the two databases agree. It is a slow sync because every record from the handheld had to be copied to the desktop.

Now that you know what to do with records during a sync, let's discuss how to do it.

The Conduit Classes

Top Of Page

You may be apprehensive about tackling two-way syncing using the conduit classes provided by Palm (sometimes called basemon and basetabl because of the filenames in which they are located). The implementation may seem murky and the examples quite complicated. If you looked over the samples, you certainly noted that there is no simple example showing how to do what you want to do. Things get even more formidable if you don't want to save your data in the format the base classes provide.

We had all these same apprehensions-many of them were well deserved. At the time this book was written, the documentation wasn't clear concerning the definitions and tasks of each class, nor was it clear what you specifically needed to do to write your own conduit (what methods you are required to override, for instance). The good news is that a detailed examination shows that the architecture of the conduit classes is sound; they do quite a lot for you, and it's not hard to support other file formats once you know what you have to change.

After diving in and working with the conduit classes, we figured out how to use them effectively. In a moment, we will show you their architecture and describe each class and its responsibilities. After that, we show you what is required on your part-what methods you must override and what data members you must set. Next, we show you the code for a complete syncing conduit that supports its own file format.

The Classes and Their Responsibilities

The classes you use to create the conduit are:

CBaseConduit Monitor

Runs the entire sync

CBaseTable

Creates a table that holds all the records

CBaseSchema

Defines the structure of a record in the table

CBaseRecord

Creates an object used to read and write each record to the table

CDTLinkConverter

Converts the Palm OS record format into a CBaseRecord and vice versa

CBaseConduitMonitor

The CBaseConduitMonitor class is responsible for directing syncing from the start to the end. It does the logging, initializes and deinitializes the sync manager, creates tables, populates them, and decides what records should go where. It is the administrator of the sync. It is also within CBaseConduitMonitor that we add all of the code from the previous chapters that handle the uploading and downloading of data.

CBaseConduitMonitor contains five functions that you need to override:

At this point, we give you a brief description of each function. Later, we'll look at actual code when examining our conduit sample.

CreateTable 

You override this to create your class derived from CBaseTable. Here's the function declaration:

long CreateTable(CBaseTable*& pBase);

ConstructRecord 

This routine creates your own class derived from CBaseRecord. Here is the function:

long ConstructRecord(CBaseRecord*& pBase, 
          CBaseTable& rtable,  WORD wModAction);

If the incoming wModAction parameter is equal to MODFILTER_STUPID, the newly created CBaseRecord object should check any attempted changes to its fields. If the change attempts to set a new value equal to the old value, the CBaseRecord object should just ignore the change, not marking the record as having changed.

SetArchiveFileExt 

This function simply sets the filename extension used for archive files. Here is the override call:

void SetArchiveFileExt();

Your override should set the m_ArchFileExt data member of CBaseConduitMonitor to a string that will be appended to the category name and used as the filename of the archive.

LogRecordData 

This function writes a summary of a record to the log. Here is the function you override:

void LogRecordData(CBaseRecord&rRec, char *errBuf);

Here are the values of the parameters:

rRec

The record to summarize

errBuff

The buffer to write the summary to

This routine is called when the monitor is going to add a log entry for a specific record (for example, when a record has been changed on both the desktop and the handheld). It writes a succinct description of the record, one that enables the user to identify the record.

LogApplicationName 

This is the function that returns the name of the conduit:

void LogApplicationName(char* appName, WORD len);

The conduit name is returned into appName (the appName buffer is len bytes long).

CBaseConduitMonitor data member

This class contains one data member that you must initialize:

m_wRawRecSize

Initialize this to the maximum size of a record in your handheld database. It is used as the size of the m_pBytes field of the CRawRecordInfo (see "The CRawRecordInfo class" on page 337). It is used to read and write handheld database records.

CBaseTable

This class is used to create a table that contains field objects for each record. The whole thing is stored in a large array. Every record contains the same number of fields in the same order. The number of rows in the array is the number of records in the table. The number of columns is the number of fields per record. You should imagine a structure similar to that shown in Figure 13-1.

-Figure 13- 1. Record structure in CBaseTable

This is not an array of arrays, but a single large one. The fields are stored in a single-dimensional array, where the fields for the first record are followed by those of the second record and so on. When it's necessary to retrieve the values in a row, a CBaseRecord is positioned at the appropriate fields in the array. It can then read from and write to the fields to effect a change to the row. The table is responsible for reading itself from a file and writing itself out. The default format is an MFC archive format.

NOTE:

This type of programming is a bit startling after months of handheld programming, where every byte of memory is precious. It's refreshing to be in an environment where memory is less limited. How profligate to just allocate a field object for every field in every row-all we can say is that its a good thing conduits aren't required to run in 15K of dynamic memory.

A conduit actually has several CBaseTables: one for the records on the handheld, one for the records on the desktop, one for the backup database during a slow sync, and one containing archived records.

Within a table, the data is handled in a straightforward manner and records are frequently copied from one table to another during a sync. While records can be individually deleted, they are normally marked as deleted and then all purged at once.

Functions you must override

This class has only one function to override:

virtual long   AppendDuplicateRecord (
    CBaseRecord&rFrom, CBaseRecord&rTo, BOOL bAllFlds = TRUE);

Here are the parameters and their values:

rFrom

The record that contains the fields that are copied from.

rTo

A record that contains the fields that get copied to.

bAllFlds

If this is true, the record ID and status should be copied with all the other fields.

This adds a new row of fields.

CBaseTable functions you can't override, but wish you could

There are two other functions that you will often wish to override. The problem is that you can't, given the flawed design of the class. These functions are:

long    OpenFrom (CString& rTableName, long openFlag);
long  ExportTo (CString& rTableName, CString & csError);

These are the routines responsible for reading a table from and writing a table to disk. Thus, any time you want to use a different file format, you should override these.

Unfortunately, these routines aren't declared virtual in the original Palm class and can't easily be overridden. Since you can't accomplish what you need to in a standard way, you have to use a far less appealing method. See "The problem-virtual reality beats nonvirtual reality" later in this chapter for a description of the unpalatable measures we suggest.

CBaseSchema

This class is responsible for defining the number, the order, and the type of the fields of each record.

Functions you must override

This class contains only one function to override:

virtual long   DiscoverSchema(void);

This routine specifies each of the fields and marks which ones store the record ID, attributes, category ID, etc.

CBaseSchema data members

There are several data members that you need to initialize in DiscoverSchema:

m_FieldsPerRow

Initialize to the number of fields in each record.

m_Fields

Call this object's SetSize member function to specify the number of fields in each record. Call the object's SetAt member function for every field from 0 to m_FieldsPerRow-1 to specify the type of the field.

m_RecordIdPos

Initialize to the field number containing the record ID.

m_RecordStatusPos

Initialize to the field number containing the status byte.

m_CategoryIdPos

Initialize to the field number containing the category ID.

m_PlacementPos

Initialize to the field number containing the record number on the handheld. If you don't keep track of record numbers, you'll initialize to an empty field.

Most conduits do not need the record numbers from the handheld and therefore have a dummy field that the m_PlacementPos refers to. Occasionally, a conduit needs to know the ordering of the records on the handheld. For example, the Memo Pad conduit wants records on the desktop to be displayed in the same order as on the handheld and has no key on which to determine the order. Its solution is to use the ordering of the records in the database as the sort order (no other conduits for the built-in applications use record numbers).

A conduit that needs record numbers would do the following:

1. Override ApplyRemotePositionMap (which does nothing by default) to read the record IDs in the order in which they are stored on the handheld.

2. Store each record number in the field referenced by m_PlacementPos.

CBaseRecord

A CBaseRecord is a transitory object that you use to access a record's fields. The fields are stored within the table itself; use the CBaseRecord to read and write data from a specific row within the table. Your derived class should contain utility routines to read and write the data in the record.

Functions you must override

This class contains a couple of functions that you must override:

virtual BOOL   operator==(const CBaseRecord&r);

This function compares two records to determine whether they are the same. It should not just compare record IDs or attributes, but should use all the relevant fields in the records. Note that the parameter r is actually your subclass of CBaseRecord.

Whenever this function is called, the two records are in different tables:

virtual long  Assign(const CBaseRecord&r);

This routine copies the contents of r to this record, including the record ID and attributes. Note that the parameter r is actually a subclass of CBaseRecord. The two records are in different tables.

Useful functions

There are also several functions that you can use to set the record ID, get or set individual attributes of the record, and so on. Here they are:

long      SetRecordId     (int  nRecId);
long  GetRecordId     (int& rRecId);
long  SetStatus       (int  nStatus);
long  GetStatus       (int& rStatus);
long     SetCategoryId   (int  nCatId);
long  GetCategoryId   (int& rCatId);
long  SetArchiveBit   (BOOL bOnOff);

BOOL        IsDeleted       (void);
BOOL  IsModified      (void);
BOOL  IsAdded         (void);
BOOL  IsArchived      (void);
BOOL  IsNone          (void);
BOOL  IsPending       (void);

The first set of routines returns information about the record ID, its status, the category ID, and whether the record should be archived. The second set of routines tells you the modified status of the record.

CBaseRecord data members

There are a couple of data members that are available for you to use:

m_fields

This data member is an array of fields for this specific record. It is initialized by the table when the table is focused (so to speak) on the record. Only one record within a table can be focused at a time.

m_Positioned

This specifies whether the table is positioned on this particular record. It starts out false, but when the table focuses on a record, it is set to true.

CDTLinkConverter

This class is responsible for converting from Palm record format to your subclass of CBaseRecord and vice versa.

Functions you must override

long  ConvertToRemote(CBaseRecord &rRec, CRawRecordInfo &rInfo);

You use this function to convert from your subclass of CBaseRecord to the CRawRecordInfo. The rInfo.m_pBytes pointer has already been allocated for you. You must write into the buffer and update rInfo.m_RecSize:

long  ConvertFromRemote(CBaseRecord &rRec, CRawRecordInfo &rInfo);

Convert from the CRawRecordInfo to your subclass of CBaseRecord. The rRec parameter is the subclass of CBaseRecord created by your CBaseTable::CreateRecord. You need to initialize rRec based on the values in rInfo.

Sales Conduit Sample Based
on the Classes

Top Of Page

Now that you have an idea what each of the classes does and which functions you override, it is time to use this information to add syncing to the Sales application conduit. We use these new sync classes for syncing the customer database. We continue to use our own routines that we created in Chapter 12 to upload the orders database and download the products database.

There is also a problem in the implementation of two of the classes: CBaseConduitMonitor and CBaseTable. We use an unorthodox approach, which involves circumventing normal inheritance and copying the classes by hand. We talk about this as part of our discussion of each of these classes in the sample conduit. Other classes are used normally.

CSalesConduitMonitor-Copying the Class

This is the class that is based on CBaseConduitMonitor. Let's look at a problem we have before going any further.

A virtual conundrum

Our customer database doesn't use categories, but CBaseConduitMonitor expects them to exist. CBaseConduitMonitor's  ObtainRemoteCategories function reads the app info block of the handheld database and causes an error if the AppInfo block doesn't exist. In the original class, there were two functions that expected information about categories. The first was  SynchronizeCategories, which is responsible for syncing the categories. We overrode this routine to do nothing. Unfortunately, a second function dealing with categories was not declared virtual in the original class and thus could not be overridden. Here is the unseemly bit of code that caused our problem:

long  ObtainRemoteCategories (void); // acquire HH Categories 

Because of this code, our function ObtainRemoteCategories never gets called, and our conduit fails with an error. After a bit of nail biting, our solution was to re-sort to copy and paste-we copy the basemon.cpp and basemon.h files to our conduit source directory and change the declaration of CBaseConduitMonitor so that ObtainRemoteCategories is virtual.

NOTE:

In a perfect world, you would never have to concern yourself with the following code. It would remain invisible to you. Doing this type of code copy is an action fraught with difficulty. If Palm Computing changes this class, you'll have to reapply this change (unless one of the changes was to add the needed virtual, in which case you could throw away your changes).

Code we wish you never had to see

Here is the class that you need to copy into your conduit source directory (note that the line of code we change is in bold):

class CBaseConduitMonitor
{
protected:
    // code deleted that declares lots of data members     
    
    virtual long  CreateTable   (CBaseTable*& pBase);
    virtual long  ConstructRecord (CBaseRecord*& pBase, 
                                    CBaseTable& rtable,
                                    WORD wModAction);
    virtual void  SetArchiveFileExt();

    // Moved to Base class.
    virtual long  ObtainRemoteTables(void);// get HH real & archive tables
    virtual long  ObtainLocalTables (void);// get PC real & archive tables
    virtual long  AddRecord             (CBaseRecord& rFromRec, 
                                         CBaseTable&  rTable);
    virtual long  AddRemoteRecord       (CBaseRecord& rRec);
    virtual long  ChangeRemoteRecord    (CBaseRecord& rRec);
    virtual long  CopyRecordsHHtoPC (void); // copy records from HH to PC
    virtual long  CopyRecordsPCtoHH (void); // copy records from PC to HH 
    virtual long  FastSyncRecords (void); // carries out 'Fast' sync
    virtual long  SlowSyncRecords       (void); // carries out 'Slow' sync
    
    // deleted function
    // virtual  long  CreateLocArchTable    (CBaseTable*& pBase);
    
    virtual long  SaveLocalTables       (const char*); 
    virtual long  PurgeLocalDeletedRecs (void); 
    virtual long  GetLocalRecordCount   (void); 
    virtual long  SendRemoteChanges     (CBaseRecord& rLocRecord);
    virtual long  ApplyRemotePositionMap(void);
    virtual long  SynchronizeCategories (void);
    virtual long  FlushPCRecordIDs      (void);
    virtual long  ArchiveRecords        (void);

    // file link related functions
    virtual long  ProcessSubscription   (void);
    virtual int GetFirstRecord (CStringArray*& pSubRecord );
    virtual int GetNextRecord(CStringArray*& );
    virtual int DeleteSubscTableRecs(CString& csCatName, 
        CBaseTable* pTable, WORD wDeleteOption);
    virtual int AddCategory(CString& csCatName, CBaseTable* pTable);
    virtual long LogModifiedSubscRec(CBaseRecord* pRecord, 
        BOOL blocalRec);
    virtual long SynchronizeSubscCategories(CCategoryMgr* catMgr);
    virtual long CheckFileName(CString& csFileName);
    virtual int  GetSubData (CString& csfilename, CString csFldOrder );
    virtual void  AddSubDataToTable( int subCatId);

    
    // Audit trail notifications (optional override)
    virtual long  AuditAddToPC          (CBaseRecord& rRec, long rowOffset); 
    virtual long  AuditUpdateToPC       (CBaseRecord& rRec, long rowOffset); 
    virtual long  AuditAddToHH          (CBaseRecord& rRec, long rowOffset); 

    // Overload with care !! 
    virtual long  EngageStandard        (void);
    virtual long  EngageInstall         (void);
    virtual long  EngageBackup          (void);
    virtual long  EngageDoNothing           (void);

Code changed here:
    // ObtainRemoteCategories changed to virtual Neil Rhodes 8/6/98
    virtual long  ObtainRemoteCategories (void); // acquire HH Categories 
    virtual long  SaveRemoteCategories          (CCategoryMgr *catMgr); 
    long  SaveLocalCategories           (CCategoryMgr *catMgr);
    long  ClearStatusAddRecord          (CBaseRecord& rFromRec, 
                                         CBaseTable&  rTable);
    long  AllocateRawRecordMemory       (CRawRecordInfo& rawRecord, WORD);
    void  SetDirtyCategoryFlags         (CCategoryMgr* catMgr);
    void  UpdatePCCategories            (CUpdateCategoryId *updCatId);
    BOOL  IsRemoteMemError              (long);
    BOOL  IsCommsError                  (long);


    // Used by FastSync and SlowSync.
    virtual long  SynchronizeRecord      (CBaseRecord & rRemRecord, 
                                         CBaseRecord & rLocRecord, 
                                         CBaseRecord & rBackRecord); 

    // code deleted that declares lots of log functions
    virtual BOOL IsFatalConduitError(long lError, CBaseRecord 
         *prRec=NULL);

public:
             CBaseConduitMonitor        (PROGRESSFN pFn, 
                                         CSyncProperties&, 
                                         HINSTANCE hInst = NULL);  
    virtual ~CBaseConduitMonitor        ();

    long  Engage                        (void);
    void  SetDllInstance                (HINSTANCE hInst);
    void  SetFilelinkSupport (long lvalue){ m_lFilelinkSupported = lvalue; }
    
    // file link public functions
    long UpdateTablesOnSubsc(void);
    int GetCategories(char  categoryNames[][CAT_NAME_LEN] );
};

There seems to be no rhyme or reason as to which functions are declared virtual and which aren't in CBaseConduitMonitor. These are not routines that get called hundreds of thousands of times a sync that never need to be overridden. We can't see any optimization that would warrant making them virtual. There's no excuse for this oversight.

Luckily, that's all that needs to be done; basemon.cpp will need to be recompiled, but that is uncomplicated.

CSalesConduitMonitor

Now we can move on to a discussion of changes we would normally make to our code and standard modifications we make to this class.

CSalesConduitMonitor Class Definition

Within this class, we do a few things. We override the category functions to do nothing. We also override  EngageStandard. We insert the calls to our uploading and downloading databases. We also need to override the class functions that every conduit must override. Here is the class definition:

class CSalesConduitMonitor : public CBaseConduitMonitor
{
protected:
// required
    long CreateTable    (CBaseTable*& pBase);
    long ConstructRecord(CBaseRecord*& pBase, 
            CBaseTable& rtable,  WORD wModAction);
    void SetArchiveFileExt();
    void LogRecordData          (CBaseRecord&, char*);
    void LogApplicationName     (char* appName, WORD);

//overridden to do nothing because we don't have categories
    virtual long  SynchronizeCategories (void);
    virtual long  ObtainRemoteCategories(void); 
// ovverriden so we can upload and download our other databases 
    virtual long  EngageStandard(void);

public:
    CSalesConduitMonitor(PROGRESSFN pFn, CSyncProperties&,
                 HINSTANCE hInst = NULL);  
};

CSalesConduitMonitor constructor

Our constructor allocates a DTLinkConverter and sets the maximum size of handheld records:

CSalesConduitMonitor::CSalesConduitMonitor(
    PROGRESSFN pFn,
    CSyncProperties& rProps, 
    HINSTANCE hInst
) : CBaseConduitMonitor(pFn, rProps, hInst)
{
    m_pDTConvert = new CSalesDTLinkConverter(hInst);
    m_wRawRecSize = 1000;   // no record will exceed 1000 bytes
}

Functions that require overriding

There are five functions that we need to override:

 CreateTable

This function simply creates a CSalesTable:

long CSalesConduitMonitor::CreateTable(CBaseTable*& pBase)
{
    pBase = new CSalesTable();

    return pBase ? 0 : -1;

ConstructRecord 

This routine creates a new CSalesRecord:

long CSalesConduitMonitor::ConstructRecord(CBaseRecord*& pBase, 
                                           CBaseTable& rtable ,
                                           WORD wModAction)
{
    pBase = new CSalesRecord((CSalesTable &) rtable, wModAction);

    return pBase ? 0 : -1;

SetArchiveFileExt 

Next, we set the suffix for our archive files as ARC.TXT in SetArchiveFileExt. Our archive file is called UnfiledARC.TXT (all our records are category 0, the Unfiled category):

void  CSalesConduitMonitor::SetArchiveFileExt()
{
    strcpy(m_ArchFileExt, "ARC.TXT");

LogRecordData 

Our LogRecordData summarizes a CSalesRecord to a log:

void CSalesConduitMonitor::LogRecordData(CBaseRecord& rRec, 
    char * errBuff)
{
    // return something of the form " city name, "
    CSalesRecord    &rLocRec = (CSalesRecord&)rRec;
    CString     csStr;
    int         len = 0;

    rLocRec.GetCity(csStr);
    len = csStr.GetLength() ; 
    if (len > 20)
        len = 20;

    strcpy(errBuff, "       ");
    strncat(errBuff, csStr, len);
    strcat(errBuff, ", ");

    rLocRec.GetName(csStr);
    len = csStr.GetLength() ; 
    if (len > 20)
        len = 20;

    strncat(errBuff, csStr, len);
    strcat(errBuff, ", ");
    strncat(errBuff, csStr, len);

LogApplicationName 

Last, but not least, we need to override the routine LogApplicationName. It returns our conduit's name:

void CSalesConduitMonitor::LogApplicationName(char* appName, WORD len)
{
    strncpy(appName, "Sales", len-1);
}

This ends the required routines. There are a few others we override.

The two category routines

We override the two category routines to do nothing. This prevents CBaseConduitMonitor from reading the app info block from the handheld and from actually trying to synchronize categories between the handheld and the desktop:

long CSalesConduitMonitor:: ObtainRemoteCategories()
{
    return 0;
}

long CSalesConduitMonitor:: SynchronizeCategories()
{
    return 0;
}

Modifying EngageStandard

Next, we override  EngageStandard so that we can call the routines we defined in Chapter 12 for copying orders from the handheld and copying products from the desktop. We have physically copied the inherited code, since we have to place our code in the middle of it. We place it after the conduit is registered with the Sync Manager and the log is started, but before the log is finished and the Sync Manager is closed.

Example 13-1 shows the entire function in all its complexity. We wanted you to see the complexity you avoid by using CBaseConduitMomitor for syncing instead of writing all of this from scratch.

 -Example  13- 1. EngageStandard

long CSalesConduitMonitor::EngageStandard(void)  
{
    CONDHANDLE  conduitHandle = (CONDHANDLE)0;
    long        retval = 0; 
    char        appName[40];
    long        pcCount = 0;
    WORD        hhCount = 0;
    Activity    syncFinishCode = slSyncFinished;


    // Register this conduit with SyncMgr.DLL for communication to HH
    if (retval = SyncRegisterConduit(conduitHandle))
        return(retval);

    // Notify the log that a sync is about to begin
    LogAddEntry("", slSyncStarted,FALSE);            

    memset(&m_DbGenInfo, 0, sizeof(m_DbGenInfo));

    //  Loop through all possible 'remote' db's
    for (; m_CurrRemoteDB < m_TotRemoteDBs && !retval; m_CurrRemoteDB++)
    {
        // Open the Remote Database
        retval = ObtainRemoteTables();
    
        // Open PC tables and load local records && local categories.   
        if (!retval && !(retval = ObtainLocalTables()))
        {
            #ifdef _FILELNK
// Process Subscriptions
// This needs to be done first before desktop records are affected 
// by other calls.
// (e.g.) FlushPCRecordIDs()... which will set all recStatus to Added 
// for a  hard reset HH
            // (m_firstDevice = eHH)
            if (!retval)
                if (m_rSyncProperties.m_SyncType != eHHtoPC)
                {
                    retval = ProcessSubscription();
                }
            #endif
            if( !(retval) )
            {
                FlushPCRecordIDs();

                if (!(retval = ObtainRemoteCategories()))
                {
 // Synchronize the AppInfoBlock Info excluding the categories
                    m_pDTConvert->SynchronizeAppInfoBlock(m_DbGenInfo, 
                                        *m_LocRealTable, 
                                        m_rSyncProperties.m_SyncType,
                                        m_rSyncProperties.m_FirstDevice);

                    // Synchronize the categories
                    retval = SynchronizeCategories();
                }
            }
    
        }

        // Synchronize the records
        if (!retval)
        {
            #ifdef _FILELNK
            // path for subsc info
            CString csSubInfoPath(m_rSyncProperties.m_PathName);
            csSubInfoPath =csSubInfoPath + SUBSC_FILENAME;  
            SubError subErr = SubLoadInfo(csSubInfoPath);
            #endif

            if (m_rSyncProperties.m_SyncType == eHHtoPC)
                retval = CopyRecordsHHtoPC();
            else if (m_rSyncProperties.m_SyncType == ePCtoHH)
                retval = CopyRecordsPCtoHH();
            else if (m_rSyncProperties.m_SyncType == eFast)
                retval = FastSyncRecords();
            else if (m_rSyncProperties.m_SyncType == eSlow)
                retval = SlowSyncRecords();

            #ifdef _FILELNK
            SubSaveInfo(csSubInfoPath);
            #endif
        }

        // If the number of records are not equal after a FastSync or
        // SlowSync: If the PC has more records, then do a PCtoHH Sync.
        // If the HH has more records, then do a HHtoPC Sync.
        if (!retval && ((m_rSyncProperties.m_SyncType == eFast) || 
                            (m_rSyncProperties.m_SyncType == eSlow)))
        {
            // Get the record counts
            pcCount = GetLocalRecordCount(); 
            if (!(retval = SyncGetDBRecordCount(m_RemHandle, hhCount)))
            {
                if (pcCount > (long)hhCount)
                    retval = CopyRecordsPCtoHH();

                else if (pcCount < (long)hhCount)
                {
                    m_LocRealTable->PurgeAllRecords();
                    retval = CopyRecordsHHtoPC();
                }
            }
        }
        if (!retval || !IsCommsError(retval)) 
        {
         // Re-check the record counts, only if we've obtained rem tables
            pcCount = GetLocalRecordCount(); 
            hhCount = 0;
            retval  = SyncGetDBRecordCount(m_RemHandle, hhCount);
            
         // If the record counts are not equal, send message to the log.
            if (pcCount < (long)hhCount)
            {
                LogRecCountMismatch(pcCount, (long)hhCount);            
                syncFinishCode = slSyncAborted;
            }
            else if (pcCount > (long)hhCount)
            {
                LogPilotFull(pcCount, (long)hhCount);           
                syncFinishCode = slSyncAborted;
            }
        }


    // This allows exact display order matching with the remote device.
        if (!retval || !IsCommsError(retval))
            if (ApplyRemotePositionMap())
                LogBadXMap();

        if (!retval || IsRemoteMemError(retval)) 
        {
            // Save all records to be archived to their appropriate files
            if (ArchiveRecords())
                LogBadArchiveErr();
                
            // Copy PC file to Backup PC file
            CString backFile(m_rSyncProperties.m_PathName);
            CString dataFile(m_rSyncProperties.m_PathName);
            backFile += m_rSyncProperties.m_LocalName;
            dataFile += m_rSyncProperties.m_LocalName;
            int nIndex = backFile.ReverseFind(_T('.'));
            if (nIndex != -1)
                backFile = backFile.Left(nIndex);
            backFile += BACK_EXT;

            // Save synced records to PC file
            if (!SaveLocalTables((const char*)dataFile))
            {
                // Clear HH status flags
                if (SyncResetSyncFlags(m_RemHandle))
                {
                    LogBadResetSyncFlags();
                    syncFinishCode = slSyncAborted;
                }
                remove(backFile);
                CopyFile(dataFile, backFile, FALSE);
            }
            else
                syncFinishCode = slSyncAborted;
        }
        if (!IsCommsError(retval))
            SyncCloseDB(m_RemHandle);
    }

// added here for sales conduit
    if (retval == 0 && m_rSyncProperties.m_SyncType == eHHtoPC ||
        m_rSyncProperties.m_SyncType == eFast || 
        m_rSyncProperties.m_SyncType == eSlow)
        retval = CopyOrdersFromHH(m_rSyncProperties);
    if (retval == 0 && m_rSyncProperties.m_SyncType == ePCtoHH ||
        m_rSyncProperties.m_SyncType == eFast || 
        m_rSyncProperties.m_SyncType == eSlow)
        retval = CopyProductsAndCategoriesToHH(m_rSyncProperties);
// done added here for sales conduit

    if (retval)
        syncFinishCode = slSyncAborted;

    
    // Get the application name
    memset(appName, 0, sizeof(appName));
    LogApplicationName(appName, sizeof(appName)); 

    LogAddEntry(appName, syncFinishCode,FALSE);
        
    if (!IsCommsError(retval))
        SyncUnRegisterConduit(conduitHandle);

    return(retval);
} 

These are all the changes to the CSalesConduitMonitor class. As you can see, there was very little complexity to the added code, especially when you realize that most of the difficulty occurs in the last routine, where we have to fold our code into a fairly large routine.

Now that we have dealt with the administration portion of the code, it is time to create the tables that hold the data.

CBaseTable-Copying the Class

We need to create the class that is based on CBaseTable. Before we can define our table structure, however, we need to deal with another class problem. Once again, the solution is to resort to copying the class as a whole, and it is for just as unsatisfying a set of reasons.

The problem-virtual reality beats nonvirtual reality

We want to store our tables in comma-delimited text files rather than in the default MFC archived file format. This is certainly a reasonable wish on our part. It is an even more attractive alternative when you realize that MFC archived files are very hard to read from anything but MFC-based code. We have no desire to create an MFC application just to read our data files, when a text-based system gives us such enormous versatility. Good reasoning on our part is unfortunately difficult to act on.

  For example, if we attempt to override CBaseTable::SaveTo and CBaseTable::OpenFrom, we don't get very far. As you might have guessed, those two member functions are not declared virtual in the original CBaseTable class. We are stuck then with seeking a workaround. The solution to this problem is to copy the basetabl.cpp and basetabl.h files to our conduit's source folder.

The CBaseTable code you have to copy

We need to modify the declaration of CBaseTable to add the virtual keywords. Here is the code we copy and the two changes we make:

class TABLES_DECL CBaseTable : public CObject
{
protected:
    friend            CBaseIterator;
    friend            CRepeateEventIterator;
    friend            CBaseRecord;
    CString           m_TableName;
    CString           m_TableString;
    CBaseSchema*      m_Schema;
    CBaseFieldArray*  m_Fields;
    CCategoryMgr*     m_pCatMgr;
    DWORD             m_dwVersion;
    BOOL              m_bOnTheMac;

//  CPtrArray         m_OpenIts;      // List of open  CBaseIterator(s) 
//  long    AddIterator           (long& ItPos, CBaseIterator *);
//  long    RemoveIterator        (long  ItPos, CBaseIterator *);

    BOOL  SubstCRwithNL        (CString &);
    BOOL  SubstNLwithCR        (CString &);
    void  Serialize            (CArchive &);
    long  WipeOutRow           (long RecPos);
    long  ReadInFields         (CArchive &ar);
    long  DestroyAllFields     (void) ;
    void  DeleteContents       (void) ;
    virtual long  ConstructProperField (eFieldTypes, CBaseField**);


public:
    DECLARE_SERIAL(CBaseTable)

    CBaseTable           ();
    CBaseTable           (DWORD dwVersion);
    virtual ~CBaseTable           ();
// change OpenFrom to virtual
    virtual long  OpenFrom (CString& rTableName, long openFlag);
    long  ExportTo (CString& rTableName, CString & csError);
// change SaveTo to virtual
    virtual long  SaveTo               (CString& rTableName);
    virtual long  Save                 (void);
    virtual long  GetRecordCount       (void);
    virtual long  GetFieldCount        (void);
    virtual BOOL  AtEof                (long nRecPos);
    virtual long  AlignFieldPointers   (long RecPos, CBaseRecord&); 
    virtual long  GetMySchema          (const CBaseSchema*& pSchema);
    virtual long  PurgeDeletedRecords  (void);
    virtual long  ClearPlacementField  (void);
    virtual long  PurgeAllRecords      (void);
    virtual long  AppendBlankRecord     (CBaseRecord&);
    virtual long  AppendDuplicateRecord (CBaseRecord&, 
                                        CBaseRecord&,
                                        BOOL bAllFlds = TRUE);

    virtual long  GetTableString           (CString& rTableString);
    virtual long  SetTableString           (CString& rTableString);
    virtual CCategoryMgr* GetCategoryManager    (void);

    virtual void DumpRecords(LPCSTR lpPathName,BOOL bAppend=TRUE);
};

Don't breathe a sigh of relief just yet-we have a complication. This isn't like the straightforward copying we did with basemon.cpp for CBaseConduitMonitor-copy, link, recompile, and everything works greats. This is a horse of an entirely different color-unlike basemon.cpp, this isn't code that is normally added to your project and linked with your remaining code. Therein lies the wrinkle. This is code that is found in a DLL in the folder with HotSync. Since the DLL is already compiled without the virtual keyword, the DLL won't cooperate by calling our derived class's OpenFrom and SaveTo.

Our solution was to statically link the basetabl.cpp code into our application and not use the table DLL at all. This also required adding the define of the symbol _TABLES to our project-thereby ensuring that the TABLES_DECL define was no longer defined as __declspec(import). This caused basemon.h to no longer declare the class as being imported from a DLL. Note that the only other choice besides imported for the TABLES_DECL define was __declspec(export). We took what was offered. Unfortunately, the result is that our conduit DLL unnecessarily exports the functions in the CBaseTable class. On the positive side, by these various machinations, we avoid having to change the contents of basetabl.cpp at all.

That is all of the unusual stuff we need to do. Now we can move to more normal overriding.

CSalesTable

We need to handle a number of things in our CBaseTable class. In our definitions, we override the two functions. We also add a couple of new routines to handle the read and write functions.

Class definition

  Here's our actual class definition (with OpenFrom and SaveTo overridden). We also include ReadCustomer and WriteRecord, which are utility functions used by OpenFrom and SaveTo:

class CSalesTable : public CBaseTable
{
public:
     CSalesTable  () ; 

// required
    virtual long  AppendDuplicateRecord (
        CBaseRecord&, 
        CBaseRecord&,
        BOOL bAllFlds = TRUE
    );
// optional overridden
    long OpenFrom(CString& rTableName, long openFlag);
    long SaveTo(CString& rTableName);
// useful
    CSalesRecord *ReadCustomer(CStdioFile &file);
    long WriteRecord(HANDLE hFile, CSalesRecord& rRec);
};

CSalesTable constructor

The constructor creates the schema and initializes it:

CSalesTable::CSalesTable()
             : CBaseTable()
{
    m_Schema = new CSalesSchema;
    if (m_Schema)
        m_Schema->DiscoverSchema();

}

CSalesTable functions

 AppendDuplicateRecord creates a new row and copies rFrom to rTo. Note that rFrom and rTo are actually CSalesRecord objects:

long CSalesTable::AppendDuplicateRecord(CBaseRecord& rFrom, 
                   CBaseRecord& rTo, BOOL bAllFlds)
{
    int     tempInt;
    CString tempStr;
    long    retval = -1;

    CSalesRecord& rFromRec = (CSalesRecord&)rFrom;
    CSalesRecord& rToRec   = (CSalesRecord&)rTo;

    // Source record must be positioned at valid data.
    if (!rFromRec.m_Positioned)
        return -1;
    if ((retval = CBaseTable::AppendBlankRecord(rToRec)) != 0)
        return retval;
    if (bAllFlds) {
        retval = rFromRec.GetRecordId(tempInt) ||
            rToRec.SetRecordId(tempInt);
        if (retval != 0)
            return retval;

        if ((retval = rFromRec.GetStatus(tempInt)) != 0)
            if ((retval = rToRec.SetStatus(tempInt)) != 0)
                return retval;

        if ((retval = rToRec.SetArchiveBit(rFromRec.IsArchived())) != 0)
            return retval;
    }

    if ((retval = rToRec.SetPrivate(rFromRec.IsPrivate())) != 0)
        return retval;

    retval = rFromRec.GetID(tempInt) || rToRec.SetID(tempInt);
    if (retval != 0)
        return retval;
            
    retval = rFromRec.GetName(tempStr) || rToRec.SetName(tempStr);
    if (retval != 0)
        return retval;
            
    retval = rFromRec.GetAddress(tempStr) || rToRec.SetAddress(tempStr);
    if (retval != 0)
        return retval;
            
    retval = rFromRec.GetCity(tempStr) || rToRec.SetCity(tempStr);
    if (retval != 0)
        return retval;
            
    retval = rFromRec.GetPhone(tempStr) || rToRec.SetPhone(tempStr);
    if (retval != 0)
        return retval;
        
    return 0;
}

This is the only required function. There are also two other functions we override.

SaveTo

Here's our version of  SaveTo. We use it to save in a comma-delimited format:

long CSalesTable::SaveTo(CString& rTableName)
{
    CSalesRecord    locRecord(*this, 0);
    CBaseIterator   locIterator(*this);
    long            err;

    CString tdvFile(rTableName);
    HANDLE tdvFileStream = CreateFile(
        tdvFile, 
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE, 
        NULL, 
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, 
        NULL
    );

    // generate the file
    if (tdvFileStream != (HANDLE)INVALID_HANDLE_VALUE) {

        SetFilePointer(tdvFileStream, 0, NULL, FILE_BEGIN);
        SetEndOfFile(tdvFileStream);
    
        err = locIterator.FindFirst(locRecord, FALSE);
        while (!err) {
            WriteRecord(tdvFileStream, locRecord);
            err = locIterator.FindNext(locRecord, FALSE);
        }
        if (err == -1)  // we reached the last record
            err = 0;

        CloseHandle(tdvFileStream);
    }
    return err;
}

It creates the file, opens it, calls WriteRecord to do the actual writing of one record, and then closes the file.

WriteRecord

Note that  WriteRecord doesn't write the attributes (modified, deleted, etc.) to a record, because it isn't necessary. By the time we write a table to disk, all deleted records should be deleted, all modified records will be synced, all archived records will be archived, and all added records will be synced. Thus, the attribute information is not relevant:

long CSalesTable::WriteRecord(HANDLE hFile, CSalesRecord& rRec)
{
    int             customerID;
    CString         csName, csAddress, csCity, csPhone;
    int             recId;
    DWORD           dwPut;
    unsigned long   len;
    const int       kMaxRecordSize = 1000;
    char            buf[kMaxRecordSize];

    // Get the record ID
    rRec.GetRecordId(recId);

    // Get the customer ID, name, address, city & phone.
    // Replace any tabs with spaces in all.
    rRec.GetID(customerID);
    rRec.GetName(csName);
    rRec.GetAddress(csAddress);
    rRec.GetCity(csCity);
    rRec.GetPhone(csPhone);
    ReplaceTabs(csName);
    ReplaceTabs(csAddress);
    ReplaceTabs(csCity);
    ReplaceTabs(csPhone);

    // Write the record to the file as (if private):
    // <customerID>\t<name>\t<address>\t<city>\t<phone>\tP\t<recID>
    //    or, if not private:
    // <customerID>\t<name>\t<address>\t<city>\t<phone>\t\t<recID>
    sprintf(
        buf, 
        "%d\t%s\t%s\t%s\t%s\t%s\t%d\r\n",
        customerID,
        csName.GetBuffer(csName.GetLength()),
        csAddress.GetBuffer(csAddress.GetLength()),
        csCity.GetBuffer(csCity.GetLength()),
        csPhone.GetBuffer(csPhone.GetLength()),
        rRec.IsPrivate() ? "P": "",
        recId
    );
    len = strlen(buf);
    WriteFile(
        hFile, 
        buf,
        len,
        &dwPut, 
        NULL
    );
    ASSERT(dwPut == len);

    // Release the string buffers
    csName.ReleaseBuffer();
    csAddress.ReleaseBuffer();
    csCity.ReleaseBuffer();
    csPhone.ReleaseBuffer();

    return 0;
}

For each of the strings that will be written (name, address, city, phone), WriteRecord replaces any tabs or newlines with spaces by using ReplaceTabs. This is necessary because it would ruin our tab-delimited format if a tab or newline occurred within a field.

ReplaceTabs

Here's the code for  ReplaceTabs:

static long ReplaceTabs(CString& csStr)
{
    char *p;

    p = csStr.GetBuffer(csStr.GetLength());

    // Scan and replace all tabs or newlines with blanks
    while (*p) {
        if (*p == '\t' || *p == '\r' || *p == '\n')
            *p = ' ';
        ++p;
    }

    csStr.ReleaseBuffer();

    return 0;
}

This is all that needs to be done to handle writing to the file.

OpenFrom

We now need to take care of reading one of these files.  OpenFrom does that. It checks for the existence of the file, opens and closes it, and handles any exceptions that are thrown:

long CSalesTable::OpenFrom(CString& rTableName, long openFlag) 
{
    char *pszName ;
    CFileStatus fStat;
    CStdioFile  *file = 0;

    pszName = rTableName.GetBuffer(rTableName.GetLength());

    // Check for the presence of the disk file, if not here get out
    // *without* invoking any of the reading code.
    if (!CStdioFile::GetStatus(pszName, fStat))
        return DERR_FILE_NOT_FOUND; 

    TRY
    {
        file = new CStdioFile(pszName, CFile::modeReadWrite |
                           CFile::shareDenyWrite);
        rTableName.ReleaseBuffer(-1);
    }
    CATCH_ALL(e)
    {
        rTableName.ReleaseBuffer(-1);
        if (file)
            file->Abort();
        delete file;
        return ((CFileException*)e)->m_cause;
    }
    END_CATCH_ALL
    
    // Get rid of current contents (if any)
    DestroyAllFields(); 

    CSalesRecord *newRecord = 0;
    TRY
    {
        while ((newRecord = ReadCustomer(*file)) != 0) {
            
            delete newRecord;
            newRecord = 0;
        }
        file->Close();
    }
    CATCH_ALL(e)
    {
        file->Abort();
        delete file;
        delete newRecord;
        return DERR_INVALID_FILE_FORMAT;
    }
    END_CATCH_ALL
    delete file;

    return 0;
}

ReadCustomer

  ReadCustomer has quite a lot of work to do. It creates a new CSalesRecord for each line in the file. It returns 0 when there are no more lines:

CSalesRecord *CSalesTable::ReadCustomer(CStdioFile &file)
{
    static char gBigBuffer[4096];
    int retval;
    if (file.ReadString(gBigBuffer, sizeof(gBigBuffer)) == NULL)
        return false;
    char *p = gBigBuffer;
    char *customerID = FindUpToNextTab(&p);
    char *name = FindUpToNextTab(&p);
    char *address = FindUpToNextTab(&p);
    char *city = FindUpToNextTab(&p);
    char *phone = FindUpToNextTab(&p);
    char *priv = FindUpToNextTab(&p);
    char *uniqueID = FindUpToNextTab(&p);
    char *attributes = FindUpToNextTab(&p);

    if (!address)
        address = "";
    if (!city)
        city = "";
    if (!phone)
        phone = "";
    if (!priv)
        priv = "";
    if (!attributes)
        attributes = "c";
    if (!uniqueID)
        uniqueID = "0";
    if (customerID && name) {
        CSalesRecord *rec = new CSalesRecord(*this, 0);
        
        if (AppendBlankRecord(*rec)) {  
            // should throw an error here!
            return 0;
            // return(CONDERR_BAD_REMOTE_TABLES);
        }
        retval = rec->SetRecordId(atol(uniqueID));
        retval = rec->SetCategoryId(0);
        
        retval = rec->SetID(atol(customerID));
        retval = rec->SetName(CString(name));
        retval = rec->SetAddress(CString(address));
        retval = rec->SetCity(CString(city));
        retval = rec->SetPhone(CString(phone));
        retval = rec->SetPrivate(*priv == 'P'); 
        
        int attr = 0;
        // 'N' -- new, 'M' -- modify, 'D'-- delete, 'A' -- archive
        // if it's Add, it can't be modify
        if (strchr(attributes, 'N'))
            attr |= fldStatusADD;
        else if (strchr(attributes, 'M'))
            attr |= fldStatusUPDATE;
        if (strchr(attributes, 'D'))
            attr |= fldStatusDELETE;
        if (strchr(attributes, 'A'))
            attr |= fldStatusARCHIVE;
        rec->SetStatus(attr);
        
        return rec;
    } else
        return 0;
}

Although WriteRecord doesn't write any attributes, ReadCustomer must handle the possibility of reading them. You might wonder how attributes could have gotten into the file. The answer is simple-the user of the desktop application that edits our comma-delimited file may have changed this record. Since we support desktop editing of records, we need to know if a modification has occurred (for the next sync).

In such instances, the routine appends a value to the end of the record. ReadCustomer adds an M as a field at the end. If the record has been deleted, it doesn't remove the record line from the file; instead, it adds a D in the last field. If the record is archived, it adds an A, and new records get marked with an N. On the next sync, all these newly marked records are dealt with by the sync code. Note that the marking is almost completely analogous to the marking done on the handheld side.

CSalesSchema

The schema class defines the number, ordering, and type of the fields. We also declare a number of constants and create one function.

Constants

These constants define the field ordering within a row for the record information we save:

#define slFLDRecordID       0
#define slFLDStatus         1

#define slFLDCustomerID     2
#define slFLDName           3
#define slFLDAddress        4
#define slFLDCity           5
#define slFLDPhone          6

#define slFLDPrivate        7
#define slFLDPlacement      8
#define slFLDCategoryID     9

#define slFLDLast           slFLDCategoryID

CSalesSchema class definition

This is very straightforward, with only one function to define:

class CSalesSchema : public CBaseSchema 
{
public:
    virtual long  DiscoverSchema     (void);

};

CSalesSchema functions

The  DiscoverSchema function must set the number of fields per record, set the type of each record, and mark which fields contain the record ID, the attributes, and the category ID. Even though our Sales application keeps its records sorted by customer number, we are still required to reserve a field for the record number:

long CSalesSchema::DiscoverSchema(void) 
{
    m_FieldsPerRow = slFLDLast + 1;

    m_FieldTypes.SetSize(m_FieldsPerRow);
    m_FieldTypes.SetAt(slFLDRecordID,       (WORD)eInteger);    
    m_FieldTypes.SetAt(slFLDStatus,         (WORD)eInteger);    

    m_FieldTypes.SetAt(slFLDCustomerID,     (WORD)eInteger);    
    m_FieldTypes.SetAt(slFLDName,           (WORD)eString);    
    m_FieldTypes.SetAt(slFLDAddress,        (WORD)eString);    
    m_FieldTypes.SetAt(slFLDCity,           (WORD)eString);    
    m_FieldTypes.SetAt(slFLDPhone,          (WORD)eString);    
    m_FieldTypes.SetAt(slFLDPrivate,        (WORD)eBool);    
    m_FieldTypes.SetAt(slFLDPlacement,      (WORD)eInteger);    
    m_FieldTypes.SetAt(slFLDCategoryID,     (WORD)eInteger);    

    // Be sure to set the 4 common fields' position
    m_RecordIdPos     = slFLDRecordID;
    m_RecordStatusPos = slFLDStatus;
    m_CategoryIdPos   = slFLDCategoryID; 
    m_PlacementPos    = slFLDPlacement;

    return 0;
}

CSalesRecord

CSalesRecord is based on the CBaseRecord class. This is the class that deals with records in the table. We have routines that get and set appropriate fields in the record.

CSalesRecord class definition

The constructor takes a wModAction parameter, which it uses to initialize its base class. Other routines just get and set the values of a customer record:

class CSalesRecord : public CBaseRecord 
{
protected:
    friend        CSalesTable;          

public:
    CSalesRecord            (CSalesTable &rTable, 
                                WORD wModAction);

    long SetID              (int ID);
    long SetName            (CString &csName);
    long SetAddress         (CString &csAddress);
    long SetCity            (CString &csCity);
    long SetPhone           (CString &csPhone);

    long SetPrivate         (BOOL bPrivate);
    
    long GetID              (int &ID);
    long GetName            (CString &csName);
    long GetAddress         (CString &csAddress);
    long GetCity            (CString &csCity);
    long GetPhone           (CString &csPhone);
 
    BOOL IsPrivate          (void);

// required overrides
    virtual BOOL  operator==(const CBaseRecord&r);
    virtual long Assign(const CBaseRecord&r);
};

Class constructor

The constructor doesn't do much:

CSalesRecord::CSalesRecord(
    CSalesTable &rTable, 
    WORD wModAction
) : CBaseRecord(rTable, wModAction)
{
}

CSalesRecord functions

There are a number of functions, all of which involve getting or setting records fields. There are routines that get or set the customer ID, name, address, city, and ZIP Code. There are also routines that compare records and assign the values of one record to another.

Getting the customer ID

 Here's the routine that gets the value of the customer ID. It gets the appropriately numbered field (checking first to make sure the table is positioned at this record) and asks the field for the current value:

long CSalesRecord::GetID(int &customerID)
{
    CIntegerField* pFld;

    if (m_Positioned && 
        (pFld = (CIntegerField*) m_Fields.GetAt(slFLDCustomerID)) &&
        pFld->GetValue(customerID) == 0)
        return 0;
    else
        return DERR_RECORD_NOT_POSITIONED;
}

Setting the customer ID

 Here's the routine that sets the customer ID. Note that if m_wModAction is equal to MODFILTER_STUPID, the code checks the value being set to see if it is equal to the current value-if it is, the update (modified) attribute of the status isn't set:

long CSalesRecord::SetID(int customerID)
{
    BOOL autoFlip   = FALSE;
    int  currStatus = 0;
    long retval     = DERR_RECORD_NOT_POSITIONED;
    CIntegerField* pFld = NULL;

    if (m_Positioned && 
        (pFld = (CIntegerField*) m_Fields.GetAt(slFLDCustomerID)))
    {
        if (m_wModAction == MODFILTER_STUPID)
        {
            GetStatus(currStatus); 
            if (currStatus != fldStatusADD)
            {
                CIntegerField tmpFld(customerID);
                if (pFld->Compare(&tmpFld))
                    autoFlip = TRUE;
            }
        }
        if (!pFld->SetValue(customerID))
        {
            if (autoFlip)
                SetStatus(fldStatusUPDATE);
            retval = 0;
        }
    }   
    return retval;
}

Because the routines to get and set the name, address, city, and ZIP, and private value are so similar to those for the customer ID, we are not bothering to show them.

Assigning one record to another

 We need an assign function that assigns one CSalesRecord to another. It copies all fields, including the record ID and attributes:

long CSalesRecord::Assign(const CBaseRecord& rSubj)
{
    if (!m_Positioned)
        return -1;
    for (int x=slFLDRecordID; x <= slFLDLast; x++)
    {
        CBaseField* pMyFld   = (CBaseField*) m_Fields.GetAt(x); 
        CBaseField* pSubjFld = 
           (CBaseField*) ((CSalesRecord&)rSubj).m_Fields.GetAt(x); 
        if (pMyFld && pSubjFld)
            pMyFld->Assign(*pSubjFld);
    }
    return 0;
}

Comparing one record to another

The comparison routine (== operator) checks to see whether one CSalesRecord is equal to another (ignoring record ID and attributes):

BOOL CSalesRecord:: operator==(const CBaseRecord& rSubj)
{
    if (!m_Positioned)
        return FALSE;
    for (int x=slFLDCustomerID; x <= slFLDLast; x++)
    {
        CBaseField* pMyFld   = (CBaseField*) m_Fields.GetAt(x); 
        CBaseField* pSubjFld =
           (CBaseField*) ((CSalesRecord&)rSubj).m_Fields.GetAt(x); 
        if (!pMyFld || !pSubjFld)
            return FALSE;
        if (pMyFld->Compare(pSubjFld) != 0)
            return FALSE;
    }
    return TRUE;
}

CSalesDTLinkConverter

This is the last class that we have in our conduit. It is the one responsible for converting a record from one format to another and vice versa. We have one function that converts a Palm OS handheld record into a CBaseRecord format, and another does the opposite.

CSalesDTLinkConverter class definition

The definition is simple with just two functions:

class CSalesDTLinkConverter : public CBaseDTLinkConverter
{
public:
    CSalesDTLinkConverter(HINSTANCE hInst);

    long ConvertToRemote(CBaseRecord &rRec, CRawRecordInfo &rInfo);
    long ConvertFromRemote(CBaseRecord &rRec, CRawRecordInfo &rInfo);

};

The HINSTANCE parameter in the constructor is there so that the converter can obtain strings from the DLL resource file, if it needs to.

CSalesDTLinkConverter constructor

Here's the constructor:

CSalesDTLinkConverter::CSalesDTLinkConverter(HINSTANCE hInst)
                        : CBaseDTLinkConverter(hInst)
{
}

Converting to Palm record format

Here's the code that converts to a handheld record. Note that it must set the record ID, the category ID, and the attributes as well as write the record contents. We use a utility routine,   SwapDWordToMotor, to swap the customer ID:

long CSalesDTLinkConverter::ConvertToRemote(CBaseRecord& rRec, 
                                            CRawRecordInfo& rInfo)
{
    long            retval = 0;
    char            *pBuff;
    CString         tempStr;
    int             destLen, tempInt;
    char            *pSrc;
    int             customerID;

    CSalesRecord& rExpRec = (CSalesRecord &)rRec;
    rInfo.m_RecSize = 0;

    // Convert the record ID and Category ID
    retval = rExpRec.GetRecordId(tempInt);
    rInfo.m_RecId = (long)tempInt;
    retval = rExpRec.GetCategoryId(tempInt);
    rInfo.m_CatId = tempInt;
    
    // Convert the attributes
    rInfo.m_Attribs = 0;
    if (rExpRec.IsPrivate())
        rInfo.m_Attribs |= PRIVATE_BIT;
    if (rExpRec.IsArchived())
        rInfo.m_Attribs |= ARCHIVE_BIT;
    if (rExpRec.IsDeleted())
        rInfo.m_Attribs |= DELETE_BIT;
    if (rExpRec.IsModified() || rExpRec.IsAdded())
        rInfo.m_Attribs |= DIRTY_BIT;
        
    pBuff = (char*)rInfo.m_pBytes;

    // customer ID
    retval = rExpRec.GetID(customerID);
    *((DWORD *)pBuff) = SwapDWordToMotor(customerID);
    pBuff += sizeof(DWORD);
    rInfo.m_RecSize += sizeof(DWORD);

    // name
    retval = rExpRec.GetName(tempStr);
        // Strip the CR's (if present) places result directly into pBuff
    pSrc    = tempStr.GetBuffer(tempStr.GetLength());
    destLen = StripCRs(pBuff, pSrc, tempStr.GetLength());  
    tempStr.ReleaseBuffer(-1);
    pBuff += destLen;  
    rInfo.m_RecSize += destLen;
    
    // address
    retval = rExpRec.GetAddress(tempStr);
        // Strip the CR's (if present) places result directly into pBuff
    pSrc    = tempStr.GetBuffer(tempStr.GetLength());
    destLen = StripCRs(pBuff, pSrc, tempStr.GetLength());  
    tempStr.ReleaseBuffer(-1);
    pBuff += destLen;  
    rInfo.m_RecSize += destLen;
    
    // city
    retval = rExpRec.GetCity(tempStr);
        // Strip the CR's (if present) places result directly into pBuff
    pSrc    = tempStr.GetBuffer(tempStr.GetLength());
    destLen = StripCRs(pBuff, pSrc, tempStr.GetLength());  
    tempStr.ReleaseBuffer(-1);
    pBuff += destLen;  
    rInfo.m_RecSize += destLen;
    
    // phone
    retval = rExpRec.GetPhone(tempStr);
        // Strip the CR's (if present) places result directly into pBuff
    pSrc    = tempStr.GetBuffer(tempStr.GetLength());
    destLen = StripCRs(pBuff, pSrc, tempStr.GetLength());  
    tempStr.ReleaseBuffer(-1);
    pBuff += destLen;  
    rInfo.m_RecSize += destLen;
    
    return retval;
} 

Converting to CBaseRecord format

Here's the code that converts from a handheld record to a CBaseRecord format. Note that it must read the record ID, the category ID, the attributes, and the record contents. We use a utility  routine,  SwapDWordToIntel, to swap the customer ID. If the record is deleted, there are no record contents. We don't try to read the record contents in such cases.

long CSalesDTLinkConverter::ConvertFromRemote(
    CBaseRecord& rRec,
    CRawRecordInfo& rInfo)
 {
    long                retval = 0;
    char                *pBuff;
    CString             aString;

    CSalesRecord& rExpRec = (CSalesRecord &)rRec;
    
    retval = rExpRec.SetRecordId(rInfo.m_RecId);
    retval = rExpRec.SetCategoryId(rInfo.m_CatId);
    if (rInfo.m_Attribs & ARCHIVE_BIT)
        retval = rExpRec.SetArchiveBit(TRUE);
    else
        retval = rExpRec.SetArchiveBit(FALSE); 

    if (rInfo.m_Attribs & PRIVATE_BIT)
        retval = rExpRec.SetPrivate(TRUE);
    else
        retval = rExpRec.SetPrivate(FALSE); 
    
    retval = rExpRec.SetStatus(fldStatusNONE);
    if (rInfo.m_Attribs & DELETE_BIT) // Delete flag
        retval = rExpRec.SetStatus(fldStatusDELETE);
    else if (rInfo.m_Attribs & DIRTY_BIT) // Dirty flag
        retval = rExpRec.SetStatus(fldStatusUPDATE);

    // Only convert body if remote record is *not* deleted..
    if (!(rInfo.m_Attribs & DELETE_BIT)) 
    {
        pBuff = (char*)rInfo.m_pBytes;
        
        //  Customer ID
        long customerID = SwapDWordToIntel(*((DWORD*)pBuff));
        retval = rExpRec.SetID(customerID);
        pBuff += sizeof(DWORD);

        // Name
        AddCRs(pBuff, strlen(pBuff));
        aString = m_TransBuff;
        retval = rExpRec.SetName(aString);
        pBuff += strlen(pBuff) + 1;

        // Address
        AddCRs(pBuff, strlen(pBuff));
        aString = m_TransBuff;
        retval = rExpRec.SetAddress(aString);
        pBuff += strlen(pBuff) + 1;

        // City
        AddCRs(pBuff, strlen(pBuff));
        aString = m_TransBuff;
        retval = rExpRec.SetCity(aString);
        pBuff += strlen(pBuff) + 1;

        // Phone
        AddCRs(pBuff, strlen(pBuff));
        aString = m_TransBuff;
        retval = rExpRec.SetPhone(aString);
        pBuff += strlen(pBuff) + 1;
    }
    return retval ; 
}

The DLL

The one remaining piece in our puzzle is the DLL where the CSalesConduitMonitor actually gets created.

DLL OpenConduit

DLL's  OpenConduit is where we put the conduit creation code:

__declspec(dllexport) long OpenConduit(PROGRESSFN pFn, 
    CSyncProperties& rProps)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    long retval = -1;
  
    rProps.m_DbType = 'Cust';// in case it needs to be created 
    if (pFn) {
        CSalesConduitMonitor* pMonitor;

        pMonitor = new CSalesConduitMonitor(pFn, rProps, myInst);
        if (pMonitor)
        {
            retval = pMonitor->Engage();

            delete pMonitor;
        }
    }
    return retval;
}

Note that we set the m_DbType field of rProps. We do this so that CBaseConduitMonitor will create the customer database on the handheld if it doesn't exist; it uses the type found in rProps.m_DbType to do the job.

We also pass our DLL's instance, myInst, as the third parameter. It is used to retrieve resource strings. The instance is stored as a global variable, along with three others:

static int ClientCount = 0;
static HINSTANCE hRscInstance = 0;
static HINSTANCE hDLLInstance = 0;
HINSTANCE myInst=0;

These globals are initialized when the DLL is opened.

DLL class definition

Here's our DLL's class declaration (as created automatically by Visual C++):

class CSalesCondDll : public CWinApp
{
public:
    //CSalesCondDll();
    virtual BOOL InitInstance(); // Initialization
    virtual int ExitInstance();  // Termination 

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CSalesCondDll)
    //}}AFX_VIRTUAL

    //{{AFX_MSG(CSalesCondDll)
        // NOTE - the ClassWizard will add/remove member functions here.
        //    DO NOT EDIT what you see in these blocks of generated code !
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

Initializing function

 InitInstance must initialize the table's DLL. This contains some field functions beyond those in the basetable.cpp file. It must also initialize the PDCmn DLL, which contains some resources for the dialog shown in ConfigureConduit:

BOOL CSalesCondDll::InitInstance()
{
    // DLL initialization 
    TRACE0("SALESCOND.DLL initializing\n");

    if (!ClientCount ) {
        hDLLInstance = AfxGetInstanceHandle();

        hRscInstance = hDLLInstance;

        // add any extension DLLs into CDynLinkLibrary chain
        InitTables5DLL();
        InitPdcmn5DLL();
    }
    myInst = hRscInstance;
    ClientCount++;

    return TRUE;
}

Exit function

We also need an ExitInstance: 

int CSalesCondDll::ExitInstance()
{
    TRACE0("SALESCOND.DLL Terminating!\n");

    // Check for last client and clean up potential memory leak.
    if (--ClientCount <= 0) 
    {
        PalmFreeLanguage(hRscInstance, hDLLInstance);
        hRscInstance = hDLLInstance; 
    }

    // DLL clean up, if required
    return CWinApp::ExitInstance();
}

DLL resources

There are a variety of strings that the Conduit Manager loads from resources (including all the logging strings). These strings have to be stored within our DLL. In our resource file, SalesCond.rc, we don't have any explicit resources. Instead, in the Resource Includes panel, we add a compile-time directive:

#include "..\include\Res\R_English\Basemon.rc"

This makes all the standard basemon resource strings part of our DLL.

Testing the Conduit

Before testing, make sure you use CondCfg.exe to register the Remote Database name for the Sales conduit as "Customers-Sles". This is what tells your conduit what database to sync.

There are some good tests you can perform to ensure that your conduit is working properly:

Sync having never run your application

Your database(s) won't yet exist. This simulates a user syncing after installing your software but before using it. If your conduit performs correctly, any data from the desktop should be copied to the handheld.

Sync having run your application once

Do this test after first deleting your databases. This simulates a user syncing after installing your software and using it. If everything works as expected, data from the handheld should be copied to the desktop.

Add a record on the handheld and sync

Make sure the new record gets added to the desktop.

Add a record on the desktop and sync

Make sure the new record gets added to the handheld.

Delete a record on the handheld and sync

Make sure the record gets deleted from the desktop.

Delete a record on the desktop and sync

Make sure the record gets deleted from the handheld.

Archive a record on the handheld

Make sure the record gets deleted from the main desktop file and gets added to the archive.

There are other tests you can make, but these provide the place to begin.

Generic Conduit

Top Of Page

Generic Conduit is the other approach to creating a conduit that handles two-way syncing. It is based on a new set of classes (currently unsupported) that Palm Computing has recently started distributing. Having seen all that is involved in creating a conduit based on the basemon and basetabl classes, you can understand why Palm Computing wanted to offer a simpler solution to developers. Generic Conduit is one of Palm's solutions to this problem-these classes are intended to make it easier to get a conduit up and running.

Advantages of Using Generic Conduit

There are some powerfully persuasive advantages to basing a conduit on these new classes:

In some cases, you don't need to write any code

Generic Conduit contains everything, including ConfigureConduit, GetConduitName, etc. If you compile and register it, it'll be happy to two-way sync your Palm database to a file on the desktop. This approach requires the use of its own file format, however. If you don't like that format, you need to customize the Generic Conduit classes to some extent.

If you do have to write code, it might not be much

The number of classes and the number of methods are much less daunting than those found in the basemon and basetabl classes.

All the source code is available

The entire source code is provided; you don't have to rely on any DLLs (basemon uses Tables.DLL for the CBaseTable class and MFC for serialization). Further, if you so desire, you can change any or all of the source code.

There's less work involved in handling records

Generic Conduit is unlike basemon, which has a schema and attempts to represent your record as fields in memory. Generic Conduit treats your record as just a sequence of bytes. Thus, records are copied from the handheld to the desktop and left untouched; the default file format stores them as is. Record comparison is accomplished by comparing all the bytes in each record to see if they are identical. This is a far cry from basemon's approach, which represents records in memory as fields and does field-by-field comparison.

The approach to conduit creation makes more sense

This Generic Conduit approach makes a great deal of sense. All that's needed for synchronization to work correctly is to compare two records to see whether they are the same or different. There's no need to know what fields exist or anything else; you just compare the bytes.

Disadvantages of Using Generic Conduit

There are also disadvantages to this approach. The good news is that they may possibly fade over time:

Generic Conduit is not supported by Palm

Palm Computing provides the Generic Conduit code as an unsupported sample. The supported way to do two-way syncing is with the basemon classes. It is certainly worth checking for the latest version of Generic Conduit and Palm's current support position before making a decision regarding its use (see http://www.Palm.com/devzone for information).

It's new

The basemon classes are used for Palm's shipping conduits and by numerous third parties. That means they work very well, and, presumably, most of the bugs have already been found and fixed. If you're an early user of Generic Conduit, you are at risk for as yet unfound bugs of who knows what nature. Once again, it is worth checking on the most recent version of Generic Conduit-as time passes this will become less of a problem.

The suggested way to create conduits is duplicate/modify

Palm Computing's suggested way to use the Generic Conduit is to duplicate the Generic Conduit source folder and then go to work making changes to their source. This approach flies in the face of good C++ inheritance programming practices, which should be to derive classes from the Generic Conduit classes and override only those routines that require modification.

Here is why this approach has two major problems:

Solution to the Disadvantages

We can't do anything about the first two problems (only time and Palm Computing have control of these issues), but we can address the third problem. Our sample that uses the Generic Conduit doesn't duplicate the original code; instead, it makes use of derived classes and virtual functions-our code consists only of the differences from the original. By doing this, we can easily use new versions of the Generic Conduit classes, and it should be very easy for you to use our code as a sample for creating a conduit.

Generic Conduit Classes

There are eight classes that affect your use of Generic Conduit. As might be expected, each has a different responsibility. Figure 13-2 shows you the inheritance relationship.

Figure 13- 2. Inheritance relationship of Generic Conduit classes

Now let's look at what each class does.

CPalmRecord

This represents a Palm record; it stores attributes, a unique ID, a category number, a record length, and a pointer to the raw record data.

CDbManager

This is the class that is responsible for a database. It defines methods for iterating through the records, adding and deleting records, etc. As you can see from Figure 13-2, it is also an abstract class; there are four derived classes that implement these methods.

CHHMgr

This class is derived from CDbManager and implements the CDbManager member functions by using the Sync Manager. This concrete subclass uses the interface of the abstract class. It can be used just like any other database, but its implementation is different. For example, its method to add a record is implemented using SyncWriteRec.

CPcMgr

This class implements the CDbManager member functions for a file on the desktop. When a file is opened, it reads all the records into memory and stores them in a list. Changes to the database are reflected in memory until the database is closed; at that point, the records are rewritten to the database.

You often create your own derived class of CPcMgr and override the functions RetrieveDB and StoreDB to read and write your own file formats.

CArchiveDatabase

This class is derived from CPcMgr. It is responsible for handling the archive files on the desktop.

CBackupMgr

This class is also derived from CPcMgr. It is responsible for the backup file on the desktop.

CPLogging

This class is responsible for logging when any type of failure occurs during syncing.

CSynchronizer

This class is responsible for handling the actual synchronization. It creates the database classes and manages the entire process (it has many of the same duties as CBaseConduitMonitor). You often override one of its member functions, CreateDBManager, to create your own class derived from CPcMgr.

Amazing as it may seem, that is all there is worth noting about the Generic Conduit classes. Now let's turn to the code based on Generic Conduit that we create for the Sales application conduit.

Sales Conduit Based on Generic Conduit

Top Of Page

NOTE:

This sample was based on an early beta version of the Generic Conduit and may not compile with the version available to you. For a more current version of the sample, see http://www.oreilly.com/ catalog/palmprog/.

CSalesPCMgr

We have derived a new class from CPcMgr, because we want to support the same a tab-delimited text format we used with the alternative conduit classes. Here is our new class:

class CSalesPcMgr: public CPcMgr
{
public:
    CSalesPcMgr(CPLogging *pLogging, char *szDbName, 
      TCHAR *pFileName = NULL, TCHAR *pDirName = NULL, 
      DWORD dwGenericFlags,
      eSyncTypes syncType = eDoNothing);

protected:
    virtual long StoreDB(void);
    virtual long RetrieveDB(void);
};

CSalesPCMgr constructor

Our constructor just initializes the base class:

CSalesPcMgr::CSalesPcMgr(CPLogging *pLogging, char *szDbName, 
   TCHAR *pFileName, TCHAR *pDirName, 
   DWORD dwGenericFlags, eSyncTypes syncType)
   :CPcMgr(pLogging, szDbName, pFileName, pDirName, dwGenericFlags,
      syncType)
{
}

StoreDB function

Our  StoreDB routine writes the list of records in text-delimited format:

long CSalesPcMgr::StoreDB(void)
{
   if ( !m_bNeedToSave) { // if no changes, don't waste time saving
       return 0; 
   }

    long retval = OpenDB();
    if (retval)
        return GEN_ERR_UNABLE_TO_SAVE;

    for (DWORD dwIndex = 0; (dwIndex < m_dwMaxRecordCount) && (!retval);
       dwIndex++){
        if (!m_pRecordList[dwIndex]) // if there is no record, skip ahead
            continue;
        retval = WriteRecord(m_hFile, m_pRecordList[dwIndex]);  

        if (retval != 0){
            CloseDB();
            return GEN_ERR_UNABLE_TO_SAVE;
        }
    }

    CloseDB();
    m_bNeedToSave = FALSE;
    return 0;
}

It calls WriteRecord, which writes line by line.

WriteRecord

This writes the record:

long  WriteRecord(HANDLE hFile, CPalmRecord *pPalmRec)
{

    DWORD           dwPut;
    unsigned long   len;
    const int       kMaxRecordSize = 1000;
    char            buf[kMaxRecordSize];
    char            rawRecord[kMaxRecordSize];
    DWORD           recordSize = kMaxRecordSize;
    long            retval;

    retval = pPalmRec->GetRawData((unsigned char *) rawRecord,
       &recordSize);
    if (retval) {
        return retval;
    }

    Customer *aCustomer = RawRecordToCustomer(rawRecord);


    // Write the record to the file as (if private):
    // <customerID>\t<name>\t<address>\t<city>\t<phone>\tP\t<recID>
    //    or, if not private:
    // <customerID>\t<name>\t<address>\t<city>\t<phone>\t\t<recID>
    sprintf(
        buf, 
        "%d\t%s\t%s\t%s\t%s\t%s\t%d\r\n",
        aCustomer->customerID,
        aCustomer->name,
        aCustomer->address,
        aCustomer->city,
        aCustomer->phone,
        pPalmRec->IsPrivate() ? "P": "",
        pPalmRec->GetID()
    );
    len = strlen(buf);
    WriteFile(
        hFile, 
        buf,
        len,
        &dwPut, 
        NULL
    );

    delete aCustomer;

    return dwPut == len ? 0 : GEN_ERR_UNABLE_TO_SAVE;
}

It calls  RawRecordToCustomer, which converts the bytes in a record to a customer:

Customer *RawRecordToCustomer(void *rec)
{
    Customer *c = new Customer;
    PackedCustomer *pc = (PackedCustomer *) rec;
    c->customerID = SyncHHToHostDWord(pc->customerID);
    char * p = (char *) pc->name;
    c->name = new char[strlen(p)+1];
    strcpy(c->name, p);
    p += strlen(p) + 1;
    c->address = new char[strlen(p)+1];
    strcpy(c->address, p);
    p += strlen(p) + 1;
    c->city = new char[strlen(p)+1];
    strcpy(c->city, p);
    p += strlen(p) + 1;
    c->phone = new char[strlen(p)+1];
    strcpy(c->phone, p);
    return c;
}

Retrieving a database

We also have a function,  RetrieveDB, that reads a text file and creates records from it. Even though m_HFile is already an open HFILE that we could read, it's easier to do it another way. We read text from a CStdioFile (it provides a routine to read a line at a time), so we open the file read-only with CStdioFile and close it once we're done:

long CSalesPcMgr::RetrieveDB(void)
{
    m_bNeedToSave = FALSE;
    if (!_tcslen(m_szDataFile))
        return GEN_ERR_INVALID_DB_NAME;

    CStdioFile *file = 0;
    TRY
    {
        file = new CStdioFile(m_szDataFile, CFile::modeRead);
    }
    CATCH_ALL(e)
    {
        if (file)
            file->Abort();
        delete file;

        return GEN_ERR_READING_RECORD;
    }
    END_CATCH_ALL

    TRY
    {
        CPalmRecord newRecord;
        while (ReadCustomer(*file, newRecord)) {
            AddRec(newRecord);
        }
        file->Close();
    }
    CATCH_ALL(e)
    {
        file->Abort();
        delete file;
        return GEN_ERR_READING_RECORD;
    }
    END_CATCH_ALL
    delete file;

    return 0;

}

Reading customer information

The previous routine relies on a utility routine that we need to write. This routine simply reads in the tab-delimited text file and turns it into a Palm record:

bool   ReadCustomer(CStdioFile &file, CPalmRecord &rec)
{
    static char gBigBuffer[4096];
    if (file.ReadString(gBigBuffer, sizeof(gBigBuffer)) == NULL)
        return false;
    char *p = gBigBuffer;
    char *customerID = FindUpToNextTab(&p);
    char *name = FindUpToNextTab(&p);
    char *address = FindUpToNextTab(&p);
    char *city = FindUpToNextTab(&p);
    char *phone = FindUpToNextTab(&p);
    char *priv = FindUpToNextTab(&p);
    char *uniqueID = FindUpToNextTab(&p);
    char *attributes = FindUpToNextTab(&p);

    if (!address)
        address = "";
    if (!city)
        city = "";
    if (!phone)
        phone = "";
    if (!priv)
        priv = "";
    if (!attributes)
        attributes = "";
    if (!uniqueID)
        uniqueID = "0";
    if (customerID && name) {
        rec.SetID(atol(customerID));
        rec.SetIndex(-1);
        rec.SetCategory(0);

        rec.SetPrivate(*priv == 'P');

        // 'N' -- new, 'M' -- modify, 'D'-- delete, 'A' -- archive
        // if it's Add, it can't be modify
        rec.ResetAttribs();
        if (strchr(attributes, 'N'))
            rec.SetNew();
        else if (strchr(attributes, 'M'))
            rec.SetUpdate();
        if (strchr(attributes, 'D'))
            rec.SetDeleted();
        if (strchr(attributes, 'A'))
            rec.SetArchived();
        
        static char buf[4096];
        PackedCustomer *pc = (PackedCustomer *) buf;
        pc->customerID = SyncHostToHHDWord(atol(customerID));
        char *p = (char *) pc->name;
        strcpy(p, name);
        p += strlen(p) + 1;
        strcpy(p, address);
        p += strlen(p) + 1;
        strcpy(p, city);
        p += strlen(p) + 1;
        strcpy(p, phone);
        p += strlen(p) + 1;

        rec.SetRawData(p - buf, (unsigned char *) buf);
    
        return true;
    } else
        return false;
}

CSalesSynchronizer

We also have a derived a class from CSynchronizer, because we want to do three things:

Unlike the previous occasion, we don't have to perform a bunch of copying tricks to handle a simple override. Everything works as expected.

Class definition

Here's our class declaration of CSalesSynchronizer:

class CSalesSynchronizer: public CSynchronizer {
public:
    CSalesSynchronizer(CSyncProperties& rProps);

protected:
    virtual long Perform(void);
    virtual long CreatePCManager(void);
};

Creating a CSalesPcMgr class

Here's the routine that creates a CSalesPcMgr:

long CSalesSynchronizer:: CreatePCManager(void)
{
    DeletePCManager();

    m_dbPC = new CSalesPcMgr(m_pLog, 
                        m_remoteDB->m_Name, 
                        m_rSyncProperties.m_LocalName, 
                        m_rSyncProperties.m_PathName,
                        m_dwDatabaseFlags
                        m_rSyncProperties.m_SyncType);
    if (!m_dbPC)
        return GEN_ERR_LOW_MEMORY;
    return m_dbPC->Open();
}

The constructor

Our constructor sets the bit field, specifying that we don't support categories, or the AppInfo block or the sort info block:

CSalesSynchronizer::CSalesSynchronizer(CSyncProperties& rProps) :
    CSynchronizer(rProps)
{
    // m_dwDatabaseFlags is a bit-field with 
    //   GENERIC_FLAG_CATEGORY_SUPPORTED
    //   GENERIC_FLAG_APPINFO_SUPPORTED
    //   GENERIC_FLAG_SORTINFO_SUPPORTED

    // we don't want any of the flags set, so we just use 0
    m_dwDatabaseFlags = 0;
}

Modifying perform to add uploading and downloading products
and orders

As we found in the basemon case, there's a fairly large routine that opens the conduit, does the appropriate kind of syncing, and closes the conduit. We need to insert our code to copy the Products database and Orders database in there. We've copied that routine and inserted our code (our added code is bold):

long CSalesSynchronizer:: Perform(void)
{
    long retval = 0;
    long retval2 = 0;

    if (m_rSyncProperties.m_SyncType > eProfileInstall)
        return GEN_ERR_BAD_SYNC_TYPE;


    if (m_rSyncProperties.m_SyncType == eDoNothing) {
        return 0;
    }
    // Obtain System Information
    m_SystemInfo.m_ProductIdText = (BYTE*) new char [MAX_PROD_ID_TEXT];
    if (!m_SystemInfo.m_ProductIdText)
        return GEN_ERR_LOW_MEMORY;
    m_SystemInfo.m_AllocedLen = (BYTE) MAX_PROD_ID_TEXT; 
    retval = SyncReadSystemInfo(m_SystemInfo);
    if (retval)
        return retval;

    retval = RegisterConduit();
    if (retval)
        return retval;



    for (int iCount=0; iCount < m_TotRemoteDBs && !retval; iCount++) {
        retval = GetRemoteDBInfo(iCount);
        if (retval) {
            retval = 0;
            break;
        }

        switch (m_rSyncProperties.m_SyncType) {
            case eFast:
                retval = PerformFastSync();
                if ((retval) && (retval == GEN_ERR_CHANGE_SYNC_MODE)){
                    if (GetSyncMode() == eHHtoPC)
                        retval = CopyHHtoPC();
                    else if (GetSyncMode() == ePCtoHH)
                        retval = CopyPCtoHH();
                }
                break;
            case eSlow:
                retval = PerformSlowSync();  
                if ((retval) && (retval == GEN_ERR_CHANGE_SYNC_MODE)){
                    if (GetSyncMode() == eHHtoPC)
                        retval = CopyHHtoPC();
                    else if (GetSyncMode() == ePCtoHH)
                        retval = CopyPCtoHH();
                }
                break;
            case eHHtoPC:
            case eBackup: 
                retval = CopyHHtoPC();
                break;
            case eInstall:
            case ePCtoHH:
            case eProfileInstall:
                retval = CopyPCtoHH();
                break;
            case eDoNothing:
                break;
            default:
                retval = GEN_ERR_SYNC_TYPE_NOT_SUPPORTED;
                break;
        }

        DeleteHHManager();
        DeletePCManager();
        DeleteBackupManager();
        CloseArchives();
    }

// added here for sales conduit
    if (retval == 0 && m_rSyncProperties.m_SyncType == eHHtoPC ||
        m_rSyncProperties.m_SyncType == eFast || 
        m_rSyncProperties.m_SyncType == eSlow)
        retval = CopyOrdersFromHH(m_rSyncProperties);
    if (retval == 0 && m_rSyncProperties.m_SyncType == ePCtoHH ||
        m_rSyncProperties.m_SyncType == eFast || 
        m_rSyncProperties.m_SyncType == eSlow)
        retval = CopyProductsAndCategoriesToHH(m_rSyncProperties);
// done added here for sales conduit
// Unregister the conduit
    retval2 = UnregisterConduit((BOOL)(retval != 0));

    if (!retval)
        return retval2;
    return retval;
}

Creating the Conduit

In our OpenConduit DLL entry point, we create our CSalesSynchronizer and call it's Perform function to do the work of synchronization:

ExportFunc long  OpenConduit(PROGRESSFN pFn, CSyncProperties& rProps)
{
    long retval = -1;
    if (pFn)
    {
        CSalesSynchronizer* pGeneric;

        pGeneric = new CSalesSynchronizer(rProps);
        if (pGeneric){
            retval = pGeneric->Perform();

            delete pGeneric;
        }

    }
    return(retval);
}

At this point, we can test the code. It works just as the basemon version did, so we will use the same tests.

As you can see, Generic Conduit makes the task of supporting two-way mirror image syncing much easier. It is simpler to derive classes, since there are no real problems with functions that should be virtual that are not. In either case, we hope that it is clearer how to add support for two-way syncing after this description of each method.


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