Environment: VC6 SP4, Win2K, Win98/ME
Introduction
One of the more tedious parts of UI coding is simply doing the grunt work of passing member data into various dialogs for user interaction, and then pulling it back out and ensuring the data is valid before recommitting it to the core part of your program. MFC realized this, and has long had the concept of DDX (dynamic data exchange) and DDV (dynamic data validation). However, those of us who are focused on download size stick to ATL and SDK style coding, meaning lots of rote code has to be done to enable the user to modify various properties and settings in our UI dialogs. With WTL, however, we now have the opportunity to gain a lot of the ease of MFC coding without the bulk. This article will show you how to make use of WTLs DDX/DDV implementation, show two custom extensions I added to WTLs implementation to extends its reach, and provide code taken from a real-life example using WTLs property sheet implementation (CPropertyPageImpl).
What is DDX/DDV exactly?
As alluded to earlier, the purpose of DDX is to provide a framework for handling the passing (both loading and retrieving) of data between your application and the UI it presents to the user via dialogs, property sheets, etc. The goal of DDV is to enable automatic verification of any changes the user made to ensure they have entered valid data, where valid is defined by your application. The benefit of using DDX is efficient, easy to read and maintain mappings between your applications variables and their presentation/modification in your various dialogs. Additionally, you will see a significant reduction in the amount of time you spend coding the basic infrastructure needed to support interactive UI.
With the overview out of the way, lets get into coding to start explaining the details involved in utilizing DDX in your app. WTL provides DDX/DDV handling via the header atlddx.h, which contains macros for creating a DDX map (same concept as ATLs message map), and the templatized class CWinDataExchange
.
Thus the first step to make use of DDX/DDV is
#include <ATLddx.h>
into your StdAfx.h or other primary header file. The one caveat here, is that if you are using WTLs Cstring, you must include AtlMisc.h before AtlDDx.h.
Thus, I always simply add the following to my StdAfx.h
#include <atlmisc.h> //CString support
#include <atlddx.h>
in StdAfx.h, right beneath atlwin.h and forget about it. Because the sample code in this article will also use property sheets to demonstrate ddx, we have to add the include for atldlgs.h, giving us a final StdAfx.h that has this segment of code in it:
... #include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlwin.h> #include <atlmisc.h> #include <atlddx.h> #include <atldlgs.h> ...
The next step is to ensure the dialog class you are using inherits from CWinDataExchange to get support for DDX like so:
class CCameraBase : public CPropertyPageImpl<CCameraBase>, public CWinDataExchange<CCameraBase>
After that, we can now get to the heart of it, which is actually connecting your applications variables to their respective UI components. This is done via the DDX message map, of which a simplistic example is listed below:
BEGIN_DDX_MAP(<your dialog class>) DDX_TEXT(<dlg resource id>,<string variable>) DDX_TEXT_LEN(<dlg resource id2>, <string variable2>, <max text length>) END_DDX_MAP()
Within the message map is where you direct WTLs DDX class how to hook up your variables to your dialogs UI elements. There are a number of macros that can be used for various data types, but lets briefly look at the two entries above. The first one DDX_TEXT, simply states that the value in <string variable> should be assigned to <dlg resource id> on load typically a string variable mapping to an edit box.
On the close of the dialog, the mapping is done in reverse the current contents of <dlg resource id> are pulled out and placed back into <string variable>. Very nice and tidy. The second macro shown above, DDX_TEXT_LEN has similar functionality to DDX_TEXT in that it joins the <dlg resource id2> to <string variable2> but you can see there is a third parameter, <max text length>. Specify a value here, and if the users text entry exceeds it, the DDV error handler will kick in. You can override the default handler, or you can use the default handler, which will beep, and set the focus back to the offending control to prompt the user to correct it. (You implement your own handler by overriding the function void OnDataValidateError(UINT id, BOOL bSave,_XData& data)
, which is demonstrated in the sample app).
There are a number of macros for the message map, usually with a variation of one for pure linkage (ala DDX_TEXT) and one for linkage and validation (ala DDX_TEXT_LEN). See summary at the end of this article.
With that, the final step is to actually tell WTL when to fire the actual data exchange. This is done by calling DoDataExchange(BOOL fParam)
with FALSE to load the dialog with your data values, and TRUE to retrieve the data from your dialog. Where to do this is up to you, but OnInitDialog
(handler for WM_INITDIALOG) is a good spot for the DoDataExchange(FALSE), or load call. For pulling back the modified data, you could put the DoDataExchange(TRUE) call (retrieve) in OnOK
for a dialog, and for property pages, you probably want to handle it in OnKillActivate()
.
DDX in Action
With an understanding of the fundamentals involved in utilizing DDX/DDV, lets move on to the sample application, where we can see DDX in action, as well as examine some limitations I found and how to work around them.
The sample application is based off of a subset of code from a commercial application that handles the viewing/monitoring of multiple wireless cameras. The relevant part of this app is that it has to allow the user to specify individual settings for up to 16 cameras and do so in a clean UI. The solution I chose was to use WTLs property page implementation to allow a nice tabbed dialog for each camera, and then use DDX/DDV to simplify the transfer back and forth of the individual settings. The sample app simply lets you play with four settings, so that you can see in the debugger how the settings are transferred and validated. Ill give an overview here of some of the more interesting parts, and after that, just tracing the code in the debugger should cement your understanding of DDX/DDV.
Because the camera settings were going to be globally used throughout the app, I made a global struct as follows (from stdafx.h):
struct _cameraprops { CString ssFriendlyName; UINT iHouseCode; UINT iUnitCode; CString ssSaveDir; BOOL fIsInSnapshotCycle; BOOL fAddTimestamp; }; extern _cameraprops g_cameraProps[4];
and allocated storage in the main cpp file (propsheetddx.cpp):
#include "maindlg.h" CAppModule _Module; _cameraprops g_cameraProps[4];
With our central storage structure setup, the next step was to create the header for handling the property sheet and create the base class for the property page handler.
class CCameraBase : public CPropertyPageImpl<CameraBas>, public CWinDataExchange<CameraBas>
I then created a single dialog in VC, which looked like so:
Next, I added the DDX message map to specify the linkage between the UI and the global struct g_cameraprops:
BEGIN_DDX_MAP(CCameraBase) DDX_TEXT_LEN(IDC_edit_CameraTitle, g_cameraProps[m_iIdentity].ssFriendlyName, 35) DDX_COMBO_INDEX(IDC_cmbo_HouseCode, g_cameraProps[m_iIdentity].iHouseCode) DDX_COMBO_INDEX(IDC_cmbo_UnitCode, g_cameraProps[m_iIdentity].iUnitCode) DDX_TEXT(IDC_edit_FileDirectory, g_cameraProps[m_iIdentity].ssSaveDir) DDX_BOOL_RADIO(IDC_radio_AddTimeStamp, g_cameraProps[m_iIdentity].fAddTimestamp, IDC_radio_NoTimeStamp) END_DDX_MAP() enum { IDD = IDD_PROP_PAGE1 };
and of course we had to invoke the DoDataExchange for load…
LRESULT OnInitDialog(...) { ... InitComboBoxes(hwndComboHouse, hwndComboUnit, m_iIdentity); CenterWindow(); DoDataExchange(FALSE); ... }
and for validating and pulling the modified data back:
BOOL OnKillActive() { DoDataExchange(TRUE); return true; }
Since we have to scale up to 16 cameras (four in the sample though), I modified the constructor of the CcameraBase class to take an integer to identify what camera it was handling:
CCameraBase(int _index) { m_iIdentity = _index; ... }
With that done, we now have a basic layout for a property page, a framework for transferring the data and pulling it back after the user has modified it, and an index to allow us to reuse the same class for all cameras. Now, to actually implement this 4x in our UI, we turn to our derived CpropertySheetImpl class, CcameraProperties. There isnt too much to it as shown below:
class CCameraProperties : public CPropertySheetImpl<CameraPropertie> { public: CCameraBase m_page1; CCameraBase m_page2; CCameraBase m_page3; CCameraBase m_page4; CCameraProperties():m_page1(1),m_page2(2),m_page3(3),m_page4(4) { m_psh.dwFlags |= PSH_NOAPPLYNOW; AddPage(m_page1); AddPage(m_page2); AddPage(m_page3); AddPage(m_page4); SetActivePage(0); SetTitle(_T("Camera and Video Input Properties")); }
Note the use of the member initialization list above in bold to show where we actually id each of the class instances with their respective camera. After that, call AddPage to hook up the classes into the tabbed layout, and specify your first page via SetActivePage. There is a small message map omitted above, but beyond that, you now have the handler for the full property sheet.
Extending DDX in WTL
Of course, things didnt all just fall into place there were two immediate problems I hit that required adding new extensions to
Since I think its very common to have combo boxes representing indexes into arrays or enums rather than trying to represent the literal textual value, I added a new macro and macro handler called DDX_COMBO_INDEX. This will handle the passing and retrieval of an index value, rather than the literal textual translation. The sample has the modified code, but it ended up like this:
#define DDX_COMBO_INDEX(nID, var) \ if(nCtlID == (UINT)-1 || nCtlID == nID) \ { \ if(!DDX_Combo_Index(nID, var, TRUE, bSaveAndValidate)) \ return FALSE; \ }
followed by:
template <class Type>
BOOL DDX_Combo_Index(UINT nID,
Type& nVal,
BOOL bSigned,
BOOL bSave,
BOOL bValidate = FALSE,
Type nMin = 0,
Type nMax = 0)
{
T* pT = static_cast<>(this);
BOOL bSuccess = TRUE;
if(bSave)
{
nVal = ::SendMessage((HWND) (Type)pT->GetDlgItem(nID),
CB_GETCURSEL,
(WPARAM) 0,
(LPARAM) 0);
bSuccess = (nVal == CB_ERR ? false : true);
}
else
{
ATLASSERT(!bValidate || nVal >= nMin && nVal <= nMax);
int iRet = ::SendMessage((HWND) (Type)pT->GetDlgItem(nID),
CB_SETCURSEL,
(WPARAM) nVal,
(LPARAM) 0);
bSuccess = (iRet == CB_ERR ? false : true);
}
if(!bSuccess)
{
pT->OnDataExchangeError(nID, bSave);
}
else if(bSave && bValidate) // validation
{
ATLASSERT(nMin != nMax);
if(nVal < nMin || nVal > nMax)
{
_XData data;
data.nDataType = ddxDataInt;
data.intData.nVal = (long)nVal;
data.intData.nMin = (long)nMin;
data.intData.nMax = (long)nMax;
pT->OnDataValidateError(nID, bSave, data);
bSuccess = FALSE;
}
}
return bSuccess;
}
With this, I could successfully handle indexes within my combo box UI. The other gotcha was that the UI I wanted to use was to have two radio buttons, representing a true/false choice for the user (see the option #4, do you wish to have timestamps added). Initially I thought DDX_RADIO was what I wanted, but that didnt do it, and neither did DDX_CHECK (didnt handle the toggling of the UI to create an exclusive selection between the two radio buttons). Thus, I added another extension, DDX_BOOL_RADIO, which took two resource ids as follows:
DDX_BOOL_RADIO(<primary radio buttonID>, <OOL variable>, <econdary radio buttonID>)
What this extension does is ensure that in the load, only one of the two buttons is selected, based on the state of the bool variable. If true, the primary ID radio is checked, the secondary radio button is initialized to unchecked, and the reverse if the initial load value is false. Obviously, you could simply use one radio button to represent the true/false state, but I wanted to make it really clear to the user what they were selecting by explicitly calling it out with two buttons and associated text. You can also find the code for DDX_BOOL_BUTTON in the modified
With that, the basic code framework for the sample application is complete we now have a UI that can elegantly handle the transfer back and forth of data between UI and internal variables via WTLs DDX/DDV, allow the user to understand what they are selecting via a tabbed property dialog, and finally, we have a codebase that can scale too any number of cameras with minimal change to the code.
Hopefully, a quick run of the sample app under the debugger will clear up any remaining questions, and you will now be able to take advantage of WTLs DDX/DDV framework to ensure you no longer have to keep writing reams of rote GetDlgItem/SetDlgItem style code for your future UI.
If you have recommendations for improving this article, or write your own extensions to atlddx.h, I would appreciate hearing about them. You can email me at [email protected]
Default Data Handlers
Heres the list of default DDX/DDV handlers:
DDX_TEXT(nID, var) DDX_TEXT_LEN(nID, var, len) DDX_INT(nID, var) DDX_INT_RANGE(nID, var, min, max) DDX_UINT(nID, var) DDX_UINT_RANGE(nID, var, min, max) DDX_FLOAT(nID, var) DDX_FLOAT_RANGE(nID, var, min, max) // NOTE: you must define _ATL_USE_DDX_FLOAT to // exchange float values. DDX_CONTROL(nID, obj) DDX_CHECK(nID, var) DDX_RADIO(nID, var)