\


 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.

  1. Přihlášení uživatele ke svému účtu  - téma dnešního článku.

  2. Zobrazení seznamu blogů, které patří přihlášenému uživateli.

  3. Zobrazení seznamu příspěvků na vybraném blogu.

  4. Zobrazení detailu příspěvku na blogu.

  5. Úprava stávajícího příspěvku na blogu.

  6. 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.Smile.

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:

  1. Stáhnout si a nainstalovat Windows Phone Developer Tools.

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

    image
     
  3. 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.

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


    image

  5. 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:

  1. Přihlašovací obrazovka se zobrazí ihned po startu aplikace.

  2. Uživateli dovolíme zadat přihlašovací jméno a heslo.

  3. 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í.

  4. 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ů.

  5. 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é.

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

image

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.

image

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.

image

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.

image

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.

image

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.

image

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

image

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"

image

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

  • 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 slavitSmile.

  • 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. Smile

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)       
Comments [0]  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”.Smile

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?

  1. DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v hlavní assembly aplikace.

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

  3. 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ů.

  4. 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.Smile

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

Tipy pro Windows Phone 7 aplikace I

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

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)       
Comments [0]  .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:

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

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

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

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

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

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

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

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

Třída PageBase:

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

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

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

 

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

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

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

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

Rozhraní IViewModelResolver

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

var statebag = new KnownTypesDictionary();

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

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

.

 

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

Tipy pro Windows Phone 7 aplikace I

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



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