\

Školení Návrhové vzory, OOP a UML


 Monday, January 03, 2011
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)

Update 4. 1.   2011  - upraven kód objektu UIHelper a spolupracující metody tak, aby byl klíč generovaný pro tranzientní stav každého view modelu skutečně unikátní. Když rozkliknete GISTy, můžete se podívat i na původní verzi kódu.

V předchozím článku jsme se podrobně věnovali vytvoření bázové třídy pro view modely včetně podpory ukládání tranzientního stavu během “tombstoningu” a na konci jsem slíbil, že další díl bude věnován hlavně propojení našich view modelů s view.

View pro nás bude hostitel view modelu a v souladu s WP7 modelem navigace mezi jednotlivými stránkami budeme za hlavního hostitele považovat potomka třídy PhoneApplicationPage. Uvidíme ale, že při vytváření View nejsme omezeni na potomky PhoneApplicationPage, protože budeme schopni vytvořit i view jako potomka třídy UserControl.

Zrekapitujme hlavní funkce, které by měl náš hostitel view modelů zvládat:

  1. Při vytvoření stránky je třeba najít ke stránce přidružený view model. Dodejme již nyní, že povinností hostitele bude dohledat i vnořené (dceřiné) view modely, jestliže je jedna stránka složena z více nezávislých view a každé view může být asociováno s jiným view modelem.

  2. Při vytvoření stránky je nutné u view modelu volat metodu Init pro základní inicializaci view modelu, jestliže view model podporuje naše rozhraní IInittialize. Znovu připomínám, co zaznělo v minulém článku, že si implementaci žádných rozhraní u view modelů nevynucujeme.

  3. Jestliže se stránka stane poprvé nebo po návratu uživatele “aktivní” (zobrazenou), je nutné volat na view modelu metodu Activate, když view model podporuje rozhraní IActivated.

    .
  4. Jestliže uživatel ze stránky odchází, hostitel na view modelu volá metodu Deactivate z rozhraní IDeactivated.

  5. Hostitel musí být schopen při “tombstoningu” uložit tranzientní stav view modelu.

  6. Po návratu z “tombstoningu” hostitel dovolí view modelu obnovit tranzientní stav.

  7. Hostitel se nás bude snažit ve view modelech odstínit od všech  WP7 nedomyšleností, záludností, nesmyslných omezení a programátorských vrtochů, které jsou nedílnou a z pohledu mobilní MS divize jistě i zábavnou součástí celkově kapriciozního životního cyklu třídy PhoneApplicationPage.

Nyní již můžeme přístoupit k vytvoření hostitele. Našim hostitelem bude třída PageBase, která je potomkem PhoneApplicationPage z WP7 frameworku. Nakonec jsem pro účely článku zvolil toto řešení, i když není problém v dalších dílech ukázat adaptér, který nás nutnosti vlastní stránky v aplikaci odvozovat z bázové třídy PageBase zbaví. Myslím si ale, že navigační model WP7 aplikací neposkytuje sám o sobě mnoho prostoru pro kreativitu, a proto nutnost dědit z PageBase místo třídy PhoneApplicationPage z WP7 za nějak zvlášť restriktivní nepovažuju.

Třída PageBase:

Kdykoli  dojde k aktivaci stránky, čímž míním první navigaci na stránku, nebo návrat na stránku pomocí tlačítka Back, je volána metoda OnNavigatedTo. My jsme přepsali metodu OnNavigatedTo a v ní vždy nastavíme svou vlastnost WasLayoutUpdateCalled na false. Vlastnost WasLayoutUpdateCalled použijeme za chvíli, zde jen řekněme, že tuto vlastnost potřebujeme k tomu, abychom poznali, že již je vytvořen celý vizuální strom ovládacích prvků a že můžeme s ovládacími prvky bez obav pracovat. Bohužel v metodě OnNavigatedTo vizuální strom prvků vytvořen být nemusí. O některých problémech, které je nutné řešit, když není zcela vytvořen vizuální strom prvků, jsem již mluvil u ovládacího prvku WebBrowser v první části tohoto seriálu.

Metoda OnNavigatedTo také dá příkaz k vyzvednutí dříve uloženého tranzientního stavu pomocí metody loadSavedTransientState z vlastnosti PhoneApplicationPage.State. Tranzientní stav, pokud existuje, je pouze uložen do vlastnosti LastSavedTransientState a zatím se s ním nijak nepracuje. Klíčem, pod kterým je uložen stav celé stránky včetně tranzientního stavu view modelů, je plně kvalifikované jméno aktuální stránky (string stateKey = GetType().FullName). Měli bychom si být vědomi, že při této implementaci nesmíme mít jednu stránku v aplikaci nahranou vícekrát, protože by různé instance stejné stránky mezi sebou sdílely stav. Když budete trvat na tom, že jedna stránka může mít v systému několik instancí, není problém změnit generování klíče, pod kterým bude stav pro každou unikátně identifikovanou instanci uložen.

Pokračujme v našem scénáři. V konstruktoru si přihlašujeme odběr události LayoutUpdated (LayoutUpdated += PageBase_LayoutUpdated;), abychom byli notifikováni, že již můžeme bezpečně pracovat s ovládacími prvky.

 

Obslužná metoda handleLayoutUpdated je po každé navigaci na stránku spouštěna  jen jednou, a proto se podíváme na hodnotu vlastnosti  WasLayoutUpdateCalled – jestliže má hodnotu true, nic dalšího neděláme. Jinak metoda zkontroluje, zda instance PageBase je nově vytvořena, k čemuž dojde při první navigaci na stránku a také po obnovení z “tombstonovaného" stavu. Jinak řečeno – když je volán konstruktor třídy PageBase, znamená to, že máme stránku v “panenském” stavu, protože v ní nejsou žádná data a dokonce ani nebyly připojeny view modely. A proto bezparametrický konstruktor PageBase nastavuje vlastnost IsNewInstance na true, abychom v metodě handleLayoutUpdated  věděli, že máme co do činění s novou instancí stránky. U nové stránky je třeba získat view modely, a pokud je stránka obnovena po “tombstoningu”, je třeba do view modelů nahrát tranzientní stav.  Při variantě “nová instance stránky” metoda handleLayoutUpdated volá metodu LoadState.

Jestliže nebyl volán konstruktor a stránka obsahuje veškerý stav, k čemuž většinou dojde, když se vrátíme bez “tombstoningu” pomocí tlačítka Back na stránku, metoda handleLayoutUpdated volá pouze pomocnou metodu handleAllActivated, která proiteruje všechny dříve nahrané view modely, a když podporují rozhraní IActivated, tak na nich zavolá metodu Activate.

Metoda LoadState ihned deleguje na metodu restoreTransientState, jíž předá v objektu ElementIndexPair odkaz na aktuální stránku (this), která představuje “root” všech prvků, a relativní index nastavený na 0 (první prvek na této úrovni). V dalším argumentu jako prvek, pro který má být obnoven stav, předá opět odkaz na aktuální stránku (this) a poslední argument, ve kterém metoda restoreTransientState očekává naposledy použitý view model, je null, protože se žádnými view modely se ještě nepracuje.

Metoda restoreTransientState nejprve zkontroluje, jestli předaný ovládací prvek představuje View. K tomu použije objekt podporující rozhraní IViewModelResolver, který je uložen ve statické vlastnosti ViewModelManager. Přesněji řečeno, ve vlastnosti ViewModelManager můžete uložit delegáta (Func), který vrací objekt realizující rozhraní IViewModelResolver. Ve statickém konstruktoru třídy PageBase ukládám do vlastnosti ViewModelManager funkci, která vytváří v mé aplikaci výchozí IViewModelResolver s názvem ViewModelResolver (ViewModelManager = () => new ViewModelResolver();)

Rozhraní IViewModelResolver

Metodě IsView předáte vybraný objekt a ona vám vrátí true, jestliže objekt považuje za view, pro které by měl existovat view model. Metoda ResolveViewModel přijme objekt představující view a dohledá k němu view model.

Pro lepší představu vám mohu bez podrobnějšího komentáře ukázat třídu, která v aplikaci používající Posterous API dohledá k view view model na základě jmenné konvence. Za View se považuje každá instance z třídy, jejíž název končí znaky “View”, a k tomuto View je vrácen view model, který se jmenuje stejně jako view, ale končí znaky “ViewModel”. K view s názvem LoginView je tedy vrácen view model s názvem LoginViewModel.

Vraťme se k metodě restoreTransientState. Když metoda zjistí, že objekt představuje view, pokusí se dohledat dříve uložený tranzientní stav. Stav je uložen pro každé view v objektu IDictionary, konkrétně v třídě KnownTypesDictionary, pod klíčem, kterým je úplné jméno ovládacího prvku – úplným jménem se rozumí jméno třídy ovládacího prvku + jména tříd všech jeho vizuálních předků (ancestors). Ke jménu třídy každého prvku je přidán pořadový indexu prvku mezi prvky na stejné úrovni vizuálního stromu. Klíč generuje třída UIHelper v metodě GetTransientStateKey.

