Daniel Kolman

Jak může GridView přijít o ViewState

| 1 comments |

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í:
  1. 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.

  2. 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.