Tuesday, October 4, 2011

Dynamic Web Controls, Postbacks, and View State

Introduction


As I've written about in two previous articles here on 4Guys - Dynamic Controls in ASP.NET and Working with Dynamically Created Controls - ASP.NET makes it easy to programmatically add Web controls. Armed with this capability, you can offer a truly customized experience for your users. For example, your site might load particular navigational elements as user controls, based upon the logged on user's preferences. Or when collecting information from your users, you might display different input fields prompting for different data based on the user's age, location, gender, and so on. One of the main challenges with working with dynamically added controls is that these controls must be programmatically added on each postback. That is, you can't just load these controls on the first page load, and then not reload them on subsequent postbacks. Failure to explicitly add the controls on each postback will cause the controls to literally disappear on postbacks. To further complicate things, the point in the page's lifecycle when dynamic controls are added is important if you want to maintain changed values across postback. For example, imagine you had a Web page that displayed a series of input form fields based on the user visiting the page. The idea here would be to allow the visitor enter some values into these custom input form fields, and then submit the form, having the data saved. If the dynamic Web controls are not added at the correct time in the page's lifecycle, the values entered by the visitor will be lost on postback.
In this article we will examine how to add dynamic Web controls to a page in such a manner that you will not need to worry about losing form field values on postback. Specifically, we'll look at how to create a page whose form fields are dependent upon the user visiting the page, and how this user can enter their data into these form fields and have it saved on form submission. Since this article builds upon concepts discussed earlier, please make sure you have read both Dynamic Controls in ASP.NET and Working with Dynamically Created Controls before tackling this article.


Understanding the Page Lifecycle


As page developers, we often think about ASP.NET Web pages consisting of two distinct portions: an HTML portion and a code-behind class. However, behind the scenes, when an ASP.NET Web page is requested for the first time (or for the first time after the page has changed) the HTML portion is autogenerated into a class from which the code-behind class is derived from. In essence, an ASP.NET Web page is represented, then, as a single class. Whenever an ASP.NET page is requested from the Web server, the corresponding page's class is instantiated and its ProcessRequest() method is invoked. This method kicks off the page lifecycle. The page lifecycle is a sequence of stages that the ASP.NET page's corresponding class progresses through. The end goal of the lifecycle is to generate the page's appropriate markup, which is then sent back to the requesting browser. The page lifecycle is composed of a number of steps, the following ones being the ones of interest for this article:
  1. Instantiation,
  2. Initialization,
  3. Load View State,
  4. Load,
  5. Save View State
The first stage if the page lifecycle is called Instantiation, and in this stage the page's control hierarchy is created. The control hierarchy is the hierarchy of server controls that exist in the page. Typically this includes an HtmlForm (the <form runat="server">), numerous LiteralControls (static HTML content in the page's HTML portion is represented as LiteralControls in the hierarchy), and the Web controls you added to the page's HTML portion. The control hierarchy is created by the autogenerated class. Since every time an ASP.NET page is requested, be it the first time or on a subsequent postback, the page class is reinstantiated and reiterates its lifecycle, each request causes the control hierarchy to be rebuilt from scratch in the Instantiation stage. As mentioned earlier, the ASP.NET page's autogenerated class's responsibility is to create the control hierarchy from the HTML portion. To understand this process, let's look at a concrete example. This example - the complete text and images below - is taken from another article of mine, Understanding ASP.NET View State, published on MSDN Online in May 2004.
Imagine you have an ASP.NET Web page with the following 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 autogenerated that contains code to programmatically build up the control hierarchy. The control hierarchy for this example can be seen in the figure below.


This control hierarchy is then converted to code that is similar to the following:


Page.Controls.Add( 
new LiteralControl(@"<html>\r\n<body>\r\n
<h1>Welcome to my Homepage!</h1>\r\n"));
HtmlForm Form1 = new HtmlForm();
Form1.ID = "Form1";
Form1.Method = "post";
Form1.Controls.Add(
new LiteralControl("\r\nWhat is your name?\r\n"));
TextBox TextBox1 = new TextBox();
TextBox1.ID = "txtName";
Form1.Controls.Add(TextBox1);
Form1.Controls.Add(
new LiteralControl("\r\n<br />What is your gender?\r\n"));
DropDownList DropDownList1 = new DropDownList();
DropDownList1.ID = "ddlGender";
ListItem ListItem1 = new ListItem();
ListItem1.Selected = true;
ListItem1.Value = "M";
ListItem1.Text = "Male";
DropDownList1.Items.Add(ListItem1);
ListItem ListItem2 = new ListItem();
ListItem2.Value = "F";
ListItem2.Text = "Female";
DropDownList1.Items.Add(ListItem2);
ListItem ListItem3 = new ListItem();
ListItem3.Value = "U";
ListItem3.Text = "Undecided";
DropDownList1.Items.Add(ListItem3);
Form1.Controls.Add(
new LiteralControl("\r\n<br /> \r\n"));
Button Button1 = new Button();
Button1.Text = "Submit!";
Form1.Controls.Add(Button1);
Form1.Controls.Add(
new LiteralControl("\r\n</body>\r\n</html>"));
Controls.Add(Form1);

The C# source code above is not the precise code that is autogenerated by the ASP.NET engine. Rather, it's a cleaner and easier to read version of the autogenerated code. To see the full autogenerated code - which won't win any points for readability - navigate to the WINDOWS\Microsoft.NET\Framework\Version\Temporary ASP.NET Files folder and open one of the .cs or .vb files.
One thing to notice is that, when the control hierarchy is constructed, the properties that are explicitly set in the declarative syntax of the Web control are assigned in the code. (For example, the Button Web control has its Text property set to "Submit!" in the declarative syntax – Text="Submit!" – as well as in the autogenerated class — Button1.Text = "Submit!";.
The page's control hierarchy created in the Instantiation stage reflects the markup in the page's HTML portion. When we add controls dynamically to an ASP.NET Web page, we are essentially building onto this control hierarchy, but at a later stage in the page lifecycle. After the Instantiation stage, the page lifecycle enters the Initialization stage. In this stage, the page's Init event is fired, along with the Init event of the page's server controls.
Following the Initialization stage, the page's view state is restored in the Load View State stage. Each ASP.NET server control is capable of maintaining its state across postbacks through a mechanism known as view state. View state is utilized when a control's state - its properties - are modified programmatically. Recall that in the Instantiation stage, when the control hierarchy is build, the controls' properties are assigned their default values (namely the values specified in their declarative syntax). If any properties are set programmatically, in the code-behind class, then these changes must be remembered across postbacks. Any state change is remembered across postback through view state.

For More About View State...
A profound understanding of view state isn't vital for this article. What's vital is to realize that prior to the Load View State the controls' properties are assigned the values specified in the declarative syntax in the HTML portion. After the Load View State, the controls' properties have the actual values (assuming there have been changes to their state on a prior postback). However, if you are interested in learning more about ASP.NET's view state, be sure to read: Understanding ASP.NET View State.

Following the Load View State stage, the page enters the Load stage, which fires the Load event for the page and its controls. You are likely familiar with the Load stage, since virtually every ASP.NET Web page out there has some code in its Page_Load event handler, which is the event handler that runs when the page's Load event fires. Sometime after the Load stage, the Save View State stage begins, which entails persisting the view state of the controls on the page into a hidden form field named __VIEWSTATE. (This hidden form field is how this view state is persisted across postbacks.)
These four stages in the page lifecycle are just a subset of the stages the page proceeds through during its lifecycle. For a more in-depth look at the lifecycle you can read Dino Esposito's article: The ASP.NET Page Object Model.

Adding Controls at the Right Time


We already know that when adding controls dynamically through the page's code portion the controls must be added on every postback. But when in the page lifecycle should the controls be added? At first guess, we might decide to put such code in the Page_Load event handler, causing the controls to be added during the Load stage of the page's lifecycle. This would work fine if we don't need to worry about saving the controls' view state across postbacks, but if we do need to persist the view state of the dynamically added controls the Load stage is not where we should be adding these controls. If we need our dynamically added controls to maintain their view state it is paramount that these controls be added before the Load View State stage. That is, these controls must exist within the page's control hierarchy before the view state is loaded. There's only one stage before Load View State - Initialization. That means, if we want our dynamic controls to persist view state we must add them to the control hierarchy in the page's Init event.
If you are using Visual Studio .NET, the code-behind class already contains an event handler for the page's Init event. This event handler (named Page_Init in VB.NET code-behind classes and OnInit in C# code-behind classes) is tucked away in the hidden "Web Form Designer Generated Code" region. What I typically do is create a separate method in the code-behind class and simply call this method from the Init event handler. Once you have configured everything so that the dynamic controls are added during the Initialization stage, you can then read/write the controls' properties in the Load stage. You'll find that the users' interactions with the dynamic controls remains "remembered" across postbacks if you follow this pattern.

A Real-World Example...


In a current project I'm working on, I needed the ability to create various input form fields based upon information about the user who was visiting the page. Each user in the system belonged to one of a set of possible categories, and a number of the input form fields on this page were specific to a category. (For example, users in the category "Employee" might have a drop-down list from which they select their boss, whereas users in the category "Manager" would not have this drop-down list, but rather might have a couple of TextBoxes for information about their division and team.) One particular data-entry page needed to display the appropriate input form fields, populate them with the user's current responses (if they had provided any), and then provide the user the ability to update (or save for the first time) their information. This involved three steps:
  1. Create the input form fields based upon the user visiting the page
  2. Populate the dynamic form fields with the user's values (if any)
  3. Save the user's responses upon the click of a "Save" button
To create the input form fields I create a method called LoadUI() that I then have called from the page's Init event handler. LoadUI()'s sole task is to create the dynamic controls based upon the current user's category. The pseudocode for this method looks as follows:

Private Sub LoadUI()
'Determine the user's type...
Dim type as UserType = AppLayer.GetUserType()

If type = UserType.Employee Then
'Add the Employee controls
Dim bossDDL as New DropDownList
...
ElseIf type = UserType.Manager Then
'Add the TextBoxes
Dim divisionDesc as New TextBox
...
ElseIf ...
...
End If
End Sub

The code shown above starts by determining the user's type. This is performed by an application layer class with a method called GetUserType(). The specifics of this method are not important; needless to say, it simply returns the user type of the currently logged on user. Next, I use a series of conditional statements to determine what set of controls to add. (In practice, I actually store the controls to add in a database, and in my LoadUI() method I retrieve the set of controls to dynamically add based on the user's type. The benefit of this approach is that adjusting the custom input form fields for a particular user type is as simple as modifying the database - there's no need to touch any of the ASP.NET code. However, with my hard-coded approach shown above, any changes to the input forms for different types would require a modification of the LoadUI() code and a recompilation/redeployment.)
On each page visit, be it the first visit to the page or on a postback, the LoadUI() method will be invoked during the page's Initialization stage. Next, we need to have the user's current values (if they exist) displayed in the custom controls when the page is first visited. This is done in the Page_Load event handler using the following code:


Private Sub Page_Load(sender as Object, e as EventArgs) Handles Page.Load
'Only want to do this on the first visit to the page
If Not Page.IsPostBack Then
'Determine the user's type...
Dim type as UserType = AppLayer.GetUserType()

If type = UserType.Employee Then
'Populate the employee's controls with the user's current values
... Get the data from the database ...
... Reference the controls and assign properties/call methods as needed ...
...
ElseIf type = UserType.Manager Then
...
End If
End If
End Sub

When loading the current values, we only do so on the first visit to the page (hence my check for Not Page.IsPostBack). To load the current values, I first need to determine the user's type, and then load and populate the values of the dynamic form fields appropriately. In order to set the properties and call the methods of the dynamically created Web controls, I need to be able to reference the controls in the control hierarchy. In Working with Dynamically Created Controls we saw how to use the FindControl() method to obtain a reference to a control in the control hierarchy. (Again, in practice, I don't use multiple conditionals, but rather determine what dynamic controls need to be populated based on information in the database. This allows me to forgo the conditionals and just have a couple of lines of code that queries the database for the values for the appropriate controls based on the user's type, and then finds those controls and assigns the user's values.)
The final step is to allow the user to update their information and save any changes back to the database. The Web page contains a Button Web and a corresponding Click event handler. In the Click event handler I again iterate through the dynamically added controls based on user type, and update the database accordingly.


Private Sub SaveData(sender as Object, e as EventArgs) Handles btnSubmit.Click
'Determine the user's type...
Dim type as UserType = AppLayer.GetUserType()

If type = UserType.Employee Then
'Read the dynamic controls' values and update the database
... Reference the controls and read their property values ...
... Save the data to the database ...
...
ElseIf type = UserType.Manager Then
...
End If
End Sub


Conclusion


In this article we saw how to work with dynamic controls so that their values and view state can be correctly persisted across postbacks. Dynamic controls, as evidenced in this articles and others on 4Guys, offer a great deal of capability, but using dynamic controls can be frustrating due to lost controls or view state if the controls are not added to the hierarchy appropriately. As we saw in this article, the general pattern to follow is:
  1. Add the dynamic controls on each page visit in the page's Initialization stage,
  2. Read/write the dynamic controls' properties and methods in the Page_Load event handler.
That's really all there is to it! Just make sure you follow this pattern, and dynamic controls become as easy to use as the static controls added in the page's HTML portion.