ASP.NET VERSIONS: 1.x
Form & Function
Building a Dynamic Form Engine
By James Culshaw
Forms can come in many types: application forms, feedback forms, questionnaires, etc. Putting these forms onto the Web makes it easy to collect data and (hopefully) easy for people to find. If your organization is anything like the one where I work you'll be constantly asked to create pages and/or small applications to collect and manage data on the Web. Rather than having a developer do custom development to create these forms every time, it would be much easier to provide a tool to allow end users to create, maintain, and publish their own forms. This article illustrates possible approaches and solutions to create dynamic Web-based forms.
First Things First
The first question that needs to be answered is, "What is a form?" In the most basic sense, a form consists of one or more pages filled with questions and answers. Each page is then separated into one or more sections, with each section containing one or more items. These items can be either a question, or a question grid (such as a timetable with a fixed number of rows and columns).
The most basic form contains a single page, with a single section, and questions requiring textual answers. When laying out a form, we are presented with two major options: using CSS or using tables. There are a lot of arguments against using tables, but in this case I feel that they provide the simplest solution, and forms do lend themselves to columnar layout.
We are presented with three quite obvious techniques for dynamically generating the forms. The first is to use the traditional approach of using code to write out the HTML that is required. The second is to define the form in XML and use XSLT to transform it into HTML. The third, if you are using ASP.NET, is to programmatically create the server controls that are required and attach them to the form.
The first approach will be familiar to a lot of Web developers and can be implemented in many different technologies (ASP, PHP, and JSP, to name but a few). The second approach of using XML and XSLT provides the benefit of separating the presentation from the content and business logic. Both of these approaches are relatively simple to implement, but they do come at a price - when a form has one or more pages, or some functionality that requires round-trips to the server, we are required to implement our own state management. The third approach, ASP.NET, is relatively straightforward to implement and provides its own state management. However, it does have some pitfalls, which I will illustrate.
For all three approaches we need to provide a definition for the form that is to be rendered. As a form is structured, a form definition could be stored as XML or in a relational database. With the second form generation technique, it is obvious that the definition must be presented in XML, but in the first and third techniques it could also be provided in a recordset or custom object model. If you are using SQL Server 2000, you could store the form definition in a relational database and then present the definition as XML using its native XML functionality.
If the form definitions are stored as XML documents, we could store them as text files or in a database as a single text field. Storing the definitions as text files provides simple maintenance, as they can be easily modified using a text editor or specialized XML editor. The problem associated with this method of storage is that it is difficult to provide versioning of the form definitions. By storing the definitions in a database, it is far simpler to implement versioning through the use of standard relational techniques.
If the form definitions are stored in a relational database, not only is versioning easily catered for, but also the answers could be queried, allowing for easy production of statistics. The downside to this approach is that a custom interface/tool for creating and maintaining these form definitions needs to be created. If you wish to create a tool to provide to end-users with little or no technical knowledge or skill, then this is a must anyway.
Having discussed the various approaches to defining and dynamically generating a form, the rest of this article will focus on how to generate forms using my preferred technique: ASP.NET.
Generating Forms Using ASP.NET
As noted earlier, XML provides the simplest way of presenting a parsed form because of its highly structured and hierarchical nature. Figure 1 shows a stripped down example of the structure that the rest of this article will relate to. It's not a neat and tidy definition, but it can be easily generated from SQL Server using FOR XML functionality, allowing the full benefits of the relational storage.
<Section sectionId="1" headingText="String">
<Question questionId="1" questionText="Text"/>
<Question questionId="2" questionText="Text"/>
<Row rowId="1" rowText="String"/>
<Row rowId="2" rowText="String"/>
<Row rowId="3" rowText="String"/>
<Column columnId="1" columnText="String"/>
<Column columnId="2" columnText="String"/>
<Column columnId="3" columnText="String"/>
Figure 1: The stripped down version.
The basis of this technique of generating forms is to dynamically create new controls and attach them to the form. Adding new controls to an ASP.NET page is simple because every server control in the .NET Framework has a Controls collection to which other server controls can be added.
The first problem to tackle is how to create the layout of the form. Using stylesheets to lay out the form usually involves using DIV and/or SPAN elements. There is no control that directly maps to these elements within the suite of ASP.NET server controls. We could create our own custom server control, but that requires a different set of skills. The Label control usually emits SPAN elements, and the Panel control DIV elements. These are possible options if this is your preferred layout mechanism, but the extraction of data from the form would be more complex than using the other option available to us.
Using tables to control the layout is a far simpler task in ASP.NET. There is a Table server control that can be created dynamically, as can its rows and cells. The code to create the layout is a simple task of adding a new row whenever we want to add a new question and then adding a cell to contain the question, and then another to contain the control for the answer to be entered into. The Cell control has two properties that are important to us: its Text property and its Controls collection. With the question cell we can set the Text property to the question text, and for the answer cell we create a new TextBox control, setting at least its ID property, and then add it to the answer cell's control collection. This process is illustrated in Figure 2.
Dim questionRow As New TableRow
Dim questionCell As New TableCell
Dim answerCell As New TableCell
questionCell.Text = questionElement.GetAttribute(
Dim answerText As New TextBox
answerText.Columns = 50
answerText.ID = "Q" & questionElement.GetAttribute(
Figure 2: Using tables to control layout is much simpler in ASP.NET.
Each control within a form needs a unique ID, regardless of which technique we use to render the form and it is especially important in ASP.NET, because if an attempt is made to add a control to an ASPX page that has the same ID as a control that is already attached, a run-time error occurs. The simplest way of generating this is to concatenate the question ID value with the letter Q. By doing this it is easier to later determine to which questions the answers are linked.
We do, of course, need a root control in the ASP.NET page to which we can attach our form. According to the SDK, the purpose of the PlaceHolder control is to be a container to store dynamically added server controls on the Web page . This fits our requirements exactly. After the form has been generated and attached to the PlaceHolder control, the form will appear in the browser when the Web page is generated.
It is at this point that we hit our first "gotcha." The controls that are dynamically created are not persisted from one HTTP request to another. To be able to capture the data when the form is submitted we need to regenerate the controls and add them to the page again. This means that we do not wrap the code to generate the form within the usual If Not Page.IsPostBack Then ... End If condition. This is, of course not a problem if using either of the other two techniques.
Extracting the data from the form is the next major task. If the technique was used of generating a form through writing the required HTML directly into the page, or through the use of XML and XSLT, we would have to access the controls via the Form collection of the Response stream. This is a pretty simple task to which most Web developers are accustomed. If ASP.NET is used to create the form through dynamically creating and adding the required controls to the Web page, extracting the data from the form requires a different approach. If we implement the form layout using a table, all we have to do is iterate through all the rows in the table control and then access the text property of the TextBox control that is contained within the Controls collection of the cell that contains it.
However, if we use CSS to lay out the page, we would need to iterate through the Controls collection of the form itself and the nesting of the controls used for layout could make this a far more complex task. Although the use of server controls makes the data extraction a more involved process, it is a minor price to pay compared to the need to create your own state management techniques. You also gain another of the main benefits of server controls - the encapsulation of advanced functionality such as calendar controls. I also believe that the overall process of creating the forms is also far more intuitive and easier to maintain using the server controls than writing HTML snippets to a string.
Relating the answers back to the correct question is the most important task when extracting the data from the form. The simplest solution that offers itself is to use the ID of the control. There are numerous ways that this can be done, but as mentioned earlier, if we create the ID of the control by concatenating the question ID value with the letter Q, it is then the simple task of stripping off the letter Q to determine the question to which the control, and thus the answer, is related. Figure 3 illustrates how simple a task this is in ASP.NET.
Dim answerControl As TextBox
Dim answerText As String
Dim questionId As Integer
For Each sectionRow As TableRow In sectionTable.Rows
questionControl = CType(sectionRow.Cells(1).Controls(0),
answerText = answerControl.Text
questionId = CType(questionControl.ID.Replace(
"Q", ""), Integer)
Figure 3: Create the ID of the control by concatenating the question ID value with the letter Q. Then, simply strip off the letter Q to determine the question to which the control is related.
Expanding the Basics
Now that we have figured out the basic principles of building a form, let's go ahead and expand our form to have more than one page. Using ASP.NET this is simple to achieve using a Panel control, which, according to the SDK, "is especially useful when you want to generate controls programmatically or hide/show a group of controls." Using the other two techniques it is a case of simply writing the HTML representing the page that is required using standard logic. The downside of this is that you need to handle the form state yourself, whereas ASP.NET does that for you.
To implement multiple pages in an ASP.NET application, rather than attaching the control(s) that represent the page to the PlaceHolder control, we attach them to a panel that is then attached to the PlaceHolder control. Showing the correct page for the user to view is then a simple task of setting to False the visibility of the Panel controls of all but the page to show, as can be seen in Figure 4.
Public Sub ShowPage(ByVal pageNumber As Integer)
Dim pageCounter As Integer = 1
For Each pagePanel As Panel In _root.Controls
If pageCounter = pageNumber Then
pagePanel.Visible = True
pagePanel.Visible = False
pageCounter += 1
Figure 4: Set to False the visibility of the Panel controls of all but the page to show.
Not all forms are laid out in the simple question/answer format. Sometimes, such as in a timetable, it is preferable to lay the questions out in a grid. In this format each answer is related to both a row and a column. Because this presentation format is row- and column-based, the obvious choice is to use a table for the layout. In the sample XML shown in Figure 1, the grid is represented by the presence of Row elements as children of a Columns element. The grid requires a table with x+1 columns, where x is the number of column elements. The first row in the table has an empty first cell, and the rest of the cells contain the text for the column to which it relates. A new row is added to the table for every Row element that exists, with the first cell containing the question text for that row. Every other cell in the row then contains the control that is required to capture the answer. As previously mentioned, each control must have a unique ID. A simple mechanism to create a unique ID is to concatenate Q, the question identifier, G, and then the row identifier. Again, by doing this it is easier to later determine to which questions the answers are linked.
If you are using a table to lay out the page, it is apparent that it would not look right to add this to the right-hand column of the containing table. In this case using a single cell that spans both columns for the layout table would be the obvious solution.
If we are using either of the first two generation methods, then the method to extract the data from the form remains the same. If we are using the ASP.NET-specific-based method involving server controls, we need to add some extra processing to handle the presence of this Table control. When using a table for layout it is simply the case of checking to see if the number of cells in a row is 1. If we are using CSS for layout, then a more complex method of handling its presence needs to be utilized.
To provide this information in a simple and reusable fashion, the answers for the entire form can be provided in an XML document. This document can then be persisted in various ways; a file saved to the hard-drive, or parsed and saved to a relational database, illustrate just as few.
The final functionality to provide is a mechanism to populate the newly created Web page with data from a previously saved form. Again, this task is made very simple using ASP.NET. Using the question ID to which the answer relates, the control ID is generated and then the FindControl method on the root control for the Form is used to retrieve a reference to the required control. If any of the other two generation methods are used, the process of populating the form with previously saved data would have to be wrapped up into its generation. In this case the XML and XSLT approach presents the easier of the two options through the ability to pass the answers into the stylesheet via a parameter, and then use XPath statements to access them. When using the dynamic server control generation method in ASP.NET, it is important at this point to note that the root control must already have been attached to the Controls collection of the ASPX page to work. If you forget to attach the Form's root control to the ASPX page before you try to populate the controls, a run-time exception will occur.
With this basic model working, it is a simple task to increase the options available for presenting questions and answers, such as providing dropdown lists of answers from which the end user can select. The main change to the code occurs when extracting the information from a submitted form. In ASP.NET, the control in the container cell's Controls collection would have to have its type interrogated to determine what it was and then handled appropriately. If we used either of the other techniques, there is no way of determining the type of control that was presented to the user, but it is simple enough to determine if more than one answer was selected.
As the inclusion of dynamic forms within multiple applications may be a requirement, it would be useful to create the form "engine" in a reusable manner. ASP.NET provides a simple mechanism to implement the code required to generate these dynamic forms within an Assembly. Using the XML and XSLT approach it is simply a case of using the stylesheets in the various applications. If you are using ASP (or similar technology), you would have to use include files.
When using a reusable "form engine," to allow the page navigation to remain implementation-independent, the list of pages within the form can be presented as a collection of Page objects. These Page objects provide the page's name to be displayed in the navigation. The Form object needs to expose a method to handle the navigation between pages. In this example a single method, ShowPage - which takes the page number for the page to show (which is based on its position in the list of pages with a starting index of 1) as its only parameter - is used to handle the page navigation. This method then walks along the collection of Panel controls and sets their visibility appropriately.
As we have seen, there are three major approaches to creating dynamic forms within a Web application. Each has its own strengths and weaknesses, and the method using server controls is, of course, specific to ASP.NET. This basic idea can be easily expanded to include far more options for question presentation types and formatting options.
The sample code accompanying this article is available for download.
James Culshaw develops ASP.NET applications for Dumfries and Galloway Council. He is also developing applications to facilitate joint working between the council and the local NHS. He has been developing .NET applications since Beta 1. Previous to that he was developing ASP applications and VB6 desktop applications.