Wednesday, August 3, 2011

How to Create Dynamic Forms in .NET (WPF, ASP .NET)

Creating forms through .Net code-behind

Why do we need Dynamic Forms

Generating data entry forms in WPF is not difficult. In fact, it is very easy and convenient to generate forms dynamically. Form generation at runtime has three major advantages over forms designed at design time:

1. You can add/remove controls without changing the code.
2. Control names and other properties can be changed without changing the code.
3. Layout of the form can be changed without changing the code.


Let’s take the example of a simple membership subscription form. The following are the initial UI requirements of this form as communicated by the customer:

1. The form will contain a label and a textbox for email address.
2. The form will contain a label and a textbox for password.
3. The form will contain a label and a textbox for first name.
4. The form will contain a label and a textbox for last name.
5. The form will contain a scrollable read-only multi-line textbox displaying the terms of use.
6. The form will contain a checkbox which says Agree to Terms
7. The form will contain a submit button.


This simple form can be designed within minutes by any .NET programmer. However, if tomorrow, the customer comes back and requests an additional text box to capture user’s phone number, the programmer needs to change the code to include these UI elements. If the customer comes back after every one month and requests the same type of change, then the work becomes redundant and a lot of useful time is wasted in adding text or check boxes or other UI elements.

The solution to this problem is to switch from creating static to dynamic forms.


What are Dynamic Forms

A dynamic form is one about which we are not sure how it will look like at runtime. A dynamic form is created in code-behind and in design-view may appear vacant because UI elements are not included in the form by the programmer at design-time.

It is painted dynamically at runtime. The controls to be painted on the form are usually stored in a database. The application simply fetches the list of UI elements to be painted from the database and for each element, creates an appropriate control on the form.

If any change is required in UI, all we need to do is add/remove/update records in the database. The client can be given a simple tool/script to do the changes in the database or the programmer could do so for them. If the programmer is charging for 5 hours to make this change but he is spending only 1 hour due to dynamic form, then it implies that the company is getting free money for additional 4 hours. This is a great strategy to increase revenues.


How to Implement Dynamic Forms

We will implement a sample dynamic form in WPF client application. The following are the assumptions:

Database

The database has the following tables:

a. Table to store form information
Form(FormId, Title, Description)

b. Table to store UI elements properties
Controls(ControlId, Name, DisplayName, Type, IsReadonly, DefaultValue)

c. Table to store values of controls like combo box/drop down list, etc.
ControlValues(ControlId, Value)

d. Table to associate many-to-many relationship of forms and controls
FormControls (FormId, ControlId, DisplayOrder )

Here is the sample data:

Form:
(1, 'Subscription', 'Membership Subscription Form')


Controls:
1, 'txtEmail', 'Email Address', 2, 'N', NULL
2, 'txtPassword', 'Password', 2, 'N', NULL
3, 'txtFName', 'First Name', 2, 'N', NULL
4, 'txtLName', 'Last Name', 2, 'N', NULL
5, 'txtTerms', 'Terms of Use', 2, 'Y', 'Some agreement text'
6, 'chkAgree', 'Agree to Terms', 6, 'N', NULL
7, 'btnSubmit', 'Submit', 10, 'N', NULL


FormControls (FormId, ControlId, DisplayOrder )
1,1,3
1,2,4
1,3,1
1,4,2
1,5,5
1,6,6
1,7,7
So, we defined a form in which the controls will appear in this order:

First Name, Last Name, Email Address, Password, Terms of Use, Agree to Terms, and Submit.

Application

The data access layer is available to read data from the tables. The layer can be written in ADO .NET or LINQ, or some other technology, all we need is a list a methods to perform operations on database tables.

We will use Stack Panels to display controls on the form. A stack panel is basically a container which can contain multiple controls to display either horizontally or vertically. We will display controls in rows, each row will be implemented through a stack panel, so that all the controls contained in one stack panel will appear in one row.

StackPanel1
StackPanel2
StackPanel3
StackPanel4
StackPanel5

StackPanel6
StackPanel7

The following code demonstrates how we are fetching the list of controls through database and how are we painting them dynamically on the form:


public void PaintForm(int FormID)

