Introduction
The ASP.NET development platform provides to easily design and publish Web forms. For certain applications however, it is necessary to keep those forms dynamically variable and allow to embed them in a dynamic fashion too. There are scenarios, where the creation of those forms is separated from the application development process.
Such dynamic forms might have the following parts:
Each of these parts has its own requirements in regard to dynamic form management:
Part | Requirement |
Form Design |
- Integration of business objects
- Enforce business rules
- Moderate skills to design forms, as for example lack of object-orientated know how
- Convenient development environment
- No limitations in regard to design and layout possibilities
- Visually appealing and seamlessly integrable in Web solution (look & feel)
- Possibility to use high level controls, such as for example for date selection
- Reuse of forms by allowing to nest them into one another
- Testing and simulation of form data entry
|
Form Deployment |
- Individual deployment cycles, independent from those of the Web application
- Remote installation
|
Data Storage |
- Differing storage media: database, file system, Web-service etc.
- Standardized format for data exchange
|
Form Hosting |
- Access to application data at run-time
- Provision of the available form elements for the form design
- Run-time security: no access to program code and data
- No impact on run-time stability
- Seamless integration of the forms into the layout of the Web application
- Enforcement of licensing issues of third party components
|
Solution Approach
The Application Provider develops the Form Controls library, which serves for designing the Web forms. The library contains elements for structuring the forms as well as the available form fields. The library will be delivered to the Form Designer in combination with a design Web site.
The actual design of the forms will be done in a Web-enabled version of Visual Studio, such as for example, the freely available Visual Web Developer Express Edition. After installation of the Form Controls library into a Visual Studio Toolbox, the form can be designed in an ASP.NET UserControl
. Apart from the provided Form Controls, it is also possible to use the ASP.NET Standard Controls and Validators in the form. Using CSS styles and JavaScript, the designer is free to adapt the form to her individual needs.
Once the design of the form is fixed, it will be deployed. Forms can be copied to the Web application in a conventional way, or might be provided from sources outside the actual Web application itself (FTP, database, etc.). Different mechanisms allow the integration of the forms into the Form Repository.
At run-time, the forms will be loaded dynamically into the Web Application while handling security relevant aspects. Depending on individual needs, run-time controls get invoked, look-ups become initialized, form data gets loaded from Data Storage and run-time variables get expanded. Replacing a placeholder, the UserControl
gets loaded in a Web page and served to the Web Browser user.
After the Web user has entered the form data and confirmed them, the collected form data gets transferred to the Data Storage.
Form Models
There exist two representational models for the Web user form:
The UI model represents each part of a form as a control and is used at form design time as well as at run-time for user data entry. The data model abstracts and contains the field values of the form.
The following overview compares the two models:
Aspect | UI model | Entity model |
Namespace | WebUserForms.Controls | WebUserForms.Data |
Based on | System.Web.UI.Control | IFormEntity |
Form | IUserForm | IForm |
Form header | IUserFormHeader | IFormInfo |
Form fields | IUserFormField
IListField
ILookupField
IExpressionField | IFormField |
Sub-form | IUserForm | IFormGroup |
Sub-form header | IUserFormHeader | - |
Nesting by | UserControl | IFormGroup.Entities |
Form Controls Library
The Form Controls Library implements the UI model of the form. Based on the ASP.NET UserControl
(*.ascx), such a form UserControl
contains the following components:
- 1 form header: collection of form related information
- 0...n form fields: name and value of a single form field
- 0...n sub-forms: sub-forms with the same structure as the main form
- 0...n form commands: control of form specific actions
The form header IUserFormHeader
contains various identification related items such as type, name and version. Additionally, run-time information such as creation and modification times are available. By inheriting from HiddenField
, the implementing UserFormHeader
results in the header data being transferred invisibly in the form.
Form fields implement the IUserFormField
interface and contain field name, value and an editing status AllowEdit
. There exist various specializations of IUserFormField
:
IListField
: Field with a list of values in ListItemColletion
ILookupField
: IListField
field with a look-up name IExpressionField
: Field with an expression
The accompanying project Controls
provides the following form field controls:
Form field control | Represented by control | Implements |
TextBox | TextBox | IUserFormField |
CheckBox | CheckBox | IUserFormField |
CheckBoxList | CheckBoxList | IListField |
RadioButton | RadioButton | IUserFormField |
RadioButtonList | RadioButtonList | IListField |
ListBox | ListBox | IListField |
LookupListBox | ListBox with lookup | ILookupField |
DropDownList | DropDownList | IListField |
LookupDropDownList | DropDownList with lookup | ILookupField |
Calendar | Calendar | IUserFormField |
HiddenVariable | HiddenField | IExpressionField |
VisibleVariable | Label | IExpressionField |
ComboBox | DropDownList Demo of a run-time control | IListField |
LookupComboBox | DropDownList Demo of a run-time control with look-up | ILookupField |
DatePicker | TextBox Demo of a run-time control | IUserFormField |
TimePicker | TextBox Demo of a run-time control | IUserFormField |
The controls HiddenVariable
and VisibleVariable
represent expressions, which are controlled by the Web application. This allows to provide the form with application data such as for example the name of the active user.
Forms can be supplemented with own or third party controls as necessary. Such controls however, can often not be delivered to the form designer, be it for deployment or licensing related issues. In such situations the design will use a placeholder control instead, which will be replaced by the actual own or third party control at run-time. The controls ComboBox
, LookupComboBox
, DatePicker
and TimePicker
demonstrate such placeholder controls.
A form can execute commands through the Button
control if required. For this to work, a control has to implement the IUserFormCommand
interface.
Design Time Support
The design mode of the form provides the designer with helpful information through coloring the form fields according to their state. Visual Studio allows to control the representation of controls at design time through inheriting from ControlDesigner
. The following ControlDesigners
are provided:
Field type | Control Designer | Provides hinting at ... |
IUserFormField | UserFormFieldDesigner |
- Missing field name
- Conflicting field names
|
ILookupField | LookupFieldDesigner |
- Missing field name
- Conflicting field names
- Missing lookup name
|
IExpressionField | ExpressionFieldDesigner |
- Missing field name
- Conflicting field names
- Missing field expression
|
IUserFormCommand | UserFormCommandDesigner |
|
The control UserFormInfoControl
lists all form information, including sub-forms. For consolidation of the form data, the UserFormInfoRenderer
makes use of the UserFormVisitior
, which will be described further down.
The control UserFormXmlInfoControl
renders the XML data of a form.
Side note: to allow debugging a ControlDesigner
, the following steps are required:
- Setting the desired breakpoints
- In the project of the control library below
Properties
> Debugging
> Start external Program
, the path to Visual Studio has to be set: ...\Common7\IDE\DevEnv.exe - If not yet the case, the control library has to be marked as Startup Project.
Project
> Set as StartUp Project
- Start debugging:
Debugging
> Start Debugging
- Open the same solution in the new Visual Studio instance
- Open the form in design mode - Debugger stops at break point
Form Controls Library Deployment
The project Controls
contains some Compilation Symbols (Project
> Properties
> Build
), which allow to determine which controls will be contained in the form controls library.
The Form Controls library has to be installed at the form designer's workstation. The control library can be installed into a Visual Studio Toolbox manually or with a tool such the Visual Studio Toolbox Manager.
For an easy start, the form designer should be provided with a Web site which shows the various aspects of form design. The included project DesignWebSite
shows such a Web site.
Form Design
Once the form controls library has been installed as a toolbox, the form designer can create a new form with the following steps:
- Open design Web site:
File
> Open
> Project/Solution
> DesignWebSite200x.sln
- Insert user control: Solution Explorer > Context menu on folder
UserForms
> New Item
> Web User Control
> Place code in separate file
=off, Name
=MyForm1 - Insert form header: Toolbox > Web User Forms >
UserFormHeader
- Setup form header: Properties > Web User Forms >
Name
=MyFormName, Type
=MyFormType - Insert input field: Toolbox > Web User Forms >
TextBox
- Setup input field: Properties > Web User Forms >
FieldName
=MyFormField, FieldValue
=Hello World - Save changes:
File
> Save All
- Execute Web site:
Debug
> Start Without Debugging
- Modify the value 'Hello World' and press button
Update Form Info
Sub-forms can be integrated by inserting the child UserControl
into the parent UserControl
.
The Web User Forms Properties allow to configure the form elements. Depending on the type of control, differing properties are available:
Form control | Property | Description | Required |
IUserFormHeader | Name | Name of the form | Yes |
| Type | Typing information | Yes |
| Description | From description | No |
| Version | Form version | No |
IUserFormField | FieldName | Name of the field | Yes |
| FieldValue | Default value | No |
IListField | FieldName | Name of the field | Yes |
| FieldValue | Default value | No |
ILookupField | FieldName | Name of the field | Yes |
| LookupName | Name of the lookup | Yes |
| FieldValue | Default value | No |
IExpressionField | FieldName | Name of the field | Yes |
| FieldExpression | Expression for the field | Yes |
| FieldValue | Default value | No |
IUserFormCommand | CommandName | Name of the command | Yes |
| CommandArgument | Command parameter | No |
The examples DatePicker
and TimePicker
demonstrate the usage of those control specific properties.
Form Business Rules
Business rules can be integrated in a variety of ways:
- Control properties as for example
TextBox.MaxLength
- Validators such as for example
RequiredFieldValidator
- JavaScript (see below)
Using any of the CLR programming languages such as C# or VB.NET is not recommended for security reasons. How to suppress the execution of such code in forms at runtime will be shown further below.
The following sample demonstrates the usage of JavaScript in a form:
<script language="javascript">
function initFormField()
{
var field = document.getElementById( "<%# TextBox1.ClientID %>" );
if ( field != null )
{
field.value = Date();
}
}
function showFormField()
{
var field = document.getElementById( " <%# TextBox1.ClientID %>" );
if ( field != null )
{
alert( field.value );
}
}
function addLoadEventHandler( func )
{
var previousHandler = window.onload;
if ( typeof window.onload != "function" )
window.onload = func;
else
window.onload = function()
{
previousHandler();
func();
}
}
addLoadEventHandler( initFormField );
</script>
<button onclick="showFormField()">Show Form Field</button>
<cc1:TextBox ID="TextBox1" runat="server" FieldName="MyField" />
Notice the access to the form control through the binding directive <%#TextBox1.ClientID%>
. This will be resolved to the controls ID
at runtime using Data Binding (see Data-Binding Expression Syntax).
Form Design Guidelines
The following rules should be respected when designing forms:
- Any Web Form Control should provide a meaningful
ID
- No usage of images (with the current implementation)
- Provide size measures in percentages wherever possible (instead of pixels)
- Avoid font and color assignments
- Use CSS styles wherever possible
- Only use input elements of the Form Controls Library
- Don't initiate postbacks
Form Deployment
Forms can be deployed either statically or dynamically. Static deployment consists of simply copying the *.ascx files into the directory of the Web application. The form repository in this case is represented by the file system of the Web application.
With dynamic deployment, the Solution Provider offers an individual way to the Form Designer to store a form into a form repository. Such a repository can be a database, a remote or FTP folder, a Web service or any other storage means.
Forms at Runtime
Usage of forms at application runtime consists of the following phases:
Loading of Form
The class UserFormLoader
allows to load the form from a virtual path. To enforce application security, this ensures the form doesn't contain .NET CLR code. UserFormLoader
uses reflection to determine the methods and fields of the form class, while ignoring those which are automatically generated by ASP.NET. To exclude other specific methods and/or fields, it is possible to provide a customized instance of a UserFormCodeValidator
to the loading process.
In case the form contains .NET CLR code which is not allowed, the loading process will raise a FormSecurityException
to prevent the use of that form. This effectively prevents the use of source code in scripts and embedded blocks:
<script runat="server" language="C#">
public void MyPublicMethod( object sender, EventArgs e )
{
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo( @"C:\" );
}
protected void MyProtectedMethod( object sender, EventArgs e )
{
System.Diagnostics.Process.Start( @"C:\AUTOEXEC.BAT" );
}
private void MyPrivateMethod( object sender, EventArgs e )
{
System.Threading.Thread.CurrentThread.Abort();
}
</script>
<%Response.Write( "Embedded Code generated output." ); %>
The method UserFormLoader.LoadUnsafe()
offers a way around this security measure and allows to load a form without this check. Use at your own risk ...
The forms get loaded from a virtual path, which normally represents a file in the local application directory. By inheriting from the class VirtualUserFormProvider
, forms can be loaded from any other source:
public class MyUserFormProvider : VirtualUserFormProvider
{
public MyUserFormProvider( string basePath )
: base( basePath )
{
}
protected override Stream LoadUserForm( string virtualPath )
{
MemoryStream stream = null;
string formControlData = GetWebFormFromMySource( virtualPath );
if ( !string.IsNullOrEmpty( formControlData ) )
{
stream = new MemoryStream( Encoding.Default.GetBytes( formControlData ) );
}
return stream;
}
}
The included VirtualUserFormFileProvider
allows to load forms from any directory (see example VirtualPage
in the RuntimeWebSite
project).
Form Runtime Controls
In case the form allows runtime controls, the RuntimeControlsAdapter
actually replaces the placeholder controls by the corresponding runtime controls. This process replaces all controls which implement the IPlaceholderControl
interface.
The provided project RuntimeControls
demonstrates the generation of runtime controls in the class ControlsAdapter
. The property DatePicker.DateFormat
demonstrates how to adapt to design time values.
Form Scripting
For forms which access a controls ID
with Javascript at runtime using <%#TextBox1.ClientID%>
, the page has to ensure proper binding by calling Page.DataBind()
:
public partial class ClientScriptFormPage : System.Web.UI.Page
{
protected override void OnLoad( EventArgs e )
{
base.OnLoad( e );
DataBind();
}
}
Lookups
Control of the forms lookups happens with the LookupAdapter
, which uses the following objects:
The LookupFieldCollector
extracts all ILookupFields
from a UserControl
. By implementing an ILookupProvider
, such lookup data can be transferred into the form from any sources:
public partial class LookupsPage: System.Web.UI.Page, ILookupProvider
{
protected override void OnLoad( EventArgs e )
{
UserCotrol userForm = new UserFormLoader( "~/MyUserForm.ascx" ).Load();
LookupAdapter.Apply( this, userForm );
FormPlaceHolder.Controls.Add( userForm );
base.OnLoad( e );
}
ILookupValueCollection ILookupProvider.GetLookup( string lookupName, string formType )
{
LookupValueCollection lookup = null;
switch ( lookupName )
{
case "MaritialStatus":
lookup = new LookupValueCollection( lookupName );
lookup.Add( new LookupValue( "Single" ) );
lookup.Add( new LookupValue( "Married" ) );
lookup.Add( new LookupValue( "Separated" ) );
lookup.Add( new LookupValue( "Divorced" ) );
lookup.Add( new LookupValue( "Widowed" ) );
lookup.Add( new LookupValue( "Engaged" ) );
lookup.Add( new LookupValue( "Annulled" ) );
lookup.Add( new LookupValue( "Cohabitating" ) );
lookup.Add( new LookupValue( "Deceased" ) );
break;
}
return lookup;
}
}
Form Variables
Expressions and variables in a form will be handled by the class VariableAdapter
, using the following objects:
The ExpressionFieldCollector
extracts all IExpressionFields
from a UserControl
. By implementing an IVariableProvider
, such variable replacements can be provided from any source:
public partial class VariablesPage: System.Web.UI.Page, IVariableProvider
{
protected override void OnLoad( EventArgs e )
{
UserCotrol userForm = new UserFormLoader( "~/MyUserForm.ascx" ).Load();
VariableAdapter.ExpandVariables( this, userForm );
FormPlaceHolder.Controls.Add( userForm );
base.OnLoad( e );
}
IVariableSet IVariableProvider.GetVariables( string formType )
{
VariableSet variableSet = new VariableSet();
variableSet.MapContentToVariable( "CurrentDate", DateTime.Now.ToString() );
variableSet.MapContentToVariable( "CurrentUser", "John Doe" );
return variableSet;
}
}
Form Persistence
Form data can be loaded and saved (and thus exchanged) through the entity model. The class UserFormAdapter
provides the necessary bridging between the UI and entity models. The following example demonstrates how to load form data from an XML file into its entity model representation and transfer that into the UI model (and from there back again into the XML file):
public partial class PersistentPage : System.Web.UI.Page
{
protected override void OnLoad( EventArgs e )
{
this.userForm = new UserFormLoader( "~/UserForms/MyUserForm.ascx" ).Load();
FormPlaceHolder.Controls.Add( this.userForm );
if ( !Page.IsPostBack )
{
LoadFormData();
}
base.OnLoad( e );
}
private void LoadFormData()
{
string fileName = MapPath( virtualFileName );
if ( !File.Exists( fileName ) )
{
return;
}
using ( StreamReader streamReader = new StreamReader( fileName ) )
{
IForm form = FormXml.Instance.Load( streamReader );
UserFormAdapter.ApplyForm( this.userForm, form );
}
}
private void SaveFormData()
{
IForm form = UserFormAdapter.ExtractForm( this.userForm );
string fileName = MapPath( virtualFileName );
if ( File.Exists( fileName ) )
{
File.Delete( fileName );
form.MarkUpdated( DateTime.Now, "DemoUser" );
}
else
{
form.FormId = "1";
form.SetCreated( DateTime.Now, "DemoUser" );
}
string directory = new FileInfo( fileName ).DirectoryName;
if ( !Directory.Exists( directory ) )
{
Directory.CreateDirectory( directory );
}
using ( StreamWriter streamWriter = new StreamWriter( fileName ) )
{
FormXml.Instance.Save( form, streamWriter );
UserFormAdapter.ApplyForm( this.userForm, form );
}
}
protected void SaveButton_Click( object sender, EventArgs e )
{
SaveFormData();
}
protected void LoadButton_Click( object sender, EventArgs e )
{
LoadFormData();
}
private UserControl userForm;
private const string virtualFileName = "~/Data/UserFormData.xml";
}
Form Commands
Handling of form commands is achieved through the class UserFormCommandManager
. Aside from providing the event for command execution, the command manager also controls the enabling of the command(s):
public partial class CommandPage : System.Web.UI.Page
{
protected override void OnLoad( EventArgs e )
{
this.userForm = new UserFormLoader( "~/UserForms/MyUserForm.ascx" ).Load();
this.commandManager = new UserFormCommandManager( this.userForm );
this.commandManager.Command += new CommandEventHandler( FormCommand );
FormPlaceHolder.Controls.Add( this.userForm );
UpdateCommands( UserFormAdapter.ExtractForm( this.userForm ) );
base.OnLoad( e );
}
private void UpdateCommands( IForm form )
{
this.commandManager.EnableCommand( commandFormLock, !form.IsLocked );
this.commandManager.EnableCommand( commandFormUnlock, form.IsLocked );
}
private void ChangeFormLock( bool isLocked )
{
IForm form = UserFormAdapter.ExtractForm( this.userForm );
form.IsLocked = isLocked;
UpdateCommands( form );
}
private void FormCommand( object sender, CommandEventArgs e )
{
switch ( e.CommandName.ToLower() )
{
case commandFormLock:
ChangeFormLock( true );
break;
case commandFormUnlock:
ChangeFormLock( false );
break;
}
}
private UserControl userForm;
private UserFormCommandManager commandManager;
private const string commandFormLock = "formlock";
private const string commandFormUnlock = "formunlock";
}
Form Utilities
There exist several helpers to control the editing status:
UserFormVisitor
: Base class for iterating the UI model, based on the Visitor Pattern - Determining the available forms can be accomplished through an
IUserFormProvider
interface implementation (see VirtualUserFormProvider
and VirtualUserFormFileProvider
) - An implementation of the
IFormProvider
interface provides available form data from any data source FieldEditEnabler
: Activates or deactivates the editing mode of all form fields corresponding to the state of the corresponding IUserFormField.AllowEdit
ValidationEnabler
: Activates or deactivates all validators - The base classes
XmlBase
/XmlSchemaBase
used by FormXml
/FormXmlSchema
can be used to serialize arbitrary objects into/from XML
Future Extensions
The component introduced herein provides room for manifold extension possibilities which would go way beyond the scope of this article:
FormImageProvider
: Insertion of images in forms GridViewField
: Table form field - Caching of virtual forms
History
- 19th September, 2012 - v1.1.0.0
- Removed support for Visual Studio 2005
- Enhanced sample application
- ReSharped source code
- 3rd April, 2012
- Added projects and solutions for Visual Studio 2010
- ReSharped source code
- Fixed article formatting
- 21st January, 2009
- Rewritten chapters Introduction and Solution Approach
UserFormCommandManager
: changed binding using the control load/unload events - Refactored
- 18th November, 2008
- Changed article description
- Updated article images
- 7th November, 2008