Metoda restoreTransientState se poté s s využitím metody prepareViewModel pokusí získat přes ViewModelManager view model (ViewModelManager().ResolveViewModel(obj), dále zjistí, jestli view model podporuje rozhraní ITransientStateManager, IInitialize a a IActivated a pokračuje takto:

  1. Jestliže máme uložen tranzientní stav (došlo k “tombstoningu”) a view model podporuje rozhraní ITransientStateManager, zavoláme metodu LoadState z rozhraní ITransientStateManager (stateManager.LoadState(LastSavedTransientState[stateKey]); a view model tak dostane šanci načíst dříve uložená tranzientní data.

  2. Když nemáme uložen tranzientní stav a view model podporuje rozhraní IInitialize, zavoláme metodu IInitialize.Init. View model tak dostane šanci nahrát data, ať už se souboru, z webové služby nebo jiného datového zdroje, která mají být zobrazena v aktuálním view.

Poté, co view model obnoví nebo inicializuje svůj stav, je volána metoda activateAndSetDataContext, která na view modelech podporujících rozhraní IActivated zavolá metodu Activate a nastaví view model jako datový zdroj (“DataContext”) view.

Nakonec pro všechny dceřiné prvky, které mohou představovat vnořená view (např. user control), rekurzivně zavoláme opět metodu restoreTransientState. K získání dceřiných prvků a jejich relativního indexu mezi prvky na stejné úrovni zanoření ve vizuálním stromu je použita metoda UIHelper.GetChildren.

Metoda LoadState, restoreTransientState a další pomocné metody, o kterých jsme mluvili v předchozích odstavcích:

Za povšimnutí stojí ve scénáři metody restoreTransientState ještě několik dalších věcí:

  • Když zjišťujeme, jestli objekt podporuje rozhraní IInitialize (metoda getInitObjectWithSyncContext), ihned do view modelu zpropagujeme synchronizační kontext (initObject.SynchContext = SynchronizationContext.Current;), aby například událost PropertyChanged mohla být vyvolávána vždy v UI vláknu.

  • Pomocná metoda selectViewModel(frameworkElement, lastViewModel, currentViewModel); odpovědná za výběr view modelu zajistí, že když IViewModelResolver nenalezne pro view žádný view model, automaticky view přiřadí view model použitý u předchozího view. Kdy to potřebujete? Třeba když stránku rozdělíte na několik nezávislých “user controls”, tak se můžete rozhodnout, jestli bude mít každý user control (view) svůj view model, nebo view model vytvoříte jen pro celou stránku a “user controls” view modely automaticky “zdědí”. To byl jen jeden příklad - úroveň zanoření view je zcela ve vaší režii. Slovo “zdědí” je v uvozovkách, protože nejde o klasické dědení DataContextu, ale k předání posledně použitého view modelu dojde i tehdy, když dvě view nemají společné “nadview”.

Poznámka: Nejprve jsem chtěl volání metod Init a LoadState na view modelech a následné nastavení DataContextu u view dát do jiného vlákna. Problém je v tomto případě s dědením view modelů. Když přiřadíte view model, na kterém jste zavolali asynchronně metodu Init, dalšímu vnořenému View, tak se může stát, že metoda Init ještě nedoběhla, view model u prvního view nastaven není, ale u vnořeného (druhého) view již nastaven je, což může vést k podivným a kvůli použití více vláken obtížně reprodukovatelným chybám. Za asynchronní zpracování časově náročných scénářů zodpovídají samotné view modely, které by měly přepisovat virtuální metodu DoInternalAsyncInit z ViewModelBase. Metodu DoInternalAsyncInit jsem popisoval v předchozím článku.

Viděli jsme, kdy hostitel volá metody Init, Activate a kdy voláním metody LoadState obnoví tranzientní stav ve view modelech. Ještě nám zbývá projít, kdy ukládáme tranzientní stav a kdy notifikujeme view modely voláním jejich metody Deactivate o tom, že k nim přidružené view (stránka) není již aktivní.

Nebudeme při ukládání tranzientního stavu nijak pátrat, jestli došlo k “tombstoningu”, ale stav view modelů do PhoneApplicationPage.State uložíme vždy, když dojde k opuštění stránky. Hlavním důsledkem tohoto rozhodnutí je, že s obsluhou metod  Application_Launching, Application_Activated, Application_Deactivated a Application_Closing, kterou jsem popisoval minule, se nemusíte kvůli “tombstoningu” trápit a zmíněné metody můžete většinou zcela ignorovat.

O opuštění stránky jsm informováni v metodě OnNavigatedFrom, kterou přepíšeme. Metoda OnNavigatedFrom voláním metody SaveState uloží stav všech view modelů a poté metoda handleAllDeactivated na všech modelech  podporujících rozhraní IDeactivated zavolá metodu Deactivate.

Popisovat kompletně logiku v metodě SaveState nemá smysl, protože tato metoda je reverzní k metodě LoadState. Pro všechny view rekurzivně vyhledá jejich view modely a dovolí jim uložit pod unikátním klíčem view, který je vygenerován třídou UIHelper, tranzientní stav. Ten samý tranzientní stav view modelů, jehož obnovení v metodě LoadState jsem popisoval výše. 

Za komentář stojí, že podmínka if (stateManager != null && stateManager != lastDataContext) ošetřuje, abychom neukládali trazientní stav view modelu, který slouží jako DataContext u více View, opakovaně, ale vždy pouze jednou.

Tranzientní stav všech view modelů je ukládán do speciálního objektu Dictionary s názvem KnownTypesDictionary.

var statebag = new KnownTypesDictionary();

Připomínám, že při tombstoningu je možné uložit jen serializovatelné objekty. Představme si nyní, že KnownTypesDictionary je potomek třídy  Dictionary<string, object>. Do takového objektu Dictionary můžeme vložit kteroukoli instanci z třídy, která je přímo či nepřimo potomkem třídy Object, ale při pokusu o serializaci dostaneme chybu, protože naše potomky DataContractSerializer nezná a očekává, že v kolekci budou jen instance třídy Object, a ne instance odvozených tříd. Pomocí atributu KnownType, který je aplikován na KnownTypesDictionary, můžeme DataContractSerializer informovat, které všechny třídy má v objektu Dictionary očekávat. Jednou z  možností, jak to udělat, je předat atributu KnownType název statické metody, která seznam “známých” tříd vrací.

Dnes si ukážeme jednoduchou verzi třídy  KnownTypesDictionary, která v metodě GetKnownTypes vrátí fixní seznam "známých” tříd. Příště si ukážeme, jak budou “známé” třídy dohledány a registrovány  automaticky bez nutnosti vytvářet seznam známých tříd vždy znovu a “natvrdo” v každé aplikaci. V dalších dílech také zjistíme, jak právě vytvořená infrastruktura nám dovolí vytvářet WP7 aplikace příslovečným lusknutím prstu, tedy spíš nečetným lusknutím prstů o klávesnici. Smile

.

 

Předcházející články:

Tipy pro Windows Phone 7 aplikace I

Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”



Monday, January 03, 2011 3:46:50 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Compact .Net Framework | Návrhové vzory | Silverlight | WP7


 Friday, December 17, 2010
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”

 

Již v prvním dílu seriálu o vývoji WP7 aplikací jsem zmiňoval nejen to, že mobilní verze Silverlightu ve WP7 je založena na Silverlightu 3, ale také, že mobilní Silverlight má své unikátní rysy, které v žádné desktopové verzi Silverlightu nenalezneme. Jednou ze změn je životní cyklus aplikace, včetně tzv. tombstoningu. Termín ”tombstoning”, kterého se i  v tomto článku budu držet, abych nemusel zavádět nějaké směšně znějící české ekvivalenty, má asi naznačovat, že WP7 podporují nejen tradiční spuštění a ukončení aplikace, ale i jakýsi hybridní stav, v němž instance naší aplikace může být dočasně ukončena (“umrtvena”) například tím, že uživatel spustí jinou aplikaci, a poté může být z hlediska uživatele WP7 telefonu naše původní aplikace navrácena k životu,  a to dokonce ve stavu, v jakém ji předtím uživatel zanechal. O smysluplnosti “tombstoningu” mám své pochybnosti a raději bych ve WP7 viděl tradiční multitasking, ale vývojářův boj s chováním “zombie” aplikací ve stavu “tombstoningu” má také něco do sebe. V tomto článku bych rád posunul boj se “zombie-tombstonovanými” aplikacemi do dalšího levelu a nabídnul pár “cheatů” .Smile

Přechody mezi stavy WP7 aplikace nám nejlépe objasní  metody pro obsluhu životního cyklu aplikace, které jsou po založení projektu automaticky vygenerovány v souboru App.xaml.cs.

Metoda  Application_Launching je  provedena jednou po spuštění aplikace. Jde o metodu, ve které můžete načíst dříve uložená data z “isolated storage”.  Tato metoda NENÍ volána při  obnovení “tombstonované” aplikace.

Metoda Application_Closing je provedena jednou při ukončení aplikace. Jde o metodu, ve které typicky uložíte data do “isolated storage”. Jde o stejná data, která  načtete při dalším spuštění aplikace v již popsané metodě Application_Launching a můžeme tedy říci, že jde o data s delší záruční lhůtou, která mají význam pro různé instance aplikace. Když stáhnete z webové služby nějaké číselníky (typy zákazníků, kategorie objednávek) a nechcete je po startu aplikace stahovat vždy znovu, uložíte si je a při příštím spuštění aplikace rychleji naběhne, protože nemusí stahovat všechna data z webových služeb ihned po startu.

Další metody se týkají již tombstoningu. Metoda Application_Deactivated je volána vždy, když je vaše aplikace “tombstonována” a v této metodě byste měli uložit všechna data, která potřebujete k tomu, abyste mohli po návratu z “tombstonovaného” stavu aplikaci zobrazit uživateli tak, jako kdyby běžela celou dobu a k žádnému “tombstoningu” nedošlo. Pro vývojáře ale “tombstoning” ve skutečnosti znamená, že aktuálně běžící instance aplikace je zlikvidována! Můžeme tedy říci, že v této metodě hlavně ukládáme data s kratší záruční lhůtou, která mají význam jen pro další instanci aplikace po návratu z “tombstonovaného” stavu. Data, která chcete mít k dispozici i po obnovení z “tombstoningu” můžete ukládat pod vámi zvolenými identifikátory v objektu typu IDictionary ve vlastnosti PhoneApplicationService.Current.State. Data, která zde uložíte, musí být serializovatelná, protože se nedrží jen v paměti telefonu, ale jsou ukládána i do souboru.

Je ale nutné si uvědomit, že když je aplikace “tombstonována”, nemáte garantováno, že se uživatel do vaší aplikace vrátí a že budete obnoveni z “tombstonovaného” stavu. I v této metodě je proto vhodné ukládat data, která ukládáte v metodě Application_Closing popsané výše.

Uživatel může také spustit vaší aplikaci znovu, tedy spustit novou instanci aplikace, aniž by se vrátil k dříve “"tombstonované” aplikaci a vy svůj stav - data, o nichž jsem říkal, že mají kratší záruční lhůtu - dříve uložený v metodě Application_Deactivated nikdy nepoužijete. Když se uživatel vrátí do vaší aplikace, což většinou nastane po stisknutí tlačítko “Back”, kdy se v zásobníku dříve spuštěných aplikací, který je spravován přímé operačním systémem, stane aktivním vaše aplikace, musíte obnovit dříve uložený stav v metodě Application_Activated. Napsal jsem, že jde o zásobník aplikací, ale je potřeba si uvědomit, že jde spíš o metaforu, protože aplikace a jejich stránky nejsou nikdy fyzicky uloženy, ale při “tombstoningu” většinou bez milosti zlikvidovány, a metoda Application_Activated je volána v nové instanci naší aplikace. WP7 si jen v zásobníku pamatují, jaké aplikace a v jakém pořadí byly  spuštěny a která stránka v konkrétní aplikaci byla aktivní.

Dále je potřeba si osvojit tato pravidla:

  • Metoda Application_Activated NENÍ volána po spuštění nové (“netombstonované”) instance aplikace. Po spuštění nové (“netombstonované”) instance aplikace je volána pouze metoda Application_Launching.
  • Metoda Application_Deactivated NENÍ volána při  úplném ukončení aplikace. Při  úplném ukončení aplikace je volána pouze metoda Application_Closing.

Zjednodušeně bychom mohli odpovědnosti metod v souboru App.xaml.cs popsat tímto fragmentem kódu:

Měli bychom ale vědět, že k tombstoningu může dojít kdykoli a podle mých testů na emulátoru i reálném zařízení může být aplikace “tombstonována”, i když se zrovna obnovuje z předchozího “tombstonovaného” stavu a je v metodě “Application_Activated“.  Autoři WP7 se s “tombstoningem” moc nepatlají a bez skrupulí po určité době zlikvidují vlákna aplikace, což poznáte podle výjimky ThreadAbortException.

Také se mi nelíbí, že bych měl v tomto jediném souboru ukládat a obnovovat data pro všechny stránky (“formuláře”) aplikace. Rozhodl jsem se, že na obsluhu těchto metod rezignuju a místo životní cyklu aplikace se budu zajímat jen o životní cyklus view modelu (presentation modelu, chcete-li) u každé stránky ve WP7 aplikaci.

Dnes si ukážeme bázovou třídu pro view modely a další podpůrné třídy se službami, která nás svou spoluprací zbaví nutnosti při psaní každé stránky myslet na to, že autoři WP7 aplikací se vyžívají v recidivě nekrofilního chování u WP7 aplikací. Drahý bratr Sigmund Freud by se po prohlídce stavového automatu “tombstonované“ aplikace od architektů WP7 na své pohovce jistě tetelil radostí nad předaným šokujícím materiálem. Smile

Nejdříve si ale navrhneme minimální množinu vlastností, kterou by měl každý view model splňovat, abychom se již nemuseli životním cyklem WP7 aplikace příliš zabývat při návrhu každé stránky.

  1. Musíme být schopni nahrát data při vytvoření nového view modulu. Také bychom měli zajistit, že když dojde k “tombstoningu” aplikace ještě před získáním všech dat, nezůstane view model v nějakém nekonzistentním stavu s třetinou nahraných dat, ale po obnovení z “tombstoningu” dostane šanci nahrát data znovu.

  2. Budeme schopni při “tombstoningu” automaticky perzistovat dočasný stav každého view modelu. Od této chvíle začneme říkat dočasnému stavu stav tranzientní. V tomto článku se zabývám jen tranzientním stavem aplikace, o perzistování “trvanlivějších” dat do “isolated storage” se pobavíme v některém z dalších článků.
  3. Po obnovení aplikace z “tombstonovaného” stavu každý view model automaticky nahraje svůj tranzientní stav.
    Pro uložení a nahrání tranzientního stavu nadefinujeme rozhrani ITransientStateManager.

  4. Měli bychom dát šanci view modelům zareagovat na to, že stránka, ke které jsou přidruženy, se stala aktivní stránkou, i na to, že k nim přidružená stránka aktivní už není, ať už proto, že došlo k “tombstoningu” nebo uživatel přešel na jinou stránku v aplikaci.
    Pro tyto účely máme rozhraní IActivated a IDeactivated.

  5. Budeme mít sice bázovou třídu pro view modely, ale její použití si nebudeme vynucovat. View model může být v aplikaci kterákoli třída. I když tato třída nebude potomkem bázové třídy pro view modely, bude moci volitelně využít většinu služeb, které jsou popsány v přechozích bodech.

  6. Jedná se o view modely, měli bychom tedy na úrovní bázové třídy podporovat rozhraní INotifyPropertyChanged, které nám dovoluje notifikovat o změně hodnoty ve vlastnostech view modelu. Rozhraní INotifyPropertyChanged je ve WPF i v Silverlight aplikacích všudypřítomné, proto si navrhneme další bázovou třídu PropertyChangedBase, která nám kromě implementace rozhraní INotifyPropertyChanged přinese další užitečné služby.

 

Jaké další užitečné služby má třída PropertyChangedBase?

Nemusíme vyvolávat událost PropertyChanged zadáním jen zadáním názvu vlastnosti (RaisePropertyChanged(“UserName”)), což může vést k chybě za běhu aplikace, když uděláme překlep v názvu vlastnosti ((RaisePropertyChanged(“UseName”)), ale můžeme předat název vlastnosti ve formě lambda výrazu, jehož syntaxe je zkontrolována již kompilátorem. K tomu nám slouží metoda RaisePropertyChangedEvent(Expression> propertyDefinition). Potomci třídy PropertyChangedBase mohou o změně hodnoty vlastnosti informovat takto:

U potomků třídy PropertyChangedBase, kterými budou i naše view modely, si můžeme také zavoláním metody RaiseAllPropertiesChanged vynutit vyvolání události o změně hodnoty pro každou vlastnost. Další metodě s názvem RaisePropertiesChanged(params string[] properties) můžeme předat názvy vlastností, pro které má být vyvolána metoda PropertyChanged.

A teď si  již můžeme vytvořit slibovanou třídu ViewModelBase, která je potomkem třídy PropertyChangedBase a podporuje všechna rozhraní zmíněná dříve.

 

V konstruktoru třída ViewModelBase přijímá titulek zobrazované stránky, který je uložen do vlastnosti PageTitle.

Metoda Init z rozhraní Initialize by měla být volána vždy, když je view model vytvořen, nebo když je po obnovení aplikace z “tombstonovaného” stavu zřejmé, že view model nenahrál všechna data, což se zjistí voláním virtuální metody IsAllInitDataLoaded, která v této abstraktní třídě vždy  vrací true a čeká na to, až odvozené konkrétní view modely dosadí svou logiku, kdy je považován view model za nahraný. Metoda Init by měla být taky volána, když odvozený view model obnovená tranzientní data považuje za špatná / zastaralá a nemůže je používat, což nám dá najevo nastavením vlastnosti IsInvalidModel na true. K rychlému zjištění, jestli  je možné view model dále používat slouží derivovaná vlastnost CanUseModel, která spojuje logiku obsaženou ve vlastnosti IsInvalidModel a metodě IsAllInitDataLoaded.

 

Metoda Initialize.Init

V metodě Init nejdříve nastavíme vlastnost IsInvalidModel na true, protože naše metoda Init probíhá a kdyby došlo k “tombstoningu”, nemáme všechna data a je potřeba volat metodu Init znovu. Poté jen načteme titulek aplikace do vlastnosti AppTitle,  inicializujeme vlastnost SuppressValidating, která slouží k dočasnému potlačení ověřování platnosti data zadaných uživatelem ve view modelu, na hodnotu false. O vlastnosti SuppressValidating budeme více mluvit v dalším článku včetně vysvětlení významu metody ValidateData a vlastnosti HasValidData.

Dále ViewModelBase nastavením vlastnosti IsInvalidModel na false sděluje, že jeho inicializace proběhla, všechna data v modelu považuje za platná a dá šanci odvozeným třídám, aby inicializovaly svá data voláním chráněné virtuální metody DoInternalInit, která má ve ViewModelBase prázdnou implementaci a do které odvozené třídy dají svou specifickou logiku pro načtení dat. Vlastnost IsInvalidModel mohou samozřejmě odvozené třídy nastavit v metodě DoInternalInit opět na true, ale my ve vlastnosti IsInvalidModel hodnotu true nenecháváme, protože si přepsání metody DoInternalInit v odvozených třídách nevynucujeme a v bázové třídě ViewModelBase nemůžeme vědět, kdy odvozené třídy považují svá data za neplatná.

V metodě Init nakonec také voláním metody další chráněné virtuální metody s názvem DoInternalAsyncInit v samostatném vlákně šanci odvozeným třídám asynchronně inicializovat svá data. Metodu DoInternalAsyncInit by měly odvozené třídy používat k časově náročné inicializaci, kterou není vhodné vhodné dávat do synchronně  volané metody DoInternalInit a blokovat tak hlavní thread aplikace.

Mohlo by vás také zaujmout, že v sekci catch má speciální zacházení výjimka ThreadAbortException – tato výjimka není propagována výše, protože ji vyvolá samotné běhové prostředí Silverlightu při násilném “tombstoningu”, jak jsem poznamenal na začátku tohoto článku.

 

Rozhraní IActivated s metodou Activate a rozhraní IDeactivated s metodou Deactivate ve ViewModelBase mají jen prázdnou implementaci a čekají na to, jakou logiku do nich vloží odvozené třídy. Mimochodem  – na tomto místě bychom měli poprvé vytušit, že budeme potřebovat “hostitele” našich view modelů, který bude vědět, kdy volat metody Activate, Deactivate, Init a další.

Třída ViewModelBase explicitně implementuje rozhraní ITransientStateManager, které jsme si zavedli pro podporu automatického ukládání a nahrávání tranzientního stavu. Metody LoadState a SaveState ale po svém vyvolání jen předají řízení chráněným virtuálním metodám DoInternalLoadTransientState a DoInternalSaveTransientState, aby dali šanci i odvozeným třídám změnit způsob uložení a nahrání tranzientního stavu, i když odvozené třídy jsou většinou spokojeni s tím, že to za ně zvládne předek ViewModelBase.

 

Třída ViewModelBase ve svém statickém konstruktoru dosazuje výchozí objekt podporující rozhraní ITransientStateHelper, které je klíčové pro uložení a obnovení tranzientního stavu view modelů a na které delegují i metody DoInternalLoadTransientState  a DoInternalSaveTransientState v předchozím výpisu.

Všimněte si, že metoda DoInternalLoadTransientState po obnovení tranzientního stavu zkontroluje, jestli se dá view model používat, a pokud ne, zavolá i v této fázi metodu Init.

if (!CanUseModel)
{

  Init();
}

A nyní se podíváme na mocné rozhraní ITransientStateHelper

Je asi zřejmé, že metoda GetTransientState vrátí v objektu Dictionary tranzientní stav objektu, který jí byl předán v argumentu obj. Metoda RestoreTransientState naopak obnoví tranzientní stav objektu v argumentu obj hodnotami v argumentu savedState.

Metoda IsTransientStateEnabledForObject zjistí, jestli je možné z předaného objektu získat tranzientní stav. Jak uvidíme, odvozené view modely mohou odmítnout uložení tranzientního stavu a kdekoli v aplikaci  můžeme jejich rozhodnutí jednoduše zjistit předáním instance view modelu této metodě.

Zde je jedna z možných realizací rozhraní IStateTransientStateHelper, kterou používá i naše třída ViewModelBase.

 

Odpovědnosti metod GetTransientState a RestoreTransientState jsem již popsal, nyní jen zmíním pár specialitek v kódu třídy TransientStateHelper.

Metoda GetTransientState zjistí, jestli předaný objekt  má tranzientní stav tak, že zavolá metodu IsTransientStateEnabledForObject(obj).

Metoda IsTransientStateEnabledForObject kontroluje, jestli třída, ze které objekt pochází, nezakázala vydání tranzientního stavu tím, že je na ní aplikován atribut [NonTransientState].

Atribut NonTransientState

Atribut NonTransientState může být aplikován nejen na celou třídu, ale i na jednotlivé vlastnosti objektu, které nemají být součástí tranzientního stavu.  Metoda GetTransientState neukládá celé view modely, ale s využitím reflexe jen hodnoty jejich vlastností. Atribut NonTransientState dovoluje vyřadit vlastnosti, které v tranzientním stavu view modelu nemají co dělat. To ale není vše – přímo metoda GetTransientState dle jmenné konvence vyřadí všechny vlastnosti, jejichž suffix je v poli IGNORE_METHOD_SUFFIX_LIST

public static readonly IEnumerable IGNORE_METHOD_SUFFIX_LIST = new[] { "Command", "Action", "Helper", "Service", "SynchContext"};

Do tranzientního stavu se tak nedostanou ve view modelech se často nacházející, ale s tranzientním stavem view modelu nic nemající a navíc většinou neserializovatelné objekty jako jsou objekty ICommand (vlastnost SelectCommand, SaveCommand), delegáti  TextboxTextChangedAction, služby (ILoggerService) a další.

Metoda RestoreTransientState obnoví tranzientní stav objektu. Za zmínku stojí, že když je předaný objekt potomkem PropertyNotificationBase, tak nastavením vlastnosti notificationBase.SuppressPropertyChangedNotification na true potlačíme dočasně vyvolání události OnPropertyChanged, protože je vhodné, abychom událost nevyvolávali, když view model neobsahuje všechna data a nevíme, kdo všechno na událost reaguje a jaká další, nyní ve view modelu se nenacházející data, by chtěl načíst. Jak jsem ale psal v požadavcích na začátku článku, view model nemusí podporovat žádná rozhraní, nemusí být potomkem ViewModelBase ani PropertyNotificationBase, a proto si ani dědění z PropertyNotificationBase v této metodě nevynucujeme. Poté, co metoda RestoreTransientState obnoví hodnoty všech vlastností, má TransientStateHelper povinnost notifikovat okolí o změně hodnot všech vlastností view modelu. Jestliže je objekt s obnoveným tranzientním stavem potomkem PropertyNotificationBase, zavoláme metodu RaisePropertiesChanged(propertyNames.ToArray()), jinak se metoda RestoreTransientState pokusí dohledat opět za pomoci reflexe na objektu metodu nazvanou RaisePropertyChangedEvent, která přijímá název vlastnosti a kterou použije pro hromadnou distribuci událostí OnPropertyChanged.

Ovládací prvky obsažené (nejen) v control toolkitu jsou  přecitlivělé na pořadí vyvolávání události, a proto jsou vlastnosti seřazeny nyní tak, aby vlastnosti začínající slovem Selected vyvolávaly událost OnPropertyChanged jako poslední. Máte-li ve view modelu kolekci nazvanou Orders (všechny objednávky) a SelectedOrder (vybraná objednávka z této kolekce), je zaručeno, že událost OnPropertyChanged pro vlastnost SelectedOrder bude vyvolána až po události OnPropertyChanged pro vlastnost Orders.

Prefixy vlastností, které mají vyvolávat události jako poslední, jsou v proměnné LAST_SET_VALUE_METHOD_PREFIX. Tyto vlastnosti jsou označeny příznakem LastInit z enumerace PropertyType.

public static readonly IEnumerable LAST_SET_VALUE_METHOD_PREFIX = new[] { "Selected" };

To by pro dnešek k třídě ViewModelBase a jejím pomocníkům stačilo:

Příště se podíváme hlavně na “hostitele” view modelů, který by měl být schopen:

  • Volat na view modelech ve “správnou dobu” metody Init, LoadState, SaveState, Activate, Deactivate.
  • Připojit view modely k view (stránce).
  • Náš hostitel bude podporovat i více view modelů na jedné stránce, včetně “dědění” a sdílení použitých view modelů mezi různými view na stránce.

A také si příště vysvětlíme, proč při získání tranzientního stavu ve ViewModelbase místo objektu Dictionary používáme vlastní třídu KnownTypesDictionary. Jak možná tušíte, i název “KnownTypes” odkazuje k tomu, že mají-li být hodnoty uložené v tranzientním stavu serializovatelné, tak DataContractSerializer, používaný infrastrukturou WP7 k serializaci tranzientního stavu, musíme přesvědčit, že v objektu Dictionary jsou jen objekty z jemu “známých” tříd.

Předcházející články:

Tipy pro Windows Phone 7 aplikace I



Friday, December 17, 2010 7:25:14 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Compact .Net Framework | Návrhové vzory | Silverlight | WP7


 Monday, December 06, 2010
Tipy pro Windows Phone 7 aplikace I

První slíbený článek na téma vývoje Windows Phone 7 aplikací je venku. Dnes si ukážeme většinou kód, který řeší některé od CTP  se vlekoucí chyby ve WP7 SDK nebo řeší některé nedomyšlenosti a omezení mobilní verze Silverlightu. Mobilní verze Silverlightu vychází totiž ze Silverlightu 3, i když se objevují náznaky, že v lednu přijde aktualizace, která z WP7 udělá zase o něco lepší systém. Nezbývá než doufat, že sousloví “pořádná aktualizace” neoznačuje jen zvukem slavnostních fanfár doprovázené antré funkce “Copy-Paste”, což se lehce sfoukne instalací nové verze běhového prostředí,  která bude vycházet ze Silverlightu 4. Dělám si legraci, i když mě samotného mrzí, že kromě několika bodů stále platí to, co jsem napsal o WP7 v nadsázce už v březnu.

V dalších článcích bych rád ukázal, jak si můžete navrhnout aplikaci, která podporuje automaticky “tombstonning”, MVVM a nepotřebuje k tomu žádné těžkotonážní frameworky ani  zbytečnými funkcemi obtěžkané DI kontajnery.

Tip 1 – listbox a a jeho náhlá indispozice pri pokusu o skrolování obsahu

Když vám najednou přestane listbox na emulátoru i v zařízení skrolovat obsah, důvodem je pravděpodobně to, že jste pozměnili layout stránky a že listbox na to odpoví “neskroluji”, Jedná se o to, že když listbox je zanořen v gridu, dalším gridu a poté ještě ve stack panelu, tak nedokáže skrolovat přes všechny položky. Sice je v dokumentaci k WP7 upozornění, že layout stránky by měl být co nejjednodušší, ale netušil jsem, že autoři listboxu si to přímo vynutí. Smile Nedám vám taxativní výčet layoutů, při kterých listbox přestane skrolovat, ale jednoduché pravidlo zní: Když listbox neskroluje, nehledám chybu ve svém kódu, ale zjednoduším layout stránky. 

Listbox  zanořený jen v gridu funguje bez problémů

Tip 2: Když u prvku WebBrowser použijete metodu NavigateToString, stránka zobrazuje chybně (nejen) české znaky.

I když prvek webbrowser zobrazuje stránky s různým kódováním bez problémů, metoda NavigateToString nikdy nezobrazí cizí znaky správně. Nepomáhá ani nastavení encoding na stránce. Po mnoha pokusech jsem napsal extenzní metodu, která “enkóduje” všechny non ascii znaky.

Na řetězci předávaném metodě NavigateToString stačí zavolat jen myHtmlString.EncodeUnicodeChars()

Tip3: Stále jsme u prvku WebBrowser. Prvek WebBrowser má sice metodu NavigateToString, ale mnohem lepší je připojit “nabindovat” (“načíst") z view modelů rovnou html řetězec.

I u mobilního Silverlightu lze použít “dependency” a “attached” vlastnosti, na které lze “bindovat” data z view modelů.

Poznámka: I když to zde není nutné, třída WebBrowserBehavior dědí z třídy Behavior, kterou naleznete v assembly System.Windows.Interactivity.dll. Tato assembly je většinou nainstalována ve složce c:\Program Files\Microsoft SDKs\Expression\Blend\Windows Phone\v7.0\Libraries\

K prvku webbrowser jsem přidal “attached” vlastnost NavigateText.

Poté již mohu "bindovat" ve view na vlastnost z view modelum, ale tím problémy teprve začínají.:) Jak asi tušíte, "attached" vlastnost NavigateText je jen speciální "proxy vlastnost" k metodě NavigateToString prvku WebBrowser.

Bohužel rozmarní vývojáři WP7 přišli s podivným životním cyklem stránky, o kterém budu více mluvit v článku zaměřeném  na podporu MVVM,  ale už dnes nás zajímá jeden jeho důsledek a tím je, že když zavoláte metodu NavigateToString ještě před tím, než je dokončeno přidání prvku webbrowser do vizuálního stromu (visual tree) UÏ prvků, tak je vyvolána výjimka InvalidOperationException. To konkrétně pro nás znamená, že nová attached “vlastnost” NavigateText může volat metodu NavigateToString po načtení dat z view modelu, ale stále ve chvíli, kdy prvek webbrowser ještě není plně inicializován. Obešel jsem to špinavým způsobem tak, že při vyhození výjimky InvalidOperationException prvkem webbrowser zaregistruji lambda handler k události WebBrowser.Loaded, ve kterém se pokusím zavolat metodu NavigateToText znovu, protože prvek WebBrowser by měl být při vyvolání události Loaded již plně inicializován a také součástí vizuálního stromu UI prvků na stránce.

Výpis kódu upravujícího chování třídy WebBrowser:

To myslím pro dnešek stačí.Smile



Monday, December 06, 2010 10:57:19 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Compact .Net Framework | Silverlight | WP7


 Monday, November 15, 2010
C# Posterous API Wrapper verze 0.0.3 – pro .NF 3.5+, Silverlight 4 a nově i s podporou pro Windows Phone 7

 

Ke stažení jsem uvolnil další verzi API wrapperu služby Posterous. Pracuji nyní na komerčním a delší dobu plánovaném projektu pro WP7, a proto jsem v rámci vývojářského ducha povznášejícího testování různých nedomyšleností a špeků tohoto nového nadělení od mobilní divize MS cvičně  přidal už v Beta/RC verzi WP7 do Posterous API zkušební knihovnu pro WP7, kterou jsem nyní otestoval a doladil na “ostré”(sic! skutečně?) verzi WP7.

Další sarkasmy na adresu WP7 si dnes odpustím, těch už bylo dost na Twitteru  Smile, ale brzy sem dám nějaké postřehy, jaké to je vrátit se do časů Silverlightu 3, na kterém je mobilní verze Silverlightu založena, a hlavně tipy, jak si vývoj mobilních aplikací usnadnit.

Posterous tento měsíc uvolnil nové API založené na JSON formátu, wrapper pracuje zatím se starším XML formátem. I pro nové JSON API chci ale udělat wrapper.

Hlavní změny v této verzi

  1. Podpora pro Windows Phone 7.
  2. Odstraněny chyby při volání Posterous API,  které byly způsobeny změnami v nastavení serveru na službě Posterous. Zvláště se jedná o http chybu  500 ihned při získávání Sites.
    BTW: Příjemné je, že vývojáři Posterous reagují na připomínky rychle a sami přiznali, že upravovali zabezpečení služeb i u starého API.
  3. Mnoho drobných změn a opravy chyb.

imageimageimageimage

Soubory ke stažení:

RStein.Posterous.API.dll  - .Net Framework 3.5+

RStein.Posterous.API.dll  - Silverlight 4

RStein.Posterous.API.dll  - Windows Phone 7

Rozcestník:

  1. Úvod do mého Posterous API
  2. Ukázka práce s Posterous API – zálohování blogu
  3. Ukázka práce s Posterous API v Silverlightu (součást spotu s informací o uvedení verze 0.0.0.2)
  4. Posterous API a RX Framework.


Monday, November 15, 2010 4:25:38 PM (Central Europe Standard Time, UTC+01:00)       
Comments [3]  .NET Framework | C# Posterous API | Silverlight | WP7


 Saturday, September 18, 2010
Pozvánka na mé kurzy OOP, UML a návrhových vzorů a odpovědi na některé dotazy zaslané emailem - podzim 2010

Opět bych Vás všechny rád  pozval na pravidelný podzimní běh kurzů Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a Pokročilé návrhové vzory a objektové principy 2. Níže také naleznete odpovědi na pravidelně se opakující dotazy v emailech.


Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1

Datum konání kurzu:  22. 11. – 24. 11. 2010

Místo konání:

Školící středisko Tutor

U Půjčovny 2
110 00 Praha 1

Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.

Podrobné informace o kurzu a možnost přihlásit se na kurz

Program kurzu
Výběr z ohlasů na kurz


Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 2

Datum konání kurzu:  29. 11. – 1. 12. 2010

Místo konání:

Školící středisko Tutor

U Půjčovny 2
110 00 Praha 1

Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.

Podrobné informace o kurzu a možnost přihlásit se na kurz

Program kurzu
Výběr z ohlasů na kurzy


Zde jsou některé nové ohlasy na kurzy. Raději podotýkám, že tato hodnocení jsou mi zaslána účastníky z vlastní vůle a já určitě účastníky nenutím, aby mi psali nějaká hodnocení, natož lichotivá,  hned na kurzu ani po něm, jak se prý děje u konkurence.:-)

pan Martin Pospíšil, společnost Team Trackers s. r. o.

Děkuji za materiály a za výborné školení. Líbilo se mi ještě víc než jednička, díky těm praktickým příkladům, které jsme řešili společně.

Ing. Rudolf Plíva, společnost NOWIRE

Děkuji za velmi zajímavé a přínosné školení!

pan Jiří Filous, společnost Česká pojišťovna a. s.

Ano, mám zájem o zasílání informací o nových kurzech a děkuji za skvělé (i když náročné:-)) školení.


Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.

Otázka: Na kurz půjdu, ale raději počkám s přihláškou na týden před školením, protože stejně jako konkurence mi dáte “last minute” slevu ne?

Odpověď: Hry s nějakou uměle nadsazenou cenou, která je potom těsně před kurzem snižována a nabízena v rámci “Last minute” nabídky, nehrajeme. Přijde mi nespravedlivé a nesmyslné, aby každý účastník platil jinou částku v závislosti na dni přihlášení. Všichni dosavadní účastníci mohou potvrdit, že zaplatili cenu, která je uvedena u školení a je jedno, zda se přihlásili 3 měsíce nebo 3 dny před kurzem.

Otázka: Na kurzu bychom rádi diskutovali záležitosti, které se týkají našeho projektu, je to možné? Je možné s vámi probrat podrobně i vývojářské specialitky, které se týkají C#, C++,Windows Forms, Silverlightu, Windows Mobile….

Odpověď: Na veřejném kurzu můžeme diskutovat o věcech, které zajímají všechny účastníky.  Specifické dotazy, které se týkají vašich projektů, je nejlepší probrat se mnou po 16. hodině, kdy skončí oficiální část kurzu, ale já jsem vám samozřejmě k dispozici od 16:00 do umdlení mého či vašeho.:-) Jestliže chcete 3 dny věnovat jen svému projektu, zvažte inhouse variantu kurzu, kde si můžete témata definovat sami. Pro podrobné informace o podmínkách inhouse kurzů napište prosím Petře Steinové (petra@renestein.net), která Vám velmi ráda obratem pošle nabídku.

