Dynamic Controls and Managing ViewState

Suggested Reading:
Understanding ASP.NET View State

Recently I’ve started learning ASP.NET (VB) in order to develop an application assigned to me at ExxonMobil. I have previously worked with ASP Classic, but the .NET framework is fairly new to me, so I’m just picking up on the architecture and how Microsoft is trying to take over my code and make things easier for me. Don’t get me wrong, I think .NET is a step in the right direction. It absolutely does a better job of following the model-view-controller design pattern by almost forcing you to keep your design code split from your logic. With that in mind, there are certainly accompanying downfalls, which I’ll talk about in this post.

For my project, I’m developing an application to track exception in the corporate Internet filter. That means when someone finds out that their vendor’s website has been blocked by WebSense (the corporate Internet filter) and they feel they are justified in having it unblocked, they must go through a process. This process will be managed by the new system I’m developing. Now, what if the vendor’s website is made up of three different, but related, domain names (e.g., http://imyourvendor.com, http://imyourvendorslegaldepartment.com). Instead of having the fill out a big form for each related websites, on the Website Address field I just wanted to provide a link that says “add another,” which would then create an additional textbox for the user to enter another related website address. Here’s a snapshot of the concept:

Now, if you’re acquainted with JavaScript you know it can’t be too hard to just plug another textbox in there without refreshing the page. On the other hand, customized JavaScript that adds DOM elements and ASP.NET are practically enemies, the reasons of which could fill an entirely separate article(s). After a few hours of battling with that idea, I decided for the sake of programmers that came later and for my own sanity, I would stick to postbacks.

One more thing to understand before we delve into code: ViewState manages changes in control values, not the controls themselves. So, if we dynamically create a new textbox control on every click of the Add Another link, we must recreate all the dynamic textboxes that we had created up until that time. On every postback, you must recreate all the dynamic controls you had created previously. That’s one thing Microsoft won’t do for you.

So, in order to track how many dynamic textboxes I had created previously, I’m going to create a property for that purpose:

' This keeps track of how many URL text fields we have on the form. It is used
' to recreate our dynamic text fields on each post-back
Private Property URLControlsCount() As Integer
Get
If (ViewState("_urlControlsCount") IsNot Nothing) Then
Return CType(ViewState("_urlControlsCount"), Integer)
Else
Return 1 ' We always want at least one URL text field on the form
End If
End Get
Set(ByVal value As Integer)
ViewState("_urlControlsCount") = value
End Set
End Property

Notice that the property value is stored within the ViewState and, as such, will be tracked across future postbacks. Also notice that if the value hasn’t been set, it will return 1. That comes into play later because I always want at least one textbox displaying on the page.

Now, let’s create the Click event for the “Add Another” button:

Protected Sub AddURLLink_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles AddURLLink.Click
Dim newTextBox As TextBox = New TextBox()

URLControlsCount += 1
newTextBox.ID = "url" + URLControlsCount.ToString()
newTextBox.CssClass = "GridFormTextBox"
URLFieldsPlaceHolder.Controls.Add(newTextBox)
End Sub

This increments the property we created in the last step so that on a future postback we will know how many controls to recreate. It then uses that control value to make an ID (convenient since URLControlsCount is already incrementing) and then adds the control to a placeholder.

Now we need to make a function to recreate the controls that we had created on previous postbacks. Here’s mine:

' We must re-create our dynamic URL controls on each postback
Private Sub CreateURLControls()
For i As Integer = 1 To URLControlsCount
Dim newTextBox As TextBox = New TextBox()
newTextBox.ID = "url" + i.ToString()
newTextBox.CssClass = "GridFormTextBox"
URLFieldsPlaceHolder.Controls.Add(newTextBox)
Next
End Sub

Now here comes the tricky part. Where are we going to call that function? If we call it from Page_Load, we won’t be able to get the values from the controls when we need to (when the user is done filling out the form and we’re ready for processing). The reason is that the values are applied to the controls at the beginning of (before?) Page_Load. If we don’t have our controls in the control tree until after the beginning of Page_Load, their values won’t be applied, so when we call…

CType(URLFieldsPlaceHolder.FindControl("url" & i), TextBox).Text

…we won’t get an error (the controls do indeed exist), but we’ll get an empty string rather than the true value.

So how about we call the function from Page_Init? While this would essentially allow us to access the values of the controls from within Page_Load (because they would have been added to the control tree before the beginning of Page_Load), we’ve run into a different problem: the ViewState for our URLControlsCount property has not been loaded yet. In other words, when our CreateURLControls() function tries to get the value of URLControlsCount, it’s not going to find the right value. In the end, we end up with the wrong number of controls (but hey, at least we would be able to access them now, right?).

So what’s the answer? 42. No, not really. We need to call the CreateURLControls() function after the ViewState has been loaded, but before Page_Load. In order to do this, let’s override LoadViewState() like so:

Protected Overrides Sub LoadViewState(ByVal savedState As Object)
MyBase.LoadViewState(DirectCast(savedState, Pair).First)
CreateURLControls()
End Sub

This allows us to load the ViewState before creating our controls, but before Page_Load. We must also override SaveViewState():

Protected Overrides Function SaveViewState() As Object
Return New Pair(MyBase.SaveViewState(), Nothing)
End Function

LoadViewState is invoked only if ViewState was saved for the given control. I don’t really understand this part as much as I should, so I won’t pretend like i do…I’ll leave it at that.

One last thing. LoadViewState() is only called on postbacks. This means that CreateURLControls() won’t get called the first time we load the page, which means that we won’t have any textbox on our page. I want to start out with one textbox on the page, so i will call CreateURLControls() from Page_Init only if it’s not a postback. Remember how in the first step I set up the property to return 1 if the ViewState value didn’t exist? That’s what we need on this occasion, and this occasion only.

Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
If Not IsPostBack Then
CreateURLControls()
End If
End Sub

Wow….how about that. Kind of a whirlwind and if you weren’t already struggling to find the answer to this problem, very little of this might make sense to you. On the other hand, if you were trying to figure this out and pouring over websites and books trying to find the answer, hopefully this revealed the piece of the puzzle you were searching for. Here’s my final code:

Imports ePACS.ISUBData.BO
Imports ePACS.ISUBData.BLL

Partial Class TestWithMaster
Inherits System.Web.UI.Page

' This keeps track of how many URL text fields we have on the form. It is used
' to recreate our dynamic text fields on each post-back
Private Property URLControlsCount() As Integer
Get
If (ViewState("_urlControlsCount") IsNot Nothing) Then
Return CType(ViewState("_urlControlsCount"), Integer)
Else
Return 1 ' We always want at least one URL text field on the form
End If
End Get
Set(ByVal value As Integer)
ViewState("_urlControlsCount") = value
End Set
End Property

Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
If Not IsPostBack Then
CreateURLControls()
End If
End Sub

Protected Overrides Sub LoadViewState(ByVal savedState As Object)
MyBase.LoadViewState(DirectCast(savedState, Pair).First)
CreateURLControls()
End Sub

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim masterPage As MasterPage = Me.Master
masterPage.Title = "Submit a Request"
masterPage.Subtitle = "Submit a request to block or unblock a website from the corporate filtering system"
masterPage.RightPanelVisibility = False
End Sub

Protected Overrides Function SaveViewState() As Object
Return New Pair(MyBase.SaveViewState(), Nothing)
End Function

Protected Sub AddURLLink_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles AddURLLink.Click
Dim newTextBox As TextBox = New TextBox()

URLControlsCount += 1
newTextBox.ID = "url" + URLControlsCount.ToString()
newTextBox.CssClass = "GridFormTextBox"
URLFieldsPlaceHolder.Controls.Add(newTextBox)
End Sub

' We must re-create our dynamic URL controls on each postback
Private Sub CreateURLControls()
For i As Integer = 1 To URLControlsCount
Dim newTextBox As TextBox = New TextBox()
newTextBox.ID = "url" + i.ToString()
newTextBox.CssClass = "GridFormTextBox"
URLFieldsPlaceHolder.Controls.Add(newTextBox)
Next
End Sub

End Class
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.