What are Dynamic Forms?
Dynamic Forms is a new way to create forms based on a "markup syntax" similar to HTML and XAML, in which you specify both the layout and ControlSources for the various controls to be displayed on the form.
The result will be a dynamically constructed form, specified in code, rather than manually creating it in the Form Designer.
The source code for this class is a single PRG file which contains the class definitions needed to create Dynamic Forms in your apps, and also includes as a ready-to-run sample to display a rendered form sample.
The procedural code at the top of the source code PRG serves as an example of how to use the DynamicForm class at run time to dynamically render a live FoxPro form to view and edit properties on an object. As listed below, you can also bind to Fields on
Cursors/Aliases/Tables/Views as well as defined Private or Public variables.
Binding Form Controls to Data
Markup Syntax – Designing your form layout
Header, Body, and Footer areas on the Form
Return Values from the form
Restoring data changes made by the user
Form Properties and Methods
Additional Markup support for setting Form and Render Engine properties
Subclassing DynamicForm and DynamicFormRenderEngine
Render Engine - Class Properties
Working with Business Objects
The following form was generated strictly from markup syntax using the DynamicForm class:
Here is the markup syntax that was used to generate the above sample form. It is only a partial display of all the capabilities that the markup syntax brings to form declarative form layout.
(See the complete markup guide for full details)
The source code PRG contains a ready-to-run code sample that follows these 7 steps. Please review it.
The basic flow goes like this:
Step 1: Working with Cursors - Open and arrange the cursor(s) that you want to display on the form. You can reference more than one Cursor/Alias/Table/View in your layout. Be sure to locate the
record pointer(s) to the correct record(s) before launching the form.
Working with an Object – Create or populate an object with various properties (aka oDataObject).
Working with Variables – Declare and define variables as Public or Private variables to bind the form control to.
Note: You can mix-and-match any of the above when building your form.
Step 2: Define your layout markup in a text string (see markup syntax below).
Step 3: Create an instance of the DynamicForm class:
loForm = CreateObject('DynamicForm')
Set the cBodyMarkup property with your markup syntax string from step 2:
loForm.cBodyMarkup = <your_markup_string>
For cursors, set loForm.cAlias to the alias to which your layout will bind.
For objects, set loForm.oDataObject to the object reference to which you layout will bind.
For Public/Private, no additional settings are required. Just use the variable name in ControlSource field of the layout.
Step 4: Set any other options/properties on loForm and/or loForm.oRenderEngine.
Learn more about the main Form layout and properties here.
Learn more about the RenderEngine Class Properties here.
Step 5: Call
loForm.Render() to create the controls on the Form:
llResult = loForm.Render()
Note: Render() is an optional step which can give you a return value to test if everything rendered okay. You can skip this step and go straight to the Show() method per step 6, if you do not wish to take this preliminary step in the process
The return value indicates if everything rendered without errors. (Any errors usually caused by errors in your cBodyMarkup markup string.)
If the Render() methods returns .F., this means there were rendering errors, which you can read from loForm.oRenderEngine.nErrorCount and get details on every error from the
loForm.oRenderEngine.oErrors collection, or the error list in a string format by calling
Learn more about rendering errors here.
Even with rendering errors, the form can still be displayed to the users (as shown in step 6) if desired. During development, showing the form (even with errors) will help you see where the errors occurred, as it will show a red error container in place
of any controls which had rendering errors.
Step 6: Call
loForm.Show() to display the form to the user:
By default, a Modal form is created, but this can be a Modeless form by passing a value of zero as the first parameter to Show().
At this point, the user is interacting with the form, and it will eventually be closed when they click Save, Cancel, or the [X] button. At that time, when using a Modal form, flow will return to the calling code where you can read any property on loForm and
loForm.oRenderEngine, and even access the rendered controls, and continue processing per step 7. For details about Modeless forms, see the Modeless Forms section under the Show() method documentation link below.
Learn more details and options for the the
Step 7: When using the default Modal form mode, when the form is closed by the user, you can test to see which button was clicked to close the form:
By default, the return values in
'Save' if the Save Button was clicked.
'Cancel' if the Cancel Button was clicked.
'' (empty) for [X] button.
Alternatively, you can test if the Save or Cancel button was clicked by testing these properties:
If the Cancel button is clicked, of if the form was closed with [X], then all changes to the object properties, cursors, and variables referenced are restored to their original values.
Need to validate user input? Learn more about
restoring data values here.
Learn more about the
Save and Cancel buttons here.
Learn more about Custom Return Values here.
Need to use Modeless forms? See the Modeless Forms section the
Show() method here.
Binding form controls to data
Besides visual layout, the markup syntax (shown in the next section) first allows you to specify the ControlSource for each control, linking it to table fields, object properties, or Public/Private variables that you want to display and bind to in your dynamically
Typical data sources are the same as what you would use in the Form Designer or at run time in your current apps, which can include:
- All or selected fields from one or more Cursor/Alias/Table/View
- All or selected properties on an object. (I.e. an oDataObject from a table row, or other populated object)
- Private or Public variables
- Any combination of the above
Binding to Aliases
For aliases, set loForm.cAlias to the alias to which your layout fields will bind, or include the alias name in the ControlSource for each field.
If you are working with multiple aliases, you will need to include the target alias names as part of the ControlSource setting in the markup.
Binding to Objects
For objects, set loForm.oDataObject to the object reference to which your layout fields will bind.
Binding to variables
For Public/Private, no additional settings are required. Just use the variable name in ControlSource field of the layout.
The form layout is driven by a custom "markup syntax” which is defined in a text string. The markup syntax contains the list of properties/fields/variables that you want to display and bind to in the generated form. It is similar to HTML markup.
Once you have defined your form layout, assign it to the cBodyMarkup property on the oRenderEngine.
The syntax for the layout markup string is:
[controlsource] .attribute1 = value1 .attribute2 = value2 | ...repeat for next field ... |
- [controlsource] is optional, but is used when you want to bind a control to a data value.
- [controlsource] can contain an explicit alias reference. i.e. MyAlias.some_field. See
- [controlsource] is not surrounded by any ' or " or [ ], it's just the field, property, or variable name.
- Each attribute corresponds to a native FoxPro property on the UI control, or a custom attribute related to controlling the flow of UI controls on the rendered form.
- Numeric and Boolean attribute values may be surrounded with single quotes or double quotes, if desired.
- Expressions can be used in attribute values if they are surrounded in parentheses, and these will be evaluated at run time to derive the value.
- There are several special attributes to adjust the flow or layout of controls on the rendered form. See
special attributes section for details.
Executing code from markup during the render cycle:
- You can also execute single lines of FoxPro code at any point during the rendering of the fields in the markup string. Simply include the code line(s) wrapped in parentheses and separated by the Pipe character delimiter, just like all other field definitions.
- Commas are optional between each name/value pair in the markup string.
- Notice that '|' is the default delimiter between each field definition in the cBodyMarkup string.
After you define the layout markup string, assign it to the
cBodyMarkup property on the form:
loForm.cBodyMarkup = lcMyBodyMarkupString
Additional supported feature in the Markup syntax
As of ver 1.4.0 and later, you can also set any property of the Render Engine, Form, or render Container in the markup block. See
this page for more details.
This allows you to entirely configure and drive all properties from the markup, without having to do it through your FoxPro code. A nice feature for anyone who wants to store fully configured form definitions in a lookup table, or those who prefer the markup
technique better than the code technique.
Note: For a complete walkthrough of these markup examples, please watch
Video #1 – Introduction and Demos.
By default, ALL fields on the oDataObject or
cAlias target will be displayed if no string is assigned to
cBodyMarkup. It is the equivalent of:
lcBodyMarkup = "" or lcBodyMarkup = "*"
Specifying selected Properties, Fields, or variables
If you do not want to display ALL fields or properties, then you can specify only the selected ones you want to display:
lcBodyMarkup = "some_field1 | some_field_2 |some_field_3 | some_field_4 | ... |"
Adding styling attributes to the markup
In addition to specifying the controlsources, you can also include styling attributes to control all native properties on the generated control, or create other effects to control the rendered form view:
lcBodyMarkup = "some_field1 .width = 100 .caption = 'Some caption' |...|"
Specifying alias name(s) within ControlSource
A global alias name can be set on oRenderEngine.cAlias, but you can override here to specify other aliases as shown:
lcBodyMarkup = "MyOtherAlias.some_field1 .width = 100 .caption = 'Some caption' | some_field_2 .width = 300 | ... |"
Note, you can specify [controlsource] in the form of an attribute if you find this format to be more clear:
.controlsource = 'field1' .attribute1 = value1 .attribute2 = value2 | ...repeat for next field or control...
or, if you need to specify the Alias name on the controlsource:
.controlsource = 'MyAlias.field1' .attribute1 = value1 .attribute2 = value2 | ...repeat for next field or control...
Once you have defined your markup layout, pass it to the RenderEngine like this:
loForm.cBodyMarkup = "...layout markup text..."
Each markup "attribute" maps to a native FoxPro property by the same name and they are applied just as you would do in FoxPro code or in the Properties window. Every native FoxPro property is supported, as well as custom properties if you are using
a custom class. Please report any issues if you find one that is not support or interpreted properly.
If you reference a custom class in the :class attribute, you can also specify its .classlibrary attribute (only required if the classlibrary or procedure is not already in place via Set ClassLib or Set Procedure).
Additionally, the following special attributes are supported to control layout and flow of the controls:
.column = n
(Optional) Most forms can be rendered in a single “column”, but you can move the flow to the top of the next “column” by specify a column number to generate the control
into. The default column is 1, but the RenderEngine will increment to the next column once the render container reaches the max height specified in the
You can also advance the column location yourself by including this attribute on any one control, and then flow of remaining controls continues in the specified column, and will wrap to the next column if
oRenderEngine.nMaxHeight is reached.
.margin-top = n
Adds additional spacing between the previous row and the current row, so that the current is pushed down from the previous.
'n' is the number of pixels
.margin-bottom = n
Adds additional spacing below the current row (after it's rendered), so that the NEXT row will be pushed further down.
'n' is the number of pixels
.margin-left = n
Adds additional to the left of the control so as to create more space between the previous control or column left edge.
'n' is the number of pixels
.row-increment = n
Instructs the engine to skip down 'n' number of rows (default is 1).
The row height is controlled by the nVerticalSpacing property on the RenderEngine.
'n' is the number of rows to skip down in the render flow.
Tip: To render the current control on the same row as the previous control,
use .row-increment = 0 as shown here:
You can include .render-if = (SomeExpressionOrFunctionCall()) to conditionally render the control.
.set-focus = .t.
This attribute, when used on a field markup, indicates this control is to have focus when form is shown.
You can achieve absolute positioning of any control using the native FoxPro Top and Left properties as attributes:
.top = n
.left = n
Once a control has been rendered using an absolute position, the next control in cBodyMarkup will resume rendering flow from this last render location.
Function calls or in attribute values
You can use function calls for any attribute value, as long as the function returns to correct data type for the targeted property. Wrap the expression in parentheses as shown here:
.caption = (GetCaptionForBlah())
.enabled = (UserIsAdmin())
.tooltiptext = (pcSomePrivateStringVar)
Field labels will be automatically added for each UI controls that is bound to an property, field, or variable.
1. The default Caption for each label is the name of the property/field/variables specified in the control source.
2. Underscore characters in the property/field/variable names will be converted to a space for cleaner caption text.
3. You can override the label caption by using .label.caption = 'Blah' attribute.
4. You can prevent a label from being displayed by setting .label.visible = .f.
5. You can set ANY FoxPro property on the label by using
'label.' a prefix on the attribute:
6. By default, field labels are rendered to the left of the input control (inline with).
.label.Caption = 'My Caption'
.label.FontBold = .t.
.label.Alignment = 1 (Right aligned with input control)
Set loForm.oRenderEngine.lLabelsAbove = .t. to render the labels ABOVE the input controls:
Return values from the form
You can get custom return values from the Dynamic Form using instances of the default “commandbutton” class, which resolves to the custom
DF_ResultButton class by default. Using these command buttons, when the button is clicked, the form will hide and the caption of the command button will be passed to the
loForm.cReturn property (any hotkey characters in the caption will be removed). This allows you to easily test
loForm.cReturn to see which of command button was clicked, and you can take the appropriate action as control passes back to the calling program.
Actually, the caption is passed to the Tag property on the button, and the button click event passes its Tag value back to
loForm.cResult and then hides the form. So, if needed, you can set the Tag value directly if you do not want to use the caption as the return value.
Custom UI control classes
By default, native FoxPro baseclasses (textbox, label, checkbox, etc.) are used on the form. If you wish to specify your own custom classes to use instead, the following properties allow you to change the defaults for all instances on the form:
cLabelClass = 'DF_Label'
cLabelClassLib = ''
cTextboxClass = 'textbox'
cTextboxClassLib = ''
cEditboxClass = 'editbox'
cEditboxCLassLib = ''
cCommandButtonClass = 'DF_ResultButton'
cCommandButtonClassLib = ''
cOptionGroupClass = 'optiongroup'
cOptionGroupClassLib = ''
cCheckboxClass = 'DF_Checkbox'
cCheckboxClassLib = ''
cComboboxClass = 'combobox'
cComboboxClassLib = ''
cListboxClass = 'listbox'
cListboxClassLib = ''
cSpinnerClass = 'spinner'
cSpinnerClassLib = ''
cGridClass = 'grid'
cGridClassLib = ''
cImageClass = 'image'
cImageClassLib = ''
cTimerClass = 'timer'
cTimerClassLib = ''
cPageframeGroupClass = 'pageframe'
cPageframeClassLib = ''
cLineClass = 'line'
cLineClassLib = ''
cShapeClass = 'shape'
cShapeClassLib = ''
cContainerClass = 'container'
cContainerClassLib = ''
cClassLib = '' && General classlib where controls can be found, if not specified above.
You can also specify :class and
:classlibrary inline on each control in the field .
SomeValue .class = 'MyTextBoxClass' .classlibrary = 'MyClassLib.vcx'
A return value from calling loForm.Render() indicates if everything rendered without errors. Any rendering errors are usually caused by errors in your cBodyMarkup markup string. The error messages and rendered form should
help you identify which control(s) had the errors.
If the Render() methods returns .F., this means there were rendering errors, which you can read from the
and you can get details on each error from the oErrors collection:
You can also call the oRenderEngine.GetErrorsAsString() method to get the errors in a string format which can be easier to read than the working with the
oRenderEngine.oErrors collection. You can see a sample of the error test and this method call in the source PRG file (in the sample procedural code at the top of the file).
Even with rendering errors, the form can still be displayed, but the error controls on the form are really intended to be a Developer tool to help you find bugs in your markup. It will show a red error container in place of any controls which had rendering
2012-10-18 = Alpha 1.5.0 released.
2012-10-26 – Alpha 1.4.1 released.
2012-10-23 – Alpha 1.4.0 released.
2012-10-08 – Alpha 1.3.0 released.
2012-09-25 – Alpha 1.2.0 released.
2012-09-18 – Alpha 1.0.0 released.
2012-09-04 – Public alpha release 0.9.0 released on VFPx.
2012-08-31 – Dynamic Forms accepted as an official VFPx Project.
2012-08-24 – Project proposal submitted to VFPx admins.
2012-05-08 – Initial concept class created and the first ever Dynamic Form was generated from a few lines of code.