Těším se s Vámi na setkání na kurzu!

René Stein



Saturday, September 18, 2010 10:17:31 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML


 Monday, August 23, 2010
C# - kontrola existence vlastnosti u typu dynamic bez vyvolání výjimky RuntimeBinderException.

Dan Steigerwald mě na Facebooku upozornil na článek “Challenge: Dynamically dynamic” na blogu Ayende Rahiena. Jak se můžete sami podívat, celá výzva se týká toho, jak zjistit, jestli u dané instance typu dynamic existuje vlastnost se zadaným jménem, aniž byste museli odchytávat  výjimku RuntimeBinderException, která vás na chybějící vlastnost sice drsně upozorní, ale zároveň vás nutí používat kód řízený výjimkami.

 

Jak vypadá kód detekující existenci vlastnosti s vy/zneužitím RuntimeBinderException?

   private static bool HasPropertyNaive(IDynamicMetaObjectProvider dynamicProvider, string name)
        {
            try
            {
                var callSite =
                                CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                                                         new[]
                                                                 {
                                                                     CSharpArgumentInfo.Create(
                                                                         CSharpArgumentInfoFlags.None, null)
                                                                 }));
                callSite.Target(callSite, dynamicProvider);
                return true;
            }
            catch (RuntimeBinderException)
            {

                return false;
            }

        }

