Dynamic Data Exchange (DDX) Internals

In general, I am a fan of the way DDX works. It is quite a neat design and has many good points going for it. In particular, it neatly encapsulates the transfer of data between class member variables and dialog controls in a single place, and with a minimum of code.

Let us take a quick look at what the DDX mechanism does and how it works.

A dialog, property page, or form will usually have a number of controls on it. Otherwise, it would not be very interesting. Most of these controls, such as edit controls, let you display and / or edit text. However, that text lives inside the control itself. For it to be useful to you, you need to get your own data into and out of the control at the proper time. That is where DDX comes into play.

There are three main parts to DDX; UpdateData, DoDataExchange and the DDX functions themselves.

You call the UpdateData member function to either get data from or put data into a control. You specify the direction of the transfer by a BOOL argument to the UpdateData function. Using a BOOL for this is, in my opinion, a bad design decision. The designers of MFC should have used an enum with appropriately named values instead. I personally always forget what the value needs to be, and am forever looking up UpdateData in the on-line help to remind me (you’d think I’d remember by now though, wouldn’t you).

Of course, what I can do is define my own enum, or even just simple #defines, for the values. For example:

#define FROM_CONTROLS_TO_MEMBERS TRUE
#define FROM_MEMBERS_TO_CONTROLS FALSE

Then I could just write:

UpdateData(FROM_CONTROLS_TO_MEMBERS);

which is much more obvious than:

UpdateData(TRUE);

The next main part of DDX is the DoDataExchange virtual function. UpdateData sets up an object called a CDataExchange that holds information about the direction of the transfer and so on and passes this to DoDataExchange to do the work. The body of DoDataExchange is generally just a whole lot of DDX (and DDV) calls.

DDX functions themselves are just global functions that do the real work of transferring the data for each individual control. There is nothing very special about them. The DDX routines for exchanging values generally entail using GetDlgItem to find the item by ID number, and calling GetWindowText and SetWindowText appropriately.

