ViewState, form fields, labels and Javascript

An interesting question came up on the ASP.NET forums asking why a TextBox which has its value changed by client-side Javascript persists those changes across postbacks, while a Label does not. And in a nut shell, this question covered two of the biggest causes of confusion among newcomers to ASP.NET: the difference between ViewState and IPostBackDataHandler; and the difference between client-side operations and server-side operations.

Untitled Document

First a look at the code that was posted by the questioner:

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="ButtonClickWithJavascript.aspx.vb"
    Inherits="ButtonClickWithJavascript" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
    http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Untitled Page</title>

    <script type="text/javascript">
        function btchange() {
            var label = document.getElementById("Label1");
            var textbox = document.getElementById("TextBox1");

            label.innerText = "Javascript changed!";
            textbox.value = "Text from Javascript!";

        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>
            <tr>
                <td>
                    <asp:Label ID="Label1" runat="server">Label</asp:Label><br />
                    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox><br />
                    <input type="button" onclick="btchange();" value="Javascript Change" />
                </td>
                <td>
                    <asp:Label ID="Label2" runat="server">Label</asp:Label><br />
                    <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox><br />
                    <asp:Button ID="Button1" runat="server" Text="Button"></asp:Button>

                </td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

Partial Class ButtonClickWithJavascript
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        If Not Page.IsPostBack Then
            Label1.Text = "Original Label"
            TextBox1.Text = "Original Text!"

        End If
    End Sub

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        Label2.Text = "Button changed"
        TextBox2.Text = Now().ToString()
    End Sub
End Class

On first load, the page looks like this:

First Page Load

Clicking the "Javascript Change" button calls the client-side btchange() function which results in the text within the label and the first textbox being changed to give this:

Javascript change to label

Clicking "Button" causes a PostBack, and fires the server-side Button1_Click() event, which results in this:

Page posted back

The value of the first TextBox which was changed using Javascript was retained, but the value of the first Label, which was changed by the same Javascript routine, was not retained. So why is this? "Is there a bug in ViewState, which 'forgets' Label values?" asked the questioner. The answer is "No", but to explain this behaviour, a quick overview of ViewState is required, with links to more detailed explanations.

ViewState's job is to manage any changes to the initial state of server controls, if those changes are made programmatically on the server, or if changes made by user interaction are passed to the server. This does not include restoring the values of form inputs such as TextBoxes or the selected item in a CheckBox. There is a common misconception that form values are managed by ViewState. They are not. Never have been. These values are managed and restored purely by the LoadPostData method in controls implementing the IPostBackDataHandler interface. TThis feature is a massive boon to web developers who were brought up on other server-side technologies, such as classic ASP, PHP etc. In the "olden" days, we used to have to manually wire up every form field to display the originally posted value, so that user's weren't presented with an empty form to fill in all over again, if it had failed server-side validation. LoadPostData means we never have to do that again.

The part that Viewstate plays in the sample page above is easily examined using Fritz Onion's ViewStateDecoder tool (which no longer seems available), but a web search of ViewState Decoder will help you find up to date alternatives.

On first load, ViewState only contains the Text value for Label1 - "Original Label ".

Viewstate decoded

Again, ViewState is not responsible for form field values, so the Text value of the TextBoxes is not included. The reason why Label1 is incuded in ViewState, but Label2 is not is because Label2's Text value was set in the aspx - at Page Initialisation. Label1's Text value was initially set in the aspx markup as "Label". It was subsequently programmatically changed in Page_Load to "Original Label ". Remember, ViewState's job is to manage any changes to the initial state of server controls, if those changes are made programmatically on the server. Label1 falls into this category. You can test this by removing the Text value of Button1 from the aspx, and setting it in Page_Load to "Button". Now you see that Button1 is added to ViewState on first load also.

When the "Javascript Change" button is clicked, the client-side script alters the Text values of both Label1 and TextBox1. Looking at ViewState now will show no changes from the initial Page_Load. The page has not been posted back, and Javascript cannot alter ViewState, so this is no surprise. When "Button" is clicked, a PostBack is caused. Now looking at ViewState, we can see that Label2 has been included.

Viewstate decoded

This is because it's initial value was programmatically changed in the Button_Click event. There were no changes in value for Label1, so its original value was restored from ViewState. The Text value of neither TextBox ever made it into ViewState at any stage. This property was managed purely by the LoadPostData method all the time.

How do we retain client-side changes to values or state of non-form field controls? Anything that happens on the client is totally shielded from the server, unless we let the server know. The server is completely unable to "read" the results of client-side operations, which is just as it should be for security reasons. So we need to let the server know that changes have taken place. One way to do this is to create a hidden field in the page. Hidden fields, being standard form fields will be looked after by their LoadPostData method, so any changes in value will be persisted across postbacks. It would be relatively simple to extend the client-side btchange() function to read and write changes to the hidden field, and copy changes to the Label.

UPDATE as a result of Jason's question:

Here's how to do just that. First, we'll add a HiddenField control to the page so that the ASPX code now looks like this:


<%@ Page Language="VB" AutoEventWireup="false" CodeFile="ButtonClickWithJavascript.aspx.vb"
    Inherits="ButtonClickWithJavascript" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
    http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Untitled Page</title>

    <script type="text/javascript">
        function btchange() {
            var label = document.getElementById("Label1");
            var textbox = document.getElementById("TextBox1");
            var hidden = document.getElementById("Hidden1");
            label.innerText = "Javascript changed!";
            textbox.value = "Text from Javascript!";
            hidden.value = label.innerText;
        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>
            <tr>
                <td>
                    <asp:Label ID="Label1" runat="server">Label</asp:Label><br />
                    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox><br />
                    <input type="button" onclick="btchange();" value="Javascript Change" />
                </td>
                <td>
                    <asp:Label ID="Label2" runat="server">Label</asp:Label><br />
                    <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox><br />
                    <asp:Button ID="Button1" runat="server" Text="Button"></asp:Button>
                    <asp:HiddenField ID="Hidden1" Value="" runat="server" />
                </td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>


You should also notice the change in the Javascript where the hidden field is referenced, and its value set to that of the label once it has been modified by the script. There is also a modification required to the code-behind. If the page is posted back, we need to grab the value of the Hiddenfield (which as you remember was modified by Javascript) and set that value to the Label control:


Partial Class ButtonClickWithJavascript
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        If Not Page.IsPostBack Then
            Label1.Text = "Original Label"
            TextBox1.Text = "Original Text!"
        Else
            Label1.Text = Hidden1.Value
        End If
    End Sub

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        Label2.Text = "Button changed"
        TextBox2.Text = Now().ToString()
    End Sub
End Class

Now, when you click the button and post the page back, the Label control's value is persisted.