A použití:

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Name"));
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Id"));            
            Console.ReadLine();
        }

 

Stejně jako v zadání na blogu Ayende metoda HasPropertyNaive pracuje s každým objektem dynamic skrytým za rozhraním IDynamicMetaObjectProvider. V metodě napodobíme chování kompilátoru C# – vytvoříme “kontext operace”, tzv. CallSite, které předáme hlavně tzv. “Binder” voláním tovární metody metody Binder.GetMember. Binder, v našem případě binder pro get akcesor vlastnosti, jejíž přítomnost testujeme a jejíž název jsme předali metodě HasPropertyNaive v argumentu name,  si lze zjednodušeně představit jako objekt, který je odpovědný za dohledání hodnoty vlastnosti u dynamického objektu za běhu aplikace.

U CallSite použijeme metodu Target, které předáme samotnou instanci callSite a objekt dynamic, u nějž chceme otestovat existenci vlastnosti. Jestliže vlastnost u objektu dynamic neexistuje, metoda Target vyvolá výjimku RuntimeBinderException a my vrátíme false, jinak ignorujeme návratovou hodnotu metody target a vracíme true, což je pro kód volající metodu HasPropertyNaive potvrzení, že vlastnost existuje.

Metoda HasPropertyNaive plní svůj účel, ale za cenu vyvolání výjimky RuntimeBinderException. A toho se týká právě “challenge”. Zkusme se výjimky zbavit.

