Maintain State With Dynamic Controls
Build a process controller to simplify multistep input.
by Garry McGlennon

April 2003 Issue

Technology Toolbox: VB.NET, ASP.NET

ASP.NET gives you the opportunity to create reusable custom controls that maintain state, and drag and drop them onto Web pages. This is a huge advantage in designing pages, because it lets you bring the Win32 programming model to the Web. ASP.NET also allows you to create the same controls dynamically at run time. Microsoft has done a good job, in general, of enabling you to create dynamic controls; however, things don't always work as planned. I'll discuss the issues you face when you create dynamic controls, and I'll show you how to address them. I'll also demonstrate the use of dynamic controls by showing you an example of a process controller; you can use the same technique for any multistep process.

You need only this line of code to create a control dynamically at run time:

Me.Panel1.Controls.Add(Page. _
   LoadControl("~/MyControl.ascx")

Figure A. House Dynamic Controls.

You must use the fully qualified path of the control you're loading; otherwise, you might get a "file not found" error. A good tip is to use the tilde (~) at the beginning, because it represents the root of the Web application when you use it with server-side controls.

You can add controls to any container control that supports the Controls collection, such as a Panel or Placeholder control. The DynamicControlHost.asx control you'll build later uses the Panel control (see Figure A). There's no visual content, because you add the content dynamically each time the page loads.

Figure B. Encapsulate Processes for Reuse.

Now I'll show you how to create a process controller that demonstrates the use of dynamic controls. It handles the series of steps or screens required to register someone on a Web site. You can use the finished control anywhere within the site by dragging and dropping it on a page. A traditional wizard-style interface that encapsulates the steps is the process controller's core (see Figure B).

Use an interface to make your code easier to write (and arguably better). Using an interface ensures that all steps in a process have the same properties, which lets you treat each step the same way:

Public Interface IProcess
   ReadOnly Property Title() As String
   ReadOnly Property Description() _
      As String
   ReadOnly Property NextStep() _
      As String
   ReadOnly Property PreviousStep() _
      As String
End Interface

The interface contains all the information you need from each step for your process controller to work. You must ensure that each step implements the entire interface, or else your code won't compile. Create a new base class that inherits from System.Web.UI.UserControl, and implement the IProcess interface. This simplifies the code because all your UI steps then inherit from this base class, ensuring that you don't forget any steps. The code for ProcessBase is in the Process.vb file in the downloadable project.

After you create your base class, you must create a custom control for each step in the process that contains the UI. Each of these controls must then inherit from the base class:

Public MustInherit Class _
   Registration_Step1
   Inherits ProcessBase
   ...
End Class

See the code behind Registration.ascx for details on how to make the wizard work with these controls.

Create a DynamicControlHost Control
Now that you have all your steps and a wizard container control, you must have a way of displaying each step within the wizard container. If you create anything at run time, you must re-create it on each postback, because the environment is still stateless and ASP.NET won't do it for you. This can make your code complex quickly, so create the DynamicControlHost control to handle all the complex tasks of dynamic control creation for you. This leaves you with a simple object model to program against. You can use an instance of the DynamicControlHost to simplify what would otherwise be a complicated task.

The amount of code your DynamicControlHost control requires isn't large, but the concepts behind it can be confusing. You want the DynamicControlHost control to handle the state management of your controls once you've created them. Once you put the DynamicControlHost.ascx control on your form and get it to load a particular control dynamically, you want to assume that control will be there when you do a postback—that is, that you don't need to re-create it. In essence, you want it to act like a control you placed on the form at design time.

Start by creating a blank user control in VS.NET. Next, put a Panel control onto your design surface and set the width and height properties to 100 percent, which ensures that it uses the entire area; name it pnlHost. That's all there is to do from a UI standpoint. The rest of the code finishes off the control (see Listing 1).

You need this code to use the control once you've placed it on a form:

Me.dchStep.UserControlPath = _ 
   "~/Registration_Step1.ascx"

The UserControlPath method in Listing 1 checks if you've loaded a control previously by checking the number of controls in the pnlHost control collection. It simply checks if the number's greater than zero, because the control supports only one control. If you haven't loaded a control yet, then you add the control to the pnlHost control and set the private m_strUserControl variable to the value of the UserControl you pass in, which will be persisted later.

Figure 1. Understand the ASP.NET Event Model.

However, if you loaded a control previously, you must check first if the new control is the same as the current one. If it isn't, you must remove the current one from the pnlHost control collection and replace it with the new one. This sounds simple enough—but you can't replace one control with another once you add a control. You must remove the first one and then add your new one. It might seem as if doing this wouldn't cause many problems, but it does. The control's state works correctly only on every second postback. I'll discuss the reasons why—and how to fix this issue—later.

You create the previous control automatically during the Page_Init event to allow the use of IsPostback. A separate routine (Init2_Load) handles the event, because seeing all your code aids maintenance—as opposed to having some code hidden in the Web Form Designer generated-code area, which is where the default Init_Load event resides. The code checks for a PostBack event, signaling that you loaded a control previously, but you need to double-check this by seeing if you've stored a control too.

Make Your Control Ready to Use
Once you determine that a control was loaded previously, you load it into the pnlHost object. This makes the control ready to use prior to the Load events, because you replaced the control before the processing of the control's state (see Figure 1). State is restored by the time the Load event occurs, making your dynamic control act just like a control you added at design time.

The Get property of the UserControlPath property checks first if the private m_strUserControl variable has been set (remember that this is a stateless environment, and this variable resets at the start of each postback); if it's been set, then it's returned. If it hasn't, then it's repopulated by getting the value that was stored with the form during the Render method, which has been overridden.

I've chosen this technique for a few reasons. ViewState isn't available to you at the time you need to know the current UserControl. You could use session state, but session data uses resources on the server and might get out of sync with the page when users click on the browser's Back button, which can cause a crash. The code uses the technique of adding a hidden input box directly to the output stream. A hidden input box is a great way to pass information from page to page, and in fact, it's the way Microsoft handles ViewState data in ASP.NET. You can retrieve this form field during the Page_Init event by using this line of code in UserControlPath's Get property:

Request.Form(Me.UniqueKey)

You use the UniqueKey property to ensure that if you have several of your controls on the one form, you can identify each uniquely. You achieve this by appending the "DynamicControl" text to the end of the control's UniqueID property.

The UserControl property is useful if you want to access the control as an object once you've created it. This code returns the first control in the pnlHost Controls collection:

Return Me.pnlHost.Controls(0)

It's time now to take a closer look at ViewState. ViewState is a wonderful piece of technology Microsoft has given us to simulate a stateful environment. However, as I mentioned previously, things don't always work according to plan.

Figure C. Count Your Controls.

The sample code includes some utilities that help you examine the postback process. The displayFormVars routine displays all the controls on a page and their IDs. Use this to see why ViewState doesn't always work as planned.

Begin by putting a breakpoint in the Default.aspx page's PreRender event, then run the sample. Continue execution the first time. When execution stops again, run this line of code in the immediate window to produce a long list of all the controls on the page (see Figure C):

utils.debug.displayFormVars(Me.Controls)

Pay Attention to ID Counters
Turn your attention to the controls' UniqueID. You can see that ID:lblDescription has a unique ID of _ctl0:lblDescription, whereas ID:txtAddress has …:_ctl1:txtAddress. The ctlx identifies the internal count in the pnlHost control's Controls collection. You've requested by this time that the DynamicControlHost render the initial step and the next step in the process. (PreRender is one of the last events for a page.) This means you've removed the first step from pnlHost to replace it with the second step. Continue the program and stop it again after you enter some details.

Figure 2. Follow the Steps.

Now, rerun the utility. Notice that the UniqueID is now …:_ctl0:txtAddress, and the same step is displayed when you continue. As I mentioned, the internal count was 1 (zero-based) the first time, but 0 the second time. This means that ViewState was unable to match the state to the UserControls because their UniqueIDs weren't exactly the same between postbacks. The internal counter keeps increasing when you add a UserControl, but doesn't decrease if you remove one, until it's re-created during postback (see Figure 2).

Any previous UserControl is re-created during the Init event when the page loads. The internal counter for pnlHost is always 0 at this time, because the collection starts from nothing. When a new UserControl is loaded (that is, when you want to swap step 1 for step 2), the new UserControl always has an internal count of 1 when it's added, because the internal counter keeps increasing. A mismatch occurs between the internal counter when the UserControl is displayed initially (1) and when the UserControl is re-created upon postback (0). ASP.NET uses this internal counter to create the UniqueID you saw previously. ASP.NET can't restore the state while the mismatch occurs.

You must add more code to your ProcessBase base class to solve this issue:

Public Overrides ReadOnly Property _
   UniqueID() As String
   Get
      If Not Me.Parent.Parent Is Nothing Then
         Return Me.Parent.Parent. _
            UniqueID & ":_ctl0"
      Else
         Return MyBase.UniqueID
   End If
   End Get
End Property

The purpose of the preceding code is to ensure that the UniqueID for each control within pnlHost remains _ctl0, regardless of what the internal counter would have made it. The check for nothing ensures that the control behaves well in the IDE; you can't use the IDE without it to design controls based on the class. This finishes the control, and you can use your controls now as if you'd placed them at design time. The final step to make it all work is to put the Registration.ascx process-controller control onto any ASPX page—Default.aspx, in this case.

ASP.NET has brought about a huge change in how to think about designing a Web site. Mastering the material I've covered here can help you produce more dynamic and exciting Web applications of many different kinds. Test-drive the sample code and let your imagination loose on the possibilities that dynamic controls and processes can bring.

About the Author
Garry McGlennon is an MCP living in Sydney, Australia, where he works as an independent contractor/consultant. He's working on a site for .NET professionals (www.dotnetprofessional.com), while he finishes off a book for Apress due later this year. Reach him at .