![]() |
Web Development »
ASP.NET »
General
Intermediate
License: The Code Project Open License (CPOL)
Web User FormsBy Jani GiannoudisUser driven runtime dynamic ASP.NET Web Forms |
C#, Javascript.NET 2.0, ASP.NET, WebForms, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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 part has its own requirements in regard to dynamic form management:
| Part | Requirement |
| Form Design |
|
| Form Deployment |
|
| Data Storage |
|
| Form Hosting |
|
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 2008 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 runtime, the forms will be loaded dynamically into the Web Application while handling security relevant aspects. Depending on individual needs, runtime controls get invoked, lookups become initialized, form data gets loaded from Data Storage and runtime 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.
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 runtime 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 | IUserFormFieldIListFieldILookupFieldIExpressionField |
IFormField |
| Sub-form | IUserForm |
IFormGroup |
| Sub-form header | IUserFormHeader |
- |
| Nesting by | UserControl |
IFormGroup.Entities |
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:
The form header IUserFormHeader contains various identification related items such as type, name and version. Additionally, runtime 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 lookup 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 |
DropDownListDemo of a runtime control |
IListField |
LookupComboBox |
DropDownListDemo of a runtime control with lookup |
ILookupField |
DatePicker |
TextBoxDemo of a runtime control |
IUserFormField |
TimePicker |
TextBoxDemo of a runtime 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 runtime. 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.
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 |
|
ILookupField |
LookupFieldDesigner |
|
IExpressionField |
ExpressionFieldDesigner |
|
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:
Properties > Debugging > Start external Program, the path to Visual Studio has to be set: ...\Common7\IDE\DevEnv.exe Project > Set as StartUp Project Debugging > Start Debugging 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.
Once the form controls library has been installed as a toolbox, the form designer can create a new form with the following steps:
File > Open > Project/Solution > DesignWebSite200x.sln UserForms > New Item > Web User Control > Place code in separate file=off, Name=MyForm1 UserFormHeader Name=MyFormName, Type=MyFormType TextBox FieldName=MyFormField, FieldValue=Hello World File > Save All Debug > Start Without Debugging 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.
Business rules can be integrated in a variety of ways:
TextBox.MaxLength RequiredFieldValidator 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">
// --- init form field ---
function initFormField()
{
var field = document.getElementById( "<%# TextBox1.ClientID %>" );
if ( field != null )
{
field.value = Date();
}
}
// --- show form field ---
function showFormField()
{
var field = document.getElementById( " <%# TextBox1.ClientID %>" );
if ( field != null )
{
alert( field.value );
}
}
// --- register form loading function ---
function addLoadEventHandler( func )
{
var previousHandler = window.onload;
if ( typeof window.onload != "function" )
window.onload = func;
else
window.onload = function()
{
previousHandler();
func();
}
}
// --- add form init function ---
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).
The following rules should be respected when designing forms:
ID 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.
Usage of forms at application runtime consists of the following phases:
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:\\");
// do something with dir
} // MyPublicMethod
protected void MyProtectedMethod( object sender, EventArgs e )
{
System.Diagnostics.Process.Start( "C:\\AUTOEXEC.BAT" );
} // MyProtectedMethod
private void MyPrivateMethod( object sender, EventArgs e )
{
System.Threading.Thread.CurrentThread.Abort();
} // MyPrivateMethod
</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 )
{
} // MyUserFormProvider
// ----------------------------------------------------------------------
protected override Stream LoadUserForm( string virtualPath )
{
MemoryStream stream = null;
// load Web-form from you source: file system, database, Web-Service...
string formControlData = GetWebFormFromMySource( virtualPath );
if ( !string.IsNullOrEmpty( formControlData ) )
{
stream = new MemoryStream( Encoding.Default.GetBytes( formControlData ) );
}
return stream;
} // LoadUserForm
} // class MyUserFormProvider
The included VirtualUserFormFileProvider allows to load forms from any directory (see example VirtualPage in the RuntimeWebSite project).
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.
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(); // resolve data-binding expressions
} // OnLoad
} // class ClientScriptFormPage
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 )
{
// load form control
UserCotrol userForm = new UserFormLoader( "~/MyUserForm.ascx" ).Load();
// lookup
LookupAdapter.Apply( this, userForm );
FormPlaceHolder.Controls.Add( userForm );
base.OnLoad( e );
} // OnLoad
// ------------------------------------------------------------------------
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;
} // ILookupProvider.GetLookup
} // class LookupsPage
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 )
{
// load form control
UserCotrol userForm = new UserFormLoader( "~/MyUserForm.ascx" ).Load();
// expand variables
VariableAdapter.ExpandVariables( this, userForm );
FormPlaceHolder.Controls.Add( userForm );
base.OnLoad( e );
} // OnLoad
// ------------------------------------------------------------------------
IVariableSet IVariableProvider.GetVariables( string formType )
{
VariableSet variableSet = new VariableSet();
variableSet.MapContentToVariable( "CurrentDate", DateTime.Now.ToString() );
variableSet.MapContentToVariable( "CurrentUser", "John Doe" );
return variableSet;
} // IVariableProvider.GetVariables
} // class VariablesPage
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 )
{
// load form control
this.userForm = new UserFormLoader( "~/UserForms/MyUserForm.ascx" ).Load();
FormPlaceHolder.Controls.Add( this.userForm );
if ( !Page.IsPostBack )
{
LoadFormData();
}
base.OnLoad( e );
} // OnLoad
// ------------------------------------------------------------------------
private void LoadFormData()
{
// file check
string fileName = MapPath( virtualFileName );
if ( !File.Exists( fileName ) )
{
return;
}
// load form data
using ( StreamReader streamReader = new StreamReader( fileName ) )
{
IForm form = FormXml.Instance.Load( streamReader );
UserFormAdapter.ApplyForm( this.userForm, form );
}
} // LoadFormData
// ------------------------------------------------------------------------
private void SaveFormData()
{
IForm form = UserFormAdapter.ExtractForm( this.userForm );
string fileName = MapPath( virtualFileName );
if ( File.Exists( fileName ) ) // update existing form
{
File.Delete( fileName );
form.MarkUpdated( DateTime.Now, "DemoUser" );
}
else // new form
{
form.FormId = "1";
form.SetCreated( DateTime.Now, "DemoUser" );
}
// ensure directory
string directory = new FileInfo( fileName ).DirectoryName;
if ( !Directory.Exists( directory ) )
{
Directory.CreateDirectory( directory );
}
// save data
using ( StreamWriter streamWriter = new StreamWriter( fileName ) )
{
FormXml.Instance.Save( form, streamWriter );
UserFormAdapter.ApplyForm( this.userForm, form );
}
} // SaveFormData
// ------------------------------------------------------------------------
protected void SaveButton_Click( object sender, EventArgs e )
{
SaveFormData();
} // SaveButton_Click
// ------------------------------------------------------------------------
protected void LoadButton_Click( object sender, EventArgs e )
{
LoadFormData();
} // LoadButton_Click
// ------------------------------------------------------------------------
// members
private UserControl userForm;
private const string virtualFileName = "~/Data/UserFormData.xml";
} // class PersistentPage
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 )
{
// load form control
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 );
} // OnLoad
// ------------------------------------------------------------------------
private void UpdateCommands( IForm form )
{
this.commandManager.EnableCommand( commandFormLock, !form.IsLocked );
this.commandManager.EnableCommand( commandFormUnlock, form.IsLocked );
} // UpdateCommands
// ------------------------------------------------------------------------
private void ChangeFormLock( bool isLocked )
{
IForm form = UserFormAdapter.ExtractForm( this.userForm );
form.IsLocked = isLocked;
UpdateCommands( form );
} // ChangeFormLock
// ------------------------------------------------------------------------
private void FormCommand( object sender, CommandEventArgs e )
{
switch ( e.CommandName.ToLower() )
{
case commandFormLock:
ChangeFormLock( true );
break;
case commandFormUnlock:
ChangeFormLock( false );
break;
}
} // FormCommand
// ------------------------------------------------------------------------
// members
private UserControl userForm;
private UserFormCommandManager commandManager;
private const string commandFormLock = "formlock";
private const string commandFormUnlock = "formunlock";
} // class CommandPage
There exist several helpers to control the editing status:
UserFormVisitor: Base class for iterating the UI model, based on the Visitor Pattern IUserFormProvider interface implementation (see VirtualUserFormProvider and VirtualUserFormFileProvider) 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 XmlBase/XmlSchemaBase used by FormXml/FormXmlSchema can be used to serialize arbitrary objects into/from XML 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 UserFormCommandManager: changed binding using the control load/unload events | You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 21 Jan 2009 Editor: Sean Ewington |
Copyright 2008 by Jani Giannoudis Everything else Copyright © CodeProject, 1999-2009 Web18 | Advertise on the Code Project |