Extending DDX/DDV for a Wizard Sheet

One of cool things that MFC provides is a DDX/DDV mechanism. Variables are associated with controls transparently. It probably can be thought as the easiest way to use the dialog box. But things were changed a little bit after the Property Sheet was introduced. I’d like to explain what has been changed, and what I did to adapt it. This article focuses on the “Wizard Sheet,” but as you know, the wizard sheet is a special kind of property sheet, and the same technique will fit a normal property sheet too. I list the class sketch here; for the complete declarations, please refer to the source code.


class CWizardPage : public CPropertyPageEx
{

public:

virtual BOOL OnSetActive();
virtual LRESULT OnWizardNext();
virtual LRESULT OnWizardBack();
virtual BOOL OnWizardFinish();
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam,
LRESULT* pResult);
//}}AFX_VIRTUAL
protected:
CWizardSheet *m_pps;
virtual void DoDataExchange(CDataExchange* pDX);
// DDX/DDV support
friend class CWizardSheet;


};

class CWizardSheet : public CPropertySheetEx
{

DECLARE_DDX_MAP()

public:

void AddPage( CWizardPage *pPage );

DECLARE_MESSAGE_MAP()
};

First of all, the question is, “Can DDX/DDV be used on a Wizard Sheet?” The answer is “Yes!” A Wizard Sheet just is a set of dialog boxes. (Check the CPropertyPage class; it directly inherits from CDialog, so there is nothing to prevent us from using the DDX/DDV.) But the problem is that each variable should be added into the CPropertyPage separately as a member variable. This means that we cannot use the wizard pages array but need to declare each individual class for each dialog box, even though the only difference is the associated member variables. Another drawback is that we must also collect all of the information from many classes.


Example:
CWizardSheet theSheet(…);
CWizardPage1 page1(…);
CWizardPage2 page2(…);
CWizardPage3 page3(…);

theSheet.AddPage(&page1);
theSheet.AddPage(&page2);
theSheet.AddPage(&page3);

if(theSheet.DoModal())
{//Get all values…
func1(page1.v1, page3.v2, page2.v1…) ;
….
}

It gets worse when there are many pages. Honestly, when I’m using a wizard sheet, I just think of it as a single object to collect some information from users, and I don’t want to separate it over five or six pages. Instead of using a dialog box, I use a wizard sheet; it’s just because I’d like people to think me a polite and friendly guy. CWizardSheet is the right place to get all of this information and not all of those individual pages. So, the first thing I did is here:


void CWizardPage::DoDataExchange(CDataExchange* pDX)
{
CPropertyPageEx::DoDataExchange(pDX);
m_pps->DoDataExchange(pDX);
}

Aha, the page does nothing; all data exchange jobs are delegated to the Sheet. But how does the sheet know which variable is associated with which control? To hard-code the relationship isn’t a good solution. Because we don’t know how many pages and how many controls will be adopted by this sheet, I decided to design a generic mechanism to bind the control and a variable pair at run time.


struct DDX_MAP_ENTRY
{
CWnd* pDlgWnd;
int nIDCtrl;
CTRLTYPE nTypeCtrl;
void *pVariable;
};

No doubt, we need nIDCtrl to represent the control and pVariable for the variable. nTypeCtrl was used to indicate the control and variable type. But why do we need pDlgWnd? We know each control’s resource ID in a dialog box is unique, but this is not true when it’s used across a couple of dialog boxes, so “pDlgWnd” is necessary. For the convenience, I defined some macros here:


#define DECLARE_DDX_MAP()
public:
const DDX_MAP_ENTRY *_pddxEntries;
virtual void DoDataExchange(CDataExchange* pDX);

#define BEGIN_DDX_MAP(theMap)
const DDX_MAP_ENTRY theMap##_ddxEntries[] =
{

#define END_DDX_MAP()
{NULL, 0, None, NULL}
};

#define DDX_ENTRY(winDlg, idCtrl, typeCtrl, varObj)
{&winDlg, idCtrl, typeCtrl, &varObj},

#define ASSOCIATE_DDX_MAP(theClassInst,theMap)
theClassInst._pddxEntries = theMap##_ddxEntries;

The usage will be like this:


