I was a little confused about the difference between <%= expression %> and <%# expression %> in ASP.NET. It seems like both work in a lot of cases, but in other cases, only the # (data binding) version works. So, I decided to dig into it a little bit. To try it out I built this simple page:

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

<!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 runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<p>Equals: <%= this.TestValue %></p>
<p>Pound: <%# this.TestValue %></p>
<p>Equals label: <asp:Label runat="server" ID="_equals" Text="<%= this.TestValue %>" /></p>
<p>Pound label: <asp:Label runat="server" ID="_pound" Text="<%# this.TestValue %>" /></p>
</div>
</form>
</body>
</html>

And the code behind is:

public partial class _Default : System.Web.UI.Page 
{
protected void Page_Load(object sender, EventArgs e)
{
_testValue = "2";
}

protected void Page_PreRenderComplete(object sender, EventArgs e)
{
// DataBind();
_testValue = "3";
}

public string TestValue
{
get { return _testValue; }
}

private string _testValue = "1";
}

Here's what the result is when the DataBind() line is commented out:

Equals: 3

Pound:

Equals label:

Pound label:

And, when it's not commented out:

Equals: 3

Pound: 2

Equals label:

Pound label: 2

At first glance it looks like the Equals label case did nothing. But, if you view source, you see:

<p>Equals label: <span id="_equals"><%= this.TestValue %></span></p>

The literal expression made it down to the browser and it's just invalid HTML. What you can see as a result is:

  • The <%= expressions are evaluated at render time
  • The <%# expressions are evaluated at DataBind() time and are not evaluated at all if DataBind() is not called.
  • <%# expressions can be used as properties in server-side controls. <%= expressions cannot.

Now, let's look at the generated code to see how it works. It builds the control tree using the following code:

        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<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3" +
".org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n\r\n<html xmlns=\"http://www.w3.org/1" +
"999/xhtml\" >\r\n"));

global::System.Web.UI.HtmlControls.HtmlHead @__ctrl1;
@__ctrl1 = this.@__BuildControl__control2();
@__parser.AddParsedSubObject(@__ctrl1);
@__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n<body>\r\n "));

global::System.Web.UI.HtmlControls.HtmlForm @__ctrl2;
@__ctrl2 = this.@__BuildControlform1();
@__parser.AddParsedSubObject(@__ctrl2);

@__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n</body>\r\n</html>\r\n"));
}

@__BuildControl__control2() isn't that intersting, it's just the <head> stuff. The guts are in @__BuildControlform1():

        private global::System.Web.UI.HtmlControls.HtmlForm @__BuildControlform1() {
global::System.Web.UI.HtmlControls.HtmlForm @__ctrl;
@__ctrl = new global::System.Web.UI.HtmlControls.HtmlForm();
this.form1 = @__ctrl;
@__ctrl.ID = "form1";

global::System.Web.UI.DataBoundLiteralControl @__ctrl1;
@__ctrl1 = this.@__BuildControl__control4();
System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
@__parser.AddParsedSubObject(@__ctrl1);

global::System.Web.UI.WebControls.Label @__ctrl2;
@__ctrl2 = this.@__BuildControl_equals();
@__parser.AddParsedSubObject(@__ctrl2);

global::System.Web.UI.WebControls.Label @__ctrl3;
@__ctrl3 = this.@__BuildControl_pound();
@__parser.AddParsedSubObject(@__ctrl3);

@__ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.@__Renderform1));
return @__ctrl;
}

This is building all of the controls in the form. First let's look at the control built with @__BuildControl__control4():

        private global::System.Web.UI.DataBoundLiteralControl @__BuildControl__control4() {
global::System.Web.UI.DataBoundLiteralControl @__ctrl;

@__ctrl = new global::System.Web.UI.DataBoundLiteralControl(2, 1);
@__ctrl.SetStaticString(0, "</p>\r\n <p>Pound: ");
@__ctrl.SetStaticString(1, "</p>\r\n <p>Equals label: ");
@__ctrl.DataBinding += new System.EventHandler(this.@__DataBind__control4);

return @__ctrl;
}

