Vidar Hasfjord - 2012-01-12

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:

  1. The wizard is just a step-wise alternative to tabbed pages, guiding the user along. There are no irreversible intermediate steps, and moving back and forth is just as valid as for tabbed pages. In this case it would make sense to commit the dialog data only at completion, i.e. when the user presses Finish; equivalent to how Apply is called only when OK is pressed in a tabbed sheet. No changes should have effect outside the wizard until then. A file import wizard is an example of this scenario.
  2. The wizard comprises one or more irreversible intermediate steps implemented at page flips. Usually the processing, i.e. the irreversible step, is done only at the next to last page flip; after which a summary page is shown, e.g. for an installer. The user is either then prohibited from going back, or allowed to go back to repeat the whole process, e.g. for a document scanning wizard.

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:

TransferWindowText (i, IDC_WEIGHT, d, &D::GetWeight, &D::SetWeight);

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.