Aneb o side-effectech, CompositeDataBoundControl-ech a Watch okně.Minulý týden jsem řešil zapeklitý problém:
GridView občas při postbacku ztratilo řádky, tj. nenačetlo je správně z ViewState. Ovládací prvek, ve kterém bylo
GridView použito, přitom na jiných webových stránkách fungoval bez problémů.
Nejdřív jsem pátral po vytváření ViewState a jeho ukládání do stránky a načítání při postbacku. Máme totiž poměrně složitou infrastrukturu odvozenou od třídy
Page, která se nechová úplně standardně a občas vyvádí psí kusy. Jenže se to ukázalo jako špatná stopa: ViewState byl normálně uložen a přítomen ve skrytém poli v postbacku. Jeho struktura vypadala dobře, zdálo se, že jej prostě
GridView ignoruje.
Trochu jsem se porejpal ve zdrojových kódech .NETu (God Bless The
Reflector) a přišel jsem na to, že
GridView má dceřinný ovládací prvek typu
Table, který při postbacku dostane svůj ViewState a stará se o správnou rekonstrukci řádek tabulky. To znamená, že tento dceřinný prvek musí být vytvořen nejpozději v
OnLoad. Jenže na mé stránce měl v události
OnLoad můj
GridView prázdnou kolekci
Controls, ale přitom jeho
ChildControlsCreated bylo true. Když neexistuje dceřinný prvek
Table, nemá ani co nahrát ViewState.
Zajímavé přitom bylo, že pokud jsem ve Watch okně Visual Studia změnil
ChildControlsCreated na false, okamžitě se přepsalo zpět na true a do kolekce
Controls se přidal prvek typu
Table! Chvíli jsem přemýšlel o woodoo a černé magii, pak jsem si ale prošel seznam ostatních položek ve Watch okně a začal jsem je zkoumat. Na první pohled nic podezdřelého neobsahovalo. Když jsem z něj ale odebral položku
Controls.Count, toto podivné chování ustalo a
ChildControlsCreated se klidně nechalo nastavit na false.
Vrátil jsem se proto s Reflectorem do kódu a našel jsem zajímavý side effect při přistupování na kolekci
Controls, zavedený třídou
CompositeDataBoundControl: Před vrácením kolekce se volá
EnsureChildControls()! To vysvětluje ono podivné chování ve Watch okně, kde jsem měl kromě jiného i
Controls.Count. Když jsem přepsal
ChildControlsCreated na false, Visual Studio se pokusilo o refresh všech položek Watch okna, takže přístupem na kolekci
Controls spustilo i metodu
EnsureChildControls(), která nastavila
ChildControlsCreated zpět na true.
A bylo jasno! Pokud přistoupím na kolekci
Controls před natažením ViewState (tedy před
OnLoad), způsobím volání metody
EnsureChildControls(). Ta zavolá
CreateChildControls(), ale pouze při prvním spuštění. No a
CreateChildControls() nevytvoří žádný ovládací prvek, pokud nemá k disposici ViewState! Takže
GridView nemá dceřinný prvek
Table který načítá ViewState a ani jej nevytvoří, protože už je označená jako
ChildControlsCreated=true.
Z toho, děti, plynou následující poučení:
- Používáte-li ViewState, nepřistupujte na kolekci Controls před událostí OnLoad. Platí to pro všechny potomky CompositeDataBoundControl, tedy GridView, FormView a DetailsView.
- Dávejte si pozor na to, co máte ve Watch okně, neboť to může mít (blahý i neblahý) efekt na běh kódu.
PS: Poslal jsem to jako
bug na Microsoft, jsem zvědavej jak se s tím hoši poperou.
PPS: Nemusej to bejt jen hoši, koukal jsem nedávno na jeden
rozhovor na Channel9 s Politou Paulus a má na stole (teda na okně:-) ocenění za návrh architektury DataBoundControls, takže možná je to její práce.