And related to this is:

        public void @__DataBind__control4(object sender, System.EventArgs e) {
System.Web.UI.Page Container;
System.Web.UI.DataBoundLiteralControl target;

target = ((System.Web.UI.DataBoundLiteralControl)(sender));
Container = ((System.Web.UI.Page)(target.BindingContainer));
target.SetDataBoundString(0, System.Convert.ToString(this.TestValue, System.Globalization.CultureInfo.CurrentCulture));
}

This is the part of the page up to the first <%#. You can see that it uses a DataBoundLiteralControl which is divided between static strings and data bound strings. When DataBind is called on the page, the event handler is called and SetDataBoundString is called on the control. This is why the value is captured at bind time. If you want to see how DataBoundLiteralControl works, you can use .NET Reflector. You can see that it's Render function looks like:

    protected internal override void Render(HtmlTextWriter output)
{
int num1 = this._dataBoundLiteral.Length;
for (int num2 = 0; num2 < this._staticLiterals.Length; num2++)
{
if (this._staticLiterals[num2] != null)
{
output.Write(this._staticLiterals[num2]);
}
if ((num2 < num1) && (this._dataBoundLiteral[num2] != null))
{
output.Write(this._dataBoundLiteral[num2]);
}
}
}

So, it basically outputs the static and data bound fields alternately. That's how it renders the first <%# value on the page if DataBind() is called. Next, let's look at the "_equals" control:

        private global::System.Web.UI.WebControls.Label @__BuildControl_equals() {
global::System.Web.UI.WebControls.Label @__ctrl;

@__ctrl = new global::System.Web.UI.WebControls.Label();
@__ctrl.ApplyStyleSheetSkin(this);
@__ctrl.ID = "_equals";
@__ctrl.Text = "<%= this.TestValue %>";

return @__ctrl;
}

You can see that the <%= is not in any way special. It's just literal text. The "_pound" control, which uses <%# on the other hand, looks like:

        private global::System.Web.UI.WebControls.Label @__BuildControl_pound() {
global::System.Web.UI.WebControls.Label @__ctrl;

@__ctrl = new global::System.Web.UI.WebControls.Label();
@__ctrl.ApplyStyleSheetSkin(this);
@__ctrl.ID = "_pound";
@__ctrl.DataBinding += new System.EventHandler(this.@__DataBinding_pound);

return @__ctrl;
}

public void @__DataBinding_pound(object sender, System.EventArgs e) {
System.Web.UI.WebControls.Label dataBindingExpressionBuilderTarget;
System.Web.UI.Page Container;
dataBindingExpressionBuilderTarget = ((System.Web.UI.WebControls.Label)(sender));
Container = ((System.Web.UI.Page)(dataBindingExpressionBuilderTarget.BindingContainer));

dataBindingExpressionBuilderTarget.Text = System.Convert.ToString( this.TestValue , System.Globalization.CultureInfo.CurrentCulture);
}

As compared to the <%= version, <%# is treated specially in parameters. It adds a data binding handler and sets the property at data bind time. That's why <%# does work in parameters and why it picks up the value at data bind time.

So, how does the first <%= work? We can see this by looking at the following function:

        private void @__Renderform1(System.Web.UI.HtmlTextWriter @__w, System.Web.UI.Control parameterContainer) {
@__w.Write(" <p>Equals: ");
@__w.Write( this.TestValue );

parameterContainer.Controls[0].RenderControl(@__w);
parameterContainer.Controls[1].RenderControl(@__w);
@__w.Write("</p>\r\n <p>Pound label: ");
parameterContainer.Controls[2].RenderControl(@__w);
@__w.Write("</p>\r\n </div>\r\n ");
}

Here we can see the @__w.Write(this.TestValue) line which is the result of the <%= line in the source. It's outputting the value of this.TestValue at render time, which explains the behavior we saw.

So, it all makes sense to me now. I hope it makes sense to you too :-)