What does runat="server" do for HTML controls

One thing I didn't fully understand when I first started using ASP.NET is exactly what runat="server" does. It sounds like some magical thing where code is running on the server for your control and not on the client. In some cases something like that does happen, but the basics are much simpler than that.

Let's take a look at the source code generated for a simple control using the trick I blogged about yesterday. We'll start with this simple page:

 <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="https://www.w3.org/1999/xhtml" >
<head>
    <title>Untitled Page</title>
</head>
<body>
    <button>foo</button>
</body>
</html>

Here's a look at some of the source generated (I've simplified a bunch of the debugging info for clarity):

         private void @__BuildControlTree(default_aspx @__ctrl) {
            
            this.InitializeCulture();
            
            System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
            
            @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl(
                        "\r\n\r\n<html>\r\n<head>\r\n    <title>Untitled</title>\r\n</head>\r\n<body>\r\n    <button>foo" +
                        "</button>\r\n</body>\r\n</html>\r\n"));
            
        }

Notice that the control tree simply has a LiteralControl with all of the HTML, including the button. Without runat="server", nothing special is done with it. But, if we add runat="server", we get:

         private global::System.Web.UI.HtmlControls.HtmlButton @__BuildControl__control2() {
            global::System.Web.UI.HtmlControls.HtmlButton @__ctrl;
            
            @__ctrl = new global::System.Web.UI.HtmlControls.HtmlButton();
            
            System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
            
            @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("foo"));
            
            return @__ctrl;
        }
        
        private void @__BuildControlTree(default_aspx @__ctrl) {
            
            this.InitializeCulture();
            
            System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
            
            @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl(
                  "\r\n\r\n<html>\r\n<head>\r\n    <title>Untitled</title>\r\n</head>\r\n<body>\r\n    "));
            
            global::System.Web.UI.HtmlControls.HtmlButton @__ctrl1;
            
            @__ctrl1 = this.@__BuildControl__control2();
            
            @__parser.AddParsedSubObject(@__ctrl1);
            
            @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n</body>\r\n</html>\r\n"));
        }

Now BuildControlTree adds a literal control for the stuff before the button, adds an HtmlButton for the button itself (with the text inside the control as a literal), and then another LiteralControl for the rest HTML on the page. What happens is that when the control tree is rendered, the HtmlButton will render itself with a <button> tag. In this case, you'll end up with the exact same HTML in the browser as our first example, but let's see what else we can do.

One thing you might notice when you set runat="server" is that the set of properties available in the Properties Window changes. When you don't have runat="server", what you are looking at is the standard <button> properties. Once you turn on runat="server" what you are looking at is the properties on HtmlButton (which are generally a superset of the HTML properties). For example, if you try adding visible="false" to the button without runat="server", the visible="false'" is just rendered as part of the LiteralControl for the HTML and the browser ignores it because that's not an HTML property on buttons. But, if you do so with runat="server", you'll see in the server side .cs source code that the HtmlButton's visible property is set to false. When the page is rendered by the server, the HtmlButton doesn't render any code for the button and you don't end up with any HTML for the button on the browser.

The other thing we can do is get programmatic access to the button in our code behind. To do so, we need to add an id to the button. If we switch the button to:

     <button runat="server" id="_button">foo</button>

Then, the generated source code will include:

     protected global::System.Web.UI.HtmlControls.HtmlButton _button;

Now, we could add the following to our Page_Load:

     protected void Page_Load(object sender, EventArgs e)
    {
        _button.InnerText = "bar";
    }

After the control tree is built, it will get into Page_Load and we'll change the InnerText of the button. When the server renders the control tree, it will output "bar" for the text of the button instead of "foo" and the resulting HTML will have <button id="_button">bar</button>.

Something interesting to note is that this works for any kind of HTML tag. In some cases ASP.NET has richer classes backing the HTML tags (like HtmlButton). But, if you just put something like <foo runat="server">, it will go through the same process, but give you an HtmlGenericControl.

One thing you can't do with the <button> is add a click handler that runs code on the server. To do that, you'll need the asp:button Web Server control. Those are somewhat different beasts than HTML controls. I'll discuss those in a future post.