Getting data in and out of dialog boxes is an essential task in GUI application programming. While OWL provided a nice set of control encapsulation classes, it failed to provide a good mechanism for transferring data from these control abstractions into and out of the dialog. The mechanism provided by OWL, the transfer buffer feature, is fraught with dangers such as buffer overflow and type mismatches 1. Transfer buffers also do nothing to help transfer data from the buffer to application data structures. In short, a better solution is desirable for new code. Here we describe the Dialog Data Transfer framework, which was developed for this purpose.
The data transfer between the application and the dialog must happen at window setup and at commit; usually the press of the OK button. The manual solution is to override the setup and commit events of the dialog and handle data transfer explicitly. In OWL the obvious way to do this is to override the SetupWindow virtual function and handle the CmOk event. The latter requires an entry in the response table for the dialog. For example:
struct TSearchArguments
{
owl_string SearchString;
uint Flags;
enum TFlags {CaseSensitive = 1, WholeWord = 2, SearchUp = 4};
};
class TSearchDialog : public TDialog
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TDialog(parent, IDD_SEARCH_DIALOG), arguments(a) {}
protected:
TSearchArguments& arguments;
virtual void SetupWindow()
{
TDialog::SetupWindow();
SetDlgItemText(IDC_SEARCH_STRING, arguments.SearchString.c_str());
CheckDlgButton(IDC_SEARCH_UP, arguments.Flags & TSearchArguments::SearchUp ?
BST_CHECKED : BST_UNCHECKED);
}
void CmOk()
{
char buf[256]; // NB! Arbitrary size!
GetDlgItemText(IDC_SEARCH_STRING, buf, COUNTOF(buf));
arguments.SearchString = buf;
arguments.Flags = IsDlgButtonChecked(IDC_SEARCH_UP) == BST_CHECKED ?
TSearchArguments::SearchUp : 0;
TDialog::CmOk();
}
DECLARE_RESPONSE_TABLE(TSearchDialog);
};
DEFINE_RESPONSE_TABLE1(TSearchDialog, TDialog)
EV_COMMAND(IDOK, CmOk),
END_RESPONSE_TABLE;
While the number of lines of boiler-plate code here is hardly dramatic, there are a few alternative ways to do this that can save some lines of code; especially the need for a response table entry. One alternative is to override CloseWindow instead of handling the CmOk event. CloseWindow has a parameter that indicates how the dialog was closed. The argument passed through this parameter can be checked to see if we should commit the control data or not. If the dialog was closed with the OK button, the value is IDOK. (Actually, CloseWindow is only called by TDialog::CmOk in a dialog, but you should test the argument anyway in case this changes in the future.)
There is another alternative that is quite obvious, but possibly overlooked by many, and that is to hi-jack the transfer buffer mechanism.
An alternative to overriding setup and commit separately is to override the TransferData function in the transfer buffer mechanism. Conveniently, TransferData is called at window setup and commit, and has a parameter TTransferDirection that tells us whether to set or get data. With the TransferData override solution the sample dialog code above shrinks to:
class TSearchDialog : public TDialog
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TDialog(parent, IDD_SEARCH_DIALOG), arguments(a) {}
protected:
TSearchArguments& arguments;
virtual void TransferData(TTransferDirection d)
{
if (d == tdSetData)
{
SetDlgItemText(IDC_SEARCH_STRING, arguments.SearchString.c_str());
CheckDlgButton(IDC_SEARCH_UP, arguments.Flags & TSearchArguments::SearchUp ?
BST_CHECKED : BST_UNCHECKED);
}
else if (d == tdGetData)
{
char buf[256]; // NB! Arbitrary size!
GetDlgItemText(IDC_SEARCH_STRING, buf, COUNTOF(buf));
arguments.SearchString = buf;
arguments.Flags = IsDlgButtonChecked(IDC_SEARCH_UP) == BST_CHECKED ?
TSearchArguments::SearchUp : 0;
}
}
};
We can now handle the data transfer in a single function, and we no longer require a response table entry.
But the main chore is the implementation of data transfer between the controls and the application data structures. Unfortunately, OWL did little to help in this area. Strings had to be handled as character arrays, and there were few helpers for converting text to other data types; edit fields to strings, check-boxes to boolean values, etc. Here the rival MFC does much better with its dialog data exchange mechanism (DDX), and the DDT framework is based on the same ideas.
Until recently there was a serious issue with TransferData. In the original OWL code TransferData(tdSetData) was called at the end of TWindow::SetupWindow in the window initialization sequence. The usual idiom for derived window classes is to override SetupWindow and do any extra setup work after calling the base class version of SetupWindow. Unfortunately the defect in the calling sequence meant that the data transfer had already performed before the setup code in the derived class had a chance to run. Fortunately, this was fixed in OWLNext 6.32. Now SetupWindow will have fully completed before TransferData is called. The dialog data transfer mechanism described here relies on this fix, as it is based on overriding TransferData.
The MFC DDX mechanism 2 is very similar to the the code above. It too is based on overriding a single transfer function. But it has a wealth of free helper functions that assist in the transfer of data. These are given a DDX prefix by convention and come as a set of functions for each control type with further overloads for different data types; e.g. DDX_Text and DDX_Check for transferring text and boolean values. In MFC our sample transfer function above may be translated as follows:
void CSearchDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX); // Must call base.
int up = (arguments.Flags & TSearchArguments::SearchUp) ? BST_CHECKED : BST_UNCHECKED;
DDX_Text(pDX, IDC_SEARCH_STRING, arguments.SearchString);
DDX_Check(pDX, IDC_SEARCH_UP, up);
if (pDX->m_bSaveAndValidate)
arguments.Flags = (up == BST_CHECKED) ? TSearchArguments::SearchUp : 0;
}
We could further abstract the data transfer between the check-box and the search flags by introducing an overload of function DDX_Check. Then our DDX transfer function would contain just three lines:
CDialog::DoDataExchange(pDX); // Must call base.
DDX_Text(pDX, IDC_SEARCH_STRING, arguments.SearchString);
DDX_Check(pDX, IDC_SEARCH_UP, arguments.Flags, TSearchArguments::SearchUp);
This is short and sweet. The pDX parameter carries information about the direction of transfer and a reference to the parent window. That gives the DDX functions all the information needed to perform the transfer.
What is lacking in the DDX mechanism is a reference to the data source. DDX leaves the data source up to the dialog class and the implementation of the transfer function. The convention is to use class members to buffer the data between the application and the dialog. This is automatically enforced by the ClassWizard in the Visual Studio IDE, and is assumed in the documentation, so it has become a deep-rooted idiom. But as with the OWL transfer buffers, this is not ideal, since it requires an extra transfer step to get data from the buffered values to and from the application data structures. In the code above we did not use the normal DDX idiom. Instead we kept a reference to our data source as a protected member and transferred the data through it. Later we will see that we can gain additional benefits by having a reference to the data source passed to us in the transfer function.
The new Dialog Data Transfer (DDT) mechanism in OWLNext is very similar to DDX. It has the same simplicity and flexibility. In addition it provides a solution to the data source problem. It allows any type of data source to be specified. It then passes a reference to the data source to the transfer function in a type-safe manner. Other than that it is very similar to DDX, except that the transfer functions use slightly different and more consistent naming, e.g. TransferEditData and TransferCheckBoxData, and we do not have to call the base class implementation in our overridden Transfer function.
Here is a DDT version of our sample dialog code above:
class TSearchDialog
: public TTransferDialog<TSearchArguments>
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TTransferDialog(parent, IDD_SEARCH_DIALOG, a) {}
virtual void Transfer(const TTransferInfo& i, TSearchArguments& a)
{
TransferEditData(i, IDC_SEARCH_STRING, a.SearchString);
TransferCheckBoxData(i, IDC_SEARCH_UP, a.Flags, TSearchArguments::SearchUp);
}
};
Note that we inherit our dialog class from TTransferDialog and pass the type of the data source we want to use as a template argument. Passing the type of the data source allows the base class to define the Transfer function with the correct parameter type for our data source. TTransferDialog inherits from TDialog (by default) and the window mix-in class template TTransferWindow. It is the latter class template that provides the framework of the transfer mechanism, so data transfer can be implemented for window classes in general, not just TDialog derivatives.
DDT offers another minor feature that DDX does not have. You can choose whether to handle the transfer in one Transfer function or handle it in separate functions for setting and getting the data. Although this could of course be done manually with a few lines of user code, the TTransferWindow base has already arranged the split, so it is up to you which functions you want to override. Here is the sample above with SetData and GetData overrides:
class TSearchDialog
: public TTransferDialog<TSearchArguments>
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TTransferDialog(parent, IDD_SEARCH_DIALOG, a) {}
protected:
virtual void SetData(const TTransferInfo&, TSearchArguments& a)
{
SetText(*this, IDC_SEARCH_STRING, a.SearchString);
Check(*this, IDC_SEARCH_UP, a.Flags & TSearchArguments::SearchUp);
}
virtual void GetData(const TTransferInfo&, TSearchArguments& a)
{
a.SearchString = GetText(*this, IDC_SEARCH_STRING);
a.Flags = IsChecked(*this, IDC_SEARCH_UP) ? TSearchArguments::SearchUp : 0;
}
};
Note that SetData and GetData are protected members. The public interface to the transfer mechanism is Set, Get and Transfer, while SetData and GetData are implementation hooks. The default implementation of Transfer forwards the work for tdSetData and tdGetData to SetData and GetData.
Also note that the SetData and GetData functions are both non-const member functions that takes a reference to a non-const data source. Ideally, SetData should take a reference to a const data source, and Get should be a const member function. But we have to make a slight trade-off to allow the functions to be called from Transfer, and to allow the implementation of the functions to use free transfer functions that require a non-const data source argument, such as TransferWindowText.
Overriding the SetData and GetData functions is most useful in situations where one or both need very distinct handling. Normally that should be rare. Often, you will find that the distinct code belongs in other functions that override the setup or the shut-down of the dialog.
DDT passes a structure to the transfer functions containing details about the transfer. It is currently defined as follows:
struct TTransferInfo
{
HWND Window;
TTransferDirection Operation;
};
The Window member is the handle to the parent window (dialog) and the Operation member tells the transfer functions what to do. Note that TTransferDirection has a three possible values; tdSetData, tdGetData and tdSizeData. It may get more states in the future (e.g. tdValidate), so you should never assume that TTransferDirection has a fixed set of possible values.
Also, DDT may extend the contents and purpose of TTransferInfo in the future, so nothing should be assumed about its size.
A very nice feature of DDX that DDT copies is the flexibility of the transfer details. You can use control encapsulation classes, but you do not have to. You can simply refer to a control by its integer child ID. You can use dialog members, control class members, or free functions to perform the transfer of data to and from a control. This also makes the framework easily extensible. You can create new free standing functions that can be general and reusable. DDT provides a set of free transfer functions for the common controls and data types, very much like the corresponding functions in DDX:
Each of these functions may have numerous overloads for different data source types; e.g. TransferWindowText has overloads for converting between text and numeric types. Every transfer function also has further overloads that allow you to identify the control by child ID, window handle or control encapsulation class. The basic signature of a transfer function can be described by the following function template:
template <class TChildId, class TDataSource>
TransferControlData(const TTransferInfo&, TChildId, TDataSource&);
The first argument is always the TTransferInfo. It tells the function which way to transfer data and the parent window involved. The second parameter identifies the child, and as noted above, this may be an integer ID, a HWND handle or a reference to a control encapsulation object; e.g. TEdit&. The third argument is a reference to the data source; e.g. owl_string& for a text source. The functions may have further parameters as needed, but the first three should always adhere to this template.
In addition to these functions, DDT also provides a set of functions with the same names used by DDX. This makes it easier to port code from MFC programs. These functions just forward the call to their Transfer counterpart.
DDX provides another function called DDX_Control. The reason there isn't a corresponding function in our list above is that, despite appearances, DDX_Control isn't a transfer function at all. It is used to sub-class a control. This is not needed in OWLNext, since a TWindow instance can easily be created to sub-class a child by passing the child handle to the TWindow constructor. An instance created like this is called an alias in OWLNext. For example:
TWindow alias(GetDlgItem(IDC_SEARCH_STRING));
alias.SetCaption("Test");
An alias can be useful whenever you have a window handle and need to use functionality within TWindow, or functions and classes that require a TWindow argument, and you have no TWindow instance readily available. In earlier versions, only TWindow could be used for aliasing, but OWLNext 6.32 now supports aliasing for most native controls as well. For example:
TCheckBox(GetDlgItem(IDC_SEARCH_UP)).Toggle();
The major distinguishing feature of DDT compared to DDX is that the data source is passed to the Transfer function as an argument. It means that the transfer mechanism is not bound to a single source. You can freely set and get temporary settings of the dialog without affecting the primary data source referenced by the dialog. At any point while the dialog is open, you can change the contents of the dialog by calling Set and retrieve the current settings of the dialog by calling Get.
Parameterized Set and Get functions make it easy to implement advanced features such as support for presets. The dialog can let the user choose among a list of presets and let the user save new ones. All you have to do is keep a collection of presets (data source instances) and add a handler to create new presets and retrieve saved ones. For example, the following code adds support for presets to our search dialog above:
void EvContextMenu(HWND, int x, int y)
{
static std::vector<TSearchArguments> presets;
TPopupMenu menu;
for (unsigned i = 0; i != presets.size(); ++i)
menu.AppendMenu(MF_STRING, i + 1, presets[i].SearchString);
menu.AppendMenu(MF_STRING, presets.size() + 1, "Add preset");
int s = menu.TrackPopupMenu(TPM_RETURNCMD, x, y, 0, GetHandle());
if (s > static_cast<int>(presets.size()))
presets.push_back(Get()); // Save preset.
else if (s > 0)
Set(presets[s - 1]); // Use preset.
}
You will have to add an entry in the response table for EV_WM_CONTEXTMENU as well, but that's all. You can now save the settings of the dialog by right-clicking within the window and selecting "Add preset" from the pop-up menu. The next time you right-click the mouse, you will see the preset on the menu. If you select a preset on the menu the dialog will be restored to the settings stored in that preset.
Note that the code above uses Get without an argument to retrieve the settings. This is simply a functional style overload of Get that creates and returns an instance of the data source, initialised with the settings of the dialog. It requires that the data source is a value type; i.e. is default and copy constructible. If your data source is not a value type, then the functional style Get is not available; trying to use it will cause a compilation error.
In this example we used a quick-and-dirty static vector to store the presets. In a properly designed application, the presets would be owned and managed by the dialog or the application. You would probably also want persistence and preset management, such as updating, renaming and deleting presets.
A little add-on to the the basic DDT mechanism is a generic dialog class that delegates the data transfer work to a specified function. The specified function may be a free function, member function, a functor (function object) or lambda function (in C++0x). Often, for simple dialogs, the transfer work is the only customisation needed for the dialog. In these cases TDelegatedTransferDialog can be used as a generic solution and save you from having to derive a dialog class. Instead, you can just write the transfer function and then create a TDelegatedTransferDialog, passing the transfer function to the constructor. Here is an example based on the code above:
void TransferSearchArguments(const TTransferInfo& i, TSearchArguments&)
{
TransferEditData(i, IDC_SEARCH_STRING, a.SearchString);
TransferCheckBoxData(i, IDC_SEARCH_UP, a.Flags, TSearchArguments::SearchUp);
}
void TSearchApplication::CmSearch()
{
static TSearchArguments a = {"", 0};
TDelegatedTransferDialog::TTransferFunction f = bind(&TransferSearchArguments, _1, ref(a));
TDelegatedTransferDialog dlg(GetMainWindow(), IDD_SEARCH_DIALOG, f);
if (dlg.Execute() == IDOK)
Search(a);
}
Note that this code require the TR1 (Technical Report 1) library extensions to the C++ standard. TR1 provides the nice function bind that creates function objects on the fly, and the function template class that can encapsulate any free function, member function, functor and lambda function. Most of TR1 is included in the upcoming update of the C++ standard (C++0x). In C++0x you can also use a lambda expression to create the transfer function in-place:
void TSearchApplication::CmSearch()
{
static TSearchArguments a = {"", 0};
TDelegatedTransferDialog dlg(GetMainWindow(), IDD_SEARCH_DIALOG,
[&](const TTransferInfo& i)
{
TransferEditData(i, IDC_SEARCH_STRING, a.SearchString);
TransferCheckBoxData(i, IDC_SEARCH_UP, a.Flags, TSearchArguments::SearchUp);
});
if (dlg.Execute() == IDOK)
Search(a);
}
You can hardly boil it down more than that!
While the utility of the TDelegatedTransferDialog is limited, it can be a time saver for simple dialogs; e.g. plain form filling.
There are two samples in OWLNext accompanying this article:
examples/Classes/dialogdatatransfer
This sample demonstrates alternative mechanisms and programming styles for transferring data to and from controls, using a simple search dialog as a case study. This is a complete sample based on the code snippets used in this article.
examples/Classes/transfer
This sample is a test ground for the Dialog Data Transfer framework. It demonstrates transfer for all the supported controls, using both control instances and child identifiers (integers). It also demonstrates the DDX-like versions of the transfer functions.
Ready-made projects for Borland C++ 5.02 and Visual Studio 2008 are provided for both samples.
The original OWL transfer buffer mechanism for dialog data transfer has many problems. OWLNext has since added checks and support code to make transfer buffers safe. But transfer buffers still have problems rooted in a poor design. New code should use more flexible alternatives such as the Dialog Data Transfer framework discussed here.