{

InitializeComponent();



// get form details

Form ThisForm = GetFormDetailsFromDB(FormId);

// fill up static content on the form first

lblHeading.Content = ThisForm.Title;

lblPurpose.Content = ThisForm.Purpose;

lblDescription.Content = ThisForm.Description;



// get the list of controls to paint on this form

var ControlsSet = (List)ThisContext.GetFormControls(FormId);



foreach (var control in ControlsSet)

{

StackPanel sp = new StackPanel();

sp.Orientation = Orientation.Horizontal;

sp.Margin = new Thickness(3);



switch (control.Type)

{ // text box

case 2:

{

TextBlock textBlock = new TextBlock();

textBlock.Text = control.DisplayName + ": ";

textBlock.Width = 80;



TextBox textBox = new TextBox();

textBox.Name = control.Name;

textBox.Width = 150;

this.RegisterControl(textBox.Name, textBox);



if (control.DefaultValue != String.Empty)

textBox.Text = control.DefaultValue;



if (control.IsReadOnly != null)

{

if (control.IsReadOnly.ToUpper() == "Y")

textBox.IsReadOnly = true;

}



sp.Children.Add(textBlock);

sp.Children.Add(textBox);



break;

}



case 3:

{ // combo box

TextBlock textBlock = new TextBlock();

textBlock.Text = control.DisplayName + ": ";

textBlock.Width = 80;



ComboBox comboBox = new ComboBox();

comboBox.Width = 80;

comboBox.Name = control.Name;

this.RegisterControl(comboBox.Name, comboBox);



var values = GetControlValues(control.Id);



foreach (var ThisValue in (IEnumerable)values)

{

comboBox.Items.Add(ThisValue.Value);

}



if (control.DefaultValue != String.Empty)

comboBox.SelectedValue = control.DefaultValue;



if (control.IsReadOnly != null)

{

if (control.IsReadOnly.ToUpper() == "Y")

comboBox.IsEnabled = false;

}



sp.Children.Add(textBlock);

sp.Children.Add(comboBox);



break;

}



case 5:

{ // date picker

TextBlock textBlock = new TextBlock();

textBlock.Text = control.DisplayName + ": ";

textBlock.Width = 80;



DatePicker datePicker = new DatePicker();

datePicker.Name = control.Name;

this.RegisterControl(datePicker.Name , datePicker);



if (control.DefaultValue != String.Empty)

datePicker.SelectedDate = Convert.ToDateTime(control.DefaultValue);



if (control.IsReadOnly != null)

{

if (control.IsReadOnly.ToUpper() == "Y")

datePicker.IsEnabled = false;

}



sp.Children.Add(textBlock);

sp.Children.Add(datePicker);



break;

}



case 6:

{ // check box



CheckBox checkBox = new CheckBox();

checkBox.Name = control.Name;

checkBox.Content = control.DisplayName;

this.RegisterControl(checkBox.Name , checkBox);



if (control.DefaultValue != null)

{

if (control.DefaultValue == "Y")

checkBox.IsChecked = true;

else

checkBox.IsChecked = false;

}



if (control.IsReadOnly != null)

{

if (control.IsReadOnly.ToUpper() == "Y")

checkBox.Enabled = false;

}



sp.Children.Add(checkBox);



break;

}





case 10:

{ // button

Button button = new Button();

button.Name = control.Name;

button.Content = control.DisplayName;

button.Width = 50;



sp.Children.Add(button);



break;

}



default:

{

break;

}

}



FormBase.Children.Add(sp);

}

}



private void RegisterControl(string ControlName, Control ThisControl)

{ // register control because we will need to read the value entered by user on form submission

if (this.FindName(ControlName) != null)

this.UnregisterName(ControlName);

this.RegisterName(ControlName, ThisControl);

}


You must be wondering that how are we determining the order of controls to display in rows(stack panels). Well, the method GetFormDetailsFromDB should do a select query on FormControls table and order by DisplayOrder. In this way, we will simply start painting controls in the same order as that of the record-set we received.

Reading Forms Data Entered by User
 The logic is pretty simple here. Go through all stack panels in the form and search each control by its name. The following code will read the values entered by user in the form. We are storing the values in a hashtable ControlValues for future use.


public void ReadFilledForm(int FormId)
{
Hashtable ControlsValues = new Hashtable();
var ControlsMap = GetFormControls(FormId)

// now save form values
foreach(var control in ControlsMap)
{
foreach (StackPanel sp in FormBase.Children)
{
Control ThisControl = (Control) FindName(control.Name);
if (ThisControl != null)
{
if (ThisControl.GetType().ToString().Equals("System.Windows.Controls.TextBox"))
{
TextBox NewTextBox = (TextBox)ThisControl;
ControlsValues.Add(control.Id, NewTextBox.Text);
}
if (ThisControl.GetType().ToString().Equals("System.Windows.Controls.ComboBox"))
{
ComboBox NewComboBox = (ComboBox)ThisControl;
ControlsValues.Add(control.Id, NewComboBox.SelectedValue);
}
else if (ThisControl.GetType().ToString().Equals("System.Windows.Controls.CheckBox"))
{
CheckBox NewCheckBox = (CheckBox)ThisControl;
if (NewCheckBox.IsChecked == true)
ControlsValues.Add(control.Id, "Y");
else
ControlsValues.Add(control.Id, "N");
}
if (ThisControl.GetType().ToString().Equals("System.Windows.Controls.DatePicker"))
{
DatePicker NewDatePicker = (DatePicker)ThisControl;
ControlsValues.Add(control.Id, NewDatePicker.SelectedDate);
}

break;
}
}
}
}


I hope the above code snippets will help readers in getting acquainted with dynamic forms creation in WPF. The same code can be reused with little changes in ASP .NET web forms.

Cheers!
JS

2 comments:

  1. Thats great. Is there a better way now to do the same. If so can you explain? Also let me know if you can upload the source code for the above

    ReplyDelete
  2. I have the similar requirement, we have to create dynamic form in wpf and open this forms in another web application. How to do this?

    ReplyDelete