03/05/2011

The Controls collection cannot be modified because the control contains code blocks (i.e. % ... %)

Home

The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>). to paskudny błąd, który generowany jest przez klasę System.Web.UI.ControlCollection. Ma to miejsce przy próbie modyfikacji kolekcji, na przykład przez wywołanie Add(Control control), w przypadku kiedy kontrolki w niej zawarte korzystają z wyrażeń postaci <% ... %> lub <%= ... %>. Istnieje kilka sposób ominięcia tego problemu:
  • Kod zawierający wyrażenia <% ... %> lub <%= ... %> umieścić w dodatkowym bloku <div runat="server">...<div/>. Na przykład taki kod:
    <form id="form1" runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
    </form>
    
    zamienimy na taki:
    <form id="form1" runat="server">
        <div runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
        </div>
    </form>
    
    W drugim przypadku modyfikacja kolekcji (form1.Controls.Add(new Label());) nie spowoduje błędu ponieważ kontrolki form oraz div posiadają oddzielne kolekcje ControlCollection i tylko kolekcja kontrolki div jest w trybie tylko do odczytu.
  • Jeśli mamy dostęp do kontrolek Telerika to możemy skorzystać z RadCodeBlock.
Poniżej jeszcze dwa, mniej ogólne, rozwiązania problemu:
  • Zastąpienie <%= ... %> przez <%# ... %>. Na przykład taki kod:
    <%= Environment.MachineName %>
    
    możemy zastąpić takim:
    <%# Environment.MachineName %>
    
  • Użyć kontrolki Literal i taki kod:
    <%= Resources.lbl.Title %>
    
    zastąpić takim:
    <asp:Literal runat="server" Text="<%$ Resources: lbl, Title %>"/>
    
Rozwiązanie problemu to jedna sprawa. Mnie zaciekawiło dlaczego tak się dzieje. Poszukiwania rozpocząłem od przyjrzenia się implementacji kolekcji ControlCollection. Tutaj jak zwykle pomocny okazał się .NET Reflector. Na początki dowiedziałem się, że kolekcja ma prywatne pole _readOnlyErrorMsg; . Jeśli jest ono różne od null i nastąpi próba modyfikacji kolekcji to generowany jest wyjątek:
...
if (this._readOnlyErrorMsg != null)
{
  throw new HttpException(SR.GetString(this._readOnlyErrorMsg));
}
...
Pole to ustawiane jest tylko w jednym miejscu, w metodzie ControlCollection.SetCollectionReadOnly(string errorMsg). Metoda ta wywoływana jest w kilku miejscach, ale tylko w jednym przypadku parametr errorMsg odpowiada komunikatowi The Controls collection cannot be modified.... Ma to miejsce w metodzie Control.SetRenderMethodDelegate(RenderMethod renderMethod). I tu pojawił się problem. Okazało się, że ta metoda nie jest nigdzie wywoływana, a przynajmniej tak twierdził .NET Reflector (i w sumie się nie pomylił).

Wywołanie tej metody znalazłem w plikach generowanych dynamicznie dla stron aspx. Znajdziemy je w katalogu z plikami tymczasowymi C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\.... Dokładej lokalizacji nie podaję ponieważ nazwy tych plików generowane są w sposób losowy. Dla przykładu z początku postu (z pętlą for) zostanie wygenerowany taki kod:
...
@__ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.@__Renderform1));
...
Metoda @__Renderform1 wygląda natomiast tak (po usunięciu dyrektyw kompilatora):
...
private void @__Renderform1(System.Web.UI.HtmlTextWriter @__w, System.Web.UI.Control parameterContainer) 
{
  @__w.Write("\r\n    \r\n        ");
  for (int i = 0; i < 5; ++i) 
  { 
    @__w.Write("\r\n    <br />\r\n    ");
    parameterContainer.Controls[0].RenderControl(@__w);
    @__w.Write("\r\n    ");
    @__w.Write( i.ToString() );
    @__w.Write("\r\n    ");
  }
}
...
Co w tym takiego zdrożnego, że silnik ASP.NET nie pozwala na modyfikowanie kolekcji? Moim zdaniem kluczowa jest linijka zawierająca odwołanie to kolekcji Controls. Jak widać indeks kontrolki do jakiej się odwołujemy jest ustawiony na sztywno w kodzie. W takim przypadku modyfikowanie kolekcji kontrolek byłoby albo bezcelowe bo i tak kontrolka nie została by wyrenderowana albo doprowadziło to wyrenderowania innej kontrolki niż trzeba.

Ciekawe posty na ten temat można znaleźć również na blogu Rick Strahl's Web Blog. Podaje tylko tytuły artykułów, a nie linki ponieważ w momencie pisania tego postu artykuły te były dostępne tylko w cache Google'a:
  • Reliably adding Controls or Markup before or after an ASP.NET Control?
  • Understanding how <% %> expressions render and why Controls.Add() doesn't work

1 comments:

Michal Franc said...

Och uratowałem godzinę googlowania dzięku temu postowi ;) Dzięki

Post a comment