Kdybychom měli testovat existenci vlastnosti jen u instancí “ExpandoObject”, měli bychom hned hotovo.

private static bool HasPropertyExpandOnly(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return ((IDictionary)dynamicProvider).ContainsKey(name);
}

ExpandoObject totiž podporuje rozhraní IDictionary a klíčem v objektu Dictionary jsou názvy vlastností.

Zadání ale vyžaduje, abychom zkontrolovali přítomnost vlastnosti u ktreréhokoli objektu dynamic typu IDynamicMetaObjectProvider. Když předáte metodě HasPropertyExpandOnly instanci dynamic, která dědí z DynamicObject nebo přímo implementuje rozhraní IDynamicMetaObjectProvider, při pokusu o přetypování instance na rozhraní IDictionary dojde k výjimce.

Problém s detekcí přítomnosti vlastnosti by také zcela zmizel, kdybychom měli zaručeno, že každá instance typu IDynamicMetaObjectProvider a s ní asociovaný “DynamicMetaObject” z metody GetDynamicMemberNames vrátí seznam s názvy všech dynamických členů.

private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return dynamicProvider
                    .GetMetaObject(Expression.Constant(dynamicProvider))
                    .GetDynamicMemberNames()
                    .Contains(name);
} 

Bohužel ani to garantováno nemáme a metoda GetDynamicMemberNames u mnoha instancí dynamic bez skrupulí vrátí prázdné pole, i když vlastnosti existují.

Musíme si tedy poradit jinak.

Následuje kód metody HasProperty včetně podpůrných konstrukcí, která pracuje s libovolnou instanci typu IDynamicMetaObjectProvider a ke zjištění, zda je, či není vlastnost přítomna, nepotřebuje vyvolávat výjimku RuntimeBinderException.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;


namespace DynamicCheckPropertyExistence
{
    class Program
    {                
        private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
        {



            var defaultBinder = Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                             new[]
                                     {
                                         CSharpArgumentInfo.Create(
                                         CSharpArgumentInfoFlags.None, null)
                                     }) as GetMemberBinder;


            var callSite = CallSite<Func<CallSite, object, object>>.Create(new NoThrowGetBinderMember(name, false, defaultBinder));


            var result = callSite.Target(callSite, dynamicProvider);

            if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }

            return true;

        }

      

    }

    class NoThrowGetBinderMember : GetMemberBinder
    {
        private GetMemberBinder m_innerBinder;        
        
        public NoThrowGetBinderMember(string name, bool ignoreCase, GetMemberBinder innerBinder) : base(name, ignoreCase)
        {
            m_innerBinder = innerBinder;            
        }
        
        public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
        {


            var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});            
            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

        }
        
    }

    class NoThrowExpressionVisitor : ExpressionVisitor
    {        
        public static readonly object DUMMY_RESULT = new DummyBindingResult();
        
        public NoThrowExpressionVisitor()
        {
            
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            
            if (node.IfFalse.NodeType != ExpressionType.Throw)
            {
                return base.VisitConditional(node);
            }
            
            Expression<Func<Object>> dummyFalseResult = () => DUMMY_RESULT;
            var invokeDummyFalseResult = Expression.Invoke(dummyFalseResult, null);                                    
            return Expression.Condition(node.Test, node.IfTrue, invokeDummyFalseResult);
        }

        private class DummyBindingResult {}       
    }
}

