In this chapter:
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 |
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 |
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.
You override this to create your class derived from CBaseTable. Here's the function declaration:
long CreateTable(CBaseTable*& pBase);
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.
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.
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.
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 |
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:
This function simply creates a CSalesTable:
long CSalesConduitMonitor::CreateTable(CBaseTable*& pBase) { pBase = new CSalesTable(); return pBase ? 0 : -1;
}
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;
}
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");
}
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);
}
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
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 |
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:
- If and when changes are made to the Generic Conduit classes (for example, bug fixes or added features), the source code to every single Generic Conduit-based conduit will need to incorporate those changes. You, the developer, need to apply any changes that were made in the Generic Conduit code to your own modified version of the code.
On the other hand, in our subclassing model, the conduits just need to be recompiled to take advantage of the newly changed code.
- Sample conduits are massive. The Generic Conduit comes with two samples: one for the address book and one for the date book. Unfortunately, these two samples have as much additional code as the Generic Conduit itself (actually, they have more, since they've got all the code from the Generic Conduit plus their own specific code). This intermingled code in the samples means they are very bad guides to creating a conduit. It is wretchedly difficult to figure out which code is for the conduit and which is specifically for the address book. Until you do, you won't know what needs to be done to write a new conduit.
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.
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.
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.
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.
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.
This class is derived from CPcMgr. It is responsible for handling the archive files on the desktop.
This class is also derived from CPcMgr. It is responsible for the backup file on the desktop.
This class is responsible for logging when any type of failure occurs during syncing.
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 |
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.