Download Latest Version DDDemo120205.zip (792.9 kB)
Email in envelope

Get an email when there's a new version of Dynamic Dialog Demo

Home
Name Modified Size InfoDownloads / Week
dyndlgdemo 2008-01-01
ddreadme.txt 2012-02-12 9.6 kB
DDDemo120205.zip 2012-02-05 792.9 kB
Totals: 3 Items   802.5 kB 1
Dynamic Dialogs (DD) is a domain specific language (DSL) for programming UI dialogs with dynamically changing content. To illustrate the concept, I will start from the prior state of the art, as illustrated in WIN32 and MFC. Newer systems are different, but not too different.

In WIN32 there is a Resource Compiler that implements a DSL for dialogs (among other things). In it, a dialog definition looks something like this:

IDD1_MY_DIALOG DIALOG <list of dialog properties including size>
BEGIN
  STATIC     "Some text", <list of static text properties including position and size>
  EDITTEXT   IDC1_MY_TEXT1, <list of text edit control properties, position and size>
  PUSHBUTTON "Button Label", IDC1_MY_BUTTON1, <list of button properties, position and size>
  etc.
  etc.
END

This is good, but is only part of the solution, because extra code has to be written to
- take action when events happen, such as pressing a button
- move application data in/out of controls, such as a text edit field
- alter visibility of controls
- move controls around for layout

In DD, the DSL is embedded in the native language, and the dialog definition looks more like this:

void deMyDialog(<optional arguments>){
  deDialog(<size arguments>, <other properties>); // all arguments can change their value dynamically at any time
  deStatic(<size arguments>, "Some text");        // the text can change at any time if an expression is used instead of a string constant
  deEditText(<size arguments>, & myStringVariable); // application data binding is direct, requiring no code
  if (deButton(<size arguments>, "Button Label")){ // action code is lexically attached directly to the control
    <actions to be taken when button is pressed>   // that invokes it
    deThrow();                                     // the reason for this line will be explained below
  }
}

Think of this procedure as painting the surface of the dialog, and then re-painting it as often as desired. It can be called many times per second. The first time it is called, it creates the controls. After that, when it is called, it simply adjusts the controls as necessary. When an event such as pressing a button occurs, the routine is called again, and the deButton function returns 1, causing the action to be executed.

There is no need for identifiers, because all the code is in one place. Of course the code can be separated if one wants to, by writing functions, but one is not forced to.

To alter visibility of controls, such as to make the static control and the text edit control depend on some boolean test, they can be embedded in a kind of IF statement, like this:

  IF(<test expression>)
    deStatic(<size arguments>, "Some text");
    deEditText(<size arguments>, & myStringVariable);
  END

