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 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
{
tstring 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, for dialogs, CloseWindow is only called by TDialog::CmOk, but you should test the argument anyway in case this changes in the future.)
There is another, possibly overlooked, alternative which is even simpler, 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 example 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), with a plethora of function overloads to convert between text and other types.
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. This defect was fixed in OWLNext 6.31. 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.
Note that this fix is not applied when OWLNext is built with the OWL5_COMPAT compatibility option. This means that you ideally should upgrade your application code to not use the OWL5_COMPAT option before starting to use DDT. If you do decide to use OWL5_COMPAT mode and DDT together, you need to be keenly aware of the calling sequence issue.
The MFC DDX framework [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 example transfer function above may be translated as follows:
void CSearchDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX); // Must call base.
int up = (arguments.Flags & arguments.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) ? arguments.SearchUp : 0;
}
We could further abstract the data transfer between the check-box and the search flags by introducing an overload for 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, arguments.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) framework in OWLNext is similar in design 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 and a reference to be passed at the construction of the dialog. This reference is passed to the transfer function DoTransferData in a type-safe manner at data transfer events. Transfer between the dialog and a data source of the declared type can also be done at any time using the GetData and SetData functions. The control data transfer functions are similar to DDX, but use slightly different and more consistent naming, e.g. TransferEditData and TransferCheckBoxData, and we do not have to call the base class implementation of our overridden transfer function.
Here is a DDT version of our example dialog code above:
class TSearchDialog
: public TTransferDialog<TSearchArguments>
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TTransferDialog<TSearchArguments>(parent, IDD_SEARCH_DIALOG, a) {}
protected:
virtual void DoTransferData(const TTransferInfo& i, TSearchArguments& a)
{
TransferEditData(i, IDC_SEARCH_STRING, a.SearchString);
TransferFlag(i, IDC_SEARCH_UP, a.Flags, a.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 declare the DoTransferData 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.
The implementation of the DoTransferData function here uses two helper functions. TransferEditData is provided by DDT. TransferFlag is provided by the application. The latter transfers the checked state of a checkbox to a corresponding bit in a flags variable.
DDT passes information to the transfer functions containing details about the transfer, such as the parent window handle and the direction of transfer. This information is passed as a reference to a TTransferInfo object, which 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 one of three possible values; tdGetData, tdSetData 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 notable issue with the single-transfer-function design is that it is fundamentally not const-correct. While getting data out of the dialog should keep the dialog constant, and setting the dialog data should leave the data source intact, a single transfer function needs non-const access to fulfill both roles. It is therefore the responsibility of the implementation of DoTransferData to adhere to the logical const'ness of the dialog and data source while getting and setting data respectively. A failure to do so is a violation of the design, since the GetData and SetData functions depend on the compliance to this rule for their own const-correctness.
Note that DoTransferData is a protected member. The public interface to the transfer mechanism is GetData, SetData and TransferData, while DoGetData, DoSetData and DoTransferData are implementation hooks. The default implementation of DoTransferData forwards the work for tdGetData and tdSetData to DoGetData and DoSetData respectively. The default implementations of these are empty.
If you prefer, you can handle the transfer in each direction separately by overriding DoGetData and DoSetData. Although this could of course be done manually with a few lines of user code, the TTransferWindow base has already arranged the split. It is up to you which functions you want to override. A notable benefit is that DoGetData and DoSetData are const-correct. Here is the example above with DoGetData and DoSetData overrides:
class TSearchDialog
: public TTransferDialog<TSearchArguments>
{
public:
TSearchDialog(TWindow* parent, TSearchArguments& a)
: TTransferDialog<TSearchArguments>(parent, IDD_SEARCH_DIALOG, a) {}
protected:
virtual void DoGetData(const TTransferInfo&, TSearchArguments& a) const
{
a.SearchString = GetDlgItemText(IDC_SEARCH_STRING);
a.Flags = IsChecked(*this, IDC_SEARCH_UP) ? a.SearchUp : 0;
}
virtual void DoSetData(const TTransferInfo&, const TSearchArguments& a)
{
SetDlgItemText(IDC_SEARCH_STRING, a.SearchString);
CheckDlgButton(IDC_SEARCH_UP, a.Flags & a.SearchUp);
}
};
Overriding the DoGetData and DoSetData functions is most useful in situations where one or both need very distinct handling. Normally that should be rare. Most often you will find that the distinct code belongs in other functions that override the setup or the shut-down of the dialog.
Like DDX, DDT offers comprehensive support for control data transfer with a lot of flexibility. You can use control encapsulation classes, or you can simply refer to a control by its handle or 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 has numerous overloads for different data source types; e.g. TransferDlgItemText has a template overload for converting between text and any type with stream operators (lexical casting). This makes it very easy to deal with numerical input fields.
Every transfer function has overloads that allow you to identify the control by child ID, window handle or control encapsulation class reference, as well as overloads for getters and setters. The basic signature of a transfer function can be described by the following function template:
template <class TChildId, class TDataSource>
void 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 child ID, a HWND handle or a reference to a control encapsulation class; e.g. TEdit&. The third argument is a reference to the data source; e.g. tstring& for a text source. The functions may have further parameters as needed, but the first three should always adhere to this template.
OWLNext 6.34 introduced further support for transferring textual, and in particular numerical data. The functions TransferDlgItemText, TransferEditData and TransferStaticData now have additional overloads that accept an extra argument of the new type TTransferFormat. This parameter package type aggregates stream formatting parameters equivalent to std::ios_base::flags, precision, width and std::basic_ios::fill.
For example, to display floating point coordinates with two decimals:
virtual void DoTransferData(const TTransferInfo& i, TPointF& p) // override
{
TTransferFormat fmt = {ios::fixed, 2}; // Show two decimals.
TransferEditData(i, IDC_X, p.x, fmt);
TransferEditData(i, IDC_Y, p.y, fmt);
}
Often the data source that we want to use is an encapsulated object with getter and setter functions. In this case the ordinary Transfer functions are less convenient, because we have to use an intermediate variable to pass as an argument. Also, we have to test the TTransferInfo argument to see if we are setting or getting. While we can use the separate DoGetData and DoSetData to avoid this latter inconvenience, life would be much easier if we could just tell the Transfer function the getter and setter to use. DDT provides overloads to do just that.
template <class TChildId, class T, class R, class A>
void TransferControlData(const TTransferInfo&, TChildId, T*,
R (T::*get)(),
void (T::*set)(A));
Instead of the data source reference in the basic signature, this signature takes pointers to an object and two member functions; a getter and a setter. The Transfer implementation deduces the type of data to be transferred based on the return type of the getter. It then creates an intermediary transfer variable and calls the basic Transfer overloads.
Usage of this indirect overload is very simple when the data source supports the getter and setter needed, i.e. the data source has members with compatible signatures and a data type compatible with the control. For example:
virtual void DoTransferData(const TTransferInfo& i, TSearchArguments& a)
{
typedef TSearchArguments D;
TransferEditData(i, IDC_SEARCH_STRING, &a, &D::GetSearchString, &D::SetSearchString);
TransferCheckBoxData(i, IDC_SEARCH_UP, &a, &D::IsSearchUp, &D::SetSearchUp);
}
Unfortunately, life is not always this easy. In practice you may have to convert the data from the control to some application-specific value. Or, the getter and setter may require extra parameters. In this case, there are no suitable data source member functions to use as a direct getter and setter for the control value. It may be tempting to add such member functions to the data source, but that creates undesirable coupling between the UI and the application logic. A better solution is to define and use proxy classes that translate between control and application values. For example:
struct TOptionProxy
{
TSearchArguments& a;
TSearchArguments::TFlag flag;
bool Get() const {return a.GetOption(flag);}
void Set(bool s) {a.SetOption(flag, s);}
};
virtual void DoTransferData(const TTransferInfo& i, TSearchArguments& a)
{
typedef TSearchArguments D;
TransferEditData(i, IDC_SEARCH_STRING, &a, &D::GetSearchString, &D::SetSearchString);
TOptionProxy searchUp = {a, a.SearchUp};
TransferCheckBoxData(i, IDC_SEARCH_UP, &searchUp, &TOptionProxy::Get, &TOptionProxy::Set);
}
DDT also offers transfer function overloads that take getter and setter functors that are fully generic. You can pass functors instantiated from manual functor classes, composed using std::bind (C++11), or written inline using lambdas (C++11). This makes it possible to write the data transfer code in a compact functional style which avoids having to define proxy classes. When there is little potential reuse of a proxy class you may prefer this alternative. For example:
TransferCheckBoxData(i, IDC_SEARCH_UP,
bind(&D::GetOption, &a, a.SearchUp),
bind(&D::SetOption, &a, a.SearchUp, _1)
);
With lambdas the same call would look like this:
TransferCheckBoxData(i, IDC_SEARCH_UP,
[&] {return a.GetOption(a.SearchUp);},
[&] (bool s) {a.SetOption(a.SearchUp, s);}
);
In addition to the Transfer functions, DDT also provides a set of functions with the same names and functionality as those provided by DDX. This makes it easier to port code from MFC programs. These functions just forward the call to their Transfer counterpart.
DDX provides a function called DDX_Control that, despite appearances, isn't a transfer function at all. Its purpose is not to transfer data but 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 since version 6.32 OWLNext now supports aliasing for most native controls as well. For example:
TCheckBox(GetDlgItem(IDC_SEARCH_UP)).Toggle();
The transfer functions are designed to be used in the implementation of DoTransferData, and they are less useful elsewhere. If you choose to split the transfer into DoGetData and DoSetData it is more natural to use control accessor and mutator functions. Also, operations such as enabling, disabling, showing and hiding controls during dialog setup, or in response to changes in dialog state, are common and require accessors and mutators. DDT offers convenience functions for these cases:
Several useful overloads are provided, such as overloads for window handle, control class instance, and integer child ID. In addition, many of these functions have overloads for sequences and arrays of child IDs. This is especially useful for enabling and disabling controls in response to dialog state changes. For example:
void BnCreditcardClicked()
{
bool s = IsChecked(IDC_CREDITCARD);
const int c[] = {IDC_VISA, IDC_MASTERCARD, IDC_AMERICAN_EXPRESS};
EnableDlgItem(c, s);
}
The major distinguishing feature of DDT compared to DDX is that the data source is passed to the DoTransferData 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 SetData and retrieve the current settings of the dialog by calling GetData.
The GetData and SetData 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(GetData()); // Save preset.
else if (s > 0)
SetData(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 GetData without an argument to retrieve the settings. This is simply a functional style overload of GetData that creates and returns an instance of the data source, initialized 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 GetData is not available, and 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 framework 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 (C++11). Often, for simple dialogs, the transfer work is the only customization 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& a)
{
TransferEditData(i, IDC_SEARCH_STRING, a.SearchString);
TransferFlag(i, IDC_SEARCH_UP, a.Flags, a.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);
}
The function bind (introduced in C++ TR1, and standardized in C++11 [3]) creates function objects on the fly, and the TTransferFunction class can encapsulate any free function, member function, functor and lambda function. In C++11 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);
TransferFlag(i, IDC_SEARCH_UP, a.Flags, a.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.
DDT currently relies on the established validation mechanism in OWL, which is based on the dialog overriding TWindow::CanClose and returning 'true' only if the dialog data is valid.
This mechanism is activated for dialogs derived from TDialog and tabbed property sheets. For property sheets in wizard mode, you have to handle data validation yourself (see below).
If an exception is thrown by DoTransferData, DoGetData, DoSetData or CanClose, the dialog will be forcefully closed and the exception will be rethrown. Unless you want this behaviour, you have to carefully handle all exceptions within your code.
This mechanism is activated for dialogs derived from TDialog and tabbed property sheets. For property sheets in wizard mode, you have to handle exceptions yourself (see below).
For dialogs derived from TDialog, TransferData is automatically called on both dialog setup and commit (OK). In a property sheet with tabbed pages, TransferData is called for a page at page setup (creation) and in response to the Apply event, usually when the whole sheet is closed by the OK button.
Sometimes you may need to update a page whenever it is activated, not only at setup, i.e. the first time it is shown. In this case you may call TransferData (tdSetData) in the SetActive handler.
Note that pages not shown may not be created at all, and hence not participate in data transfer.
Also note that TransferData (tdGetData) is not called at all for a property sheet in wizard mode.
While TransferData (tdSetData) is called for each page at setup, as in a tabbed property sheet, in a wizard you will have to decide when to call TransferData (tdGetData) and handle it yourself.
You can call TransferData (tdGetData) in the KillActive handler, i.e. whenever a user leaves a page by navigating forward or back. Conveniently, KillActive is not called when the user cancels the wizard. Alternatively, you can call TransferData (tdGetData) in response to WizNext and WizBack as you see fit. Or, often the best option, you can delay the call to TransferData (tdGetData) until the whole wizard is completed, i.e in the WizFinish handler, and then call it for each page. This is analogous to how Apply is called for each page when a tabbed property sheet is committed.
Also, since there is no commit handling, the normal data validation mechanism is not activated for a wizard, and CanClose will not be called unless you manually do so. One way of getting the normal behaviour for free is to call Apply which internally calls CanClose before calling TransferData (tdGetData). Then refuse leaving the page or closing the wizard if Apply returns a negative result. Alternatively, you can call CanClose manually at the appropriate places in your code.
Note that unhandled exceptions in a wizard may lock up the application. Hence you should carefully handle all exceptions within your code. If you want the standard exception handling for TDialog derivatives and tabbed property sheets, you can get this for free by calling Apply instead of directly calling TransferData (tdGetData).
Most sheets and wizards should not commit data until the whole sheet or wizard is committed by the user pressing OK or Finish. This means that you should think carefully about the data source you use for the pages, when to call TransferData (tdGetData), and when to commit data to the application. The most appropriate solution might be to use a specially designed class to hold the complete sheet or wizard state. This class can then serve as a data source for DDT, storing dialog settings (persistence), presets handling, etc.
The original OWL transfer buffer mechanism for dialog data transfer has many problems. OWLNext has since added checks and extensions 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. Existing code that so far has preferred the manual approach may also benefit from a transition to DDT.
There are two examples in OWLNext accompanying this article:
Ready-made projects for C++Builder and Visual Studio are provided for both examples.
Bugs: #286
Discussion: New OWL app using Embarcadero Berlin 10.1
Discussion: Can I replace pDialog->Execuite with pDialog->Create + pDialog->DoExecute() ?
Discussion: OWL 6 SafeTransferBuffer
Discussion: OWL 6 SafeTransferBuffer
Discussion: Using C++ Builder VCL Style
Discussion: Experimenting OWL6.30 to OWL6.42
Discussion: Ted Neward's book Advanced OWL 5.0
Feature Requests: #201
Wiki: Examples
Wiki: Features
Wiki: Frequently_Asked_Questions
Wiki: History
Wiki: Knowledge_Base
Wiki: Safe_Transfer_Buffers
Wiki: Upgrading_from_OWL
Wiki: Vidar_Hasfjord
DDT in property sheets and wizards
[[User:Vattila|Vattila]] 08:26, 27 June 2011 (UTC)
While working on the OWLMaker wizard DDT conversion (Revision 911), a couple of interesting issues came up.
The TPropertySheet::Apply implementation calls TransferData(tdGetData), so the DDT call sequence is usually fully automatic, and the user only has to override Transfer. But, in wizard mode, Apply is not used, so unless handled manually, TransferData(tdGetData) never gets called. In OWLMaker I handled this in WizNext or KillActive overrides, whatever seemed most appropriate for the page in question. Should we implement default handling in TPropertySheet, and if so, how?
This raises a broader design question; when should pages in a wizard be committed? This may of course depend on the application, but I think it is useful to categorise the use cases. I can see only two distinctly different scenarios:
The first scenario is probably the most common one, so it seems useful to support it in DDT by calling TransferData automatically for all the pages in WizFinish. This would encourage wizard implementations with simple and predictable behaviour for the best user experience.
OWLMaker is clearly in the first category, but the current implementation is inconsistent. For example, if you go through part of the wizard and then regret your changes and press Cancel, changes may already have been committed.
[[User:Jogybl|Jogybl]] 14:30, 28 June 2011 (UTC)
Actually there is another category: choices on a page can affect the content (and even the availability or order) of the next pages. Changes are not irreversible, but need to be transferred to the controlling logic in the code. In OWLMaker the choices made on the first two pages - library location (version) affects the available compilers, and then the selected compiler affects the available options on the Libraries page.
[[User:Vattila|Vattila]] 18:32, 28 June 2011 (UTC)
Those are complicating factors, but I do not consider them in a separate category, since these factors shouldn't change the commit characteristics. It is just dialog state. Think about it like this: Imagine the wizard was implemented as a huge single page. Then you would deal with the issues you mention by ordinary enabling, disabling, showing and hiding groups of dialog items on that page. The problem in a property sheet or wizard is that this requires communication between pages. The usual advice is to keep the cross-page dialog state in the sheet/wizard and let the pages communicate through that. I read a couple of articles describing how to do this in DDX by redirecting the transfer of a page to the sheet as whole; i.e. keep all the DDX dialog state members in the sheet instead of the pages.
If we apply this to OWLMaker, we ideally should have an intermediary object between OWLMaker and each page; i.e. the sheet or some OWLMakerWizard object. This object should keep track of the state of the wizard and commit to OWLMaker only on WizFinish. That said, I don't consider this important; except as a test case for any eventual wizard support in DDT.
Getters and setters
[[User:Vattila|Vattila]] 09:41, 26 June 2011 (UTC)
Limitations of the basic DDX-like design became apparent when working on converting OWLMaker to DDT. While the RCConvertDlg and similar simple parameter collection dialogs fitted well with the variable transfer idiom, the OWLMaker wizard pages use getters and setters to communicate with some application-specific object. The DDX design does not work so well in this scenario. You need to split up the transfer into a set and get clause, and the convenience of a single Transfer function mostly vanishes.
The new Transfer* overloads (Revision 909) for functors and member functions address this short-coming. For example, instead of passing a variable as the data source, you can now pass an object pointer and two member functions; a getter and a setter:
The Transfer implementation deduces the type of data to be transferred based on the return type of the getter. It then creates an intermediary transfer variable and calls the basic Transfer* overloads.
You can also pass two general functors, e.g. instantiated from manual functor classes, composed by using the std::tr1::bind function, or written inline using C++0x.
Check out the revised code in the repository as well as the usage in OWLMaker. I plan to update the [[Dialog Data Transfer|main page]] with description and examples of the new features soon.