When it comes to writing your dialog class, you simply call UpdateData(TRUE); (or use a #define to make it more obvious) at the start of a function where you want to get values from the controls and do something with them. Similarly, you call UpdateData(FALSE); at the end of a function when you want to copy updated values back in again. All you then need to supply is appropriate DDX lines in your DoDataExchange function to set up the relationships.

Before going any further, there is a special DDX function called DDX_Control that I should at least mention. Rather than exchanging data, this DDX function associates a CWnd-derived member variable with a control. This is association is called subclassing (it is one of several uses of this term) and the happens on the first UpdateData(TRUE) call. This call is usually the one that CDialog::OnInitDialog makes for you. Once the control and the member variable are associated, the DDX_Control call does nothing, and you can then access the control directly using the member variable. This is useful is particularly useful if you have derived you own class from an appropriate MFC CWnd-derived class and want it to applt to a particular control. It is also useful for doing things such as enabling and disabling controls. Anyway, I will not go any further into DDX_Control, as it is not relevant for this particular discussion.

Therefore, as I said earlier, the DDX mechanism neatly encapsulates the transfer of data between class member variables and dialog controls in a single place, and with a minimum of code.

However, it does lack some flexibility when it comes to numeric values, particularly float’s and double’s. The problem is that the built-in DDX routines do not let you specify a format. For example, if you want to show exactly two decimal places for you numbers, you are out of luck.

One common way around this is to use a CString and convert your double or float value to and from the CString as appropriate. As always, there are number of ways of doing this, some of which are ‘better’ than others.

The most obvious is to put code to convert your CString to a numeric value code after you UpdateData(TRUE), and to convert from numeric to CString just before the UpdateData(FALSE). For example:

void CMyDialog::MyFunction() {
 UpdateData(TRUE);
 sscanf(m_text, "%.2f", &m_dval);
 // do work with m_dval
 m_text.Format("%f", m_dval);
 UpdateData(FALSE);
}

This will work, but has the disadvantage of duplicating this code in every function.

Another way is to put the conversion in its own function. Here I have copied the MFC UpdateData conversions. I would rather be consistent with MFC than try to use an enum (even though, as I mentioned above, I feel that using a BOOL is not good design).

void CMyDialog::ConvertData(bool bSaveAndValidate) {
 if (bSaveAndValidate) {
  sscanf(m_text, "%.2f", &m_dval);
 } else {
  m_text.Format("%f", m_dval);
 }
}
void CMyDialog::MyFunction() {
 UpdateData(TRUE);
 ConvertData(TRUE);
 // do work with m_dval
 ConvertData(FALSE);
 UpdateData(FALSE);
}

Now you can have all your conversion code in the one place. However, you still have to remember to call it. The next improvement should be just screaming out at you. Put the conversion calls somewhere inside the UpdateData processing itself. The obvious place being in the DoDataExchange function that you are (almost certainly) already overriding. Even thought your DoDataExchange function probably looks like a bunch of DDX lines within some cryptic // {{ AFX_DATA_MAP comments, it is still just a normal member function that can have good old-fashioned C++ code in it. And as long as you don’t go adding non-DDX lines between those AFX_DATA_MAP comments, Class Wizard will still be happy.

Now you can end up with code like this:

void CMyDialog::ConvertData(bool bSaveAndValidate) {
 if (bSaveAndValidate) {
  sscanf(m_text, "%.2f", &m_dval);
 } else {
  m_text.Format("%f", m_dval);
 }
}
void CMyDialog::DoDataExchange() {
 if (! bSaveAndValidate)
  ConvertData(bSaveAndValidate);

 CDialog::DoDataExchange(pDX);

 //{{AFX_DATA_MAP(CXAboutDlg)
 DDX_Text(pDX, IDC_DVAL, m_text);
 //}}AFX_DATA_MAP

 if (bSaveAndValidate)
  ConvertData(bSaveAndValidate);
}

Now we just need to call UpdateData in our functions as usual.

However, we still have to remember to write the ConvertData function and add it to our DoDataExchange function for every dialog. To save us from this drudgery, we can derive our own CDialog-derived class that adds ConvertData as a virtual function (which does nothing by default) and call it from DoDataExchange in the same class. We could even use the technique from the last article to provide a template class with multiple-inheritance to add the same functionality to property pages and form view.

However, a nicer way would be to just write our own DDX function that takes a format. This is fairly straightforward to do, and would be even easier is the MFC designers had not made all the nice little helper functions that the built-in DDX routines use into local statics, which means we cannot use them ourselves. But, in this case, it is not too much of a problem, as we can call the built-in in DDX_Text function to do the bulk of the work for us.

void DDX_Text_Formatted(CDataExchange* pDX,
                        int nIDC,
                        double& value,
                        LPCTSTR lpszOutFormat=_T("%f"),
                        LPCTSTR lpszInFormat=_T("%lf"))
{
 CString temp;

 if (! pDX->m_bSaveAndValidate)
  temp.Format(lpszOutFormat,value);

 DDX_Text(pDX,nIDC,temp);

 if (pDX->m_bSaveAndValidate)
  sscanf(temp,lpszInFormat,&value);
}

Now we can use DDX_Text_Formatted wherever we would have used DDX_Text for double float or other values and specify the format that we want. One can also provide other DDX functions that call DDX_Text_Formatted with specific output format strings (such as forcing two decimal places).

There are still two remaining things to note.

Firstly, due to the strangeness of the C++ language and the C runtime library, Format (and sprintf etc) make different assumptions about the size of a value specified by a “%f” format type. Format (and spprintf etc) assume that “%f” always means a double, whereas sscanf assumes that “%f” means a float unless you prefix the ‘f’ with an ‘l’. So, if you end up providing you own value for lpszInFormat, make sure you use the correct format specifier or you will end up with garbage results!

The second point is a problem shared with the built-in DDX routines as well. If you call UpdateData(FALSE) while the user is trying to enter a value (for example, in a ON_EN_CHANGE handler), then you can stuff up the cursor position. The reason for this is that whenever you call SetWindowText, the cursor goes back to the start of the edit field. DDX is usually smart enough not to call SetWindowText unless the text has actually changed. But, with floating point numbers, there is a problem. If you type the characters “123.0” into an edit box with a DDX_Text for doubles, and you are calling UpdateData(FALSE) after each character (in an ON_EN_CHANGE handle), then all goes well until you type the ‘.’. When you get to the ‘.’, the cursor jumps back to the start of the string and the ‘.’ does not appear!! The reason is that after typing the ‘.’, the numeric value of the entry is 123 which, when converted to a string, is “123′. So that means the call to UpdateData(FALSE) will in turn call DDX_Text and try to change the edit box text from the “123.” that you just typed in into just “123”. The text value is different, so DDX_Text calls SetWindowText to update the edit box, and that resets the cursor back in front of the first character in the edit box. This behaviour makes it very difficult to enter floating-point numbers. For this reason, try to avoid EN_CHANGE and similar messages for numeric edit boxes.

So, we’ve had a look at how DDX works, how to improve it in the area of floating point number formats, and seen an interesting ‘bug’ in the way DDX handles floating point number. That is enough for this article see you next time.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read