This article was originally published in the April 2003 issue of Visual Studio Magazine. Their online version has been archived, so it has been reproduced here. The articles code requires updating for .NET 2.0 which will be the subject of a future post.
ASP.NET has brought about a huge change in how we as developers think about designing a web site. We all know that ASP.NET now gives us the opportunity to create reusable custom controls which we can drag and drop onto our pages, which can even remember the information entered between postbacks, using the viewstate mechanism built into ASP.NET. This has proven to be a huge advantage in designing our pages, by allowing us to bring our more traditional programming model to the Web for the first time. ASP.NET also allows us to add these same controls dynamically at runtime.
The sample project which we will create is a relatively simple one, which should clearly demonstrate all the issues that you will face when trying to create dynamic controls yourself. In general Microsoft has done a good job with their architecture and made it really quite straightforward. However, there are circumstances where things do start to get somewhat more interesting, and we will tackle this area in detail so that you will be armed with all the knowledge you need to tackle any dynamic control issue.
Why Use Dynamic Controls?
The ability to add controls at runtime gives us the ability to create some very sophisticated web sites. We could for example have the home page render completely differently based on the users profile. We could also dynamically load the modules or controls that the user wanted so that the experience would be unique for each user. This is a particularly powerful technique if you need to change branding of your site dynamically. However, this is not the only use, if you were to create a Wizard, then you would want to load different screens based on the previous selection.
Creating our first Dynamic Control Page
The good news is that ASP.NET allows us to create dynamic controls very easily, well it does in part but I’ll come to that later. To dynamically create a control at runtime all we need to do is use the following line of code.
You should note however that the control to be loaded must be the fully qualified name otherwise you may get a file not found error. A good tip is to use the ‘~’ tilde at the beginning as it is a special character which represents the root of the web application, when used with Server side controls.
This is the most basic way to display a dynamic control. In the example above we just added the control to the pages control collection. However, you can do the same thing for any container control such as a Panel or Placeholder control. In most cases you will probably want to use a Panel or Placeholder as it will allow easier positioning on the page. This is precisely what we use in our sample project, see Figure 1. As you can see we have arranged three Panel controls on the page to represent a Title, Menu/Login and Content area. This should be a familiar web design to most of you. As you can see there isn’t any real visual content yet, as we will be adding this content dynamically each time the page loads. We will be creating several simple User Controls to insert into the panel areas which we can easily create in VS.NET. It should be kept in mind that these controls can comprise any number of ASP.NET Controls. To demonstrate the use of dynamic controls we will create a series of steps or screens to guide a potential user of our site through registration. To keep it simple we will assume that anyone who isn’t registered will want to be and make our registration page our home page. You will I trust not replicate this in your site!
Due to the time it would take to describe exactly what I’ve built, I’d prefer to mention the ‘interesting’ points of code and ask that you download the sample project which contains all the code. For instructions on how to make use of these samples check out the sidebar.
Creating a Wizard Control
The sample consists of the following User Controls
||Banner no advertising
||Banner with advertising
||Container for each step
||Get Username & Password
||Get personal Details
||Confirm Data Collected
||Allow user to login
||Sample of a menu
What we want to do is create a Wizard interface that we can then dynamically load our steps into at runtime. This has several advantages, namely that it keeps the code for each step simple and also allows you to change the Wizards look and feel anytime without going back to the individual steps. Figure 2 shows the structure of our Wizard interface, the other controls such as the Banner are simply text written onto the control, one having some advertising the other not. All of these controls will be dynamically loaded into the Template shown in Figure 1 as previously discussed. The UseControl in the middle of the Wizard will be discussed later in the article.
Looking at the Wizard template you’ll notice that I’ve decided to keep the traditional Wizard style interface and have a Title and Description for each step. These details will be taken from each step as it is loaded into the control. The question then arises how do we achieve this? The best way is to make use of Interfaces. As we know an Interface allows us to create a type of contract that each object that implements it must adhere to. Then when we want to reference those objects, we can be guaranteed that if they have implemented the interface that it will have at least those properties and methods defined by the interface. Another great advantage of using interfaces is that it allows us to use early binding in our code, and therefore reduces potential errors. So I’ve created the IWizard interface which has the following signature (a signature is a common term to describe the properties and methods that make up an interface).
1: Public Interface IWizard
2: ReadOnly Property Title() As String
3: ReadOnly Property Description() As String
4: ReadOnly Property NextStep() As String
5: ReadOnly Property PreviousStep() As String
6: End Interface
I have added a few extras to help us know what the next and previous steps in the wizard are. This will help our container control (ie Wizard.ascx) know which control to load next and which buttons should be enabled etc. As this is directly related to the Wizard UI I’ve put this Interface code in the Wizard.ascx file. You can see how all this code has been implemented by looking at the code behind file for the Wizard.
Now that we have defined our Wizard interface we need to ensure that each step implements it. To see how this is done check Listing 1. A thing to note about this code is how we determine the next step. In essence we check that the information entered on the screen for that step is valid. If it is then return the next step, if not then return the current step.
Now that we have coded all the steps to incorporate our IWizard interface and have coded the Wizard itself, its time to see if it works. When we run our first sample application (Example1.aspx), we get a screen that should look like that of Figure 3. Here you can see that we have loaded three controls dynamically into the Template (see Listing 2). In this case, as the user has not logged in, they are presented with the ability to register. To keep things simple you can see that the code merely checks a QueryString parameter for a value of ‘True’.
Maintaining State between PostBacks
When we enter our details and press next we notice that we get a rather unexpected result, none of the controls we added are still there. The reason for this is because of the way we normally code our ASP.NET Form_Load events. Normally we would do the following as is the case in our first example (see Listing 2).
1: If Not Me.IsPostBack Then
3: End If
If we hadn’t created these controls dynamically this style of coding would be both fine and preferable, as it means we don’t have to do any extra work. However, this doesn’t work because despite how much Microsoft have done for us, we are still living in a stateless environment, and to that end if you create something at runtime, ASP.NET won’t keep track of it for you. You will need to do that for yourself. This means you can’t make use of this feature as you must always recreate the controls each time.
Listing 1 – Wizard_Step1.ascx
1: #Region "IWizard Interface"
3: Public ReadOnly Property Title() _
4: As String Implements IWizard.Title
6: Return "User Registration"
7: End Get
8: End Property
10: Public ReadOnly Property Description() As _
11: String Implements IWizard.Description
13: Return "Please enter the Username and Password you wish to use on our site."
14: End Get
15: End Property
17: Public ReadOnly Property NextStep() As String _
18: Implements IWizard.NextStep
20: If Me.txtName.Text.Length > 0 AndAlso _
21: Me.txtPassword.Text.Length > 0 Then
22: Return "~/Wizard_Step2.ascx"
24: Return "~/Wizard_Step1.ascx"
25: End If
26: End Get
27: End Property
29: Public ReadOnly Property PreviousStep() As String _
30: Implements IWizard.PreviousStep
32: Return "Start"
33: End Get
34: End Property
36: #End Region
We could overcome this by removing the IF condition and it would fix our initial problem by recreating the controls during each postback, however there are still many issues that need to be resolved such as the following.
- We need to keep track of the last control, load it and have its events fire before we remove it to replace it with the control for the next step.
- Replace current control with the next one.
This process can start to become rather complicated and it would be much better if we were able to wrap this complexity up into a user control. This is exactly what we will be tackling next.
Creating a Dynamic Control Host Control
To make our coding life easier and to solve the problems associated with creating dynamic controls, we can create a control which handles all the more complex tasks for us. This then leaves us with a simple object model to program against. This is what we will be using in Example 2, and it makes what would otherwise be a complicated task easy.
The amount of code required for our control isn’t particularly large, but the concepts behind it all can be a bit confusing. I will try to explain the code presented in Listing 3, and point out the issues as we go along.
Listing 2 – Load Event of Example1.aspx
1: Private Sub Page_Load(ByVal sender As System.Object, _
2: ByVal e As System.EventArgs) Handles MyBase.Load
3: 'Put user code to initialise the page here
4: ' Create the page based on the user who has entered
5: If Not Me.IsPostBack Then
6: If Request.QueryString("Registered") = "True" Then
7: ' Top security here, you might want to do better
8: ‘ authentication
9: Me.pnlBanner.Controls. _
11: Me.pnlMenu.Controls. _
14: Me.pnlBanner.Controls. _
17: Me.pnlContent.Controls. _
19: End If
20: End If
21: End Sub
Basically what we want the control to handle for us is the whole state management of our controls once we tell it to create it. That is once we put our DynamicControlHost.ascx on our form and tell it to dynamically load a particular control, we want to assume that when we do a post back it will just be there, and we as the user of the control don’t have to worry about recreating it. In essence we want it to act like a control we placed there at design time.
To start with, we create ourselves a blank User Control in VS.NET. Next we put a Panel control onto our design surface and set the width and height properties to be 100%. This will ensure that anything we put into this will use the entire area. We then want to name it and Panel1 is not my recommendation, so we will name it ‘pnlHost’ which should make it somewhat easier to understand later. That’s all there is to it from the UI standpoint. Now let’s examine the code in Listing 3 which contains all the code necessary to finish off the control.
Listing 3 – Dynamic Host Control
1: Public MustInherit Class DynamicControlHost
2: Inherits System.Web.UI.UserControl
3: Protected WithEvents pnlHost As _
6: Web Form Designer Generated Code Here.
7: Dim m_strUserControl As String = ""
9: Private ReadOnly Property UniqueKey() As String
11: Return Me.UniqueID & "DynamicControl"
12: End Get
13: End Property
14: Private Sub Init2_Load(ByVal sender As _
15: System.Object, ByVal e As System.EventArgs) _
16: Handles MyBase.Init
17: 'Now change the ID of this control to that of what
18: 'it was previously if it is a postback
19: If Me.Page.IsPostBack Then
20: If Me.UserControlPath.Length <> 0 Then
21: Me.pnlHost.Controls. _
23: End If
24: End If
25: End Sub
26: Property UserControlPath() As String
28: If m_strUserControl.Length = 0 Then
29: If Not Request.Form(Me.UniqueKey) _
30: Is Nothing Then
31: m_strUserControl = Request.Form(Me.UniqueKey)
32: End If
33: End If
35: Return m_strUserControl
36: End Get
37: Set(ByVal Value As String)
38: If Me.pnlHost.Controls.Count > 0 Then
39: ' check that the current control isn't the same
40: ' as this new one
41: If Me.UserControlPath <> Value Then
42: ' Remove old items because we can't simply
43: ' replace them!
44: Me.pnlHost.Controls. _
46: ' Add New Items
47: Me.pnlHost.Controls. _
49: ' Store the new values
50: m_strUserControl = Value
51: End If
53: Me.pnlHost.Controls. _
55: m_strUserControl = Value
56: End If
57: End Set
58: End Property
60: ReadOnly Property UserControl() As Control
62: Return Me.pnlHost.Controls(0)
63: End Get
64: End Property
66: Protected Overrides Sub Render(ByVal writer As _
68: ' We want to include a HTML Form field to store the
69: ' current UI Object
71: writer.Write("<input type=""hidden"" name=""" & _
72: Me.UniqueKey & """value=""" & _
73: Me.UserControlPath & """/>")
74: End Sub
75: End Class
Looking at Listing 4 we can see how the code in Listing 3 is used. One of the first things to note is that we can still use our Me.IsPostBack statement, this is due to the fact we have put our custom control on the form which ASP.NET will automatically recreate for us. The only code we actually need to write to use the control is the following.
1: Me.dchBanner.UserControlPath = "~/Banner.ascx"
Where we simply set the path of the Usercontrol we wish to load. Now looking at the code in Listing 3 we can find out what is happening when we set this property.
First we check if we have previously loaded a control by checking the number of controls in the pnlHost control collection. As our control will only ever support one at a time we simply check if its greater than zero. If we haven’t loaded a control yet, then we simply add the control to the Panel object and set the private strUserControl to the value of the Usercontrol, which we will store later.
If however we had loaded a control then we would need to first check if the new control is the same as the current one. If it is not, we need to remove it from the Panel control collection and replace it with the new one. This sounds simple enough, but as luck would have it once you add a control you can’t simply replace it with another, you must remove it and then add your new one.
This doesn’t seem like it would cause many problems, but it does! At the moment our control would only work every second time, to understand why this is so we need to get a very close look at how ViewState works, which is our next Topic, but before we do we need to finish discussing the other properties.
As mentioned earlier we can now check for PostBack, this is possible due to our control automatically recreating the previously loaded control during its Page_Init event. I’ve decided to use a separate routine that handles the event as I find it makes maintenance easier if you can see all your code, as opposed to having some hidden in the Form Designer code area.
The code is fairly straight forward in that is simply checks for a PostBack event signaling that we should have a control to load back too, but we double check this by seeing if we have a control stored. The technique used to store the previous control is worth discussing and is wrapped up in the UserControlPath property and Render method which is discussed below.
Once we have determined a control has been previously loaded we load it into the Panel object ready to use. As you will notice from Figure 5, this makes the control ready to be used prior to the Load Events, thereby making our dynamic control act just like a control added at design time.
Back to how we store the current UserControl, we can look at the property Get of the UserControlPath property which first checks if the local variable has been set (remember that this is a stateless environment and this variable is reset at the start of each PostBack), if it is then this is returned. However if it isn’t then it is repopulated by getting the value that was stored with the form. The value is stored with the form during the Render method which has been overridden. I have chosen this technique for a few reasons.
At the time we need to know the current UserControl, ViewState is not available to us, and so we can’t use that. Another alternative would have been to use session state which I did use in the first versions of this control. However, apart from using resources on the Server, which is never a good thing, it suffered a major issue with users using the browser back button. As the current control is stored on the Server if a previous page is sent back, our control will attempt to load a control which is incompatible with the ViewState data held in the form. When this happens it all ends in a crash! So there had to be a better way. Server controls were out as they again weren’t available to us in the Page_Init event, and the hidden input box control didn’t seem to render for some reason. Therefore I used the technique of adding the hidden input box directly to the output stream. A hidden text box (as Classic ASP programmers will know) is a great way to pass information from page to page, and in fact is the way Microsoft handle their ViewState data. This form field will be available during the Page_Init using the following line of code, as used in the property Get of UserControlPath.
The UniqueKey property is used to ensure that should we have several of our controls on the one form each will be able to be identified uniquely. This is achieved by simply annotating the text ‘DyanmicControl’ to the end of the controls UniqueID property.
UserControl is our last property and is useful if you want to access the control as an object once you’ve created it. The code is very straight forward, basically just returning the first control in the panels control collection.
1: Return Me.pnlHost.Controls(0)
Getting a close look at ViewState
We would have all heard of and probably even looked at ViewState by now. However how does ASP.NET make use of the hidden variable? View state is a wonderful piece of technology that Microsoft has given us to simulate a state full environment. Most of the time we need not worry too much about how it works, because it just does. However when we start to get a bit tricky as we are in this example we do need to understand what’s happening under the covers, because something isn’t working right and we need to understand why.
I’ve written two little utilities which let us get a better look at what’s happening, you’ll find the code in the download. One lists all the form controls and their contents, and the other lists the contents of ViewState.
To see what is happening when we run our project lets put a break point on the following line in Example2.aspx, which is located in the PreRender event. This line does nothing except to allow us code to place a break point on, now run the project.
Press continue when the code stops, the first page will then appear to run fine so enter a username and password, now before the next screen appears the code will stop again. At this point type the following into the command window.
1: web.utils.debug.displayFormVars (Me.Controls)
This will produce a long list of all the controls that are on the page, now look for the following section.
The thing I’d like to draw your attention to is the UniqueID of the dynamic control host content control (dchContent), and one of the controls that it contains like the textbox for the Name. At the moment the UniqueID is dchContent:_ctl0:dchStep:_ctl1:txtAddress. Lets, now continue the program and stop it again after we enter some details.
Now if you do the same thing again you’ll notice that the UniqueID is now dchContent:_ctl0:dchStep:_ctl0:txtAddress, but why is it different, and what does it mean? What it means is that unless the UniqueID’s are exactly the same between postback’s ViewState will not work!, and neither will the events on the control. However, if you try running the page again, by reentering the data you’ll notice as pointed out earlier that on the second attempt it does in fact work. Looking at the UniqueID’s again after the postback you’ll notice it’s the same as the previous. So why is this happening? The reason is quite simple, basically when ever you add a control to a container ASP.NET keeps an internal count which it then uses to calculate the UniqueID for that control. The reason it does this is because you could have 4 texboxes all with the same name (such as in a grid), and .NET needs to identify them all uniquely, it does this by means of a UniqueID. This is all fine, except when you want to remove an item from the collection and add a new one. The problem is the internal counter doesn’t reset, which means if you add ten controls then remove them all again then add one more control the internal counter would be 10 not 0. This makes sense because unless .NET can guarantee a UniqueID for every control it generates the ViewState mechanism just wouldn’t work properly. So what does this mean for our control? Well it means that unless we can always make the UniqueID for each control the same on postback our control simply won’t work!
Listing 4 - Example 2 Page Load
1: Private Sub Page_Load(ByVal sender As System.Object, _
2: ByVal e As System.EventArgs) Handles MyBase.Load
3: ' Put user code to initialize the page here
4: ' Create the page based on the user who has entered
5: If Not Me.IsPostBack Then
6: If Request.QueryString("Registered") = _
7: "True" Then
8: ' Top security here, you might want to do
9: ' better authentication
10: Me.dchBanner.UserControlPath = "~/Banner.ascx"
11: Me.dchMenu.UserControlPath = "~/menu.ascx"
13: Me.dchBanner.UserControlPath =
15: Me.dchMenu.UserControlPath = "~/Login.ascx"
16: Me.dchContent.UserControlPath = _
18: End If
19: End If
20: End Sub
I’m not completely satisfied with the solution I’ve found, however it does seem to do the trick, and is very easy to implement. The interesting thing is you don’t have to touch our custom control. Instead you need to add a little bit of code to each control which will be created dynamically. As we can override properties in .NET we are able to override the UniqueID property of all our controls. So to get everything to work you simply include this piece of code in all your custom controls.
1: Public Overrides ReadOnly Property UniqueID() As String
3: Return Me.Parent.Parent.UniqueID & ":_ctl0"
4: End Get
5: End Property
The purpose of this code is to ensure that the UniqueID for each control within the container remains _ctl0 regardless of what the internal counter would have made it. Now given that we have basically removed the safeguard that Microsoft has put in, there are some ramifications. You will only be able to include one control per instance of the DyanamicControlHost control. However, given that we don’t support more than one anyway that’s not a big issue, but it is one to remember if you use the technique elsewhere. Also you may have problems if the control is used outside the dynamic control host control.
So given that you’ve added this code to your controls the DyanamicControlHost is now complete and makes using dynamically created controls as easy to use as those we create at design time (see Listing 4). Note that in the code download the first step in the wizard (Wizard_Step1.ascx) has this code commented out to demonstrate how it worked without the code. You will need to uncomment this code again to make example 2 work properly.
As an alternative in the second step, I have tried to demonstrate another technique. Now that we have a full OO language we can take advantage of Inheritance, and so I have created a WizardBase class which includes the overridden UniqueID property and the IWizard interface. So you can now Inherit from this class instead of System.Web.UI.UserControl. This means instead of implementing the properties of IWizard you now override the properties of the base class instead. The base class has been included in the Wizard.ascx code behind file. This method preferable as it ensures you don’t forget to override the UniqueID property while still forcing you to implement all the IWizard properties.
Storing Data after PostBack
Now that we have solved the issues with the dynamic controls being created we need look at how we can persist data between round trips to the server, so that we can make use of the data when the finish button is pressed.
To do this we need to store the data between postbacks in a way that’s easy to retrieve. The .NET framework provides a number of nice ways to do this. To give more options when storing our data, I designed a simple Data Storage component for post back data (see Figure 4), which takes advantage of the .NET features but simplifies their use. This currently makes use of the cache object, but could be reconfigured to use just about anything. The code is included as part of the download but was a bit lengthy to include here.
So using our new Data Storage system it becomes a simple matter of storing and retrieving information as the following examples demonstrate.
1: Web.Data.Store("Registration.Name", Me.txtName.Text)
3: strName = CType(Web.Data.Retrieve("Registration.Name", ""),String)
Note that the first parameter takes a key, this is used to identify the data once it has been stored. Looking at the UML model of the data object we can see that it is overloaded and allows for a default value if so desired. Another overloaded parameter is that of sessionWide which determines whether this value is cleared when the clear method is called. Remember not to store many items here as it will remain in memory until the session has ended.
We are now ready to start collecting our information from the forms. Each form simply stores the current value of its controls during the changed event of the particular control. As all the code for this is virtually identical, I’ll only detail one of them.
1: Private Sub txtName_TextChanged(ByVal sender As System.Object, _
2: ByVal e As System.EventArgs) Handles txtName.TextChanged
3: ' We want to store this information so that we can use it later on.
4: Web.Data.Store("Registration.Name", Me.txtName.Text)
5: End Sub
To retrieve the data once stored is again a simple process as this excerpt from the third step in the Wizard shows.
1: Me.lblName.Text = CType(Web.Data.Retrieve("Registration.Name", ""), String)
Notice how we always use the CType function, this is because the Retrieve method returns back a data type of Object. If you turn Option Strict On, which you should do as it actually makes your code run faster as well as reducing potential errors. You’ll notice that you get an error if you don’t explicitly cast the returned object into the correct data type of the receiving object. In this case the Text property of the Label control has a type of String.
It’s also interesting to note that it’s important to re-populate controls during the Page_PreRender event and not the Page_Load event. The reason for this is evident if you examine Figure 5, here you can see that the Page_Load events fires before the events of individual controls. This means that if you re-populate the controls during the Load event you would in fact overwrite any changes the user had made, which would not be very desirable.
So once we have wired up all the controls to store their values into our new Data object, we can then make use of this data to redisplay it in our last step of the sample. In reality we would pass the data to a database and do other validation, but we have kept the sample simple.
Bits & Pieces
That’s covered the core section of the article, however there are a few other bits and pieces which are worth knowing when you start to create your own implementations.
Caching is a new feature in ASP.NET and we have the ability to cache at either the page level or control level. Making use of this with your dynamic controls means you can create container sites where most of the work is already cached and only the inner content has to be processed.
Also, it is possible to create one page that then dynamically loads each of the content controls into the content area via a query string parameter, such as;
Your page can then dynamically load the URL into the content area. An advantage of this technique would be that if you needed to change the layout of the main pages you’d only have to do it in one place.
We have covered a large amount of material in this article, material which if mastered will help you produce more dynamic and exciting web applications. The techniques discussed here were varied from data storage to process flows using a Wizard metaphor. I encourage you to test drive the code supplied and let your imagination loose on the possibilities that dynamic controls can bring.
Source - DynamicControls.zip (72 kb)