Proč se metoda HasProperty obejde bez vyvolání výjimky? Použil jsem trik, kdy objektu CallSite nepředávám přímo výchozí GetMemberBinder, ale vlastní NoThrowGetMemberBinder, který je potomkem bázové třídy GetMemberBinder z DLR. Můj NoThrowGetMember v kostruktoru přijímá další objekt GetMemberBinder, který interně použije pro zjištění hodnoty vlastnosti. Metoda HasProperty předává instanci NoThrowGetMember do konstruktoru tovární metodou Binder.CreateBinder vytvořený výchozí C# Binder, takže nemusíme v třídě NoThrowGetMember naštěstí duplikovat veškerou logiku pro přístup k vlastnosti, která je  již součástí výchozího C# Binderu.

NoThrowGetBinderMember se spoléhá na to, že při pokusu o přístup k dynamickým metodám a vlastnostem u objektu IDynamicMetaProvider třída GetMemberBinder dovoluje odvozeným třídám, aby aplikovaly vlastní logiku pro práci s “dynamickými” členy v tzv. “fallback” metodách. NoThrowGetBinderMember tedy dostane šanci dohledat vlastnost v přepsané metodě FallbackGetMember.

Metoda FallbackGetMember pracuje takto:

1. Použije metodu Bind předaného Binderu (m_innerBinder) , které předá jako první argument “DynamicMetaObject” v argumentu target a druhým argumentem je prázdné pole objektů “DynamicMetaObject”. Výchozí Binder udělá svou práci a vrátí nám další DynamicMetaObject, který si uložíme do proměnné retMetaObject.

var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});
 

2. V retMetaObject je vyhodnocovací výraz (Expression tree) pro získání hodnoty vlastnosti, který pro vlastnost Name může vypadat zjednodušeně takto. Tučně je vyznačena část, která je odpovědná za vyvolání výjimky, jestliže vlastnost neexistuje.

IIF(ExpandoTryGetValue(Convert($arg0), value(System.Dynamic.ExpandoClass), 0, "Name", False, value), value, throw(new RuntimeBinderException("'System.Dynamic.ExpandoObject' does not contain a definition for 'Name'")))
IIF(ExpandoCheckVersion(Convert($arg0), value(System.Dynamic.ExpandoClass)), {var value; ... }, gotoCallSiteBinder.UpdateLabel)

My ale výjimku vyvolávat nechceme, a proto vlastním vizitorem NoThrowExpressionVisitor modifikujeme “expression tree” tak, že  místo vyvolání výjimky, když vlastnost neexistuje, vrátíme hodnotu statické proměnné  DUMMY_RESULT. Vlastnost Expression u proměnné retMetaObject je určena pouze pro čtení, proto vytvoříme nový DynamicMetaObject s upravenou “Expression” a původními restrikcemi a uložíme ho do proměnné finalMetaObject, která je také návratovou hodnotou metody FallbackGetMember.

 


            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

Úplný kód třídy NoThrowExpressionVisitor naleznete ve výpisu výše.

Metoda HasProperty  vyvolá metodu Target na objektu CallSite a zkontroluje, zda její návratová hodnota je referenčně shodná s hodnotou v proměnné NoThrowExpressionVisitor.DUMMY_RESULT a pokud tomu tak je, vrátí false, protože nyní místo vyvolání výjimky byla vrácena zástupná hodnota signalizující “vlastnost u objektu dynamic neexistuje”, jinak vrátí true -  “vlastnost existuje”.

 

var result = callSite.Target(callSite, dynamicProvider);
if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }
            
            

Použití metody HasProperty.

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";            
            Console.WriteLine(HasProperty(testDynamicObject, "Name"));
            Console.WriteLine(HasProperty(testDynamicObject, "Id"));
            Console.ReadLine();
        }

/*Výsledek:
True 
False
*/

Zkoušel jsem metodu HasProperty použít i na zjišťování existence vlastnosti u potomků třídy DynamicObject pro zpracování rss a vše funguje dle očekávání.

“Challenge” pokořen. :-) Obvyklá poznámka na závěr – za nic neručím, kód nemusí fungovat v dalších verzích DLR, C# a .Net Frameworku, ale to vy určitě víte.:)



Monday, August 23, 2010 2:33:03 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | LINQ | Programátorské hádanky


 Monday, May 24, 2010
Omezení pro argumenty šablony (template) v C++ napodobující “where“ omezení pro generické argumenty v C#

Tento článek je hlavně reakce na stížnost,  kterou měl kolega-vývojář z firmy, kde vývojáři použivají C++ i C#. Stížnost byla zaměřena na to, že na rozdíl od C# není možné  v C++ zkontrolovat v době kompilace, zda argument předaný do šablony implementuje vyžadované rozhraní, nebo je potomkem námi vyžadované třídy.

Zdůrazním hned v úvodu, že v článku mluvím o “klasickém” C++ a ne o jeho .Net dialektu C++/CLI, který podporuje šablony i generiku.

Příkladem v C# může být následující třída Collection, která vyžaduje, aby za generický parametr T byla dosazena třída podporující rozhraní IBusinessObject. Podrobněji jsem podobnou kolekci už v éře př. lq. (čteme před Linqem) :-) popisoval třeba zde.

public class Collection<T> : where T : IBusinessObject
{
}

Šablony v C++ jsou i přes povrchní syntaktickou podobnost zcela odlišnou jazykovou konstrukcí při srovnání s generikou v C#, a protože jsou “uzavřené šablonové typy” generovány již v době kompilace, kompilátor sám zajistí, že typ předaný do šablony podporuje všechny námi vyžadované metody, vlastnosti a operátory. Přesto existují situace, kdy chceme garantovat, že do naší “šablonové” kolekce jsou vkládány jen objekty podporující stejné rozhraní, nebo vyžadujeme, aby vkládané objekty byly povinně potomky nějaké třídy. Nestačí nám tedy, když argument šablony podporuje metodu Commit, která má stejnou signaturu jako metoda Commit z třídy Transaction, ale chceme mít již v době kompilace ověřeno, že předaná instance je potomkem zřídy Transaction. Důvod? Třeba to, že metoda Commit z třídy Transaction má v sobě vysokoúrovňový scénář, na který spoléháte (vzor Template method, jehož název nemá nic společného s šablonami ani generikou :-) ).

Pro novou verzi jazyka C++0x bylo plánováno, že různá omezení parametru šablony bude možné vyjádřit pomocí tzv. “Concepts”. Bohužel Concepts ani v C++0x  nakonec nebudou a my si musíme vystačit s výrazovými  prostředky současné verze jazyka C++. S jednou výjimkou - v článku použiju i jednu elegantní konstrukci z C++0x (implementována ve Visual Studio 2010), statickou verifikaci pomocí klíčového slova static_assert, ale ukážu, jak snadno můžeme static_assert nahradit.

Mějme běžnou šablonu třídy Collection. V našem případě jde jen o dostatečný draft třídy Collection s konstruktorem, destruktorem a prázdnou implementací metod Add a Remove.

template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {        
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

Za generický parametr T lze nyní substituovat “cokoli”, ale my chceme omezit typ T pouze na instance podporující rozhraní IBusinessObject.

class IBusinessObject
{


public:
    IBusinessObject(void);
    virtual void CommitChanges() = 0;
    virtual void RollbackChanges() = 0;
    virtual ~IBusinessObject(void);
};

V C++ samozřejmě za rozhraní považujeme třídu, jejíž všechny metody jsou abstraktní, a proto převod dvou omezení ze C# ( potomek třídy, implementor rozhraní (realizace) ) se nám v C++ redukuje na nalezení ekvivalentu omezení “parametr šablony musí být potomkem námi vyžadované třídy”.

K vyjádření takového omezení si zavedeme pomocnou šablonovou třídu IsDerivedTest:

#pragma once

template
    <typename Base, typename Derived>
class TestIsDerived
{
    private:
        struct Is_Derived_Helper
        {
            int dummy;
        };
        
        struct Not_Derived_Helper
        {
            int dummy;
            int dummy2;
        };    
    
    private:
        TestIsDerived(void);
        ~TestIsDerived(void);

        static Is_Derived_Helper Test(Base* base);                
        static Not_Derived_Helper Test(...);

    public:                
        enum TestResult
        {
            IsDerivedResult = ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper))))
        };
};

Co třída TestIsDerived umí?


Dva parametry šablony TestIsDerived mají výmluvné názvy – Base (bázová třída) a Derived (třída, u níž chceme zkontrolovat, zda je z Base odvozena)

Tuto třídu nechceme nikdy instanciovat, proto jsou její konstruktor a destruktor privátní.  U privátních struktur Is_Derived_Helper a Not_Derived_Helper je důležitá jen jedna věc – musí mít odlišnou velikost (sizeof(Is_Derived_Helper) != sizeof(Not_Derived_Helper ), a proto první obsahuje jednu "dummy" proměnnou typu int a druhá struktura dvě “dummy” proměnné typu int.

Při kontrole zda parametr šablony Derived je odvozen z parametru šablony Base postupujeme takto:

  1. Máme dvě privátní statické metody nazvané Test. Tyto funkce jsou jen deklarovány, jejich definici nepotřebujeme, protože nebudou nikdy zavolány. Důležité je jen to, že jedna metoda Test vrací Is_Derived_Helper  a druhá metoda Test vrací Not_Derived_Helper  - připomňme si, že jde o struktury mající různou velikost.
  2. Metoda Test, která vrací strukturu Is_Derived_Helper, přijímá jako argument pointer na parametr Base, a může tedy přijmout i pointer na Derived, pokud Derived je potomkem Base. Druhá metoda Test (Test(…)) bude vyvolána vždy, když se nepodaří Derived* převést na Base*.
  3. Do hodnoty IsDerivedResult v enumeraci TestResult uložíme výsledek výrazu ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper)))) - “voláme” metodu Test tak, že přes operátor static_cast přetypujeme konstantu 0 na pointer na Derived . Ač může vypadat přetypování 0 na Derived* jako “zvěrstvo” za účelem rychlého poslání programu do řiti říše binárního nebytí,  slovo voláme je v předchozí větě záměrně v uvozovkách, protože jak jsem již psal, k žádnému volání metody Test nikdy nedojde. V době kompilace ale kompilátor zjistí, jakou variantu metody Test by zavolal pro Derived* a my z velikosti “potenciální a nikdy skutečně nevrácené” návratové hodnoty zvolené metody Test jsme schopni poznat, jestli je Derived potomkem Base. Pokud je Derived potomkem Base, bude zvolena varianta Test(Base* base), která vrací Is_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude pravdivá  - uložíme do IsDerivedResult hodnotu 1. Pokud Derived není potomkem Base, bude zvolena varianta metody Test(…), která vrací Not_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude nepravdivá (uložíme do IsDerivedResult hodnotu 0). Znovu zdůrazňuji, že k vyhodnocení podmínky dojde již v době kompilace.

