Tuesday, 15 February 2011
FAQ k WP7 a pár specialitek navíc – aneb co jste vždy chtěli vědět o WP7, ale v Microsoftu se vám báli odpovědět
Na twitteru i v emailu se opakuje stále několik dotazů od lidí, kteří koketují s myšlenkou pořídit si WP7 telefon nebo chtějí pro WP7 vyvíjet. Účelem tohoto článku není suplovat uživatelský informační servis o WP7, který u nás skvěle dělá Smartmania a kterému se i v zahraničí věnuje mnoho webů, ale nabídnout snad trochu jiné informace než ty, které různí nájemní pisálci stále kopírují z tiskových zpráv a oficiálních prezentací a které další sociálně-altruističtí trotlové bezmyšlenkovitě linkují, sdílejí a lajkují.
Je Windows Phone 7 zcela nový operační systém, který Microsoft skutečně vyvinul “na zelené louce” a který nemá NIC společného s Windows Mobile?
Je i není. Neuspokojivá odpověď ve stylu hárající chytré horákyně, já vím. Windows Phone 7 jsou postaveny na operačním systému Windows CE, na kterém běžely i Windows Mobile. Dnešní oficiální název zní Windows Embedded CE a aktuální verze má pořadové číslo 6. I Windows Mobile v poslední verzi 6.5 byly stále založeny na Windows CE 5. Jak vidíte, Microsoft nám to moc neulehčuje a číslo verze Windows Mobile není nijak závislé na verzi použitých Windows CE. WP7 dle všech indicií běží na buildu Windows CE 6.
Windows CE 6 mají oprotí Windows CE 5 několik výhod. A z toho plyne, že i WP7 využívají nových rysů v systému a netrpí hlavními problémy Windows Mobile aplikací.
- Ve Windows CE 5 mohlo běžet najednou maximálně 32 procesů. Ano, celých 32 procesů musí stačit každému, ale s tím, jak se smartphony/PDA/MDA vyvíjely a výrobci ihned po startu zařízení obsazovali stále více procesních slotů pro své a se zařízením dodávané aplikace, se tento limit kupodivu ukázal nedostačující. Podpora cca 32 000 procesů ve Windows CE 6 je pořádné zlepšení.
- Ve Windows CE 5 měl každý proces 32 MB virtuální paměti a všechny procesy sdílely 4 GB adresní prostor. Nechci zabíhat do detailů, ale způsob nahrávání dynamických knihoven a jejich sdílení mezi procesy mohl vést velmi rychle k fragmentaci paměti a k chybě “nedostatek paměti”, i když fyzické paměti bylo dostatek. V CE 6.0 jsou 2 GB virtuálního paměťového prostoru vyhrazeny pro kernel a 2 GB pro každý proces. Správa paměti v CE 6 má některé nepříjemné dopady na vývojáře kernel/user driverů, ale pro uživatele i vývojáře aplikací je tento paměťový model mnohem lepší.
Mobilní Silverlight ve WP7 interně využívá Compact .Net Framework, který můžete znát z Windows Mobile. Jestliže pro WP7 vyvíjíte, poznáte to i podle toho, že při ladění aplikace a pokusu editovat zdrový kód vám Visual Studio hlásí, že Compact .Net Framework aplikace nemohou být při ladění upravovány.
Shrnuto – Windows Phone 7 mají nový kabátek (UI Metro), nová běhová prostředí pro uživatelské aplikace (Silverlight, XNA Framework) a vylepšený podvozek (CE 6). Psát a hlavně v článcích zdůrazňovat, že WP7 nemají se staršími Windows Mobile NIC společného, mi přijde v lepším případě naivní, v tom horším okázale neprofesionální. Už minulý rok jsem psal několikrát , že Microsoft by těžko za pár měsíců vyvinul zcela nový mobilní operační systém.
Mohu v ČR nakupovat aplikace v marketplace?
Stručná odpověď: Ne.
Složitější odpověď. Microsoft podle neoficiálních informací uvažuje, že marketplace v ČR bude dostupný “v létě 2010 2011”.
Neoficiálně to jde – stačí mít zahraniční platební kartu, založit nějaký Live účet, kde nastavíte, že pocházíte třeba z USA/UK. Ale musíte být asi velcí blázni nebo nadšenci, abyste si přes tyto obstrukce kupovali dnes (psáno 15.2. 2011) v ČR WP7 telefon. Počítejte také s tím, že když Microsoft zpřístupní marketplace pro ČR, a vy budete chtít používat svůj běžný Live účet, zakoupené aplikace mezi různými Live účty nepřevedete.
Mohu dnes dát svou WP7 aplikaci do marketplace?
Stručná odpověď: Ne.
Složitější odpověď. Pokud chcete dát aplikaci do marketplace, vyvíjejte se zahraničním partnerem. Je možné také se s trochou kreativity při tvorbě vývojářského účtu (v komentářích tohle sousloví vysvětlovat raději nebudu) registrovat jako vývojář z jiné země, pokud máte zahraniční platební kartu, ale riskujete, že Microsoft na tento “trik” přijde a zablokuje vám prodej aplikací, protože porušujete jeho podmínky.
Dobře, jsem smířen s tím, že se do market place prozatím nedostanu, ale přesto chci svou aplikaci otestovat na reálném zařízení, a ne jen v emulátoru. Jak to udělám, když mi WP7 telefon nedovolí nahrát vlastní aplikace?
Nejjednodušší je dnes použít Chevron WP7 k neoficiálnímu odemknutí telefonu. Poté co telefon odemknete, můžete nahrávat své vlastní aplikace. Podrobný návod, jak odemknout telefon, je na XDA-Developers, takže jej zde nebudu opakovat. Autoři Chevronu používají na odemčení WP7 telefonu jednoduchý trik. Požadavek na odemčení telefonu cílený na servery Microsoftu přesměrují na lokální počítač a odemčení telefonu povolí. Výhodou je, že nejde o vážný zásah do zařízení, nevýhodou, že pro Microsoft bude snadné tuhle “díru” (dá se pro tohle amatérské zabezpečení najít nějaký jiný eufemismus?) uzavřít další plánovanou aktualizací. Microsoft ale po setkání s autory Chevronu přislíbil, že sám umožní “co nejdříve” vývojářům nahrání a ladění vlastních aplikací v telefonu, aniž by bylo nutné otevírat a platit za vývojářský účet. Neznámou proměnnou je, které jednotky pro měření času používá Microsoft, kde, soudě podle stále odkládaného data první aktualizace pro WP7, korporátní čas evidentně plyne pomaleji, a jestli optimistická fráze “co nejdříve” zahrnuje alespoň předpokládanou dobu životnosti WP7 na trhu smartphonů.
Mohu pro WP7 vyvíjet v Compact .Net Frameworku?
Ne. I když Microsoft Compact .Net Framework používá a při nahrání Silverlight aplikace je patrné z debug logů, že se nahrávají CNF assembly, vlastní CNF aplikaci nenapíšete. Ale – v poslední části tohoto FAQ o prozatím hypotetické možnosti spouštět CNF aplikace na WP7 zařízení znovu píšu.
Mohu pro WP7 vyvíjet nativni (C/C++) aplikace? Slyšel jsem o nějakém Microsoftem vyvinutém nativním frameworku Iris.
Stručná odpověď: Nativní aplikace nejsou podporovány, můžete vyvíjet jen Silverlight aplikace nebo aplikace využívající XNA Framework. Na Iris v klidu zapomeňte.
Složitější odpověď: Nejprve vyřídíme IRIS, což je framework, ke kterém jsem se také vyjadřoval na twitteru.
Twitter: Objevily se bajky o tajném nativním frameworku IRIS pro #WP7. IRIS ale připomíná spíš mix CNF/WPF + P/Invoke. Nativní kód =stále WIN CE API |
Zde jen dodám, že IRIS si můžete představit jako framework, který v MS vyvinuli vývojáři, kteří asi nesnášejí technologie WPF/XAML/Silverlight vytvořené jejich kolegy, a proto si navrhli vlastní značkovací jazyk mixovaný s řízeným kódem a množstvím API (P/Invoke) volání. IRIS Microsoft pravděpodobně používá v shellu a ve svých WP7 aplikacích. Jestliže jste se ptali, jak to, že u MS aplikací třeba prvky Pivot a Panorama podporují plynulejší skrolování než stejné prvky v Silverlightu, v použití IRIS frameworku leží pravděpodobně odpověď. V Microsoftu buď občas neví levice, ce dělá pravíce, anebo mobilní Silverlight není zas tak odladěn a vyšperkován, jak by se mohlo z prezentací demo aplikací u MS evangelistů zdát, a teprve IRIS je tím zázrakem, který rozanimuje výchozí uživatelské rozhraní a dovede recenzenty WP7 systému k stále opakované větě o “ďábelské rychlosti WP7 aplikací”. Věta “o ďábelské rychlosti WP7 aplikací” platí paradoxně do té doby, než začnete další aplikace skutečně instalovat, k čemuž se nadšení recenzenti už asi nedokopou. Trochu sarkasticky dodejme, že v bajtech zhmotněná IRIS snad není ve WP7 proto, aby poslala brzy bezduché WP7 do křemíkového pekla, stejně jako kdysi mytologická Iris pomohla Dídó uvolnit duši z těla.
Nativní kód ale některé aplikace napsané v Silverlightu používají. Jestliže si prohlédnete XAP soubory (XAP soubor – distribuční soubor Silverlight aplikace) aplikací od výrobcu zařízení, zjistíte, že používají knihovnu GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll. Jak název assembly napovídá, měla by sloužit jako most k nativnímu kódu.
Aplikace od výrobce tuto assembly používají pro dynamické volání metod COM objektů. Zkusil jsem si napsat jednoduchou COM assembly ve VS 2008 pro Windows CE, protože VS 2010 z rozhodnutí nějaké moudré hlavy ve vývojářské centrále Microsoftu podporu pro vývoj CE aplikací již neobsahuje. COM knihovnu jsem nazval SL_COM2.dll.
Poté jsem založil projekt v Silverlightu, přidal do něj soubor nazvaný WPInteropManifest.xml, který obsahují i aplikace od výrobců. Soubor WPInteropManifest.xml má jednoduchý obsah.
Do projektu jsem přidal knihovnu GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll. i svou COM knihovnu SL_COM2.dll a u obou knihoven jsem nastavil build akci na content, aby obě knihovny byly přidány do výsledného XAP souboru. Žádné COM Interop knihovny nevygenerujete, COM typy musíte naimportovat z COM knihovny “ručně”.
Pomocí reflexe zaregistrujeme COM knihovnu na cílovém zařízení s využitím metod v assembly GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll.
Tento kód projde, ale na posledním řádku dostanete výjimku, která znamená, že vaše COM knihovna je nekompatibilní s aktuální verzí operačního systému. Musíte si na internetu “najít” a nainstalovat správný Platform builder, což udělal asi Ch. Welsh, nebo si pohrát s PE hlavičkami knihoven, což jsem udělal já. COM knihovna by mohla být po odstranění všech překážek i branou ke všem API funkcím, které si vystavíte v COM objektech. Nepředpokládejte ale, že vaše aplikace bude při využívání COM knihovny přijata do marketplace.
Když jsou problémy s umístěním aplikací do marketplace, je naděje, že brzy bude pro WP7 existovat “jailbreak” a my budeme mít obchod s neoficiálními aplikacemi? Dají se WP7 “hacknout”?
Všechno jde, jen to stojí čas a peníze. A musíte mít přitom víru, že WP7 z trhu rychle nezmizí.
Hlavní problém je podle mě v tom, že Silverlight aplikace běží ve striktním bezpečnostním modelu, který není snadné překonat. Takže možnosti, které mě napadají:
Metoda pokus-omyl – začneme hledat slabiny v zabezpečení celé CE platformy a doufat, že Microsoft někde udělal další školáckou chybu podobnou té, kterou využil Chevron, jen teď někde v mnohem nižších vrstvách systému. Třeba kdyby Microsoft ponechal i byť jen trochu pootevřen přístup k RAPI, to by byla paráda. Sice se dá “získat” a použít knihovna SmartDevice.Connectivity, která RAPI interně využívá, ale neodemčený telefon nespolupracuje.
Ukázka volání:
Daleko nadějnější mi přijde pokračovat tak, že se do CE registrů přidá vlastní driver, který po startu zařízení shodí výchozí WP7 shell a dovolí vám nainstalovat a spouštět nativní aplikace i aplikace psané v Compact .Net Frameworku. V dalším kroku by to chtělo nového hostitele Silverlight aplikací, který bude vstřícnější k neoficiálním aplikacím. Na některých HTC zařízeních se průnik do registru již podařil… Takže nezbývá než zopakovat: Všechno jde, jen to stojí čas a peníze.
Snad jste v tomhle článků nalezli alespoň něco zajímavějšího než v konfekčních článcích o WP7.
Tuesday, 15 February 2011 15:23:05 (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky | Silverlight | WP7
Monday, 24 January 2011
Tipy pro Windows Phone 7 aplikace V – vytváříme prvni aplikaci (a stavíme ji na vytvořených základech)
V předchozích článcích jsme si vytvořili miniframework pro view modely a ukázali si hostitele našich view modelů. Přišel čas naše znalosti, idiomy a návrhové vzory zakódované ve formě aplikační infrastruktury v našem miniframeworku využít při tvorbě konkrétní aplikace. Pro účely tohoto článku i následujících článků jsem se rozhodl, že vytvoříme aplikaci, která nám dovolí spravovat vlastní blog na doméně Posterous s využitím Posterous API.
Hlavní případy užití, které budeme v aplikaci podporovat.
- Přihlášení uživatele ke svému účtu - téma dnešního článku.
- Zobrazení seznamu blogů, které patří přihlášenému uživateli.
- Zobrazení seznamu příspěvků na vybraném blogu.
- Zobrazení detailu příspěvku na blogu.
- Úprava stávajícího příspěvku na blogu.
- Zadání nového příspěvku na blog.
Jako vždy nás tato prozatím letmo načrtnutá témata dovedou k dalším podivným zákoutím vývoje WP7 aplikací a my se z nich s úsměvem záludnostmi WP7 poučeného idiota zoceleného harcovníka pokusíme dostat..
Skutečně vás nechci urážet popisem klikání ve Visual Studiu ani popisem základů XAMLu, “data bindingu”, “behaviors”, takže jen napíšu, že byste před vytvářením aplikace měli:
- Stáhnout si a nainstalovat Windows Phone Developer Tools.
- Ve Visual Studiu založte nový projekt Windows Phone Application – nejlepší bude, když ho pojmenujete jako já RStein.PosterousReader.WP, abyste nebyli zmateni názvy jmenných prostorů dále v článku.
- Vývojářský život je na tomto projektu jednodušší o to, že k práci s Posterous API použijeme můj C# Posterous API Wrapper pro WP7. Po stažení přidejte referenci na knihovnu RStein.Posterous.API.SLM do svého projektu.
A i když to dnes ještě není tak nutné, zřiďte si na Posterous vlastní účet, abyste mohli aplikaci později testovat na reálných datech. C# Posterous API pro nás v triádě Model-View-ViewModel bude představovat model, což má pro nás výhodu, že se můžeme stále soustředit na view a view modely, o které šlo i v předchozích článcích, a model můžeme považovat za černou skříňku.
- V novém projektu vytvořte hlavně dvě nové složky View a ViewModels. Na obrázku jsou červeně podtrženy další složky, které si již dnes doporučuju přidat do projektu - Behaviors, Extensions, HostServices, Icons, SpecialTypes, UI, UIServices a hlavně ViewModels a Views.
- Předpokládám, že jste schopni do svého projektu vložit kód tříd, které jsem popisoval v předchozích článcích. Seznam předchozích článků s výpisy kódu naleznete na konci tohoto článku.
Dnes vytvoříme přihlašovací obrazovku do naší aplikace. Pro lepší představu je zde obrazovka, kterou byste měli mít hotovou na konci dnešního článku.
Nejprve určíme, které funkce musí obrazovka a její podkladový kód zvládat:
- Přihlašovací obrazovka se zobrazí ihned po startu aplikace.
- Uživateli dovolíme zadat přihlašovací jméno a heslo.
- Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.
- Jestliže dojde k “tombstoningu” stránky, bude jméno i heslo po návratu z “tombstonovaného” stavu zachováno – ověříme si tak poprvé in vivo, že naše třída ViewModelBase podporuje “tombstoning” přesně dle našich požadavků.
- Po návratu na přihlašovací obrazovku z jiné části aplikace bude zachován jen obsah textového pole “uživatelské jméno”, textové pole “heslo” bude prázdné.
- Po kliknutí na tlačítko Přihlásit nebudeme zadané jméno a heslo ihned ověřovat, ale uložíme oba údaje pro použití na dalších stránkách aplikace. Poté přesměrujeme uživatele na další stránku se seznamem blogů, kde bude jméno a heslo využito k získání seznamu blogů uživatele. Důvodem je to, že v Posterous API se jméno a heslo zasílá při každém webovém požadavku, žádné jednorázové přihlášení neexistuje a nemá smysl generovat nějaký “dummy” požadavek jen pro ověření hesla. Jestliže se na stránce se seznamem blogů, na kterou z přihlašovací obrazovky přesměrováváme, data kvůli neplatným přihlašovacím údajům získat nepodaří, aplikace nás vrátí na přihlašovací obrazovku. Vytvoření stránky se seznamem blogů bude téma dalšího článku.
A začínáme:
Do složky Views vložte nové View pro přihlášení uživatele. V dialogu Add new item vyberte Windows Phone Portrait Page a pojmenujte ji MainLoginView.xaml.
Jedná se o úvodní stránku aplikace, a proto v deskriptoru WP7 aplikace nazvaném WAMppManifest.xml, který naleznete v projektové složce Properties, změníme startovací stránku na View/mainLoginView.xaml.
I když to není u přihlašovacího dialogu nutné, v dnešním článku si ukážeme, jak můžeme jednoduše složit jedno view z dalších nezávislých view a také to, že view nemusí být jen stránka (Page), ale i “User Control”. Naše MainLoginView bude zobrazovat titulek aplikace (Posterous klient), titulek stránky (Přihlášení) , ale textová pole “heslo”, “uživatelské jméno” a tlačítko “Přihlásit" se” bude obsahovat “user control” LoginView, který můžeme použít jako součást i zcela jiného view (stránky) v aplikaci. Do složky Views přidejte Windows Phone User Control a nazvěte jej LoginView.xaml.
Zkompilujte (Build) “solution”.
Do view MainLoginView vložte tento kód:
Jak jsem zmiňoval v jednom z předešlých článků, všechny stránky v aplikaci by měly být potomkem naší třídy PageBase, která je hostitelem pro view modely. Proto je naše stránka uzavřena v elementu <controlex:PageBase>. XML jmenný prostor controleex je u mě namapován na xmlns:controlex="clr-namespace:RStein.PosterousReader.WP.UI". V projektovém adresáři UI musíte mít tedy třídu PageBase.
Také v “code behind souboru” musíte třídu MainLoginView učinit potomkem PageBase.
Hlavní xaml pro přihlášení uživatele obsahuje “User Control” LoginView, na který se v MainLoginView odkazujeme. (<views:LoginView Grid.Row="1"></views:LoginView>). XML jmenný prostor views je mapován na jmenný prostor RStein.PosterousReader.WP.Views v C# (xmlns:views="clr-namespace:RStein.PosterousReader.WP.Views) – v našem případě tedy na projektový adresář Views.
Do view s názvem LoginView vložte následující XAML:
Jak bylo vidět na snímku obrazovky na počátku článku, LoginView obsahuje hlavně textové pole pro zadání jména uživatele (txtName) a prvek pro zadání hesla uživatele (txtPassword) a tlačítko “Přihlásit se”. U prvků pro zadání uživatelského jména a hesla vás mohou zarazit jen tagy <i:Interaction.Behaviors> a atributy jako behaviors:TextboxPasswordAttachedProperties.TextBoxChangedAction, jejichž význam vysvětlím za chvíli. Také vás hned upozorním, že tlačítko “Přihlásit se” je speciální tlačítko ((<controlex:ButtonEx) dovolující reagovat na stisknutí tlačítka uživatelem tak, že spustí metodu Execute objektu ICommand, což standardní WP7 tlačítko nezvládá. Znovu připomínám, že Silverlight ve WP7 je bohužel založen na poměrně staré verzi 3 desktopového Silverlightu. Kód třídy ButtonEx uvidíte také za chvíli.
Máme view, ve view používáme “data binding”, ale nemáme ještě view modely, které fungují pro view jako zdroj dat.
V každém view modelu naší aplikace budeme používat objekt IPosterousApplication ke komunikaci s Posterous, budeme chtít z view modelů navigovat na další view v aplikaci a také bychom měli být schopni v každém view modelu získat uživatelské jméno a heslo zadané na na námi vytvářené přihlašovací obrazovce. To znamená, že společné vlastnosti a služby view modelů pro naši aplikaci můžeme soustředit v bázové třídě PosterousViewModelBase. Do projektové složky Views vložte novou třídu PosterousViewModelBase a do ní zkopírujte následující kód.
PosterousViewModelBase je potomkem naší staré známé třídy ViewModelBase
V konstruktoru třída PosterousViewModelBase přijímá odkaz na objekt IPosterousApplication, který je základním rozcestnikem pro přístup k Posterous API, navigační službu INavigationService, která view modelu dovolí navigovat na další view v aplikaci (např. ze seznamu článků na detail vybraného článku), a titulek zobrazené stránky. Titulek převezme a nabízí poté ve vlastnosti PageTitle bázová třída ViewModelBase.
Navigační služba je představována rozhraním INavigationService.
Základní implementace rozhraní INavigationService pro WP7 je dostupná ve třídě DefaultWPNavigationService a pouze obaluje navigační služby dostupné v samotných WP7 aplikacích.
Rozhraní INavigationService i třídu DefaultWPNavigationService vložte do projektové složky HostServices.Více si o navigaci mezi různými view povíme v dalších článcích.
Vraťmě se k PosterousViewModelBase. V PosterousViewModelBase máme vždy uloženo ve statických vlastnostech LastUsedUserName a LastUsedPassword poslední zadané přihlašovací údaje, které může každý view model využít při získání nebo úpravě dat z Posterous.
Poznámka: Kdyby někoho z vás pohoršovali statické vlastnosti, klidně si jako domácí úkol napište “CredentialsManager”, který bude injektován stejně jako již zmíněné dvě další služby do view modelů. Prozatím nechci komplikovat kód víc, než je nutné.
My máme dvě view, MainLoginView a LoginView, a proto vytvoříme i dva view modely. Opět zdůrazňuju, že tato volba je na vás a díky “dědění view modelů” bychom mohli ponechat třeba dvě view a vytvořit pro ně jen jeden společný view model.
Do projektové složky ViewModels přidejte třídu nazvanou MainLoginViewModel – view model pro MainLoginView. Název view modelu nyní musí odpovídat konvenci “NázevView +ViewModel”. Zdůrazňuju i zde, že jde jen o konvenci a že si můžete jednoduše napsat jiný IViewModelResolver, který dohledá dle vašich zcela jiných projektových konvencí k view vhodný view model.
MainLoginViewModel je sympaticky jednoduchý objekt. Má jen delegující konstruktor, ve kterém předá své bázové třídě PosterousViewModelBase vyžadované povinné argumenty posterousApplication, navigationService a titulek stránky.
Texty v aplikaci nejsou prozatím lokalizovány a titulky stránek jsou uloženy ve třídě GlobalConstants. Do “rootu” projektu přidejte třídu GlobalConstants.cs
Název aplikace (APP_MAIN_TITLE) , podobně jako titulek stránky, vydává ve vlastnosti AppTitle opět ViewModelBase
Jen o trochu složitější bude view model pro LoginView. Do projektové složky ViewModels přidejte třídu LoginViewModel.
V LoginViewModelu máme opět delegující konstruktor, jedinou změnou oproti MainLoginViewModelu je to, že jako titulek stránky předáváme prázdný řetězec, protože předpokládáme, že titulek na stránku dodá “nadřízené” view.
Vlastnosti a jejich význam
Název vlastnosti | Popis |
TextChangedAction | Akce, která má být vyvolána, když se ve view změní text přihlašovacího jména nebo hesla. Tato akce je v konstruktoru inicializována tak, že si vynutíme opětovné vyhodnocení toho, zda může být proveden LoginCommand. V článku popíšu, proč je to (prozatím?) řešeno takto. |
LoginCommand | Objekt podporující rozhraní ICommand, který v metodě Execute zavolá metodu handleLogin. Metoda handleLogin uloží zadané jméno a heslo do statických vlastností LastUsedPassword a a LastUsedUserName a přesměruje uživatele na stránku se seznamem blogů. Instanční vlastnost UserPassword je při každém pokusu o navigaci na stránku se seznamem blogů “vyčištěna” tím, že je do ní uložen prázdný řetězec, a při návratu na přihlašovací obrazovku tedy není automaticky předvyplněno heslo, což byl jeden z našich požadavků na přihlašovací dialog. LoginCommand může být proveden jen tehdy, když bylo ve view zadáno a do view modelu přes oboustranný (two way) binding zpropagováno uživatelské jméno a heslo. Pokud vlastnosti UserName a UserPassword mají hodnotu null nebo obsahují prázdný řetězec, LoginCommand nemůže být proveden. |
UserName | Zadané přihlašovací jméno na službu Posterous. V metodě DoInternalInit, která je volána na začátku životního cyklu view modelu předvyplníme uživatelské jméno posledním zadaným uživatelským jménem, které jsme dříve uložili do statické vlastnosti LastUsedUserName. |
UserPassword | Heslo na službu Posterous. |
Co potřebujeme, abychom mohli náš LoginViewModel zkompilovat?
Do třídy GlobalConstants přidejte URL dalšího view se seznamem blogů a článků.
Stránku PostsListView.xaml vytvoříme v dalším článku, nyní ji celou vytvářet nemusíte. Postačí do složky Views dát novou stránku (Page) s názvem PostsListView.xaml.
Jak jsem již psal, ve WP7 nemáme bohužel podporu pro objekty ICommand. Třída DelegateCommand, jejíž instancí je LoginCommand, je minimalistickou implementací rozhraní ICommand.
Pokud podobnou třídu nemáte, vložte si do projektu třídu DelegateCommand. U mě je v jmenném prostoru RStein.PosterousReader.Common.
Třídě DelegateCommand můžete předat dva delegáty. Delegát executeAction (“co má být vykonáno”) je spuštěn v metodě Execute z rozhraní ICommand. Delegát canExecuteAction představuje implementaci metody CanExecute (“může být nyní command vykonán?”). Minimalistická implementace je to proto, že nijak nepoužívám událost CanExecuteChanged, a namísto toho jsem si pro “binding” vystavil speciální vlastnost CanExecuteCommand, která v get akcesoru deleguje na metodu CanExecute.
Vlastnost TextChangedAction v LoginViewModelu je delegát typu Action, který nám pomáhá vyřešit jeden z požadavků na přihlášení.
“Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.”
To znamená, že potřebujeme po každém přidání nebo smazání znaku v textboxu pro zadání jména uživatele i v textboxu pro zadání hesla zjišťovat, jestli můžeme ve view zpřístupnit tlačítko pro přihlášení. Když je alespoň jeden textbox prázdný, tlačítko pro přihlášení není dostupné, když je vyplněno alespoň jedním znakem jméno i heslo, tlačítko pro přihlášení je dostupné.
Jak je tento požadavek ve view a view modelu splněn?
Ve view LoginView je vlastnost IsEnabled tlačítka “Přihlásit se“ z třídy ButtonEx “nabindována” na vlastnost CanExecuteCommand objektu LoginCommand ve view modelu.
Do projektové složky UI si přidejte třídu ButtonEx, která doplňuje standardní WP7 tlačítko o jednoduchou podporu objektů ICommand.
Vlastnost CanExecuteCommand musí vracet true, pokud je v každém textboxu alespoň jeden znak, jinak false. Jak ale ve view modelu zjistíme, že uživatel zadal nebo smazal v některém textovém poli další znak? Do view modelu se při oboustranném bindingu zpropaguje hodnota z textového pole až po opuštění textového pole uživatelem, ale my přitom musíme reagovat ve view modelu na zadání každého znaku.
Takové chování textových polí ve WP7 skutečně nemáme a musíme si ho dopsat, a to nejlépe za pomoci ”attached” vlastností a objektů Behavior<T>. Nejprve se podívejme, jak vypadají objekty TextBox a PasswordBox v XAMLu, když jsou rozšířeny o “attached” vlastnost TextboxPasswordAttachedProperties.TextBoxChangedAction, která je “nabindována” na nám již známou vlastnost TextChangedAction typu Action ve view modelu. “Attached” vlastnost TextBoxChangedAction tedy říká: “Hej, kdykoli se změní u prvku zadaný text, musí NĚKDO ochotný zavolat delegáta TextChangedAction, abychom ve view modelu nebyli odříznuti od novinek ve světě view.”
A ten někdo bude náš objekt Behavior , který zase světu sděluje: “Mám dobré vychování, a když mi dovolíte vstoupit do bran tagu PasswordBox nebo TextBox, delegáta TextChangedAction zavolám, i když to sám WP7 TextBox a PasswordBox nezvládne.”
Další nepříjemností ve WP7 je, že i když objekt Behavior má mít pro TextBox a PasswordBox stejné chování, musíme napsat dva objekty Behavior pro každý prvek, protože TextBox ani PasswordBox spolu kupodivu nemají moc společného. My se alespoň pokusíme kód v obou objektech Behavior neduplikovat.
Do projektové složky Behaviors přidejte třídu TextboxPasswordAttachedProperties a v ní vytvořte attached vlastnost TextBoxChangedAction.
Do složky Behaviors přidejte abstraktní třídu TextChangedBehaviorBase, která bude fungovat jako základ pro dva podobné objekty Behavior určené pro TextBox (TextBoxTextChangedUpdateSourceBehavior) a PasswordBox (PasswordTextChangedBehavior). Do projektu musíte přidat referenci na knihovnu System.Windows.Interactivity, kterou naleznete většinou ve složce c:\Program Files\Microsoft SDKs\Expression\Blend\Windows Phone\v7.0\Libraries\System.Windows.Interactivity.dll. Bez této knihovny není dostupná bázová třída Behavior<T>.
Metoda RunTextChangedAction se pro objekt Textbox a PasswordBox asociovaný s tímto objektem Behavior pokusí dohledat a spustit delegáta v “attached” vlastnost TextBoxChangedAction. Metoda UpdateSource požádá odvozené třídy o vydání "objektu BindingExpression” voláním metody GetBindingExpression. Vrácený objekt “BindingExpression“ by měl zapouzdřovat propojení vlastnosti view modelu (např UserName) s textem v textovém poli. Po kontrole, že se jedná o oboustranný (two way) binding, metoda UpdateSource zavolá na objektu BindingExpression UpdateSource, což způsobí přenesení hodnoty zadané uživatelem v textovém poli ve view do zdrojové vlastnosti (např. UserName) ve view modelu.
Potomek TextBoxTextChangedUpdateSourceBehavior v metodě GetBindingExpression vrátí BindingExpression pro svou vlastnost Text. V přepsané metodě OnAttached, která je volána vždy, když je objekt Behavior asociován s textboxem, si přihlásíme odběr události TextChanged TextBoxu a při každé změně textu voláme zděděné a výše popsané metody UpdateSource a RunTextChangedAction. V metodě OnDetaching si nezapomeneme odběr události TextChanged odhlásit.
Pro PasswordBox máme dalšího potomka PasswordTextChangedBehavior, který se od třídy TextBoxTextChangedUpdateSourceBehavior liší jen tím, že v metodě GetBindingExpression vrací “BindingExpression” pro vlastnost Password a zadávaný text sleduje přes událost PasswordChanged.
V LoginView si nezapomeňte zkontrolovat, že máte namapovány správně xml prefixy behaviors a i na správné jmenné prostory v C#, abyste mohli využívat novou attached vlastnost TextBoxChangedAction a objekty TextBoxTextChangedUpdateSourceBehavior a PasswordTextChangedBehavior.
prostoryxmlns:behaviors="clr-namespace:RStein.PosterousReader.WP.Behaviors"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Dodejme, že náš delegát TextChangedAction ve view modelu simuluje změnu objektu LoginCommand, který je přidružen k tlačítku Přihlásit se a tlačítko Přihlásit tedy po každém zadání znaku zjistí, jestli má být dostupné. Zopakujme, že vlastnost IsEnabled tlačítka je “nabindována” na vlastnost LoginCommand.CanExecuteCommand.
Aplikaci můžete spustit.
Poznámka: Ještě si ale nezapomeňte do složky UIServices dát kód rozhraní IViewModelResolver a třídy ViewModelResolver, které jsem popisoval v článku propojení view modelu s view (stránkou), protože bez třídy ViewModelResolver by aplikace nebyla schopna pro naše MainLoginView a LoginView dohledat právě vytvořené view modely. A v aplikaci musíte mít samozřejmě všechny další třídy z předchozích článků, které jsou odpovědné za “tombstoning” apod.
U aplikace si můžete zkontrolovat, že:
- Přihlašovací dialog plní všechny požadavky, které jsme vypsal na začátku článku.
- Nemusíme se nijak starat o “tombstoning” aplikace. Zmáčkněte tlačítko Win, poté se tlačítkem Back vraťte do aplikace a všechny hodnoty v textových polích zůstanou zachovány.
- Tlačítko pro přihlášení je dostupné jen tehdy, když textová pole pro zadání jména a hesla jsou vyplněna. Pokud alespoň jedno pole vyplněno není, tlačítko je neaktivní.
- I naše minimalistická podpora rozhraní ICommand vede k tomu, že ve View nemáme žádný “code behind”. Logika stojící za view je jen ve view modelech a XAML tahá data z view modelů přes “data binding”. Pokud přesně tohle to považujete za důležité, budete určitě nadšeni. Jak uvidíte v dalších článcích, já jsem vůči strategii “vše do XAMLu” hodně skeptický, ale XAML puristé
si mohou jít po dnešku jistě ožrat držku slavit.
- I když šlo jen o přihlašovací formulář, vytvořili jsme si další skládací kostky (attached vlastnosti, Behavior, PosterousViewModelBase), které se nám budou hodit při psani dalších view a view modelů (nejen) v této aplikaci.
Zauvažujte nad tím, jestli by se nám také nehodila nějaká podpora ve view modelech pro ukládání nejen tranzientního stavu, ale i pro ukládání “trvalejšího” stavu, který bude dostupný i v další nezávislé instanci aplikace. Možná by stálo za to, aby si aplikace pamatovala poslední zadané přihlašovací jméno nejen při “tombstoningu”, ale aby přihlašovací jméno bylo nabídnuto i při novém spuštění aplikace. A z hlediska vývojáře bezbolestná podpora pro ukládání perzistentního stavu (data s “delší záruční lhůtou”) bude námětem dalšího WP7 intermezza. Doufám, že se těšíte.
Předchozí č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”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Monday, 24 January 2011 17:48:26 (Central Europe Standard Time, UTC+01:00)
C# Posterous API | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Monday, 10 January 2011
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Dnešní článek je jen “intermezzem”, protože doplňuje předchozí článek o slíbenou informaci, jak můžeme automaticky registrovat serializovatelné třídy, jejichž instance nesou tranzientní stav, který je uložen v KnownTypesDictionary. Přechozí článek končil ukázkou, jak můžeme vrátit staticky definovaný seznam typů, dnes se poohlédneme po trochu dynamičtějším řešení. I když postup není omezen jen na WP7 aplikace, ale můžete ho použít v kterékoli aplikaci, která využívá (WCF) DataContractSerializer, my se zaměříme v článku opět hlavně na řešení WP7 specialitek.
Přečetli jste si první odstavec a nevíte, co je tranzientní stav? Hledáte marně v MSDN typ KnownTypesDictionary? Nedivte se, milostný akt sice můžete rozjet bez předehry, ale abyste rozuměli tomuto intermezzu, pečlivě a pomalu si přečtěte jako předehru k intermezzu tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”, i díl třetí, kde se bavíme o KnownTypesDctionary.
Předehru máte za sebou, takže víte, že KnownTypesDictionary je specializovaný objekt Dictionary, ve kterém je klíčem řetězec a hodnotou libovolný objekt a že v objektech KnownTypesDictionary ukládáme tranzientní stav view modelů při každém “tombstoningu”. I když KnownTypesDictionary může jako hodnotu nést libovolný objekt, my musíme instruovat DataContractSerializer, které objekty a hlavně z kterých odvozených tříd má v objektu Dictionary očekávat.
Když do KnownTypesDictionary ukládáme v jedné aplikaci instance z tříd OrderVO a InvoiceVO a v jiné aplikaci instance TwitterPost a ObservableCollection<TwitterPost >, musíme vždy znovu zmíněné třídy registrovat pomocí atributu KnownType.
Pro připomenutí následuje výpis kódu, kterým jsme končili a který registruje serializovatelné třídy v aplikaci “natvrdo” pomocí statické metody GetKnownTypes, jejíž název je předán do konstruktoru atributu KnownType.
Napevno zadrátované typy v KnownTypesDctionary nám nemusí vadit v jedné aplikaci, ale když chceme používat KnownTypesDctionary v mnoha různých aplikacích, musím řešení upravit.
Místo abychom třídy v metodě GetKnownTypes registrovali přímo, delegujeme odpovědnost za vrácení serializovatelných typů na specializovaného poskytovatele.
BTW: To víte, že první a okouzlující zákon vztahu mezi třídami zní: “Když je třída vyčerpána množstvím odpovědností, vždy si může přičarovat otroka, který špinavou práci udělá za ni”?. Občas se tomuto zákonu také říká SRP – princip jedné odpovědnosti třídy”.
Poskytovatel je objekt podporující rozhraní IKnownTypeProvider s jednou samopopisnou metodou.
Refaktorizujeme třídu KnownTypesDictionary, aby při vracení serializovatelných typů využívala objekt IKnownTypeProvider.
Do nové statické vlastnosti KnownTypeProvider je ve statickém konstruktoru přiřazen DefaultKnownTypesProvider, o kterém budeme mluvit za chvíli, ale do vlastnosti KnownTypeProvider můžete klidně vložit pro účely vaší aplikace lépe přizpůsobený IKnownTypeProvider.
Metoda GetKnownTypes vrátí všechny staticky registrované typy v poli KNOWN_TYPES společně s typy, které dodá IKnownTypeProvider. Typy jsou získány jen při prvním volání metody GetKnownTypes a jsou cachovány v proměnné _cachedKnownTypes, abychom opakovaným získáním typů z IKnownTypeProvider zbytečně neplýtvali výkonem WP7 telefonů ani trpělivostí uživatele. Předpokladem tohoto přístupu samozřejmě je, že v aplikaci je fixní množina serializovatelných typů, která se za běhu aplikace již nemění. Jestli vám to nevyhovuje, odstraníte cache z KnownTypesDictionary za 10 sekund.
Jaké odpovědnosti bude mít DefaultKnownTypesProvider?
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v hlavní assembly aplikace.
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v dalších assembly, ze kterých je složena aplikace.
- DefaultKnownTypesProvider nalezne další a pro aktuální aplikaci specifické objekty podporující rozhraní IKnownTypeProvider a vrátí seznam všech typů z těchto providerů.
- Pro všechny nalezené typy T zaregistruje i kolekci Observable<T>. To znamená, že pro třídu OrderVO označenou atributem DataContract je automaticky vrácen z poskytovatele nejen její Type (typof(OrderVO), ale i deskriptor kolekce typeof(ObservableCollection<OrderVO>).
Kód třídy DefaultKnownTypesProvider:
V metodě GetKnownTypes vyzvedneme všechny typy z “hlavní” (vstupní) assembly. Hlavní assembly získáme v metodě getExecutingAssembly přes vlastnosti třídy Deployment.
Typy z hlavní assembly sloučíme s typy v dalších assembly voláním metody getRefencedAssembliesTypes(). Ve WP7 jsem bohužel nenašel způsob, jak seznam dalších assembly získat, a proto další assembly poskytují objekty IAssemblyTypesProvider nalezené v hlavní assembly.
Chcete-li tedy automaticky vyhledat typy označené atributem DataContract v další assembly, vložte do hlavní assembly třídu podporující rozhraní IAssemblyTypesProvider.
Do proměnné collectionTypes v metodě GetKnownTypes vygenerujeme pro všechny nalezené typy jejich kolekce. Možná trochu složitě vypadající kód jen zabraňuje tomu, abyste nalezené deskriptory tříd typu ObservableCollection<T> v proměnné types balili znovu do kolekce ObservableCollection. Jinými slovy, když v proměnné types bude kolekce ObservableCollection<OrderVO>, do proměnné collectionTypes nebude generována kolekce ObservableCollection<ObservableCollection<OrderVO>>.
Na konci vracíme nalezené typy sloučené s vygenerovanými typy kolekcí ObservableCollection<T> a dalšími a pro aplikaci specifickými typy z ostatnich poskytovatelů . Za poskytovatele považujeme další objekty IKnownTypeProvider nalezené v hlavní assembly.
A tím máme hotovo.
Připomínám, že DefaultKnownTypesProvider můžete využít nejen ve WP7 aplikacích, ale ve všech aplikacích v .Net Frameworku, kde je používán DataContractSerializer a vy chcete automaticky registrovat odvozené serializovatelné typy.
Příště již začneme stavět aplikaci založenou na našem "mini frameworku”, abyste viděli, k čemu tyto počáteční díly seriálu včetně intermezza, které právě čtete, vůbec byly.
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”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Monday, 10 January 2011 13:37:12 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Návrhové vzory | Silverlight | WP7
Monday, 03 January 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:
- 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.
- 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.
- 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.
. - Jestliže uživatel ze stránky odchází, hostitel na view modelu volá metodu Deactivate z rozhraní IDeactivated.
- Hostitel musí být schopen při “tombstoningu” uložit tranzientní stav view modelu.
- Po návratu z “tombstoningu” hostitel dovolí view modelu obnovit tranzientní stav.
- 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:
- 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.
- 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.
.
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, 03 January 2011 15:46:50 (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Friday, 17 December 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ů” .
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.
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.
- 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.
- 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ů.
- 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.
- 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.
- 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.
- 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, 17 December 2010 19:25:14 (Central Europe Standard Time, UTC+01:00)
C# | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Monday, 06 December 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í. 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čí.
Monday, 06 December 2010 10:57:19 (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Silverlight | WP7
Monday, 15 November 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 , 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
- Podpora pro Windows Phone 7.
- 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.
- Mnoho drobných změn a opravy chyb.
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:
- Úvod do mého Posterous API
- Ukázka práce s Posterous API – zálohování blogu
- Ukázka práce s Posterous API v Silverlightu (součást spotu s informací o uvedení verze 0.0.0.2)
- Posterous API a RX Framework.
Monday, 15 November 2010 16:25:38 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Silverlight | WP7
Saturday, 18 September 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, 18 September 2010 10:17:31 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Monday, 23 August 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, 23 August 2010 14:33:03 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | LINQ | Programátorské hádanky
Monday, 24 May 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:
- 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.
- 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*.
- 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, 24 May 2010 15:50:36 (Central Europe Standard Time, UTC+01:00)
C# | Nativní kód