Creating Dynamic Data Entry User Interfaces
Summary: Scott Mitchell demonstrates one method of dynamically generating ASP.NET data entry forms based on the data you're editing. (22 printed pages)
When creating a data-driven Web site, one of the most common tasks Web developers are faced with is creating data entry forms. Data entry forms are Web pages that provide the system's users with a means to input data. The task of creating a particular data entry form typically starts with hammering out the requirements that spell out specifically what information needs to be collected from the user. With the requirements defined, the next stage is designing the data entry Web Form, which involves creating the graphical user interface, as well as writing the code that updates the database with the user's inputs.
When the data entry form requirements are well-known in advance, and when such data entry forms are identical across all users for the system, creating such entry forms is hardly challenging. The task becomes more arduous, however, if the data entry forms need to be dynamic. For example, consider a company's Internet Web application whose purpose is to collect information about the product purchased by a customer; a sort of online product registration system. With such an application, the questions the user is presented with might differ based on what product they purchased, or if they purchased the product from a store or from the company's Web site.
When faced with needing to provide dynamic data entry user interfaces, as in the example mentioned above, one option might be to "brute force" a solution. You could create a separate Web page for each product your company sells, with each page having the specific data entry elements needed. The problem with this naive approach is that it requires adding new pages when new products are released. While creating these new pages might not be terribly difficult, it is time consuming and prone to errors without sufficient debugging and testing time.
Ideally, when new products are released, a non-technical coworker could specify what questions are required through an easy-to-use Web-based interface. Such a system is quite possible with ASP.NET thanks to the ability to dynamically load controls on an ASP.NET Web page at runtime. With just a bit of an initial investment in development and testing time, you can create a reusable, dynamic data entry user interface engine. One that allows even the least computer savvy users the ability to easily create customized data entry forms. In this article, we will look at the fundamentals of working with dynamic controls in ASP.NET, and then I will present a complete, working dynamic data entry system that can be easily customized and extended.
As you know, ASP.NET Web pages are comprised of two parts:
- An HTML portion that contains static HTML markup and Web controls, added through a declarative syntax.
- A code portion that can be implemented as a separate class file (as with Visual Studio .NET), or in a
<script runat="server">block in the HTML file.
The Web controls of an ASP.NET Web page are added at design-time through a declarative syntax that spells out the Web control to be added and its initial property values, like so:
<asp:WebControlName runat="server" prop1="Value1" prop2="Value2" ... propN="ValueN"> </asp:WebControlName>
What's important to understand is that when the ASP.NET page is visited for the first time, or for the first time after its HTML portion has been modified, the ASP.NET engine automatically converts this mix of static HTML content and Web control syntax into a class. The role of this auto-generated class is to create the control hierarchy. The control hierarchy is the set of controls that compose the page—the static HTML markup is translated into
LiteralControl instances, and the Web controls are translated into instances of the corresponding class type (for example, the
<asp:TextBox> is translated into an instance of the
TextBox class in the
The control hierarchy is called such because it is an actual hierarchy of controls. Each ASP.NET server control can have a set of child controls and a parent control. When the auto-generated class constructs the control hierarchy, it places the Page class instance that represents the ASP.NET page at the top of the hierarchy. The Page class's children controls are those top-level server controls defined in the page's HTML, which is typically some static HTML markup along with the Web Form's server control. (An ASP.NET page's Web Form—that
<form runat="server"> tag—is implemented as an instance of the
HtmlForm class, which can be found in the
The Web Form, like any other server control, can contain children controls. The children controls of a Web Form are those controls found within the Web Form itself. Even controls within the Web Form may have children themselves: a Panel control's contents constitute its children controls; when binding data to a DataGrid, its resulting contents makeup its set of children controls. Because the top-level
Page class may have children, which may have children, which may have children, and so on, this set of controls constitutes the control hierarchy.
To help hammer home this concept, which is vital to understand when working with dynamic controls, imagine that you had an ASP.NET page with the following content in its HTML portion:
<html> <body> <h1>Welcome to my Homepage!</h1> <form runat="server"> What is your name? <asp:TextBox runat="server" ID="txtName"></asp:TextBox> <br />What is your gender? <asp:DropDownList runat="server" ID="ddlGender"> <asp:ListItem Select="True" Value="M">Male</asp:ListItem> <asp:ListItem Value="F">Female</asp:ListItem> <asp:ListItem Value="U">Undecided</asp:ListItem> </asp:DropDownList> <br /> <asp:Button runat="server" Text="Submit!"></asp:Button> </form> </body> </html>
When this page is first visited, a class will be auto-generated containing code to programmatically build up the control hierarchy. The control hierarchy for this example can be seen in Figure 1.
Figure 1. Control hierarchy
Programmatically Working with the Control Hierarchy
As aforementioned, each ASP.NET server control can contain both a set of children controls and a parent control. The children controls are accessible through the server control's
Controls property, which is of type
ControlCollection class provides functionality to:
- Determine how many children controls there are, using the read-only
- Add a new item to the controls collection using the
- Remove all child controls with the
Clear()method, or remove a specific control with either the
To add a control to the control hierarchy as a child of control X, simply create the control's corresponding class instance and add it to the
Controls collection of control X. For example, to add a Label control to the
Controls collection of the
Page class, you could use the following code:
'Create a new Label instance Dim lbl as New Label 'Add the control to the Page's Controls collection Page.Controls.Add(lbl) 'Set the Label's Text property to the current date/time lbl.Text = DateTime.Now
Adding a control to the end of the
Controls collection will cause the control to appear at the bottom of the Web page. If you need more control over the placement of the dynamically added control, you can add a PlaceHolder Web control to the page, specifying the location in the hierarchy to add one or more dynamic controls. To add the dynamic controls in that location, simply add them to the PlaceHolder's
Controls collection. For example, if you wanted to place the Label within a certain spot in the Web Form, you could add a PlaceHolder control like so:
<html> <body> ... <form runat="server"> ... <asp:PlaceHolder runat="server" id="dateTimeLabel"></asp:PlaceHolder> ... </form> </body> </html>
To add the dynamic Label from our past example, rather than using
Page.Controls.Add(lbl), you'd use
dateTimeLabel.Controls.Add(lbl), thereby adding the Label to the PlaceHolder's
Controls collection as opposed to the
Controls collection. Figure 2 provides a graphical illustration of the control hierarchy both before and after the dynamic Label has been added to the PlaceHolder's
Figure 2. Graphical illustration of the control hierarchy both before and after the dynamic Label has been added
Typically it is best to add dynamic controls to the end of the
Controls collection using the
Add() method, rather than adding it to a specific location in the collection using
AddAt(). The reason is because of the manner in which view state is saved as each control records its view state along with the view state of its children controls. When saving its children controls' view states, each control records the child control's view state along with the ordinal index of the control in the
Upon postback, when the view state is reloaded, the process reverses itself, with each control loading its child controls' view states. The control reloading its view state enumerates through the view state information, applying the view state for the control in the specified position in the
Controls collection. Problems can arise if, prior to the view state being loaded, you insert a control into the
Controls collection in a position other than the tail end because the view state information for each child control is tied to a specific index in the
To see how adding a dynamic control to a position other than at the end can cause a problem with reloading the view state, consult Figure 3. Figure 3 shows a server control p with three children controls: c0, c1, and c2, with control c1 having some view state persisted across postback. If, on postback, a dynamic control, c, is added to the front of p's
Controls collection, when reloading the view state p will attempt to reload c1's view state in index 1, which is now occupied by c0.
Figure 3. Server control p with three children controls
The same view state related issues can arise when removing controls. All of this, of course, depends on when in the page lifecycle you're adding or removing controls. For a more thorough discussion of view state, the page life cycle, and issues surrounding adding and removing dynamic controls and view state, be sure to read my earlier article Understanding ASP.NET View State.
Accessing Dynamically Added Controls
When adding static Web controls to an ASP.NET page, Visual Studio .NET automatically adds references to the Web controls in the code-behind class. These references to the Web controls allow for strongly-typed access to the controls, their properties, and their methods. When working with dynamically-added controls there are a couple of techniques that can be employed to access a control's properties, methods, and events.
One approach is to find the dynamic control through an exhaustive examination of the control hierarchy. The following code, for example, illustrates how to recursively iterate the control hierarchy rooted at a specified control. Such code could be useful, for example, if a number of DropDownList controls had been dynamically-added to a specified PlaceHolder. In such a case you could enumerate the PlaceHolder's control descendents by calling
RecurseThroughControlHierarchy(PlaceHolderControl), adding code to the "Do whatever it is you need to do with the current control,
c" section that would check to see if
c was of type DropDownList and, if so, would take some action.
Private Sub RecurseThroughControlHierarchy(ByVal c as Control) 'Do whatever it is you need to do with the current control, c 'Recurse through c's children controls For Each child as Control in c.Controls RecurseThroughControlHierarchy(child) Next End Sub
The above approach works well if you have a number of similar server controls that you need to work with en masse. Oftentimes, though, you might have a gaggle of dissimilar controls that you need to be able to access individually at different times, performing different actions on each control. To programmatically work with a specific dynamically-added control, you can use the
FindControl(ID) method to search for a control by its
FindControl() method is defined in the
System.Web.UI.Control class, so all server controls, from TextBoxes to PlaceHolders to Web Forms, have this method available.
Calling a control's
FindControl() method does not necessarily search all of the control's descendent controls.
FindControl() only searches the current naming container. Controls that implement
INamingContainer behave as a naming container, meaning that they create their own
ID namespace in the control hierarchy. For example, the DataGrid control is a naming container. Given a DataGrid with ID
myDataGrid, its children controls'
IDs are prefixed with their parent's
ID, as in
myDataGrid:childID. What is important to realize is that
FindControl() is only enumerating the set of children controls or controls in the naming container, and not all descendents of the parent in the control hierarchy. (Furthermore, to search beyond the first level of controls in a naming container, you'll need to use the properly scoped
ID.) The point is, when using
FindControl() to hunt for a dynamically-added control, call
FindControl() from the parent control of the dynamic control (typically a PlaceHolder control).
When using the
FindControl() approach, code similar to the following is used to assign a unique ID to the dynamically-added control and to, later, reference said control.
'When adding the control, set the ID property Dim tb As New TextBox PlaceHolderID.Controls.Add(tb) tb.ID = "dynTextBox" 'At some later point in the page lifecycle, 'reference the dynamic TextBox Dim dTB As TextBox dTB = CType(PlaceHolderID.FindControl("dynTextBox"), TextBox)
FindControl() method locates a control using its
ID, when using this technique to access dynamically-added controls it is vital that each dynamically-added control have its
ID property assigned a unique and identifiable value. There are various methodologies that can be employed, depending on the situation. As we'll see later in this article, when examining a dynamic data entry user interface engine, each dynamic question is represented by a row in a database, which contains a unique primary key field. This primary key field value is what is used in the ASP.NET page as an
ID for each dynamically-added control. If you do not need to differentiate among the dynamically-added controls, another technique is to give them sequentially increasing numbers as IDs, like
myDynCtrl1 for the first dynamically added control,
myDynCtrl2 for the second, and so on.
The Page Lifecycle and Dynamic Controls
Whenever an ASP.NET Web page is visited, be it on an initial page visit or on postback, the control hierarchy is rebuilt from scratch each and every time by the class auto-generated by the ASP.NET engine. Not only is the control hierarchy reconstructed, but the controls' events are rewired to their specified event handlers. Therefore, when adding dynamic controls to an ASP.NET page, it is vital that you make sure to add these controls on every page visit. Many developers starting off with adding dynamic controls will do so using the following pattern:
'In the Page_Load event handler... If Not Page.IsPostBack Then 'Add dynamic controls... End If
The problem with this code is that it adds the dynamic controls only on the first page visit and not on subsequent postbacks. If you attempt to utilize such code, you'll find that whenever a postback occurs, your dynamic controls disappear from the page. Therefore, you must be certain to add all dynamic controls on all page visits by moving the code out of the
If Not Page.IsPostBack conditional statement.
One important question adding dynamic controls raises is when in the page lifecycle to add such controls. As I discussed in Understanding ASP.NET View State, an ASP.NET page proceeds through a number of steps whenever a request arrives. Let's take a brief moment to recap the germane stages in a page's lifecycle. For a more in-depth look, be sure to turn to the view state article, focusing on the article's The ASP.NET Page Lifecycle section.
A Review of the ASP.NET Page Lifecycle
The first stage in a page's lifecycle is Instantiation, during which the auto-generated class builds up the control hierarchy from the static controls defined in the page's HTML portion. As the control hierarchy is constructed, the properties of each added control are assigned the values specified in the declarative syntax. Following Instantiation is the Initialization stage, at which point the static control hierarchy has been constructed, but the view state has yet to be reloaded (assuming the page request is a postback). If the page request is a postback, after Initialization comes the Load View State stage. Here the page percolates the view state data it found in the hidden
VIEWSTATE form field, and each control in the control hierarchy updates its state, if needed.
If the page request is a postback, the Load Postback Data stage follows the Load View State stage. In this stage, the form field values sent are inspected and corresponding controls' properties are updated accordingly. For example, the text entered by the user into a TextBox Web control is sent back through the POST mechanism, signaling both the name of the TextBox control and the value entered by the user. The page takes these values, locates the appropriate TextBox in the control hierarchy, and assigns its
Text property to the value received.
The next stage is the Load stage, which is when the
Page_Load event handler fires. There are more stages following the Load stage, such as raising postback events, saving the view state, and rendering the Web page, but these are not relevant to the topic of dynamic controls, and therefore don't warrant a discussion. Figure 4 shows a graphical illustration of the events as a page proceeds through during its lifecycle.
Figure 4. Page lifecycle
Determining When in the Page Lifecycle to Add Dynamic Controls
The question of when to add dynamic controls in the page lifecycle can be summarized as thus—dynamic controls need to be added previous to loading of the view state and reloading of postback data because we want any view state or postback values specific to the dynamic controls to be properly added. Given these constraints the natural place to add dynamic controls is the Initialization stage because it transpires before the Load View State and Load Postback Data stages.
In the Initialization stage, however, neither the view state nor the postback data has yet to be restored, so it's ill-advised to access or set properties of controls—either dynamic or static ones—that might be stored in view state or modified by a postback value, as these values will be overwritten by the view state and postback values during later stages in the lifecycle. The pattern I use when working with dynamic controls is as follows:
- In the Initialization stage I add the dynamic controls to the control hierarchy and set the
- In the Load stage, I assign any needed initial values to the dynamic controls within an
If Not Page.IsPostbackconditional statement.
I need to add the dynamic controls on each and every postback, but I set their property values only on the first page load since these values will be maintained in view state. The following code snippet illustrates this pattern:
'In the Init event of the Page, add a dynamic TextBox Dim tb as New TextBox PlaceHolderID.Controls.Add(tb) tb.ID = "dynTextBox" 'In the Page_Load event handler, set the properties 'of the TextBox If Not Page.IsPostBack Then Dim dTB As TextBox dTB = CType(PlaceHolderID.FindControl("dynTextBox"), TextBox) dTB.Text = "Some initial value" dTB.BackColor = Color.Red 'initial BackColor End If
In addition to loading dynamic controls in the Initialization stage, you can also add them in the Load stage, without any ill side effects. When a control is added to another control's
Controls collection, the added control is immediately entrenched in the lifecycle with its new parent control. For example, if the parent control is in the initialization stage, the added control's Init event is raised, brining it in sync with its parent. If the parent is in the Load stage or later, the added child control immediately passes through the Initialization, Load View State, Load Postback Data, and Load stages.
There is one caveat to be aware of when adding controls in the Load stage. After a control completes its Load View State stage, it begins to track changes to its view state. What this means is any property changes after the Load View State stage are automatically persisted to the control's view state. Before a control begins to track changes to its view state, property value changes are not persisted to view state. If you add a control in the Initialization stage and then set its properties in the Load stage, there is no problem because between the Initialization stage and Load stage the Load View State stage has transpired, and the control's track view state changes flag has been raised. That is, if the dynamic control is added in the Initialization stage, by the time the Load stage is running, assigning properties to the dynamic control will be persisted to view state.
Note The "track view state changes flag" cannot be modified by a page developer. The
System.Web.UI.Control, from which all ASP.NET server controls are derived from, provides only protected access to this flag. Specifically, there's a protected, read-only property named
IsTrackingViewStatethat indicates if view state is being tracked or not, as well as the protected
TrackViewState()method that indicates view state tracking should begin. This method is called automatically by all controls at the end of the Initialization stage.
If, however, you don't add the dynamic control until the Load stage, it is vital that you do not set any of the dynamic control's properties until after you've added the control to the control hierarchy. To understand why, consider what would happen if the following code were executed in the Load stage:
Dim tb as New TextBox If Not Page.IsPostBack Then tb.BackColor = Color.Red 'initial BackColor End If PlaceHolderID.Controls.Add(tb)
As you can see, on each page load a TextBox is created. On just the first page load, the TextBox's
BackColor property is set to Red, and then, on every page load, the control is added to the control hierarchy. While the TextBox's background color will indeed be Red on the first page load, the problem is that on postback the TextBox's background color will revert back to the default (no background color). This is because the TextBox's
BackColor property assignment is not being persisted to view state, so it's lost on postback. It's lost because the TextBox, like any other server control, doesn't begin tracking view state until after the Load View State stage. But the TextBox doesn't pass through this stage until after it's been added to the control hierarchy, so the
BackColor assignment is not persisted to view state. To correct this, be sure to first advance the control through its Load View State stage by adding it to the control hierarchy, and then assigning its properties, like so:
Dim tb as TextBox PlaceHolderID.Controls.Add(tb) If Not Page.IsPostBack Then tb.BackColor = Color.Red 'initial BackColor End If
This detail is not pertinent if you are adding your dynamic controls in the Initialization stage. For a more in-depth discussion on this, refer to my blog entry Control Building and View State Lesson for the Day.
Events and Dynamic Controls
Just like static server controls, dynamically-added controls can have their events tied to event handlers. Just as you have to add a control to the control hierarchy on each and every page visit, you need to wire up the dynamic control's event to the specified event handler on each and every page visit. Part of the challenge in doing so is that you need to have an appropriate event handler defined in the class. If your controls are truly dynamic, then how could you have any idea as to what event handlers will be needed in the code-behind class? In my experience, I've found that the best solution to working with events and dynamic controls is to use User Controls rather than singleton Web controls. With a User Control, I can embed specific event handlers and programming logic within the User Control's code portion. We'll see how to dynamically add User Controls in the next section.
If you must tie a dynamically-added Web control's event with an event handler, be certain to do so on every page visit. The following code, which is included in this article's download, demonstrates how to associate a dynamically-added Button Web control's
Click event with an existing event handler. (
ph is the name of a PlaceHolder control on the page. For an example of wiring an event to an event handler with C#, along with a more detailed look at event handling in the .NET Framework, see Peter Bromberg's article Delegates to the Event.)
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim b As New Button ph.Controls.Add(b) If Not Page.IsPostBack Then b.Text = "Click Me" End If AddHandler b.Click, New EventHandler(AddressOf Me.ButtonClickEventHandler) End Sub Private Sub ButtonClickEventHandler(ByVal sender As Object, ByVal e As EventArgs) Response.Write("The button has been clicked!") End Sub
Over the past couple of years, I've worked on a number of projects that have required dynamic data entry user interfaces, user interfaces that depend on one of more user-influenced factors. A fundamental requirement for all of these projects was that these dynamic interfaces needed to be able to be easily created, updated, and deleted by users who are not computer-savvy. Over the course of these projects, I've developed a dynamic data entry user interface engine that allows for developers to create the user interface building blocks that non-developers can then piece together to form user interfaces specific for particular users.
In the remainder of this article, we'll step through a simplified version of this engine. Specifically, the demo in this article shows how to have a unique data entry user interface for customers based on the customer's type. Regular customers, for example, are presented with a different user interface than online-only customers, or bulk purchasing customers.
The fundamental pieces of the dynamic data entry user interface engine are:
- User Interface Building Blocks: the user interface building blocks are User Controls, the creation of which are the responsibility of the developers on the team. These building blocks are designed to be specific only in the type of information they collect, but not in the specific data requested. For example, one of the UI building blocks included in this demo is a building block prompting the user for an integer value as input. The User Control contains a TextBox along with a CompareValidator to ensure that the user enters a valid Integer value. This building block can be assembled into a dynamic data entry user interface by associating it with a question like, "How old are you?" or, "How many miles is it from your home to your place of work?"
- Questions: a question is a customized building block, created by non-computer-savvy users through a Web-based interface. A question associates some text with a UI building block.
- Differentiating Variables: each dynamic data entry user interface is predicated on one or more variables. For example, for an online product registration site, the user interface might hinge on what product was purchased. For data entry of employee information, the UI might differ on the employee's department. For the engine presented in this article, the differentiating variable is hard-coded as the customer type.
- Dynamic Questions: for a given differentiating variable, a set of questions are specified. The combined mapping of questions and differentiating variables forms the dynamic questions of the system.
- Dynamic Answers: When a dynamic data entry form is completed for a given customer, the customer's information must be saved to the database. The set of answers for a given customer are the dynamic answers in the system.
The User Interface Building Blocks piece of the dynamic data entry user interface engine is implemented as User Controls in the ASP.NET application. The remaining pieces are implemented as database entities. Figure 5 shows the engine's entity-relationship diagram, depicting how the various pieces are represented in the database.
Figure 5. Entity-relastionship diagram
When examining Figure 5, first notice the
dq_Questions table. This table's records represent questions in the system. For each question there's some text associated with the question (
QuestionText) and a User Control (
ControlSrc field contains the filename of the User Control, like
DQIntegerInput.ascx. Next, in the bottom-left hand corner there's the
dq_Customers table. Each customer has a specific customer type, all of the types being spelled out in the
Dynamic questions, which are the set of mappings between questions and customer types, are implemented through the
dq_DynamicQuestions table. There, a question and customer type are tied together, along with a sort order, which indicates the order in which the questions are presented for a particular customer type. Finally, the dynamic answers are stored in the
dq_DynamicAnswers table, which ties each dynamic question with a specific customer. Because we cannot be sure of the type of answer from a given question—it might be a string, a Boolean, an integer, and so on—the
dq_DynamicAnswers table has six columns, one for every data type the system allows. A given question can only have a single type, and for its answer, the corresponding field will have the answer's value, while the remaining columns will have
Note A couple of quick notes on the data model. I decided to use a synthetic primary key on the
DynamicQuestionID) rather than making
QuestionIDa composite primary key to allow for duplicate questions for a given customer type. For example, a question might be, "Additional comments," and use a User Interface Building Block that contains a multi-line TextBox. Because you might want to have an "Additional comments" question after a number of other questions, I decided to allow for repeat questions.
dq_Questionstable is in its simplest form. In past projects I have bounced between leaving this table very simple and embedding specifics in the User Interface Building Blocks, versus adding additional fields to this table that interplay with the UI. For example, an application might need to be able to indicate that some questions are required, while others are optional. In such a system there are two ways to tackle the problem. The first is to place the responsibility in the UI Building Blocks. That is, rather than creating a single UI Building Block for, say, integer inputs, two would be created—one that utilizes a RequiredFieldValidator to ensure that a value was entered, and one that imposes no such condition. When forming the question, the administrator could choose which UI Building Block to use based on if the question was required or not. An alternative approach would be to add a
Requiredfield to the
dq_Questionstable, and use only one UI Building Block. With this second approach, each UI Building Block would need a
Requiredproperty and would be responsible for enabling or disabling the proper validation controls based on this property value.
dq_DynamicAnswerstable, with its six answer-related fields, only allows for scalar answers from a UI Building Block. That is, a UI Building Block's answer can be a string, integer, double, date, currency, or Boolean. But what if we need the UI Building Block to have a more complex answer, like an Address, which might have several fields in itself? Such complex answers will need to be serialized by the UI Building Block into one of the acceptable types when returning the answer. When displaying the answer, such results will need to be deserialized accordingly. To aid in this you can rely on the inherent binary serialization capabilities of .NET's , but to do so you might want to add a
BinaryAnswerfield to this table of type
Design Rules for the User Interface Building Blocks
In order to facilitate a truly dynamic data entry user interface with developer-designed User Controls, it is vital that the User Controls used as the UI Building Blocks provide a base level of functionality. This base level of functionality is spelled out in the
IUIBuildingBlock interface. This interface defines three properties that all UI Building Blocks must implement:
- DataType: a read-only property that returns the data type of the answer provided by the UI Building Block. Must be one of the values from the
- QuestionText: the question's text to display in the UI Building Block.
- Answer: the answer for the UI Building Block.
To illustrate how these properties are used, let's look at a simple UI Building Block. Imagine that we want to create a UI Building Block that prompts the user for an integer input. We can do so by creating a new User Control that contains the following content in its HTML portion:
<asp:Label id="dqQuestion" runat="server" CssClass="DQQuestionText"></asp:Label>: <asp:TextBox id="dqAnswer" runat="server" CssClass="DQAnswer" Columns="4"></asp:TextBox> <asp:CompareValidator id="CompareValidator1" runat="server" CssClass="DQErrorMessage" ErrorMessage="You must enter a number here." ControlToValidate="dqAnswer" Type="Integer" Operator="DataTypeCheck"></asp:CompareValidator>
This markup includes:
- A Label Web control (
dqQuestion) that displays the UI Building Block's
- A TextBox (
dgAnswer) into which the user will enter their integer value
- A CompareValidator to ensure that the input entered is indeed an integer.
The source code portion of the User Control is fairly simple. It has the User Control's class implement the
IUIBuildingBlock interface, and provides the logic for the three required properties:
Public Class DQIntegerQuestion Inherits System.Web.UI.UserControl Implements IUIBuildingBlock ... Public ReadOnly Property DataType() As DQDataTypes Implements IUIBuildingBlock.DataType Get Return DQDataTypes.Integer End Get End Property Public Property Answer() As Object Implements IUIBuildingBlock.Answer Get If dqAnswer.Text.Trim() = String.Empty Then Return DBNull.Value Else Return dqAnswer.Text End If End Get Set(ByVal Value As Object) dqAnswer.Text = Value End Set End Property Public Property QuestionText() As String Implements IUIBuildingBlock.QuestionText Get Return dqQuestion.Text End Get Set(ByVal Value As String) dqQuestion.Text = Value End Set End Property End Class
DataType property returns the data type that is returned by the User Control—Integer. The
QuestionText property simply reads from or writes to the
dqQuestion Label control's
Text property, while the
Answer property reads from or writes to the
Text property. That's all there is to the UI Building Block. For simple UI Building Blocks, like this one, there is minimal code and HTML markup, but don't let the simplicity of this example belie the true power of UI Building Blocks. Because User Controls can have multiple Web controls that contain event handlers and so forth, you can build rich UI Building Blocks. One of the UI Building Blocks included in this article's code download illustrates how to have two dependent DropDownLists in a UI Building Block.
Note When creating UI Building Blocks, be sure to put them all in the same directory. The specific directory doesn't matter, though. In the
Web.configfile you'll find an
<appSettings>element with the key name
buildingBlockPath. This setting needs to provide the reference to the User Control directory. In the code download, the default path is
~/UserControls/, but feel free to change this if you'd like.
For more information on the benefits of using interfaces with dynamically-loaded User Controls, be sure to read Tim Stall's article Understanding Interfaces and their Usefulness.
Creating Questions and Associating Them With Customer Types
To make creating dynamic data entry user interfaces a task non-developers can easily perform, I have created a Web-based administration interface for creating questions and associating them with customer types. This interface is available in the code download for this article.
There are two germane pages in the administrative interface. The first,
CreateQuestion.aspx, allows an administrator to create a new question. Recall that a question is a specific question text and UI Building Block. The Web page is fairly simple, providing a means for the user to enter the question text and select a User Control from the UI Building Blocks directory (whose path is specified in the
Web.config file). Figure 6 shows a screenshot of this page.
Figure 6. Web-based interface for non-developers
The next screen in the administrative interface allows for the administrator to specify what questions, and in what order, are tied to each customer type. The interface, shown in Figure 7, is pretty self-explanatory. The administrator selects a customer type from the top-most DropDownList and can then add questions from the second DropDownListBox. The DataGrid lists the current questions for the selected customer type, allowing the user to remove questions from the list or reorder them through the up and down arrows.
Figure 7. Web UI for choosing the order of questions
Displaying Dynamic Questions and Saving the Results
Once the questions have been created by the system administrator and mapped to a particular customer type, data for a customer can be entered. The page
EnterData.aspx takes in the customer's ID through the querystring and builds up the dynamic data entry user interface for the customer's customer type. This page has three methods of interest:
- BuildDynamicUI(): this method is called from the
Page_Initevent handler (which executes during the Initialization stage in the page's lifecycle), and builds up the dynamic controls for the appropriate customer type. As discussed earlier,
BuildDynamicUI()simply adds the necessary controls to the control hierarchy.
- Page_Load: the
Page_Loadevent handler assigns the initial, default values to the dynamically-added Web controls. For example, if a user has already supplied some of the values for a particular customer, when the page is visited these values are populated into the appropriate dynamic controls. These properties are only set on the first page visit and not on subsequent postbacks.
- btnSaveValues_Click: this method is wired up to the Save button's Click event. It enumerates the dynamically-added controls and updates the database.
Let's take a brief look at these three methods. The
BuildDynamicUI() method is called from the
Page_Init event handler. This event handler is automatically added by Visual Studio .NET in the "Web Form Designer Generated Code" region.)The method grabs the customer's ID from the querystring and then populates a
SqlDataReader with the dynamic questions for the specified customer type. This
SqlDataReader is then iterated through. For each record, the specified User Control is loaded and added to the
dynamicControls PlaceHolder. Each dynamic control is given an ID of the form
Private Sub BuildDynamicUI() 'Called from Page_Init CustomerID = Convert.ToInt32(Request.QueryString("ID")) ... 'Get the list of dynamic controls for the specified customer reader = SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, _ "dq_GetDynamicQuestionsForCustomerType", _ New SqlParameter("@CustomerTypeID", CustomerTypeID)) 'For each question, add the necessary user control While reader.Read Dim dq As UserControl = _ LoadControl(ResolveUrl(buildingBlockPath & _ reader("ControlSrc"))) CType(dq, IUIBuildingBlock).QuestionText = reader("QuestionText") dq.ID = String.Concat("dq", reader("DynamicQuestionID")) dynamicControls.Controls.Add(dq) dynamicControls.Controls.Add(New LiteralControl("<br /><br />")) End While reader.Close() End Sub
Note In the sample code that is included with this article, I use the Microsoft Data Access Application Block (DAAB) version 2.0 to access the database. The DAAB's
SqlHelperclass provides a wrapper for accessing data from a Microsoft SQL Server database with one line of code. For more information on the DAAB, be sure to visit the official Data Access Application Block for .NET page, and read John Jakovich's article Examining the Data Access Application Block.
Also, as the code shows, to dynamically load a User Control you need to use the
method rather than creating a new instance of the User Control class. For a more thorough discussion on why, including a in-depth look at User Controls, be sure to read An Extensive Examination of User Controls.
Next, in the
Page_Load event handler, the customer's current answers to the dynamic controls are retrieved from the database and iterated. The corresponding dynamic control is referenced and its
Answer property is set to the answer from the database. This is only done on the first page visit, and not on subsequent postbacks, because we don't want to overwrite the value the user entered for one of these form fields.
'Get the answers for this customer 'Get the list of dynamic controls for the specified customer Dim reader As SqlDataReader = _ SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, _ "dq_GetDynamicAnswersForCustomer", _ New SqlParameter("@CustomerID", CustomerID)) While reader.Read Dim dq As IUIBuildingBlock = dynamicControls.FindControl(String.Concat("dq", reader("DynamicQuestionID"))) If Not dq Is Nothing Then Select Case dq.DataType Case DQDataTypes.String dq.Answer = reader("StringAnswer").ToString() Case DQDataTypes.Integer dq.Answer = Convert.ToInt32(reader("IntegerAnswer")) Case DQDataTypes.Double dq.Answer = Convert.ToSingle(reader("DoubleAnswer")) Case DQDataTypes.Date dq.Answer = Convert.ToDateTime(reader("DateAnswer")) Case DQDataTypes.Currency dq.Answer = Convert.ToDecimal(reader("CurrencyAnswer")) Case DQDataTypes.Boolean dq.Answer = Convert.ToBoolean(reader("BooleanAnswer")) End Select End If End While
Finally, when the user clicks the Save button, the
Controls collection is enumerated, and for each dynamically-added control that has been answered, the answer is written back to the database.
'Create the needed parameters Dim stringParam As New SqlParameter("@StringAnswer", SqlDbType.NText) Dim integerParam As New SqlParameter("@IntegerAnswer", SqlDbType.Int) Dim doubleParam As New SqlParameter("@DoubleAnswer", SqlDbType.Decimal) Dim dateParam As New SqlParameter("@DateAnswer", SqlDbType.DateTime) Dim currencyParam As New SqlParameter("@CurrencyAnswer", SqlDbType.Money) Dim booleanParam As New SqlParameter("@BooleanAnswer", SqlDbType.Bit) 'Enumerate each answer and save it back to the database For Each c As Control In dynamicControls.Controls If TypeOf c Is IUIBuildingBlock Then 'Mark all of the parameters as NULL stringParam.Value = DBNull.Value : integerParam.Value = DBNull.Value doubleParam.Value = DBNull.Value : dateParam.Value = DBNull.Value currencyParam.Value = DBNull.Value : booleanParam.Value = DBNull.Value 'Determine which parameter needs to be set Dim uib as IUIBuildingBlock = CType(c, IUIBuildingBlock) Select Case uib.DataType Case DQDataTypes.String stringParam.Value = uib.Answer Case DQDataTypes.Integer integerParam.Value = uib.Answer Case DQDataTypes.Double doubleParam.Value = uib.Answer Case DQDataTypes.Date dateParam.Value = uib.Answer Case DQDataTypes.Currency currencyParam.Value = uib.Answer Case DQDataTypes.Boolean booleanParam.Value = uib.Answer End Select Dim dynamicQuestionID As Integer = Convert.ToInt32(c.ID.Substring(2)) SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, "dq_AddDynamicAnswer", _ New SqlParameter("@CustomerID", CustomerID), _ New SqlParameter("@DynamicQuestionID", dynamicQuestionID), _ stringParam, integerParam, doubleParam, _ dateParam, currencyParam, booleanParam) End If Next
The dynamic data entry user interface engine is a good starting point for developing such a system for your Web application, but was not designed to afford seamless integration into existing systems. It was designed as a demonstration, not as a complete, working system. One part of the system that is not fully complete is the administrative interface that, while functional, is far from being a complete system. In particular, there is the question of how to handle removing a dynamic question from a particular customer type. For example, imagine that an administrator has configured the system so that online-only customers are asked a Boolean question, "Was this the first product you've purchased from our company?"
Now, imagine that a number of customers had answered this question. What should happen, then, if the administrator decides to remove this question from the set of questions for online-only users? Should the corresponding answers be deleted from the
dq_DynamicAnswers table? Should they be saved in order to provide a historical view of past answers? You'll have to decide the answer to this question for your application. Right now, the administrative interface doesn't do anything when you remove a dynamic question for a custom type, which means if you have one or more customers who have answered that question, you'll get an exception and the question won't be removed because doing so would violate the referential integrity established in the database.
In this article, we examined how to create a dynamic data entry user interface by utilizing dynamic controls in ASP.NET. As we discussed in the first half of this article, ASP.NET pages consist of a control hierarchy, which is usually composed strictly of statically-defined controls. However, at runtime we can manipulate this control hierarchy by adding dynamic controls to the
Controls collection of existing controls in the hierarchy. We also looked at techniques for accessing dynamically-added controls and common patterns for adding and interacting with these controls.
The second half of the article looked at a specific implementation for creating and using dynamic data entry user interfaces. The engine examined allows for non-technical users to easily create questions based on User Interface Building Blocks, which are ASP.NET User Controls created by developers. Armed with these questions, these non-technical administrative users can then associate a set of questions with a particular customer type. A single Web page,
EnterData.aspx, displays and saves the appropriate data entry form fields and values based on the customer visiting the page.
Being able to manipulate an ASP.NET page's control hierarchy at runtime is a powerful and useful tool that has applications in many common scenarios. Armed with this article, you should be able to confidently work with dynamic controls in your ASP.NET pages.
Special Thanks to...
Before submitting my article to my MSDN editor I have a handful of volunteers help proofread the article and provide feedback on the article's content, grammar, and direction. Primary contributors to the review process for this article include Milan Negovan, Marko Rangel, Hilton Giesenow, Carlos Santos, Dave Donaldson, and Carl Lambrecht. If you are interested in joining the ever-growing list of reviewers, drop me a line at firstname.lastname@example.org.
Scott Mitchell is the author of six books, founder of 4GuysFromRolla.com, and an all around great guy. He has been working with Microsoft Web technologies since 1998. Scott works as an independent consultant, trainer, and writer. He can be reached at email@example.com or via his blog, http://ScottOnWriting.NET.