Download demo project - 35 Kb
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 WTL�s DDX/DDV implementation, show two custom extensions I added to WTL�s implementation to extends its reach, and provide code taken from a real-life example using WTL�s 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 ATL�s message map), and the templatized class CWinDataExchange
.
Thus the first step to make use of DDX/DDV is to add
#include <ATLddx.h>
into your StdAfx.h or other primary header file. The one caveat here, is that if you are using WTL's 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 WTL�s 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 (a la DDX_TEXT
) and one for linkage and validation (a la 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 WTL�s 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. I�ll 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 isn�t 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 didn�t all just fall into place � there were two immediate problems I hit that required adding new extensions to <ATLDDX.H>to get the DDX framework to do some additional handling. The first item was regarding the combo boxes � the generic ddx implementation would be to specify DDX_INT
and pass in the combo box id to handle the mapping of the camera�s unit code & house code to their respective combo box. However, the gotcha is that DDX_INT calls Get/SetDlgItemInt underneath, which merely places or retrieves the textual representation of an integer. For the cameras, which use X10 wireless protocol, both the unit code and house code represented an index into an array of values, not the values themselves�.example: house code A is represented by 0, B is 1, etc. Under the default DDX implementation, if I passed in its intrinsic value 0, I would get a combo box showing 0, which is not what I wanted.
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)
{
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 didn�t do it, and neither did DDX_CHECK
(didn�t 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>,
<BOOL variable>,
<Secondary 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 <ATLDDX.H> file.
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 WTL�s 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 WTL�s 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 less_wright@hotmail.com
Default Data Handlers
Here�s 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)
DDX_CONTROL(nID, obj)
DDX_CHECK(nID, var)
DDX_RADIO(nID, var)