\

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


 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