What this means is if the test expression is true, the controls appear. If it is false, they do not appear. (Not only that, if the controls do not appear, controls that follow, such as the button, move up to take their place. That's the default behavior and can be overriden.)

What if you need to edit an array of variables, not just one? This can be written:

  FOR(i = 0, i < n, i++)
    <other controls>
    deEditText(<size arguments>, & myStringVariable[i]);
    <other controls>
  END

What this means is the controls inside the FOR statement appear in n copies (where n can dynamically change).

IF statements and FOR statements can be arbitrarily nested, and the overall function can be broken up into multiple sub-functions, if desired.

The net result of this for the programmer is, while it has a learning curve, it shortens the source code by at least an order of magnitude, for all but the most trivial dialogs. This accordingly reduces development time and the opportunity for introducing bugs.

--------------------------------------------------------------------------

It is based on a control structure called differential execution (DE). Routines to be run under DE are prefixed with the initials "de". It must be used with care. DE routines can call non-de routines, but not the reverse.

When a DE routine is run, there is a global mode variable, consisting of two bits, and the values are:

  0 (The program actually spends no time in mode 0.)
  1 ERASE
  2 SHOW
  3 UPDATE

This mode controls how each statement behaves. In addition, there are two sequential data files, one being read from and the other being written to, in much the same manner as the serialization mechanism in MFC. Between execution passes, the file which was written on the prior pass becomes the one which is read in the new pass. The mode variable controls the reading and writing like this:

  1 Read. Each statement reads the values it wrote last time.
  2 Write. Each statement writes the values it will read next time.
  3 Read+Write. Each statement both reads and writes. Since prior state info is present, it can detect changes and move or update controls as needed.

The files are actually just byte arrays in memory, so the cost of reading and writing is minimal.

The main intellectual challenge is in the way the IF and other control statements are handled. IF is the simplest case. The first thing it does is to obey the mode and read and/or write the value of its test expression. Then break down by cases. If the mode is:

  1 ERASE  It executes the body according to the prior value of the test expression.
  2 SHOW   It executes the body according to the current value of the test expression.
  3 UPDATE If the test expression has not changed, it executes the body, or not, according to that value.
      But, if the test expression was false and is now true, it executes the body in SHOW mode.
           If the test expression was true and is now false, it executes the body in ERASE mode.

It may take some thinking to understand that this does the right thing. (There is a proof.) When that is understood, the extension to FOR, WHILE, ELSE, ELIF, and SWITCH statements is straightforward.

There is some more complication to this. One rule that must be followed carefully is the "ERASE-mode rule". ERASE mode exists only for the purpose of removing controls from the dialog, typically because the data edited by those controls has ceased to exist, so it is essential not to refer to it. So basically, any "normal" computation, such as incrementing variables, indexing arrays, following pointers, calling non-DE functions, must be protected from being executed when the mode is ERASE. In ERASE mode, it is best to think of current data and arguments as not even existing.

Another complication (of which the programmer need not really be aware) is the handling of events like clicking a button or typing a character into a text edit control. There is another global variable, a boolean, named "bCommandMode", which is normally false. When an event occurs such as clicking a button, the Windows ID of that button is saved, and then the DE procedure is executed in UPDATE mode, but with bCommandMode set to true. This alters behavior in the following way:

  - All writing is suppressed, and no controls are created, altered, or destroyed, regardless of the mode.
  - When control gets to the deButton function, the ID is checked. If the ID is correct for that button, deButton returns true, causing the action code to be executed.
  - When the action code completes, it must throw an exception which is caught by the top-level routine that called the DE function. That is because the action may have invalidated the data structure that the rest of the DE function may be depending on being there.

In this way, the execution in command mode has no effect on the visible contents of the dialog, and the old file to be read from will simply be read again on the next pass. At the same time, if something has altered the application data structure since the last pass, the command-mode pass will handle the change smoothly the same way a normal UPDATE pass handles it.

------------------------------------------------

Properties of DD:

Performance: Other than calling non-DE functions, the main cost of each UPDATE pass is in reading and writing values and comparing. These are simple operations, and the number of them to be done is proportional to the number of UI controls currently visible, which is typically less than 1000.

Storage: The sequential file contains mainly the arguments of the currently visible controls, so it is not too large and is easily held in memory.

Number of controls: Typical UIs done with DD are highly conditional, containing the equivalent of numerous nested "property pages". Under the current paradigm, this would imply possibly many thousands of controls in existence, of which only a small number is visible at any one time. Under DD, controls are not made invisible. Rather they are simply created and destroyed on demand, so the number of them that could potentially be displayed is unlimited. Of course, to obviate the cost of creating controls, used ones can easily be kept in pools and re-used.

Identifiers: A good part of the work of the present paradigm of UI programming is the need to invent unique ID numbers or names for all the controls, and keep track of them in the code. In DD, each control has an identifier, but it is automatically generated, and the programmer never has to be aware of it. This allows a given dialog to contain large numbers of controls, in linear or two-dimensional arrays, if desired.
Source: ddreadme.txt, updated 2012-02-12