BEGIN_DDX_MAP(MakeCert)
DDX_ENTRY(MakeCertPages[0], IDC_LABEL, Edit, strLabel)
DDX_ENTRY(MakeCertPages[0], 1000, CheckBox, email)
DDX_ENTRY(MakeCertPages[0], 1001, CheckBox, file)
….
DDX_ENTRY(MakeCertPages[2], IDC_DTPICKER_END,
DateTimePicker, TimeEnd)
END_DDX_MAP()
ASSOCIATE_DDX_MAP(MakeCertSheet,MakeCert);

Now it’s the time to finish the CWizardSheet::DoDataExchange(CDataExchange* pDX).


void CWizardSheet::DoDataExchange(CDataExchange* pDX)
{
const DDX_MAP_ENTRY *_pddxEntries = this->_pddxEntries;
if( !_pddxEntries ) return;

while( _pddxEntries->nIDCtrl )
{
if( pDX->m_pDlgWnd == _pddxEntries->pDlgWnd)
{
switch (_pddxEntries->nTypeCtrl)
{
case Edit:
DDX_Text(pDX, _pddxEntries->nIDCtrl,
*(CString*)_pddxEntries->pVariable);
break;
case CheckBox:
DDX_Check(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case RadioBox:
DDX_Radio(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case ListBoxIdx:
DDX_LBIndex(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case ComboBoxIdx:
DDX_CBIndex(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case ListBoxTxt:
DDX_LBString(pDX, _pddxEntries->nIDCtrl,
*(CString*)_pddxEntries->pVariable);
break;
case ComboBoxTxt:
DDX_CBString(pDX, _pddxEntries->nIDCtrl,
*(CString*)_pddxEntries->pVariable);
break;
case ScrollBar:
DDX_Scroll(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case Slider:
DDX_Slider(pDX, _pddxEntries->nIDCtrl,
*(int*)_pddxEntries->pVariable);
break;
case MonthCalCtrl:
DDX_MonthCalCtrl(pDX, _pddxEntries->nIDCtrl,
*(CTime*)_pddxEntries->pVariable);
break;
case DateTimeCtrl:
DDX_DateTimeCtrl(pDX, _pddxEntries->nIDCtrl,
*(CTime*)_pddxEntries->pVariable);
break;
}
}
_pddxEntries++;
}
}

Things seem done for the most part, but wait a minute; there’s a special “DDX_” function in MFC. It’s called “DDX_Control.” MFC uses it to transfer the data between a subclassed control and a CWnd instance. If we are using an activeX control, we will need it. For example, if I want to use the “MS Date and Time Picker,” I need to associate it with a CDTPicker (inherits from CWnd) instance. So, is what I need do just like this?


case DateTimePicker:
DDX_Control(pDX, _pddxEntries->nIDCtrl,
*( CDTPicker*)_pddxEntries->pVariable);
break;

Not at all! What “DDX_Control” does is just to subclass a control; this means you can operate the control only when the control is alive. Once the sheet was closed, you cannot get any data from the CDTPicker! I like the “MS DT Picker” because it looks cool, but all I need is that it gives me a CTime just like the normal DateTimeCtrl does. So, I declared:


struct CDTPickerEx : public CDTPicker, public CTime
{
};

and added these into CWizardSheet::DoDataExchange:


case DateTimePicker:
CDTPicker& picker=static_cast<CDTPicker&>
(*(CDTPickerEx*)_pddxEntries->pVariable);
CTime& timeVal= static_cast<CTime&>(
*(CDTPickerEx*)_pddxEntries->pVariable);
DDX_Control(pDX, _pddxEntries->nIDCtrl, picker);

if(pDX->m_bSaveAndValidate){
timeVal = CTime(picker.GetYear().intVal,
picker.GetMonth().intVal,
picker.GetDay().intVal,
picker.GetHour().intVal,
picker.GetMinute().intVal,
picker.GetSecond().intVal);
}
else{
//We did not support it yet!

}
break;

It’s the end of my article, but not the end of the story. There are many things we can improve in the DDX/DDV; the Mediator pattern could be a good hint. But, after all, we should remember it’s the lazy guys (not the smart guys) who are improving the world.

Downloads


Download demo project – 77 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read