Scéna je připravena, pozveme hlavní aktéry:

Mějme dvě třídy. Třída Invoice je potomkem třídy IBusinessObject a třída NotBusinessObject (nomen omen :) ) překvapivě není.:)



class NotBusinessObject
{
public:
    NotBusinessObject(void);
    virtual ~NotBusinessObject(void);
};

#pragma once
#include "IBusinessObject.h"

class Invoice : public IBusinessObject
{
    public:
        
        virtual void CommitChanges();
        virtual void RollbackChanges();
        
        Invoice(void);
        ~Invoice(void);
};

#include "StdAfx.h"
#include <iostream>
#include "Invoice.h"

using namespace std;

Invoice::Invoice(void)
{
}


Invoice::~Invoice(void)
{
}

void Invoice::CommitChanges()
{
    cout << "Commit";
}

void Invoice::RollbackChanges()
{
    cout << "Rollback";
}
        

Naše třída Collection má přijímat jen instance potomků třídy IBusinessObject.

#pragma once
#include "TestIsDerived.h"


template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {
     static_assert(TestIsDerived<IBusinessObject, T>::IsDerivedResult, "Invalid base class, IBusinessObject required");
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

Do destruktoru jsme vložili statickou kontrolu, kdy námi vytvořené pomocné třídě TestIsDerived substituujeme za parametr šablony Base třídu IBusinessObject a za parametr Derived přímo parametr šablony třídy Collection, který musí být vždy potomkem IBusinessObject. Do destruktoru jsme kód vložili proto, aby bylo garantováno, že při instanciaci šablony Collection ke statické kontrole vždy v době kompilace dojde – kód bychom mohli vložit i do konstruktoru, ale budeme-li mít konstruktorů více, museli bychom kontrolu duplikovat v každém konstruktoru. Statická kontrola v destruktoru spoléhá na to, že vytvořený konkrétní objekt Collection<T> je v aplikaci zlikvidován  - buď jde o likvidaci automatické (lokální) proměnné, nebo vy sami použijete operátor delete atd. Jestliže by destruktor objektu v aplikaci nikdy nebyl volán, kompilátor nebude destruktor v šablonové třídě při rozvíjení šablony instanciovat, stejně jako nikdy neinstanciuje další nepoužité metody v šabloně.

A použití:

Tento kód bez problémů projde. Invoice je potomkem IBusinessObject.

// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"

int _tmain(int argc, _TCHAR* argv[])
{    
    Collection<Invoice> col1;
    
}

Tento kód nahlásí chybu. NotBusinessObject není potomkem třídy IBusinessObject.

// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"

int _tmain(int argc, _TCHAR* argv[])
{    
    Collection<NotBusinessObject> col1;
        
}

Ve Visual Studiu 2010 dostanu v době kompilace tuto chybu:

Error    1    error C2338: Invalid base class, IBusinessObject required    c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h    16    1    TemplateConstraint_Console

Tím je náš úkol splněn, ale jak jsem sliboval, ukážu nyní, i jak se můžeme obejít bez klíčového slova static_assert z C++0x.

Třída TestIsDerived zůstane beze změny, ale napíšeme další dvě šablony, ve kterých budeme reagovat na hodnotu IsDerivedResult z třídy TestIsDerived, což původně dělal přímo static_assert.

#pragma once
template
<int T>
struct Invalid_Base_Class
{
    public:
        Invalid_Base_Class(void)
        {

        }
        ~Invalid_Base_Class(void)
        {

        }
};

template<>
struct Invalid_Base_Class<0>
{
    private:
        Invalid_Base_Class(void);
        ~Invalid_Base_Class(void);
};
 

Šablona Invalid_Base_Class má jeden šablonový parametr typu int. Struktura není svým rozhaním zajímavá, podstatný je pro nás jen název struktury, abychom v době kompilace viděli chybovou hlášku “Špatná bázová třída”. První šablona Invalid_Base_Class je běžnou strukturou, která bude použita pro instanciaci všech hodnot typu int krome hodnoty 0. Pro hodnotu 0, o níž víme, že je uložena v hodnotě enumerace TestIsDerived::IsDerivedResult, jestliže DERIVED NENÍ potomkem BASE, je určena explicitní specializace Invalid_Base_Class<0>, která má ale privátní konstruktor, takže pokus o její vytvoření vždy již při kompilaci selže.

V hrubé podobě, tedy bez vlastního makra assert_is_derived apod., které skryje práci s našimi strukturami Invalid_base_Class a které by zde jen zamlžovalo průzračnost řešení, můžeme kontrolu na nutnost šablonového parametru T třídy Collection<T> dědit z třídy IBusinessObject přepsat takto:

#pragma once
#include "Invalid_Base_Class.h"


template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {
        Invalid_Base_Class<TestIsDerived<IBusinessObject, T>::IsDerivedResult> invalid_base;
        invalid_base;
        
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

V destruktoru se pokusíme instanciovat strukturu Invalid_Base_Class, které jako šablonový argument předáme hodnotu v IsDerivedResult.

Collection<Invoice> opět bez problémů projde, ale při pokusu vytvořit Collection<NotBusinessObject> dostanu při kompilaci tuto chybovou zprávu:

Error    1    error C2248: 'Invalid_Base_Class<0>::Invalid_Base_Class' : cannot access private member declared in class 'Invalid_Base_Class<0>'    c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h    14    1    TemplateConstraint_Console

Řešení by se dalo přisladit dalším syntaktickým cukrem, ale princip by měl být z článku zřejmý.
Triky s šablonami jsou úžasné a převod některých rysů generiky ze C# není zase tak problematický. Jen ta nonšalantní elegance, která je C# vlastní, v přihroublém, ale o to výkonnějším, C++ trochu chybí. :) Kdyby někoho triky se šablonami zaujaly, doporučuji ke studiu Alexendrescovu knihu Modern C++ Design: Generic Programming and Design Patterns Applied.



Monday, May 24, 2010 3:50:36 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Nativní kód


 Wednesday, April 07, 2010
Pozvánka na kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 – jaro 2010

Update: Kurz je zcela obsazen včetně náhradníků.

Rád bych Vás pozval na další běh kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1. Pokud se někdo z Vás (oprávněně) diví, proč tak pozdě a proč Vás nezvu i na kurz OOP 2, níže v tomto spotu nalezne odpovědi.

Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1

Datum konání kurzu:  14. 6. – 16. 6. 2010

Místo konání:

Školící středisko Tutor

U Půjčovny 2
110 00 Praha 1

Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.

Podrobné informace o kurzu a možnost přihlásit se na kurz

Program kurzu
Výběr z ohlasů na kurz

Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.

Otázka: Proč se kurz nekoná na předchozím místě (v hotelu Villa), kde byli naši kolegové?

Odpověď: Po vyhodnocení ohlasů na kurz jsme zjistili, že účastníci nebyli  (stejně jako já :-)) příliš spokojeni s jídlem. Také jsme při domlouvání hotelu byli ujišťováni, kolik účastníků se může na kurz přihlásit  a jak lze pro ně uspořádat pohodlně prostory, a tyto informace se v průběhu kurzu jevily jako marketingové fráze až blafy velmi optimistickými nebo nepočítaly s průměrným Evropanem, ale standardním liliputem.:-) Stručně řečeno – poměr cena/výkon nebyl u čtyřhvězdičkového hotelu příliš dobrý. Jedinou výhodou hotelu Villa bylo parkování, nyní bude lepší na ”poslední míli” použít  MHD.

Otázka: Proč se koná kurz tak pozdě (oproti přechozím rokům) a proč se nekoná kurz OOP2.

Odpověď: Byl a jsem vytížen tento rok dalšími projekty a nalézt vhodné termíny bylo problematické.  Také jsme sháněli nové prostory, kde se bude kurz konat (viz předchozí odpověď). Termín kurzu OOP2 vycházel již na červenec a v době dovolených nemá smysl kurz pořádat. Při větším zájmu vyhlásím (co nejdříve) více termínů kurzu OOP2 na podzim.

 

Těším se na setkání na kurzu!



Wednesday, April 07, 2010 12:48:36 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML


 Sunday, March 21, 2010
Výhody a nevýhody softwarových továren

Emailem jsem dostal zajímavou otázku, jaký je můj názor na softwarové továrny a kde vidím výhody a nevýhody softwarových továren. Odpověď nakonec publikuji i zde – už jen proto, že jsem si při jejím psaní uvědomil, že na továrnu kladu stejné nároky jako na kteroukoli další knihovnu v systému a že výběr softwarové továrny se u mě moc neliší od výběru třeba ORM Frameworku. Nejde o taxativní výčet výhod a nevýhod, ale spíš o volně nahozená témata, která mě za 20 minut psaní příspěvku napadla.

SW továrny jsou jen pokračováním trendu, že nemá smysl vynalézat znovu kolo ani v případě, že by bylo o dvě setiny procenta krásnější než to staré, které je již navržené a hotové. SW továrna říká – namísto opakování chyb svých předchůdců použijte ověřené zkušenosti, osvědčené praktiky, doporučené postupy a již otestovaný kód. Namísto obecného návrhového vzoru nebo vágně popsaného doporučovaného postupu máte k dispozici osvědčenou knihovnu, základní průvodce a odborný polštář, který by měl zabránit, že napácháte v dané oblasti velké množství hrubých bezpečnostních nebo jen s duchem technologie neslučitelných chyb.

Takže výhody:

1) Osvědčená řešení ihned po ruce. Tím se cíl SW faktory moc neliší od obecných frameworků a návrhových vzorů.

2) Není nutné psát infrastrukturní kód přímo v SW firmě (ISV). Je možné se soustředit na problémovou doménu zákazníka a ne na to, jak centrálně publikovat události nebo jak propojit různé pohledy v aplikaci tak, aby spolu dokázaly komunikovat .

3) Pokud stejnou továrnu používáte napříč všemi projekty, je zaučení nového vývojáře na dalším projektu jednodušší a rychlejší.

