UGTS Document #78 - Last Modified: 8/29/2015 3:23 PM
Pitfalls of Server Side Code Blocks in ASP.NET
Controls Collection Cannot Be Modified
ASP.NET web forms provide a great deal of flexibility, but not all of the features are fully supported. If you've ever tried to add server side controls dynamically to a page, you may have run across the following runtime HttpException:
The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>).
This error will occur whenever you have an ASPX page with Outside Code (<% ... %>) or Equals (<%= ... %>) blocks, and somewhere the page is dynamically adding server side controls to the Page.Controls tree. 'Outside' means that these blocks are not contained within a server side control (runat='server'). The error does not occur for Data Binding blocks (<%# ... %>). The Exception will specifically be raised at the time you try to add the control to the Controls tree, and the Controls tree has IsReadOnly = true.
Why does this happen, and why doesn't it happen when these tags are under a server side control like <asp:Placeholder> ?
If you compare the generated code for your ASPX page in Reflector for the case of having dynamic code blocks inside and outside server side tags (runat='server'), you'll see that the code generated for the BuildControlTree and Render methods has a subtle difference:
At this point you may be thinking: what’s the big deal about having a custom rendering function for the page, why should that stop me from modifying the Page.Controls collection?
Well, it turns out that ASP.NET has to override the custom rendering method for the page because dynamic code blocks are a whole lot more powerful than simple text substitution and HTML generation. You can have a dynamic code block which starts a for loop, then have literal HTML and more dynamic code blocks, and then have a dynamic code block to end the loop, which allows you to generate a kind of HTML repeater. You can do all kinds of program flow control and branching with dynamic code blocks up to the point where the page looks more like a method than a document. To render that page properly, all of that code needs to go into the custom rendering function, and the rendering function that ASP.NET generates has to hard-code server side control references by index into the Controls tree. Because of this, ASP.NET takes a heavy handed approach in this case and locks down the Controls tree, denying you the ability to modify it, so that those lookups remain correct (they don't crash or render the wrong controls), and so that you don't add controls and then wonder why they don't render on the page.
At this point you may object: 'It might make sense to override render if there are <% ... %> code blocks, because those might contain program flow statements such as loops and branching, but why is that necessary if the page only has <%= ... %> blocks, which seemingly cannot change program flow? Why should ASP.NET adopt such a heavy-handed restriction to handle an edge case that people rarely use?' Rick Strahl asks this question on his website. I can only guess that there may be pathological cases Microsoft is aware of where program flow can change even for these kinds of tags during rendering, or it may be necessary for backwards compatibility, or maybe Microsoft just goofed up and applied the restriction where it wasn’t necessary. Whichever the reason, it does make development of dynamic pages painful with ASP.NET.
There are two workarounds for this problem:
Again it bears repeating that neither workaround is necessary unless the page has server side controls, and some of those controls are added dynamically. A simple analysis of these two approaches suggests that you should divide up the web pages in your site into two types - those which use server side controls (web form pages), and those which don't (raw HTML pages). Web form pages should either use data binding expressions with DataBind calls, or they should wrap the dynamic code in server side tags. Raw HTML pages can use dynamic code blocks without any problems.
Data Binding Blocks Output Nothing
Data Binding blocks don't have the Controls collection problem that Equals blocks have, but they have their own pitfall - unlike Equals blocks, if you do not call Page.DataBind, data binding blocks do nothing. This webpage discusses the difference between Data Binding and Equals blocks, and here is an article specifically on Data Binding. The summary of the differences is that both types compile to dynamic code on the page, but that Data Binding blocks are evaluated at the PreRender step, whereas Equals blocks are evaluated later at Render. Because of this, data binding expressions can be used to dynamically assign property values to server side controls, but Equals block can't. If you try it, the Equals block text is just passed verbatim as a literal string to the property, which is almost certainly not what you want.
Data Binding blocks don't have the Controls collection modification issue because they don't have the ability that Equals blocks have that gets them into trouble - they can't be used to make changes to program flow like Equals blocks do. Becuase they do all of their work at PreRender, they don't directly affect how the Render step is to be executed.
Dynamic property values are the primary reason why Data Binding blocks exist - to bind dynamic data to controls. You can also have Data Binding expressions outside of control properties, but they will be replaced at PreRender time with an empty string unless the page has called the DataBind function at some point before PreRender in the page's lifecycle. This point bears repeating: Data Binding expressions do nothing unless the Page.DataBind method is called, either by the CodeFile or CodeBehind or an inherited base class.
But you might say, why can't ASP.NET detect for me that I obviously need to call DataBind and just do it for me, or perhaps throw an error if DataBind had not been called but there was a Data Binding expression on the page? First, Microsoft chose not to do it for you to force you to make the choice of where to do it, because sometimes DataBind calls do database access, and that can be very slow. On the initial request, you might not want to do it at all, and instead wait for a postback event. This is also why no Exception is thrown if DataBind is never called. It's a legitimate use of a Data Binding block to display empty content on the initial page load so that the page loads more quickly, and only fill in content with data as the user requests it. Personally I prefer to make sure that all data binding calls are fast, and always call DataBind in a base class from which all web pages inherit. But that approach may not work for your particular website.
Controls are Null until Init
The ASP.NET page lifecycle shows that the control which you add through the designer and markup on the ASPX page are initialized just prior to the Init event. This includes creation on those controls. Prior to that, at the PreInit event, all theses controls are null. Normally this is not an issue for you unless you are using C#, where you must explicitly add event handlers for all events because there is no Handles keyword. When you add event handlers, you should wire the page's event in the contructor for the page, and the controls events during Page.Init. If you try to wire up the event handlers for the controls during PreInit, the page will crash because at that point the controls are still null.
Custom Controls have Null Child Control Members
Custom controls have an overridable method CreateChildControls, but it does not work like you might expect it to. On the first request to a page, it is called after the Page.Load event, and it is only called before on postbacks to the page, unless you also call the method EnsureChildControls earlier than that. EnsureChildControls is a protected method of the Control base class - it is meant to be called from within your custom control class, usally from within the control's Init event, unless you have a good reason to call it earlier or later than that. If you don't do this, you'll probably get null reference exceptions when you try to set the properties of your custom controls during the Init or Load events.
Master Pages and Dynamic Control Creation at PreInit
If you try to add controls dynamically to the Controls collection in a web page that uses a master page, you might get the error:
Content controls have to be top-level controls in a content page or a nested master page that references a master page.
Usually people get this error when they try to add a master page to an existing page, but they do an incomplete job of it, and leave some content in the page which is outside the page directives and asp:Content tags. In this case, the cause of the error is straightforward - the page uses a master page, and it has content which is sitting outside of the content tags - and this is not allowed.
However, you may also get this error if you're trying to add controls dynamically to the Page.Controls collection at the PreInit event. The error (if you receive it) will be not be raised immediately when you try to add the control, but just afterwards from the call stack:
System.Web.UI.MasterPage.CreateMaster(TemplateControl owner, HttpContext
context, VirtualPath masterPageFile, IDictionary contentTemplateCollection)
As the call stack shows, the page is setting up the master page relation immediately after PreInit, and at this point the CreateMaster function sees controls in the Controls collection which are sitting outside of asp:Content tags, which is bad - the master page does not know where to put them.
If you dynamically add the controls at the Init event, there is no
problem - because at that point the master / content layout has already been
determined, and after that, controls are appended to the end of the page
(even past the closing </html> tag).
This is also the recommended way according to the ASP.NET life cycle to handle the issue - create the dynamic
control (if necessary) during PreInit, and then add it to the Controls
collection at Init. If you want better control over where the control
is placed in the page, then you should add it to a subcontrol instead of
Page.Controls, insert it somewhere in the middle of the Controls list
instead of appending it to the end, or set Visible = false so that only the
control does not show up in the page's HTML at all, but the state is
maintained in the __VIEWSTATE value.