Daniel Kolman

NHibernate, lazy proxy třídy a chyba C# kompilátoru

| 0 comments |

Kompilátor C# v .NET verze 2.0 obsahuje chybu, díky které při použití where T : class pro omezení typového parametru generické třídy vygeneruje špatný IL kód. Díky tomu se takovou assembly vůbec nepodaří nahrát do paměti, runtime vyhodí výjimku:

System.BadImageFormatException: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)

Popis naleznete na Microsoftu nebo taky na Ayendeho blogu.

Když používáte NHibernate, a zvlášť pokud jste právě přešli na verzi 1.2, může se tato výjimka objevit i když vůbec where T : class nepoužijete. Jak to? NHibernate vám při načtení objektu může místo vaší třídy vrátit dynamicky generovanou proxy třídu (a ve verzi 1.2 je to default). V ní jsou všechny virtual members nahrazeny proxy metodou, která při prvním přístupu k objektu tento objekt načte z databáze (materializuje). No a když ve vašem objektu použijete virtuální metodu s generickým parametrem, např:

protected virtual T GetProperty<T>(string name)
{
...
}

snadno se dopracujete k výše zmíněné výjimce. Je to pravděpodobně (hlouběji jsem to nezkoumal) způsobeno tím, že generátor dynamické proxy třídy (Castle.DynamicProxy.dll) se pokouší emitovat kód, který podmínku where T : class obsahuje, a pokus nahrát tuto třídu do paměti skončí s chybou. Proto používáte-li NHibernate a "lazy" načítání objektů, nepoužívejte virtuální generické metody.

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.