4) SW továrna používá praktiky osvědčené v dané technologii. Takže odlišně jsou zakódovány původně platformně nezávislá doporučení a všeobecně zaměřené vzory v SW továrně pro ASP.Net a jinak pro Silverlight. Místo popisu obecných návrhových vzorů je již vzor adaptován na konkrétní cílové prostředí.

Nevýhody:

1) Kód nemáte pod kontrolou a musíte se spolehnout, že továrna sama neobsahuje příliš kritických chyb ani otravných chyb s nižší prioritou. To platí o jakékoli abstrakci (frameworku, knihovně), kterou na projektu používáte, ale myslím, že ani v Microsoftu stále nemají SW továrny stejnou podporu jako samotný .Net Framework. A také by měla být známa alespoň orientační roadmapa SW továrny, abyste věděli, kam autoři směřují a jestli jsou si vědomi nevýhod a omezení stávající verze SW továrny. Od továrny, která se ihned po svém uvedení honosí přídomky „experimentální, testovací, zkušební“, bych dal ruce pryč. Jen ti nejodvážnější z nás si dovolí svým zákazníkům za půl roku po zatracení SW továrny jako slepé uličky tvrdit, že musí trochu poštelovat kardiostimulátor výkonného srdce aplikace a že místo plánovaných dvou hodin za přidání dvou vlastností do objednávky zákazník zaplatí dva měsíce migrace na jinou SW továrnu. Nenechal bych se zmýlit tím, že některé firmy pro odvážlivce používající jejich experimentální SW továrny a frameworky razí lichotivý titul „early adopter“ – méně korektní a pravdě bližší překlad z marketingového slovníku totiž zní „natěšený všehoschopný blbec“.

2) Univerzální řešení jako je SW továrna může být pro jednoduché aplikace zabijákem. Více času strávíte integrací aplikace do rámce vynucovaného SW továrnou než psaním obchodní logiky specifické pro aplikaci a důležité pro zákazníka.

3) U některých složitějších aplikací zjistíte, že rámec SW továrny je příliš těsný a vy potřebujete SW továrnu pro svůj projekt upravit nebo rozšířit. Náklady na rozšíření služeb , „hackování“ a ohýbaní SW továrny pro danou problematiku mohou převážit nad výhodami SW továrny. Z velké radosti nad úsporou času v raných fázích projektu, kde byla továrna použita, můžete ještě před dodáním první verze projektu a po pořádném časovém skluzu, vynuceném třeba přepisem částí továrny, které pro výkonnostních testech  nestačí stávajícím požadavkům na projekt, přejít k jadrným kletbám nad šílenou SW továrnou, v níž si úprava jednoho modulu kaskádově vynutí úpravy všech dalších modulů. Místo používání původně slibované elegantní černé skříňky s jednoduchými službami se prohrabujete nevábnými vyhřezlými vnitřnostmi mizerně navržené SW továrny.

Z výhod a nevýhod snad vyplývá můj postoj k SW továrnám. SW továrny jsou dalším evolučním stádiem na cestě, jejímž cílem je využít pro danou problematiku osvědčená řešení a postupy, nepsat stále se opakující kód nebo v každém řešení už jen změnou typu klienta (Web, Windows Forms, Silverlight, mobilní aplikace) neprocházet znovu a znovu všechny slepé vývojářské uličky specifické pro použitou technologii. Objevování objeveného ponechme těm, kterým stačí zařvat vítězoslavně heuréka, i když jsou v pořadí stí nebo tisící, kteří stejnou infrastrukturní nebo funkční trivialitu konečně zakódovali rozumným a v životním cyklu projektu dále udržovatelným způsobem.

A samozřejmě si nemyslím, že tohle evoluční stádium je konečné. :)



Sunday, March 21, 2010 11:57:10 AM (Central Europe Standard Time, UTC+01:00)       
Comments [6]  .NET Framework | Analytické drobky | Návrhové vzory | UML


 Wednesday, February 24, 2010
Podivné? chování při explicitním přetypování typu dynamic ve Visual Studiu 2010 RC

Na twitteru jsem psal, že si pohraju s implementací rozhraní ve třídě přes automatickou delegaci na privátní  proměnnou s využitím nového typu dynamic v C# 4.0. Jestliže se dobře pamatuji, většinou se po nějakém takovém řešení pídí Delphisté. Z příkladu níže bude asi jasné i pro ostatni, co mám předchozími hutnými větami na mysli .

Při hraní si s typem dynamic jsem ale narazil na zvláštní chování při explicitním přetypování a chtěl bych poprosit někoho dalšího z mých čtenářů o vyzkoušení stejného chování ve Visual Studiu 2010 (nejlépe nejen na RC, ale i na starší Betě 2, kterou jsem už smazal). Příklad níže je jen jednoduchý “jednosměrný” prototyp, na kterém vynikne problém s explicitním přetypováním.

Zde je mnou zmiňovaná podivnost (problém):

Mějme rozhraní IWorker:

   public interface IWorker
    {
        void DoWork();
    }

A třídu Worker, která toto rozhraní implementuje.

 class Worker : IWorker
    {
        #region Implementation of IWorker

        public void DoWork()
        {
            Console.WriteLine(GetType().ToString());
        }

        #endregion
    }

Dále máme  třídu Order, která rozhraní IWorker neimplementuje, ale má privátní proměnnou m_worker implementující toto rozhraní, kterou předá své bázové třídě DirtyCastBase. DirtyCastBase je třída, která zajistí, že bude-li klient přetypovávat instanci Order na rozhraní IWorker, tak toto přetypování projde a klient dostane jako implementora instanci m_worker.

 public class Order : DirtyCastBase
    {
        private IWorker m_worker;
        public Order() : base()
        {
            m_worker = new Worker();
            SetImplementors(m_worker);
        }
    }

Třída DirtyCastBase je potomkem třídy DynamicObject, která nám v .Net 4.0 dovoluje reagovat na “dynamická volání” a přidat jednoduše “dynamické chování” přepsáním metod začínajících písmeny Try (TryGetMember, TrySetMember ) apod. Já jsem přepsal metodu TryConvert, která se s využitím chráněné virtuální metody TryFindImplementor pokusí nalézt objekt, který podporuje rozhraní vyžadované uživatelem. Deskriptor rozhraní je předán ve vlastnosti Type argumentu binder.

public class DirtyCastBase : DynamicObject
    {
        private IEnumerable<Object> m_implementors;
        private Dictionary<Type, object> m_castContext;

        public DirtyCastBase()
        {
            m_castContext = new Dictionary<Type, object>();
        }
        

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            Type requestedType = binder.Type;            

            Tuple<bool, Object> FindResult =  TryFindImplementor(m_implementors, m_castContext, requestedType);
            
            if (FindResult.Item1)
            {
                result = FindResult.Item2;
                return true;
            }

            return base.TryConvert(binder, out result);
        }

        
        protected virtual Tuple<bool, Object> TryFindImplementor(IEnumerable<Object> implementors, Dictionary<Type, object> currentCastContext, Type requestedType)
        {
            if (implementors == null)
            {
                throw new ArgumentNullException("implementors");
            }
            if (currentCastContext == null)
            {
                throw new ArgumentNullException("currentCastContext");
            }
            if (requestedType == null)
            {
                throw new ArgumentNullException("requestedType");
            }

            object result = null;

            bool found = m_castContext.TryGetValue(requestedType, out result);
            
            
            if (!found)
            {
                
                result = (from implementor in implementors
                          where implementor != null
                          let type = implementor.GetType()
                          where requestedType.IsAssignableFrom(type)
                          select implementor).FirstOrDefault();

                found = result != null;
            }
            
            if (found)
            {
                m_castContext.Add(requestedType, result);
            }
            return new Tuple<bool, object>(found, result);

        }

        protected void SetImplementors(params object[] implementors)
        {
            SetImplementors(implementors.AsEnumerable());
        }

        protected void SetImplementors(IEnumerable<object> implementors)
        {
            if (m_implementors != null)
            {
                throw new InvalidOperationException();
                
            }
                        
            m_implementors = implementors ?? Enumerable.Empty<Object>();
        }
    }
 

V našem případě by tedy třída Order by měla dovolit přetypování na rozhraní IWorker, i když sama toto rozhraní neimplementuje. Zanedbejme nyní, že není zachována referenční identita při konverzi i že vydáváme jako implementora privátní objekt, protože pro demonstrovanou techniku to není příliš podstatné.

Tento kód ověří, že přetypování projde. Využíváme implicitní  (“bez závorek”) konverzi. Samozřejmě že je nutné instanci Order přiřadit do proměnné typu dynamic.

    class Program
    {
        static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker = order;

            worker.DoWork();
            Console.ReadLine();
        }
    }

Implicitní konverze projde, a jak jsem očekával, je vyvolána naše metoda TryConvert.

Když ale projde implicitní konverze, proč explicitní konverze selže a metoda TryConvert vyvolá výjimku?

 static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker =  (IWorker) order;

            worker.DoWork();
            Console.ReadLine();
        }

Ihned je vyvolána výjimka InvalidcastException {"Unable to cast object of type 'DynamicCastTest.Order' to type 'DynamicCastTest.IWorker'."}

Z call stacku výjimky (at CallSite.Target(Closure , CallSite , Object ) se dá odvodit, že výjimku vyhodil C# DLR binder, který se ale ani nepokusil zavolat metodu TryConvert. Metoda TryConvert by ve vlastnosti Explicit argumentu binder měla dostat příznak, že šlo o explicitní konverzi, ale tato metoda evidentně není volána.

Přitom z dokumentace metody TryConvert se dá usoudit, že by metoda TryConvert měla být při explicitní konverzi volána.

Ještě jsem zcela do detailu nestudoval všechna pravidla pro typ dynamic ve specifikaci C# 4.0, ale tohle na mě působí jako bug. Pokud projde implicitní konverze, proč by byla zcela zakázána explicitní? To vůbec není v souladu s pravidly kompilátoru pro konverze v C#:

“The set of explicit conversions includes all implicit conversions. This means that redundant cast expressions are allowed.” (C# specification sekce 6.2 Explicit conversions)

A C# DLR binder se i za běhu snaží podle mých dosavadních zkušeností vždy co co nejvěrněji napodobit chování C# kompilátoru.

Anebo už jsem dnes utahaný, nejde o žádnou anomálii a něco triviálního ve svém kódu přehlížím? :-)

Tedy znovu. Můžete někdo spustit projekt ve svém Visual Studiu (nejlépe i v Betě 2) a podělit se o výsledek?

Zde je projekt ke stažení.

Díky!



Wednesday, February 24, 2010 7:19:03 PM (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | C#