I’ve been trying to understand the problem of the inability to use Controls.Add() when <% %> tags are present in a container for some time and today as I’m working on this page I finally understand why this is a problem while I’m looking at the innards of the generated classes that ASP.NET 2.0 creates dynamically from the ASPX markup…
When a container contains static text or plain control definitions the entire control parsing is set up by using AddParseObject() to the control. So if I have a container that contains some literal text and a text box the parse tree method for the container generated by ASP.NET will look like this:
private global::System.Web.UI.WebControls.Panel @__BuildControlPanel2() {
global::System.Web.UI.WebControls.Panel @__ctrl;
@__ctrl = new global::System.Web.UI.WebControls.Panel();
this.Panel2 = @__ctrl;
@__ctrl.ApplyStyleSheetSkin(this);
@__ctrl.ID = "Panel2";
System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
@__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n here\'s some literal text:\r\n "));
global::System.Web.UI.WebControls.TextBox @__ctrl1;
@__ctrl1 = this.@__BuildControltxtPanel2();
@__parser.AddParsedSubObject(@__ctrl1);
@__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n "));
return @__ctrl;
}
All of the control layout and formatting for the Panel is handled in the Build method which creates the instance and adds the controls to the parse tree.
If the Panel also contains markup text (for example adding a simple <%= DateTime.Now %> and script block) the Build routine only handles adding the controls, but doesn’t handle any of the layout tasks – it doesn’t create literal controls. Instead it uses SetRenderMethodDelegate() to delay the control layout task until render time.
Assume we have a Panel on a page that looks like this:
<asp:Panel ID="Panel1" runat="server" Height="251px" Width="517px">
<%= DateTime.Now %>
<asp:GridView ID="grdListInPanel" runat="server">
</asp:GridView>
<%
int x = 4;
Response.Write(x);
%>
<br />
<asp:Button ID="btnGoInPanel" runat="server" OnClick="Button1_Click" Text="Button" />
</asp:Panel>
The generated Parse Tree method then looks something like this:
private global::System.Web.UI.WebControls.Panel @__BuildControlPanel1() {
global::System.Web.UI.WebControls.Panel @__ctrl;
@__ctrl = new global::System.Web.UI.WebControls.Panel();
this.Panel1 = @__ctrl;
@__ctrl.ApplyStyleSheetSkin(this);
@__ctrl.ID = "Panel1";
@__ctrl.Width = new System.Web.UI.WebControls.Unit(517, System.Web.UI.WebControls.UnitType.Pixel);
global::System.Web.UI.WebControls.GridView @__ctrl1;
@__ctrl1 = this.@__BuildControlgrdListInPanel();
System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
@__parser.AddParsedSubObject(@__ctrl1);
global::System.Web.UI.WebControls.Button @__ctrl2;
@__ctrl2 = this.@__BuildControlbtnGoInPanel();
@__parser.AddParsedSubObject(@__ctrl2);
@__ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.@__RenderPanel1));
return @__ctrl;
}
Notice that the Build method does nothing with the <% %> blocks and doesn't inject any literal controls. All of this is deferred into the RenderPanel1 method. A Render method for a control is only created IF there is markup script in the container. Otherwise the rendering is managed entirely through the Controls collection of the container control. The Render method of the Panel that contains the markup code then looks like this:
private void @__RenderPanel1(System.Web.UI.HtmlTextWriter @__w, System.Web.UI.Control parameterContainer) {
@__w.Write("\r\n ");
@__w.Write( DateTime.Now );
@__w.Write("\r\n ");
parameterContainer.Controls[0].RenderControl(@__w);
@__w.Write("\r\n ");
int x = 4;
Response.Write(x);
@__w.Write("\r\n <br />\r\n ");
parameterContainer.Controls[1].RenderControl(@__w);
@__w.Write("\r\n ");
}
Notice that the method writes out literal code directly into an HTML TextWriter and simply embeds <%= DateTime.Now %> as a simple writer.Write() call – very efficient. The code snippet that was embedded in the panel (the int x=4 and Response.Write()) is directly embedded into the class method as pure code that executes at rendering.
ASP.NET generates this class at compile time, and notice that the Control indexes are hard coded. It’s clear that if you tried to inject a control into the Controls collection here wouldn’t have any effect at all – the control would never render since the control collection is not being accessed by using the Enumerator.
There are a couple of questions that spring to mind here. SetRenderMethodDelegate() allows this class to hook the container’s Rendering pipeline, but why isn’t the container rendering the controls that were added to the collection? If I understand the way that Render() and SetRenderMethodDelegate() works shouldn’t the original Render() of the Panel still fire? At least that’s what I’m seeing…
This should be fixable by the ASP.NET engine
The other thing that comes to my mind is that this problem of not allowing <% %> and Control.Add() should be relatively easy to overcome. Instead of creating this custom RenderMethod() for the code, it seems to me that <% %> expressions could be created as separate methods of the generated class. You could then have a DynamicExpression control that gets added to the control tree and sets up delegates that call these custom methods.
I can see some problems with this for complex code snippets. Some things you can do with snippets at least couldn’t be handled by calling out to a delegate. For example something like this:
<% if (!this.Item.IsInStock)
{
%>
Out of Stock (re-stock) <%= this.Item.DeliveryDate %>)
<% } %>
Ok, not a great example, since that sort of thing is much better encapsulated in a control and driven from CodeBehind, but ASP.NET does allow this sort of syntax. However, for those that use code or expressions in a page I think usage like the above is rare.
But it seems that at least <%= %> expressions COULD be handled by an external callout mechanism. Code snippets are much less likely to be in a page, but <%= %> expressions are often unavoidable, especially in script code where you might frequently need <%= this.txtName.ClientID %> to properly identify a control in the page. There are no easy workarounds for this particular problem short of sticking a literal control in the page for each and assign each one of them – Not.
Other Posts you might also like