marți, 31 mai 2011

About the "The Controls collection cannot be modified because the control contains code blocks" symptom

As the error message says, if a control contains code blocks (but not databinding expressions, ie <%# ..), its Controls collection can't be altered, by adding another control at runtime using Controls.Add. However, this rule doesn't apply for child controls, for example if a control uses other control with code blocks, then the first control's Controls collection (the parent's one) has no limitations.

Usually this error is encountered mostly when working with the <head runat="server".
Suppose there is a code block which uses some code to decide which protocol to use to download some external scripts, depending on the connection used by the client.
 <head runat="server">
<% if (!HttpContext.Current.Request.IsSecureConnection)
{ %>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" type="text/javascript"></script>
<% }
else
{%>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" type="text/javascript"></script>
<% } %>
</head>
.. and later in the code behind it is decided to add some meta information using
((HtmlHead)Page.Header).Controls.Add(new HtmlMeta {....});

This will raise the above error, the collection is altered and the collection "contains" code blocks.

Less obviously, or at least for a beginner, is when code blocks are used on templated controls, for example in a LayoutTemplate. This time the Controls collection is modified by the control's creator( ASP .Net team or a third party), not by the developer.
     <asp:ListView ID="lvItems" runat="server" DataSourceID="edsCourses">
<LayoutTemplate>
<h2>Search results <%= !String.IsNullOrEmpty(Request.Params["search"]) ? "for &quot;" + HttpUtility.HtmlEncode(Request.Params["search"]) + "&quot;" : String.Empty %></h2>
..............

Why using code blocks, anyway? Well, some would say this is an anti-pattern, but I would like to consider it a nice feature, a painless one. In the previous ListView I could use an <asp:Literal ID="ltSearchTerm and a handler for ListView.DataBound event, where I would do something like ((Literal)lvItems.FindControl("ltSearchTerm")).Text = ... . So: add a handler, find the control plus additional checks for null and set the Text property. A good/defensive practice, but might be anti-RAD. Of course, if the project is an Web Application type, this needs to be compiled.

I've seen on web several techniques to safely use code blocks.
  1. Wrap method, using placeholders
  2. UserControl method
  3. Call on DataBind()

1. Wrap Method: wrap the code block with an <asp:PlaceHolder
<head runat="server">       
<asp:PlaceHolder runat="server">
<% if (!HttpContext.Current.Request.IsSecureConnection)
{ %>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" type="text/javascript"></script>
<% }
else
{%>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" type="text/javascript"></script>
<% } %>
</asp:PlaceHolder>
</head>

In this way the code block is not considered part to the Header.Controls collection, instead is a child of that PlaceHolder and this one a child of Header. The PlaceHolder is good because it doesn't emit any html. The Controls collection can be changed, which means the metadata can be added with no exception raised.
In a master page I saw the suggestion to wrap with an <asp:ContentPlaceHolder. I would use this if it is going to be used further in the pages. In this case the content will be replaced.

2. User Control method

The idea is to move the code block into an user control. This it doesn't need to have any code behind so the file ascx.cs and ascx.designer.cs can be removed.
<%@ Control Language="C#" AutoEventWireup="true" %>
<h2>Search results <%= !String.IsNullOrEmpty(Request.Params["search"]) ? "for &quot;" + HttpUtility.HtmlEncode(Request.Params["search"]) + "&quot;" : String.Empty %></h2>

and use as

<asp:ListView ID="lvItems" runat="server" DataSourceID="edsCourses">
<LayoutTemplate>
<ldb:SearchResultsTopHeader ID="ldbSearchResultsTopHeader" runat="server" />

I prefer this to the placeholder when the control will be used in more than one place.

3. Call on DataBind()

Any expression <%= is transformed in a binding expression, <%# and the parent control calls DataBind() (ie. Page.Header.DataBind()). This works on header, but for sure it doesn't work for a LayoutTemplate. And maybe more important, the code blocks in here <% ... %> could be very complicated and not so easy to transform them in databinding expressions. .. And there is introduced the call on DataBind() also.

Niciun comentariu:

Trimiteţi un comentariu