\


 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, 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  Smile, ale brzy sem dám nějaké postřehy, jaké to je vrátit se do časů Silverlightu 3, na kterém je mobilní verze Silverlightu založena, a hlavně tipy, jak si vývoj mobilních aplikací usnadnit.

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

Hlavní změny v této verzi

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

imageimageimageimage

Soubory ke stažení:

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

RStein.Posterous.API.dll  - Silverlight 4

RStein.Posterous.API.dll  - Windows Phone 7

Rozcestník:

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


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


 Tuesday, 02 February 2010
Hrátky s Reaktivním frameworkem (RX extenze)

V předchozím článku jsem ukazoval, jak volat asynchronně metody z C# Posterous API v Silverlightu. C# Posterous API nabízí asynchronní zpracování pomocí jednoho z doporučovaného přístupu k asynchronním operacím v .Net Frameworku – metoda s konvenčním sufixem Async (LoadPostsAsync) spustí vykonání operace v jiném vlákně a výsledky operace jsou nabídnuty v argumentech události, která je (opět) jen dle jmenné konvence spojena s asynchronní operací (událost LoadPostsCompleted). C# Posterous API nenabízí ve svém rozhraní  metody pro podporu dalšího a již od verze 1.0 .Net Frameworku přítomného asynchronního vzoru, který je spojen s dvojicí metod začínajících prefixem Begin a End. (BeginGetRequest, EndGetRequest, BeginRead, EndRead apod.)

Dále předpokládám, že oba přístupy k vytváření asynchronních opreací znáte a že jste si vědomi i toho, jak se způsob práce s asynchronními API odlišuje od práce s běžnými synchronnními metodami.

V již odkazovaném článku bylo dobře patrné, jak je řízení toku asynchronních operaci odlišné od sady volání běžných synchronních operací.

Pro připomenutí:

posterousAccount.SitesLoaded += (o, e) =>
                      {
                          throwIfAsyncEx(e.Exception);
                          posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
                                                                          {
                                                                              throwIfAsyncEx(e2.Exception);
                                                                              Posts = (from p in e2.Value
                                                                                      select new ViewPost
                                                                                                 {
                                                                                                     Title = p.Title,
                                                                                                     Body = p.Body,
                                                                                                     Url = p.Url

                                                                                                 }).ToList();                                                                                                            
                                                                              
                                                                          };
                          posterousAccount.PrimarySite.LoadAllPostsAsync();
                      };



posterousAccount.LoadSitesAsync();

Jediné, co tento kód dělá, je, že nejprve (!) nahraje všechny blogy (příkaz k asynchronnímu nahrání posterousAccount.LoadSitesAsync(); je na posledním (!) řádku. Na prvním (!) řádku máme zpracování výsledku volání metody LoadSitesAsync, ve kterém opět nejdříve (!) lambdou přihlášenou k odběru události  PostsLoaded (posterousAccount.PrimarySite.PostsLoaded += (_, e2)) řekneme, jak zpracujeme výsledek následného (!) volání další asynchronní metody (posterousAccount.LoadSitesAsync());. Tato “inverzní“ práce s asynchronními metodami a zpracováním jejich výsledku je na hony a možná ještě dále vzdálena intuitivní práci se synchronními metodami.:-)

Zkusme se nyní podívat, jak by nám s “převrácením starších asynchronních metod z hlavy zpět na synchronní nohy” mohl pomoci RX Framework. Úplné základy v tomto článku nezazní a začátečníky odkazuji na sérii přednášek na Channel 9, kde dozvíte i zajímavé podrobnosti o genezi celého RX Frameworku  a matematické dualitě rozhraní IEnumerable a IObservable (jinými slovy o společných rysech dobře známých GoF návrhových vzorů Iterátor a Observer).

Současné příklady jsou vytvořeny v aplikaci Windows Forms pro .Net 3.5. Silverlight má své zvláštnosti a a rozchození příkladů v SL si zaslouží další článek, protože teď by řešení problémů specifických pro SL zamlžovalo cíl příkladu. Aplikace je pro .Net 3.5, protože stejná aplikace pro .Net 4.0 hlásí konflikt (ambiguous reference) mezi NF typy a RX typy.

Upozornění: Nic z toho, co napíšu neberte ani jako dogmata ani, nedej bože, jako best practices. RX Framework je v Betě, zdokumentován je mizerně a z jednoho řádku u každé metody se dá jen těžko bez dalších experimetů vytušit, co přesně metoda dělá. Tento článek je výsledkem hraní si pro účely jednoho projektu, kam se RX extenze hodí  a zjednodušují (alespoň to tak prozatím vypadá :-) ) dost rutinních činností.

Zde j výsledek našeho snažení, abychom měli motivaci se RX Frameworkem zabývat.

 var resultPosts = from sites in account.GetSites()
                              from site in sites.ToObservable()
                              from posts in site.GetPosts()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

Získání blogů (Sites) i blogpostů (post) je stále asynchronní, ale výsledný kód vypadá jako běžný LINQ (To Enumerable) dotaz. Žádné inverzní volání a práce s výsledkem, jen prostý dotaz, jehož zvláštností je pouze to, že v některých místech voláme metodu ToObservable.

Jak jsem dosáhl tohoto výsledku?

Podíváme-li se na první řádek, vidíme, že voláme metodu account.GetSites. Metoda GetSites součástí C# Posterous API není a jedná se o extenzní metodu. Tato extenzní metoda je zvláštní tím, že její návratovou hodnotou je je jedno z klíčových rozhraní v RX Frameworku – rozhraní IObservable<T>.

        public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)

Rozhrani IObservable má v RX Frameworku podobný význam jako rozhraní IEnumerable v celém .Net Frameworku.  Zjednodušeně můžeme rozhraní IObservable popsat jako ceduli, kterou třída implementující rozhraní dává celému světu najevo: “Miluju voyery, jestliže chcete sledovat, co se ve mně děje, dejte mi sem pozorovatele a já na sebe všechno podstatné, co se od této chvíle stane, postupně  vyzvoním ”.

Rozhraní IObservable je tedy příslib, že zainteresovaný pozorovatel dostane data, která třída podporující toto rozhraní nabízí. Svůj zájem pozorovatel deklaruje tak, že předá odkaz sám na sebe do metody Subscribe.

public interface IObservable<T> 
{
 IDisposable Subscribe(IObserver<T> observer); 
}

Pozorovatel (IObserver) reaguje (proto reaktivní framework) na informace, které jsou mu poskytnuty objektem podporujícím rozhraní IObservable.

public interface IObserver<T> 
{ 
void OnCompleted(); 
void OnNext(T value); 
void OnError(Exception exn); 
}

Metoda OnNext je na IObserver volána vždy, když Observable objekt má k dispozici další data. Metodou OnError Observable objekt signalizuje chyby a metodou OnCompleted Observeru říká “jsem u konce, nic dalšího už pro tebe nemám”.

Naše metoda GetSites tedy říká – zavolejte mě a já vám nabídnu IObservable objekt, který, až budou data k dispozici, vašemu observeru (IObserver) vydá kolekci (IEnumerable) objektů IPosterousSite.

Extenzní metoda GetSites vypadá takto:

 public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
        {
             
            checkAccountNotNull(account);            
             var sitesEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousSite>>>(handler => account.SitesLoaded += handler,
                                                                                                handler => account.SitesLoaded -= handler)
                                        .Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);



             return sitesEvents.GetFinalObservableEvents(account.LoadSitesAsync);
            
        }

 

Po kontrole, zda předaný IPosterousAccount není null, využijeme pomocnou metodu Observable.FromEvent z RX Frameworku, která nám vrátí IObservable objekt. Tento IObservable objekt notifikuje případného observera o každé nastalé události sites.Loaded. V našem případě Observera notifikuje o právě jedné události, protože jsme použili metodu Take (Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT)) a konstanta DEFAULT_TAKE_EVENTS_COUNT má hodnotu 1. Jak si můžete všimnout, metoda FromEvent nám dovoluje s událostmi, které postupně nastávají, zacházet jako (s potenciálně nekonečnou) kolekcí hodnot. Metodě FromEvent jsme pouze museli říct, jaká třída nese argumenty událost (EventArgsValue<IEnumerable<IPosterousSite>) a poskytli jsme ji dva delegáty pro registraci/deregistraci obslužných handlerů, které nám RX Framework předá  (handler => account.SitesLoaded += handler, handler => account.SitesLoaded –= handler). U našeho volání metody Take bych ještě poznamenal, že po vyvolání první události dojde automaticky RX Frameworkem k deregistraci obslužného handleru.

Proměnná sitesEvents je IObserver tohoto typu.

IObservable<IEvent<EventArgsValue<IEnumerable<IPosterousSite>>>>

Argumenty události jsou vždy zabaleny do instance třidy IEvent, která je vydána zaregistrovanému observeru v jeho metodě OnNext. Všimněte si ale, že návratovou hodnotou metody GetSites je již Observable, který observeru předá hodnoty bez IEvent (IObservable<IEnumerable<IPosterousSite>>).

Vidíme, že na sitesEvents je volána další má extenzní metoda GetFinalObservableEvents, které je předán delegát Action ukazující na asynchronní metodu account.LoadSitesAsync, a výsledek volání GetFinalObservableEvents je vrácen klientovi.

Metoda GetFinalObservableEvents:

  public static IObservable<TEventData> GetFinalObservableEvents<TEventData>(this IObservable<IEvent<EventArgsValue<TEventData>>> sourceEvents, Action runAction)
        {
            if (sourceEvents == null)
            {
                throw new ArgumentNullException("sourceEvents");
            }
            var retObservable = new DelegateObservable<TEventData>(
                observer =>
                    {
                        
                        var eventObserver = new EventObserver<TEventData>(observer);
                        var unsubScribe = sourceEvents.Subscribe(eventObserver);
                        runAction();
                        return unsubScribe;
                    });

            return retObservable;
        }

 

Metoda GetFnalObservableEvents vrací opět Observable, ale tentokrát jde o Observable typu IObservable<TEventData>  - jinými slovy, v našem případě IObservable<IEnumerable<IPosterousSite>>. Jak je toho dosaženo? Zdrojový IObservable objekt nazvaný sourceEvents je předán instanci třídy DelegateObservable, což je v současném scénaři již ten hledaný Observable podporující rozhraní IObservable<IEnumerable<IPosterousSite>>. DelegateObservable je tedy adaptér, který převádí události zabalené do IEvent na “rozbalené” hodnoty očekávané observerem. DelegateObservable je můj pomocný IObservable, který dostává do konstruktoru lambdu představující tělo jeho metody Subscribe, abychom nemuseli reimplementovat rozhraní IObservable v různých třídách stále dokola.

Výpis třídy DelegateObservable

 public class DelegateObservable<T> : IObservable<T>
    {
        private readonly Func<IObserver<T>, IDisposable> m_subscribeDel;

        public DelegateObservable(Func<IObserver<T>, IDisposable> subscribeDelegate)
        {
            m_subscribeDel = subscribeDelegate;
            if (m_subscribeDel == null)
            {
                throw new ArgumentNullException("subscribeDelegate");
            }
        }

        #region Implementation of IObservable<out T>

        public IDisposable Subscribe(IObserver<T> observer)
        {
            if (observer == null)
            {
                throw new ArgumentNullException("observer");
            }
            
           return m_subscribeDel(observer);
        }

        #endregion
    }

Předaná lambda v našem případě vytvoří instanci třídy eventsObserver, což je observer, který bude zpracovávat přicházející události, a do konstruktoru mu podhodí observer předaný klientským kódem – eventsObserver je tedy další adaptér, který je zdopovědný za “rozbalení” dat z instance IEvent a za předání těchto dat klientskému (“konečnému”) observeru.

Třída EventObserver:

 public class EventObserver<T> : IObserver<IEvent<EventArgsValue<T>>>                        
    {
        private readonly IObserver<T> m_innerObserver;
        private bool m_exceptionOccured;

        public EventObserver(IObserver<T> innerObserver)
        {
            if (innerObserver == null)
            {
                throw new ArgumentNullException("innerObserver");
            }
            
            m_innerObserver = innerObserver;
            m_exceptionOccured = false;
        }

        #region Implementation of IObserver<T>
        

        public virtual void OnNext(IEvent<EventArgsValue<T>> eventData)
        {
            if (eventData.EventArgs.Exception != null)
            {
                OnError(eventData.EventArgs.Exception);
                return;
            }
            //Rozbalení a předání dat Observeru
            m_innerObserver.OnNext(eventData.EventArgs.Value);
        }

        public virtual void OnError(Exception exception)
        {
            m_innerObserver.OnError(exception);
            m_exceptionOccured = true;
        }

        public virtual void OnCompleted()
        {
           //Chyba ukončí sekvenci sama o sobě
            if (!m_exceptionOccured)
            {
                m_innerObserver.OnCompleted();                
            }
        }

Třída EventObserver implementuje rozhraní IObservable s těmito generickými argumenty - IObserver<IEvent<EventArgsValue<T>>> . V C# Posterous API všechny události předávají svá data v instanci třídy EventArgsValue<T>, což znamená, že pro naše účely je EventObserver univerzálně použitelný observer pro zpracování výsledků asynchronní operace.

Pro úplnost zde je výpis třídy EventArgsValue

 public class EventArgsValue<T> : EventArgs
    {
        private readonly T m_value;
        private readonly Exception m_exception;

        internal EventArgsValue(T value, Exception exception)
        {
            m_value = value;
            m_exception = exception;
        }
            
        public T Value
        {
            get
            {
                return m_value;
            }
        }

        public Exception Exception
        {
            get
            {
                return m_exception;
            }
        }
    }

V lambdě předané do objektu DelegateObservable také musíme spustit asynchronní operaci – to je volání Action delegáta runAction, který nám předala již metoda GetSites. Každý IObservable také z metody vrací objekt implementující rozhraní IDisposable – volání metody Dispose dovoluje klientovi odpojit se objektu IObservable. Lambda  předaná do instance DelegateObservable vrátí  IDisposable objekt, který je vydán po připojení EventObservera k “streamu události“ (sourceEvents).

Přidání extenzních metod k dalším třídám je triviální – zde je extenzní metoda pro IPosterousSite, která nahraje všechny blogposty.

public static IObservable<IEnumerable<IPosterousPost>> GetPosts(this IPosterousSite site)
        {            

            throwIfSiteNull(site);
            var postsEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousPost>>>(handler => site.PostsLoaded += handler,
                                                                                               handler => site.PostsLoaded -= handler)
                                      .Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
            

            return postsEvents.GetFinalObservableEvents(site.LoadAllPostsAsync);
            
            

        }
 
 

A nyní se můžeme znovu podívat, jak naše API použijeme v klientském kódu – díky alternativní implementaci Query vzoru v RX frameworku  můžeme používat staré dobré známé LINQ dotazy.

Metoda loadPosts:

private void loadPosts()
        {
            toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;

            IPosterousApplication app = PosterousApplication.Current;
            IPosterousAccount account = app.GetPosterousAccount("<Posterous user name>", "Posterous password");

            var syncContext = SynchronizationContext.Current;

            var resultPosts = from sites in account.GetSites()
                              from site in sites.ToObservable()
                              from posts in site.GetPosts()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

            resultPosts.Subscribe(post =>
                                      {
                                          lock (m_threadsSet)
                                          {
                                              m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
                                          }


                                          syncContext.Post(_ =>
                                                               {
                                                                   var UCpost = new UC_Post
                                                                                    {
                                                                                        Title = post.Title,
                                                                                        Body = post.Body
                                                                                    };

                                                                   flowLayoutPanel1.Controls.Add(UCpost);
                                                               },
                                                           null

                                              );
                                      },

                                  ex => syncContext.Post(_ =>
                                                             {

                                                                 throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
                                                             },

                                                         null),

                                  () => syncContext.Post(_ =>
                                                             {
                                                                 lock (m_threadsSet)
                                                                 {
                                                                     m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
                                                                 }

                                                                 toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
                                                             },
                                                         null

                                            ));



        }

V proměnné resultPosts jsou uloženy všechny blogposty (IPosterousPost) ze všech blogů (IPosterousSite). Blogy i blogposty jsou nahrány asynchronně, ale v klientském kódu nevidíme žádná specialitky kvůli asynchronnímu nahrávání dat. Na proměnných sites i posts v dotazu je volána další extenzní metoda z RX Frameworku ToObservable, protože jak víme, výsledkem volání asynchronních metod byly typy IEnumerable<IPosterousPost> a IEnumerable<IPosterousSite>.

Důležité je, že zpracování dotazu je opět “lazy” – to znamená, že k získání dat dojde až poté, co k resultsPosts zaregistruju svého Observera metodou Subscribe (analogie “Lazy” vyhodnocování v LINQ To IEnumerable a procházení dotazu v cyklu foreach). Metoda Subscribe má několik variant a jedna z nich nám dovoluje pro metody OnNext, OnError a OnCompleted předat delegáty, aniž bychom byli nuceni vytvářet svou třídu implementující rozhraní IObserver.

První delegát (OnNext) vezme předaný post a vytvoří pro něj UserControl, který vloží do FlowPanelu na formuláři. Ještě předtím pro zajímavost do Hashsetu ukládám identifikátory vláken, které se na zpracování dotazu podílí. S prvky na formuláři můžeme pracovat jen z UI threadu, a proto je vložení User controlu provedeno přes SynchronizationContext uložený do proměnné syncContext před spuštěním dotazu.

Druhý delegát (OnError)  pouze přes SynchronizationContext zpropaguje výjimku, která nastala při asynchronním zpracování, do UI threadu. Všimněte si, jak je zpracování výjimek jednoduché – rozdíl vynikne při srovnání s opakovaným voláním metody throwIfAsyncEx v kódu na začátku tohoto článku.

Třetí delegát (OnCompleted) naplní listbox na formuláři ID použitých threadů a změní text ve status baru.

Zde je výsledný formulář. V listboxu nahoře si můžete všimnout, že u mě byly k vykonání dotazu použity celkem 3 thready.

 

AsyncForm

 

Tím bychom mohli skončit, ale RX Framework má pro asynchronní operace ještě další zajímavou podporu. Pomocí metody Observable.FromAsyncPattern můžeme vytvořit IObservable rychle a bezpracně ze standardního a výše již zmíněného asynchronního Begin/End vzoru. V C# Posterous API metody Begin*/End* nejsou, proto je zkusme dodat pomocí extenzních metod.

Rozhraní IPosterousAccount bude obohaceno o extenzní metody BeginLoadSites a EndLoadSite.

Metoda BeginLoadSites

public static IAsyncResult BeginLoadSites(this IPosterousAccount account, AsyncCallback callback, object context)
        {
            checkAccountNotNull(account);
            var loadSiteAction = new Action(account.LoadSites);
            
            return RXEventsHelper.GetAsyncResultEx(loadSiteAction, callback, context);
       
            
        }

Jak vidíte, přesně dle konvencí .Net vzoru metoda vrací odkaz na rozhraní IAsyncResult a přijímá callBack, což je tedy u tohoto vzoru metoda, která má být vyvolána po dokončení asynchronního zpracování, a jak také vzor vyžaduje, posledním argumentem je libovolný objekt reprezentující libovolný “stavový token” operace, který v metodě End* klient používá pro korelací mezi požadavkem a odpovědí.

Veškerá práce je přenesena na metodu GetAsyncResultEx v mém RXEventsHelperu – metoda vyžaduje, abyste ji poslali v delegátu Action metodu, která má být spuštěna asynchronně.

Metoda RXEventsHelper.GetAsyncResultEx.

 public static IAsyncResult GetAsyncResultEx(Action runAction, AsyncCallback callback, object context)
        {
            if (runAction == null)
            {
                throw new ArgumentNullException("runAction");
            }

            Exception ex = null;
            
            var proxyCallback = new AsyncCallback(ar =>
                                                      {
                                                          IAsyncResult proxyResult = new AsyncResultEx(ar, runAction);                                                                                         
                                                           callback(proxyResult);
                                                      });

            return runAction.BeginInvoke(proxyCallback, context);
                      

            
                       
        }

Hlavním trikem je využití možností delegátů – každý delegát v .Net Frameworku vždy obsahuje asynchronní metody BeginInvoke a EndInvoke, které splňují nároky asynchronního vzoru. My tedy na předaném delegátu runAction zavoláme metodu BeginInvoke, ale místo klientské callBack Funkce podhodíme svou proxy funkci (proxyCallback), která po dokončení asynchronního volání připraví pro naši End metodu vlastní IAsyncResult (AsyncResultEx).

Třída AsyncResultEx zapouzdřuje původní IAsyncResult  (argument ar předaný  do konstruktoru v předešlém výpisu) a navíc, když na její instanci zavoláme metodu EndAction, na předaném delegátovi (argument runAction v předešlém výpisu) je zavolána metoda EndInvoke, čehož využije naše metoda EndLoadSites.

Třída AsyncResultEx

 public class AsyncResultEx : IAsyncResult
    {
        
        #region private variables
        private IAsyncResult m_originalAsyncResult;
        private readonly Action m_originaldelegate;
        #endregion private variables

        public AsyncResultEx(IAsyncResult origAsyncResult, Action originaldelegate)
        {
            if (origAsyncResult == null)
            {
                throw new ArgumentNullException("origAsyncResult");
            }


            m_originalAsyncResult = origAsyncResult;
            m_originaldelegate = originaldelegate;
        }

        #region properties
        public virtual IAsyncResult OriginalAsyncResult
        {
            get
            {
                return m_originalAsyncResult;
            }

        }

        public virtual Action OriginalDelegate
        {
            get
            {
                return m_originaldelegate;
            }
        }
        
        #endregion properties

        #region methods

        public virtual void EndAction()
        {
            
            if (OriginalDelegate != null)
            {
                OriginalDelegate.EndInvoke(OriginalAsyncResult);
            }                                                

        }
            
        #endregion methods
        #region Implementation of IAsyncResult

        
        public virtual bool IsCompleted
        {
            get
            {
                return m_originalAsyncResult.IsCompleted;
            }
        }

        public virtual object AsyncState
        {
            get
            {
                return m_originalAsyncResult.AsyncState;
            }
        }

        public virtual WaitHandle AsyncWaitHandle
        {
            get
            {
                return m_originalAsyncResult.AsyncWaitHandle;
            }
        }

        public virtual bool CompletedSynchronously
        {
            get
            {
                return m_originalAsyncResult.CompletedSynchronously;
            }
        }
                
        #endregion
    }

Extenzní metoda EndLoadSites

 public static IEnumerable<IPosterousSite> EndLoadSites(this IPosterousAccount account, IAsyncResult result)
        {
            checkAccountNotNull(account);
            
            var exResult = result as AsyncResultEx;
            
            if (exResult == null)
            {
                throw new ArgumentException("result");
            }
            
            exResult.EndAction();
                                               
            return account.Sites;
        }

Zde vidíme volání metody EndAction na podhozené instanci AsyncResultE. Poté metoda EndLoadSites jen vrátí kolekci Sites objektu account, protože ta  nyní již musí být po asynchronním volání naplněna daty.

Se stávající infrastrukturou si opět si můžeme rychle připravit další Begin a End metody. Zde jsou extenzní metody BeginLoadPosts a EndLoadPosts pro IPosterousSite.

public static IAsyncResult BeginLoadPosts(this IPosterousSite site, AsyncCallback callback, object context)
        {
            throwIfSiteNull(site);
            var loadPostsAction = new Action(site.LoadAllPosts);
            
            return RXEventsHelper.GetAsyncResultEx(loadPostsAction, callback, context);
        }

        public static IEnumerable<IPosterousPost> EndLoadPosts(this IPosterousSite site, IAsyncResult result)
        {
            throwIfSiteNull(site);
            var exResult = result as AsyncResultEx;

            if (exResult == null)
            {
                throw new ArgumentException("result");
            }

            exResult.EndAction();

            return site.Posts;
        }

 

A metoda loadPosts2, která dělá to samé, co předchozí metoda loadPosts, ale používá naše nové extenzní Begin/End metody.

 private void loadPosts2()
        {
            toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;

            IPosterousApplication app = PosterousApplication.Current;
            IPosterousAccount account = app.GetPosterousAccount("posterousname", "posterouspassword");

            var syncContext = SynchronizationContext.Current;            


            var resultPosts = from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
                              from site in sites.ToObservable()
                              from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

            resultPosts.Subscribe(post =>
                                      {
                                          lock (m_threadsSet)
                                          {
                                              m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
                                          }


                                          syncContext.Post(_ =>
                                                               {
                                                                   var UCpost = new UC_Post
                                                                                    {
                                                                                        Title = post.Title,
                                                                                        Body = post.Body
                                                                                    };

                                                                   flowLayoutPanel1.Controls.Add(UCpost);
                                                               },
                                                           null

                                              );
                                      },


                                  ex => syncContext.Post(_ =>
                                                             {
                                                                 
                                                                 throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
                                                             },

                                                         null),

                                  () => syncContext.Post(_ =>
                                                             {
                                                                 lock (m_threadsSet)
                                                                 {
                                                                     m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
                                                                 }

                                                                 toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
                                                             },
                                                         null

                                            )


                );


        }

Upozornil bych jen na dvě specialitky či zrádná místa, která (alespoň v této BETA verzi RX) dělají kód méně intuitvním, než by bylo žádoucí:

Jedná se o tyto dva řádky:

from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())  
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()) 

Metoda FromAsyncPattern přijímá delegáty na naše asynchronní metody, ale místo spolehnutí se na typovou inference jsem musel generický argument předat explicitně(Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>) – pokud argument nezadáte, kompilátor hlásí “ambigous reference”.

Dále je patrné, že výsledek funkce FromAsyncPattern, kterým je další funkce vracející IObservable, je předán jako argument metodě Observable.Defer. Metoda Observable.Defer zajistí, že k vyhodnocení předané funkce dojde až poté, co je k výsledkům dotazu přihlášen observer – jinými slovy, metoda Defer nám pomáhá zachovat “lazy” vyhodnocení dotazu.

Dotaz bude fungovat i v této podobě (bez Defer):

var resultPosts = from sites in  Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()
                              from site in sites.ToObservable()
                              from posts in Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()
                              from post in posts.ToObservable()
                              where post.Private == false
                              select post;

Ale jeho vyhodnocení už není “lazy”. Všimněte si závorek na konci výrazu - FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()  - výsledné IObservable získám okamžitým zavoláním funkce vrácené z metody FromAsyncPattern. Vadit vám to začne v okamžiku, kdy zkonstruujete dotaz, ihned se odpálí asynchronní volání, dojde k chybě a vy budete mít v aplikaci neošetřenou výjimku v threadu na pozadí, protože IObserver ještě není přihlášen (není možné zavolat druhého delegáta - ex => syncContext.Post(_ => { throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex); }, null), ).

 

Snad se vám tato exkurze líbila. Já ještě na RX Framework konečný názor nemám, ale něco neodbytného ve mně říká, že by mohlo jít o další LINQ, který otřese programátorským světem. :-) Některé extenze pravděpodobně zahrnu do samostatného jmenného prostoru v C# Posterous API.



Tuesday, 02 February 2010 07:43:00 (Central Europe Standard Time, UTC+01:00)       
Comments [3]  .NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms


 Thursday, 28 January 2010
C# Posterous API pro Silverlight 4 (a .Net Framework 3.5) – verze 0.0.0.2

Stáhnout C# Posterous API pro Silverlight 4.0

Stáhnout C# Posterous API pro .Net Framework 3.5

Vše podstatné k C# Posterous API naleznete v úvodním článku.

Poznámky ke změnám v této verzi:

  • Kvůli verzi pro Silverlight přidány další asynchronní metody tak, aby bylo možné získat data z webu asynchronně, jak to Silverlight vyžaduje a jak je toto chování v aplikacích vynuceno  asynchronními metodami v rozhraní platformních tříd - příkladem budiž rozhraní tříd WebRequest a WebResponse. Pokud se pokusíte zavolat synchronní verzi metody v SL z UI vlákna, měli byste z Posterous API dostat výjimku – to je lepší varianta, než skončit v paralyzovaném stavu, kdy celá aplikace na nic nereaguje.

Ukázka uložení  změn v blogpostu – přidání komentáře. :


        var comment = post.CreateNewComment(updateText);
        post.SaveChangesCompleted += (_0, _1) =>
                                         {
                                             Assert.IsTrue(comment.Id > 0);
                                             TestComplete();
                                         };
        post.SaveChangesAsync();

  • V Silverlight verzi aplikace nedostanete v událostech rozhraní IRawRequestResponsePublisher k modifikaci objekty HttpWebRequest a HttpWebResponse, které jsou použity pro stažení obrázku autora (IAuthor.AuthorPicture) a stažení média (IPosterousMedium.Medium). Důvodem je, že pro stažení používám svého potomka třídy WebClient. Tento potomek přepisuje metody GetWebRequest a GetWebResponse, které jsou poté nabídnuty v událostech dostupných přes rozhraní IRawRequestResponsePublisher. V Silverlightu je ale třída WebClient označena jako kritický kód (Critical) a takzvaný transparentní  (Transparent) kód, pod který patří i námi psaný kód, nemá právo z takto označené infrastrukturní třídy podědit. Kromě dvou výše zmíněných omezení ale IRawRequestResponsePublisher pracuje stejně jako v desktopové verzi  a v další verzi API uvažuji, že místo WebClienta použiju na stažení obrázku autora i všech dalších médií přímo třídy WebRequest a WebResponse, které budou opět nabídnuty i v rozhraní IRawRequestResponsePublisher.
  • Silverlight má omezený přístup k souborovému systému, a proto naleznete v blogpostu (IPosterousPost) a twitter postu (ITwitterPost) další dvě metody pro přidání médií. Ve verzi 0.0.0.1 bylo možné předat pouze cestu k souboru, nyní můžete předat jakýkoli stream – např. Stream načtený z IsolatedFileStorage.
    Nové metody:
    void AppendMedium(Stream stream, string mediaName, Action<Stream> releaseStreamFunction);
    void AppendMedium(Stream stream, string mediaName);

Kromě streamu a názvu média můžete do jedné z variant funkce AppendMedium předat delegáta typu Action<Stream> , který bude vyvolán v okamžiku, kdy Posterous API  již se streamem nepotřebuje pracovat. Nejasná zodpovědnost, kdo vlastní a hlavně uvolňuje zdroje alokované objektem stream, mě právě vedla k tomu, že v první verzi API pro .Net Framework 3.5 jste mohli pouze předat název souboru a životní cyklus FileStreamu byl zcela pod mou kontrolu. Jestliže delegáta releaseStreamFunction nepředáte, máte životní cyklus streamu zcela ve svých všemocných rukou. Posterous API ani jeho autor si nepřejí být vyzváni na souboj  žádným šíleným omnipotentním vývojářem :-), a proto na předaném streamu Posterous API nikdy nevolá metodu Close ani Dispose.

Ukázka práce s metodou AppendMedium:

IPosterousPost newPost = site.CreatePost(DateTime.Now + " Media Silverlight",
                                                         "Príliš žlutoucký kun úpel dábelské ódy", true);

                IsolatedStorageFileStream stream = new IsolatedStorageFileStream("AlbumArtSmall.jpg ", FileMode.Open, IsolatedStorageFile.GetUserStoreForSite());
                
                newPost.AppendMedium(stream, "mojeSL.jpg", isoStream => isoStream.Close());
                               
                newPost.AddTag("Všechno a nic");
                newPost.AddTag("C# omnia vincit");
                newPost.SaveChangesCompleted += (_, __) =>
                                            {
                                                Assert.IsTrue(newPost.Id > 0);
                                                TestComplete();
                                            };
                newPost.SaveChangesAsync();

Jednoduchá ukázka použití API v Silverlightu. V jednoduchém boxu na stránce zobrazíme titulek spotu a url spotu. Výsledek vypadá takto:

SLBox

 

Ve Visual Studiu jsem založil projekt typu běžná navigační aplikace v Silverlightu. Zde je jednoduchý “ViewModel” pro stránku:

public class VM_Posts : INotifyPropertyChanged
    {
        public static readonly string ASYNC_EXCEPTION_TEXT = "Error while retrieving data. See inner exceptions for details";
        
        private bool m_isBusy;
        private IEnumerable<ViewPost> m_posts;

        public VM_Posts()
        {
            m_posts = null;
            IsBusy = false;
            LinkClickCommand = new DelegateCommand<String>(link => Debug.WriteLine(link) /*m_navigationServices.HandleExternalLink(link)*/,
                                                             _ => true);
            init();
        }

        
        private void init()
        {

                IsBusy = true;
                IPosterousApplication posterousApp = PosterousApplication.Current;
                IPosterousAccount posterousAccount = posterousApp.GetPosterousAccount("<PosterousUserName>", "PosterousPassword>");

                posterousAccount.SitesLoaded += (o, e) =>
                                                    {
                                                        throwIfAsyncEx(e.Exception);
                                                        posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
                                                                                                        {
                                                                                                            throwIfAsyncEx(e2.Exception);
                                                                                                            Posts = (from p in e2.Value
                                                                                                                    select new ViewPost
                                                                                                                               {
                                                                                                                                   Title = p.Title,
                                                                                                                                   Body = p.Body,
                                                                                                                                   Url = p.Url

                                                                                                                               }).ToList();                                                                                                            
                                                                                                            
                                                                                                        };
                                                        posterousAccount.PrimarySite.LoadAllPostsAsync();
                                                    };



                posterousAccount.LoadSitesAsync();
            
            
        }

       private void throwIfAsyncEx(Exception exception)
        {
            if (exception != null)
            {
                IsBusy = false;
                throw new Exception(ASYNC_EXCEPTION_TEXT,exception);
            }
        }
        
        

        public IEnumerable<ViewPost> Posts
        {
            get
            {
                return m_posts;
            }
            private set
            {
                
                m_posts = value;
                if (m_posts != null)
                {
                    IsBusy = false;
                }
                OnPropertyChanged(new PropertyChangedEventArgs("Posts"));
                
                
            }
        }

        

        public bool IsBusy
        {
            get
            {
                return  m_isBusy;
            }
            private set
            {
                m_isBusy = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsBusy"));
            }
        }

        public ICommand LinkClickCommand
        {
            get;
            private set;
        }

        #region Implementation of INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        #endregion
    }

    public class ViewPost
    {
        public string Title
        {
            get;
            set;
        }

        public string Body
        {
            get;
            set;
        }
        

        public string Url
        {
            get;
            set;            
        }
    }
}

V privátní metodě init, která je volána ihned po vytvoření instance třídy WM_Posts, nahrajeme blogy (Sites –> metoda LoadSitesAsync) a poté opět asynchronně z primárního blogu (PrimarySite) nahraju všechny blogposty (metoda LoadAllPostsAsync). Jestliže při některém asynchronním volání dojde k výjimce, metoda throwIfAsyncEx výjimku zpropaguje do UI vlákna.

Z nahraných blogpostů jsou vlastností Title, Url a Body přeneseny do instancí pomocné třídy ViewPost a kolekce objektů ViewPost je uložena do vlastnosti Posts. Třídu ViewPost vytvořit musíme, protože “Binding” dat, který budeme používat v XAMLu, nedokáže v Silverlightu z bezpečnostních důvodů pře reflection přistupovat ke člelnům “internal” třídy deklarované v jiné assembly. Třída PosterousPost, která v Posterous API implementuje rozhraní IPosterousPost, je označena jako internal. Ze stejného důvodu v Silverlightu nemohu použít místo explicitně definované třídy ViewPost anonymní datový typ – i anonymní datové typy jsou reprezentovány ve výsledném aplikačním dll třídami s modifikátorem viditelnosti “internal” a kód pro “binding” dat je ve zcela jiné platformní assembly.

Kromě vlastnosti Posts nabízí třída WM_Posts vlastnost IsBusy, kterou v XAMLu  využijeme k zobrazení indikátoru informujícího uživatele, že právě získáváme data. Vystaven je také LinkClickCommand, který nůže být zavolán například v okamžiku, kdy je stisknuto nějaké tlačítko reprezentující hyperlink s URL blogpostu.

A zde je pro úplnost ještě XAML. Památeční XAML, protože mi při jeho psaní nejméně a bez jakékoli nadsázky 50x spadlo Visual Studio 2010 Beta 2 (s Resharperem).:-)

<navigation:Page x:Class="SL_PosterousAPITest.Home"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
    xmlns:ct="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
    xmlns:local="clr-namespace:SL_PosterousAPITest;assembly=SL_PosterousAPITest"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
    Title="Home"
    Style="{StaticResource PageStyle}">

    <Grid x:Name="LayoutRoot">
        <Grid.Resources>
            <local:VM_Posts x:Key="PostsView"></local:VM_Posts>            
            <Style x:Name="PostTitleStyle" TargetType="TextBlock">
                <Setter Property="FontWeight" Value="Bold" />
                 <Setter Property="Foreground" Value="IndianRed" />
                <Setter Property="FontSize" Value="14" />
            </Style>
            <Style x:Key="PostsRectangle" TargetType="Rectangle">
                <Setter Property="RadiusX" Value="30" />
                <Setter Property="RadiusY" Value="30" />                
                <Setter Property="Fill">
                    <Setter.Value>
                        <LinearGradientBrush>
                            <GradientStop Offset="0" Color="LightYellow" />
                            <GradientStop Offset="1" Color="LightBlue" />                            
                        </LinearGradientBrush>
                    </Setter.Value>
                </Setter>
            </Style>
            <Style TargetType="ItemsControl">
                <Setter Property="Width" Value="500" />
                <Setter Property="Height" Value="360" />
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <ScrollViewer BorderThickness="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
                                <Grid>                                    
                                    <Rectangle Style="{StaticResource PostsRectangle}">                                        
                                    </Rectangle>
                                    <ItemsPresenter Margin="15,15,0,0"/>
                                    <ct:BusyIndicator IsBusy="{Binding Path=IsBusy}" 
                                                      DisplayAfter="0" 
                                                      HorizontalAlignment="Center" 
                                                      VerticalAlignment="Center"                                                       
                                                      />
                                    
                                    
                                </Grid>
                            </ScrollViewer>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>        
        <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">

            <StackPanel x:Name="ContentStackPanel" DataContext="{StaticResource PostsView}">

                <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                                   Text="Home"/>
                <ItemsControl Name="itemsPosts" ItemsSource="{Binding Mode=OneWay, Path=Posts}">                  
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Vertical">
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Style="{StaticResource PostTitleStyle}" Text="{Binding Path=Title}"></TextBlock>                                
                                 </StackPanel>
                                <HyperlinkButton Content="{Binding Path=Url}" 
                                                 Command="{Binding Source={StaticResource PostsView}, Path=LinkClickCommand}"                                                  
                                                 CommandParameter="{Binding  RelativeSource={RelativeSource Self}, Path=Content}"
                                                 FontSize="12"
                                                 ></HyperlinkButton>                                
                            </StackPanel>
                        </DataTemplate>                        
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </StackPanel>

        </ScrollViewer>
    </Grid>

</navigation:Page>


Thursday, 28 January 2010 16:33:08 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# Posterous API | Silverlight


 Wednesday, 20 January 2010
Ukázka práce s Posterous API – zálohování blogu

Stáhnout výsledné exe -RSPosterousBackup.

Po jednoduchém přehledu možností mého C# Posterous API wrapperu se nyní podíváme, jak se dá API použít k zálohování vašeho blogu. Pro účely tohoto článku předpokládám, že jste úvodní článek o API wrapperu četli.

Zálohovač blogu (RSPosterousBackup.exe) je jednoduchá konzolová aplikace, které stačí předat uživatelské jméno (parametr –u)  a heslo (parametr –p)  vašeho účtu na Posterous a také adresář vašem počítači (parametr bd), do kterého chcete blog zazálohovat.

Jednoduchá ukázka:

RSPosterousBackup.exe -u:rene@renestein.net -p:mojeheslo -bd:c:\_Archiv\PosterousBackup

Blog uživatelského účtu reneATrenestein.net s heslem mojeheslo bude zazálohován do adresáře c:\_Archiv\PosterousBackup.

Program referencuje samozřejmě knihovnu RStein.Posterous.API a pro (své, uznávám :-)) pobavení jsem také použil RX for .Net Framework 3.5 SP1. Z RX Frameworku jsou referencovány assembly System.CoreEx, System.Interactive a System.Threading. Nejzajímavější na RX Frameworku je, že pro verzi 3.5 .Net Frameworku zpřístupňuje Parallel Linq – PLINQ, který je součástí připravovaného .Net Frameworku 4.0.

Ještě upozornění – v žádném případě nechci tvrdit, že kód, který uvidíte, využívá PLINQ správným způsobem. Program je jen pískovištěm, na kterém jsem si zkoušel a ověřoval, co RX a PLINQ umí a jak vypadá výsledný kód.

Po spuštění RSPosterousBackup.exe funkce Main pouze předá argumenty z příkazové řádky metodě BackupData v třídě BackupEngine, která představuje výkonný mozek celého zálohovače Posterous blogu.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new BackupEngine();
            engine.BackupData(args);
            Console.ReadLine();
        }
    }
}

 

Zde je třída BackupEngine

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using RStein.Posterous.API;

namespace RSPosterousBackup
{
    public class BackupEngine
    {
        #region constants
        public static readonly string POSTEROUS_USER = "-u";
        public static readonly string POSTEROUS_PASSWORD = "-p";
        public static readonly string BACKUP_DIRECTORY= "-bd";
        public static readonly string HELP_KEY = "-?";
        public static readonly string HELP_ALT_KEY = "-h";
        public static readonly string POSTEROUS_FILE_EXTENSION = ".posterous";
        public static readonly string POSTEROUS_MEDIA_FORMAT = "Media_{0}";
        public static readonly string POSTEROUS_COMMENT_FORMAT = "Comment_{0}";
        public static readonly string POSTEROUS_SITE_FORMAT = "Site_{0}";
        #endregion constants
        #region private variables        
        #endregion private variables

        #region constructors
        public BackupEngine()
        {
        }
        #endregion constructors
        
        public void BackupData(string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }

            var parser = new CommandLineParser();
            Dictionary<string, string> cmSwitches = parser.Parse(commandLine);

            Debug.Assert(cmSwitches != null);

            if (cmSwitches.Keys.Any(key => (key.Equals(HELP_KEY, StringComparison.OrdinalIgnoreCase)) ||
                                       (key.Equals(HELP_ALT_KEY,StringComparison.OrdinalIgnoreCase))))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;
            }

            if (!checkSwitches(cmSwitches))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;                
            }

            backupDataInner(cmSwitches);
        }

        private void backupDataInner(Dictionary<string, string> cmSwitches)
        {
            string pUser = cmSwitches[POSTEROUS_USER];
            string pPassword = cmSwitches[POSTEROUS_PASSWORD];
            string backupDir = cmSwitches[BACKUP_DIRECTORY];
            try
            {
                IPosterousAccount account = PosterousApplication.Current.GetPosterousAccount(pUser, pPassword);
                if (!Directory.Exists(backupDir))
                {
                    Directory.CreateDirectory(backupDir);
                }

                var currDirBackupName = new StringBuilder(DateTime.Now.ToLocalTime().ToString());
                currDirBackupName.ToSafeFileName();
                
                
                string currentBackupDir = Path.Combine(backupDir, currDirBackupName.ToString());
                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });


                var processedPosts = (from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()
                                         .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))
                                        
                                         .Do(postPost =>
                                                 {                                                  
                                                     string siteDir = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                                     
                                                     var titlBuilder = new StringBuilder(postPost.Title + postPost.Id.ToString());
                                                     titlBuilder.ToSafeFileName();

                                                     string postDirName = Path.Combine(siteDir,
                                                                                       titlBuilder.ToString()
                                                                                       );
                                                     
                                                     Directory.CreateDirectory(postDirName);
                                                     string postFileName = Path.Combine(postDirName, titlBuilder.ToString() + POSTEROUS_FILE_EXTENSION);
                                                     using (var fileStream = File.Open(postFileName, FileMode.Create))
                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                     {
                                                         stremWriter.Write(postPost.Body);
                                                     }

                                                     postPost.Media
                                                         .AsParallel()
                                                         .Run(media =>
                                                                 {
                                                                                                             
                                                                     var mediaName = new StringBuilder(String.Format(POSTEROUS_MEDIA_FORMAT, Guid.NewGuid()));
                                                                     mediaName.ToSafeFileName();
                                                                     string mediaFile = Path.Combine(postDirName, mediaName.ToString());


                                                                     using (var fileStream = File.Open(mediaFile, FileMode.Create))
                                                                     {


                                                                         media.Content.CopyToStream(fileStream);

                                                                     }
                                                                 });


                                                     postPost.Comments
                                                         .AsParallel()                                                         
                                                         .Run(comment =>
                                                                 {
                                                                                                             
                                                                     var commentFileName = new StringBuilder(String.Format(POSTEROUS_COMMENT_FORMAT, Guid.NewGuid()));
                                                                     commentFileName.ToSafeFileName();
                                                                     string commentFile = Path.Combine(postDirName, commentFileName.ToString());


                                                                     using (var fileStream = File.Open(commentFile, FileMode.Create))
                                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                                     {


                                                                         stremWriter.WriteLine(comment.Author.Name);
                                                                         stremWriter.Write(comment.Body);

                                                                     }
                                                                 });
                                                 })

                                     select post).ToList();





                logMessage(String.Format(GlobalConstants.FINAL_BACKUP_MESSAGE_FORMAT, processedPosts.Count));
                
                
            }
            catch (Exception e)
            {
                 Trace.WriteLine(e);
                 Console.WriteLine(e);                 
            }
           
            
        }

        private bool checkSwitches(Dictionary<string, string> cmSwitches)
        {            

            return ckeckIfSwitchNullOrEmpty(POSTEROUS_USER, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(POSTEROUS_PASSWORD, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(BACKUP_DIRECTORY, cmSwitches);
            
        }

        private bool ckeckIfSwitchNullOrEmpty(string switchKey, Dictionary<string, string> cmSwitches)
        {
            string val;
            cmSwitches.TryGetValue(switchKey, out val);
            if (String.IsNullOrEmpty(val))
            {
                logMessage(String.Format(GlobalConstants.INVALID_CML_SWITCH_FORMAT_STRING_EX, switchKey));
                return false;
            }
            return true;
        }

        private void logMessage(string message)
        {
            
            Console.WriteLine(message);
        }
    }
}

Třída BackupEngine nejdříve v metodě BackupData pomocí instance třídy CommandLineParser rozpársuje příkazový řádek a voláním pomocné metody checkSwitches zkontroluje, zda byly předány všechny vyžadované parametry. Jestliže nějaký parametr chybí nebo byl zadán parametr pro zobrazení nápovědy (-h, –?), program zobrazí nápovědu a k zálohování nedojde.

Ihned po dokončení všech předběžných kontrol je volána privátní metoda backupDataInner, která je odpovědná za zálohování blogu. Metoda backupDataInner získá odkaz na Posterous účet (PosterousApplication.Current.GetPosterousAccount(pUser, pPassword); a poté pro každou Site (samostatný blog) založí nový podadresář v adresáři, jehož názvem je aktuální lokální datum a čas a který je vytvořen v adresáři předaném v parametru –bd uživatelem. 

Adresářová struktura pro každý zálovaný blog:

<adresář určený –bd přepínačem>\<adresář  - názvem je aktuální datum a čas>\Site_<Site Id>

Příklad adresářové struktury:

"c:\_Archiv\PosterousBackup\20.1.2010 15_57_07\Site_851694"

Zálohován tedy není jeden blog, ale všechny blogy, které jsou asociovány s daným posterous účtem.

Můžete si všimnout, že pro založení podaresáře používvám jednu z RX extenzních metod – metodu Run.

                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });

Metoda Run je náhradou za extenzní metodu ForEach, kterou jste si dříve museli sami dopsat nebo jste byli nuceni použít metodu ForEach ve třídě List takto.

account.Sites.ToList().ForEach(site => {//zbytek kódu lambdy identický s kódem výše…}); 



Zdůrazním, že metoda Run vykoná nějaký vedlejší efekt nad každým elementem v IEnumerable, tedy v našem případě založí adresář, a dále již s elementy nepracuje – vrací void. Za chvíli uvidíme metodu, která nám umožní to samé, co metoda Run, ale elementy pošle po “vykonání vedlejšího efektu nad elementem” (tedy spuštění námi předané funkce jakou je např. lambda  pro založení adresáře) ke zpracování dalším LINQ extenzním metodám.

Signatura metody Run:

public static void Run<TSource>(
    this IEnumerable<TSource> source,
    Action<TSource> action
)

Dále do proměnné processedPosts uložíme pomocí speciálního a pro naši kratochvíli jediného LINQ dotazu všechny zpracované blogposty (instance podporující rozhraní IPosterousPost). Jak vidíte, stačí se přes Posterous API dotázat do kolekce Sites (blogy) a poté z každého blogu zpracovat všechny blogspoty.

from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()…

Na více místech v dotazu si můžete všimnout volání extenzní metody AsParalllel, kterým dáváte najevo, že zpracování jednotlivých blogpostů a také médií (IPosterousMedium) a komentářů (IPosterousComment) může proběhnout ve více vláknech – o detaily se ale postará PLINQ, vy sami žádná nová vlákna nespouštíte ani nespravujete.

Můžete si také všimnout, že na několika místech volám extenzní metodu Do .Metoda Do pracuje podobně jako před chvílí zmiňovaná metoda Run. Na každý element v zdrojové kolekci aplikuje předanou funkci, ale poté na rozdíl od metody Run element předá k dalšímu zpracování.

Signatura metody Do:

public static IEnumerable<TSource> Do<TSource>(
	this IEnumerable<TSource> source,
	Action<TSource> action
)
 

Zde vypíšeme přes RX extenzní metodu Do titulek právě zpracovávaného blogpostu a Id vlákna, které blogspot zpracovává. Tato metoda je zde jen na ukázku, že každý element ve zdrojové kolekci je metodou Do předán dále ke zpracování

 .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))

Na metodu Do navazuje další metoda Do, ve které proběhne zpracování každého blogspotu. V adresáří každé Site (blogu) je vytvořen pro každý blogspot vytvořen nový podadresář, jehož názvem je titulek (vlastnost Title) společně s  Id blogpostu. Do tohoto podadresáře je uložen text blogspotu. Soubor s textem blogspotu má příponu posterous a také jsou do podadresáře uložena média (zvukové, obrazové a video soubory) a všechny komentáře k blogspotu.

Hlavní část programu je za námi. Zde jsou ještě výpisy pomocných tříd.

Třída CommandLineParser pro pársování hodnot předaných uživatelem v příkazovém řádku.

using System;
using System.Collections.Generic;
using System.Linq;

namespace RSPosterousBackup
{
    public class CommandLineParser
    {
        #region constants

        public static readonly char COMMAND_PARTS_SEPARATOR = '-';
        public static readonly char COMMAND_VALUE_SEPARATOR = ':';
        public const int MIN_KEY_PARTS = 1;
        public const int MAX_KEY_PARTS = 2;
        #endregion constants
        
        #region constructors
        public CommandLineParser()
        {
        }
        #endregion constructors
        #region methods
        public virtual Dictionary<string, string> Parse (string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }
            
            return parseInner(commandLine);
        }

        private Dictionary<string, string> parseInner(string[] commandLine)
        {            
            var dict = (from part in commandLine
                        let keyValuePair = part.Split(new[] {COMMAND_VALUE_SEPARATOR}, MAX_KEY_PARTS)
                        select new
                                   {
                                       Key = keyValuePair.First().Trim().ToLower(),
                                       Value = keyValuePair.Length > MIN_KEY_PARTS ? keyValuePair.Last().Trim() : String.Empty
                                   }).ToDictionary(kv => kv.Key, kv => kv.Value);

            return dict;
        }

        #endregion methods
    }
}

Konstanty

namespace RSPosterousBackup
{
    public static class GlobalConstants
    {
        public static readonly string INVALID_CML_SWITCH_FORMAT_STRING_EX = "Invalid switch {0}";
        public static readonly char SAFE_FILE_PATH_CHAR = '_';        
        public static readonly string POST_BACKUP_MESSAGE_FORMAT = "Processing post: {0}  - in thread {1}";
        public static readonly string FINAL_BACKUP_MESSAGE_FORMAT = "Total posts: {0}";
        public static readonly string CMDLINE_SHOW_USAGE =
@"Usage: 
RSPosterousBackup.exe -u:<posterous user name> p:<posterous password> -bd:<backup directory>
Example:RSPosterousBackup.exe -u:GIC@Roma.com p:Rubicon -bd:c:\PosterousBackup
RSPosterousBackup.exe -?  - show this help";
                                                                 
    }
}

Třída StringBuilderExtensions s extenzní metodou ToSafeFileName, která v navrhovaném jménu souboru nahradí nepovolené znaky podtržítkem.

using System.IO;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    public static class StringBuilderExtensions
    {
        public static void ToSafeFileName(this StringBuilder builder)
        {
            Path.GetInvalidFileNameChars().Run(ch => builder.Replace(ch, GlobalConstants.SAFE_FILE_PATH_CHAR));
        }
    }
}

Stáhnout výsledné exe -RSPosterousBackup.



Wednesday, 20 January 2010 17:54:21 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  .NET Framework | C# Posterous API | LINQ | RX Extensions


 Friday, 15 January 2010
Projekt C# Posterous API – verze 0.0.0.1 Alfa

 

Stáhnout knihovnu – download

Jak jsem avizoval minulý týden na twitteru, píšu C# wrapper webového API zajímavé služby Posterous.

Pár odkazů na začátek:

Jestliže nevíte, co je Posterous, přečtěte si článek na Živě.

Popis Posterous API. Hned na začátku zdůrazním, že autoři Posterous API nepovažují API za kompletní a za sebe dodám, že je to na mnoha místech vidět.:)

Několik důležitých informací:

  1. Projekt musí být stažen z mých stránek, jakékoli vystavování knihovny na jiném webu a serveru je zakázáno.
  2. Knihovnu jako celek v této verzi můžete používat dle libosti na komerčních i nekomerčních projektech. Zakázáno je samozřejmě vydávání knihovny za vlastní, její dekompilace a použití jen části knihovny.:) Jako autor knihovny nic negarantuji, nezodpovídám za případné přímé ani nepřímé škody vzniklé použitím knihovny a na opravu chyb knihovny není žádný nárok. Chyby lze reportovat na emailovou adresu PosterousAPI@renestein.net.
  3. Teprve dnes padlo rozhodnutí, že API v kódu musejí být komentovány v češtině. API zatím komentováno není a tento spot by vám měl pomoci se v knihovně zorientovat. Posterous API je součástí většího projektu. Posterous jsem si vymyslel a přidal do projektu sám a i když jsme s partnerem dohodnuti, že s Posterous knihovnou si mohu dělat, co chci, dokumentace musí být v češtině – stejně jako zbytek projektu. Pokusím se ale připravit i EN dokumentaci.
  4. Knihovna je zkompilována ve VS 2010 BETA 2 pro .Net Framework 3.5. Chci připravit i verze pro Compact .Net framework a Silverlight.

 

A nyní jž k samotnému API.

Branou k funkcím knihovny je třída PosterousApplication a její statická vlastnost Current.

Nejdříve se podíváme, jak pracovat s účtem Posterous. Metoda GetPosterousAccount vrací odkaz na objekt IPosterousAccount, který reprezentuje účet uživatele na službě Posterous.

using RStein.Posterous.API;
IPosterousAccount m_account = PosterousApplication.Current.GetPosterousAccount("posterousUserName", "posterousPassword");
  public interface IPosterousAccount : IExtensionInterface, IApplicationHolder
    {
        string Name { get; }
        IEnumerable<IPosterousSite> Sites { get; }
        void LoadSites();
        void LoadSitesAsync();
        event EventHandler<EventArgsValue<IEnumerable<IPosterousSite>>> SitesLoaded;
        IPosterousSite PrimarySite {get;}
        
    }

Nejzajímavější vlastností v rozhraní IPosterousAccount je vlastnost Sites, která obsahuje kolekci všech “blogů” uživatele. Kolekce Sites, stejně jako většina dalších vlastností a kolekcí i u jiných objektů, je naplněna daty až při prvním přístupu.

Jestliže chcete pracovat s výchozím blogem uživatele, můžete využít vlastnost IPosterousAccount.PrimarySite.

Rozhraní IPosterousSite

public interface IPosterousSite : IExtensionInterface
    {
        int Id { get; }
        string Name { get; }
        string Url { get; }
        bool IsPrivate { get; }
        bool IsPrimary{ get;}
        bool AreCommentsEnabled{ get; }
        IPosterousAccount PosterousAccount { get; set; }

        IEnumerable<IPosterousPost> Posts { get; }
        int TotalPosts { get; }
        int LoadedPosts { get; }
        void LoadAllPosts();
        void LoadAllPostsAsync();
        event EventHandler<EventArgsValue<IEnumerable<IPosterousPost>>> PostsLoaded;        

        IEnumerable<string> Tags { get; }
        bool IsTagsLoaded { get; }        
        void LoadTags();
        void LoadTagsAsync();
        event EventHandler<EventArgsValue<IEnumerable<string>>> TagsLoaded;
        
        IEnumerable<IPosterousPost> GetPostsByTag(string tag);
        void GetPostsByTagAsync(string tag);
        event EventHandler<EventArgsValue<IEnumerable<IPosterousPost>>> PostsByTagLoaded;

        IEnumerable<IPosterousPost> GetPostsInPage(int page, int recordsCount);
        void GetPostsInPageAsync(int page, int recordsCount);
        event EventHandler<EventArgsValue<IEnumerable<IPosterousPost>>> PostsInPageLoaded;

        IPosterousPost CreatePost(string title, string body, bool autopostAll);

        
    }

Každý blog (IPosterousSite) obsahuje blogspoty  - objekty podporující rozhraní IPosterousPost.

Assert.IsTrue(m_account.PrimarySite.Posts.Count() > 0); 
 public interface IPosterousPost : IEntityWithClientState
    {
        string Link { get; }
        string Title{ get; set; }        
        string Url { get; }
        int Id { get;}
        string Body {get; set;}                    
        DateTime PostDate { get; }
        int Views { get; }
        bool Private { get; }
        IAuthor Author { get; }
        bool AreCommentsEnabled { get; }                
        IPosterousComment CreateNewComment(string commentBody);
        void AppendMedium(string filePath);        
        IEnumerable<IPosterousComment> Comments { get; }
        IEnumerable<IPosterousMedium> Media { get; }
        IEnumerable<String> Tags{ get; }
        void AddTag(string tag);
        IPosterousSite Site { get; }
        void Refresh();
        
    }

Při přístupu k vlastnosti Posts jsou staženy všechny blogspoty v dávkách po 50 položkách. 50 položek najendou je interní omezení Posterous API. Jestliže nechcete nahrávat všechny blogspoty, můžete sami “stránkovat” a nahrávat blogspoty pomocí metody GetPostsIn Page.

//Nahraje z první stránky dva blogspoty
var posts = m_account.Sites.First().GetPostsInPage(1, 2);

Můžete také nahrát pouze blogspoty označené vybraným tagem. Seznam dostupných tagů zjistíte ve vlastnosti IPosterousSite.Tags. Dle mých zkušeností ale vrácení blogpostů nefunguje v Posterous API zcela správně a občas blogposty vráceny nejsou.

//Vrátí se blogposty označené tagem "Všechno a nic"
var posts = m_account.Sites.First().GetPostsByTag("Všechno a nic");

 

Kromě dalších zajímavých a samopopisných informací v každém blogspotu naleznete i kolekci komentářů k blogspotu (rozhraní IPosterousComment) a informaci o přiložených souborech (audio, foto, mp3… – rozhraní IPosterousMedium ) .

public interface IPosterousComment : IExtensionInterface 
    {
        int Id {get;}
        IAuthor Author {get;}
        DateTime CreateDate {get;}        
        string Body {get;}        
        IPosterousPost Post{get;}
        
        
    }
 public interface IPosterousMedium : IExtensionInterface
    {
        MediumType Type { get;}
        string Url { get;  }
        Stream Content { get; }
        int FileSize { get; }
        IDictionary<string, object> ExtendedInfo { get; }
        bool IsContentLoaded { get;}
        void LoadContent();
        void LoadContentAsync();   
        event EventHandler<EventArgs> ContentLoaded;
    }
 

U médií se vlastnost Content opět naplní až při přístupu a jakékoli další informace o médiích stažené z Posterous naleznete v kolekci ExtendedInfo – např. informace o náhledu obrázku.

Nové blogspoty je samozřejmě možné vytvářet i s médii.

//Nový post, první argument – titulek blogspotu, druhý argument tělo blogspotu, 
//třetí argument - pokud je true dojde automaticky k rozeslání postu na všechny další registrované služby -//(Twitter, FB...)
IPosterousPost newPost = m_account.PrimarySite.CreatePost("Obrázek HUDBA TeST",
                                                "Příliš žluťoučký kůň úpěl ďábelské ódy", true);

//Přidání obrázku
            newPost.AppendMedium(@"c:\Users\STEIN\Documents\Hudba\Once\AlbumArtSmall.jpg");
//Přidání mp3
            newPost.AppendMedium(@"c:\Users\STEIN\Documents\Hudba\Once\01_falling_slowly.mp3");
//Uložení postu na server
            newPost.SaveChanges();

Posterous bohužel nevrací po uložení automaticky veškeré informace o novém spotu (informace o médiích apod.), takže jsem zvolil mechanismus, kdy po volání SaveChanges je vždy ještě volána metoda Refresh, která přes další (Bit.Ly) API dotáhne podrobnosti, aby programátor nemusel na získání dodatečných údajů myslet a volat metodu Refresh sám.

Metodu Refresh ale samozřejmě sami volat můžete a získate tak vždy aktuální data ze serveru.

Uložený blogspot můžete editovat – ne všechny údaje lze nyní uložit na server, podívejte se na současný stav web API.

string updateText = "Updated " + DateTime.Now.ToString();
            post.AppendMedium(@"c:\Users\STEIN\Documents\Hudba\Dylan Bob - Time Out Of Mind\AlbumArt_{6DF0A444-4F68-489B-AFCF-A985B02166BB}_Large.jpg" );
            post.Body = updateText;
            post.Title = updateText;
            post.SaveChanges();

K uloženému blogspotu můžete přidávat nové komentáře.

var comment = post.CreateNewComment(updateText);            
            post.SaveChanges();
  

Posterous API dovoluje vytvořit zjednodušený nový blogpost, aniž byste museli mít na Posterous účet. K publikaci vám stačí předat jméno a heslo stávajícího twitter účtu.  Url nového blogspotu automaticky publikuje na twitter. Jestliže máte Posterous účet svázaný s twitter účtem, blogspot se uloží na vašem primárním blogu (Site). Toto API se hodí hlavně pro rychlou publikaci obrázků na twitter a  Posterous toto API považuje za alternativu ke službě TwitPic.

Nejprve opět přes vstupní objekt PosterousApplication získáte twitter účet (rozhraní ITwitterAccount) .

m_twitterAccount = PosterousApplication.Current.GetTwitterAccount("twitter_name", "twitter_password");
    public interface ITwitterAccount : IApplicationHolder
    {
        string UserName{ get; }
        ITwitterPost CreatePost(string title, string body, bool postToTwitter);        
    }
 

A takto vypadá rychlá publikace obrázku

//Nový post, první argument – titulek blogspotu, druhý argument - tělo blogspotu, třetí argument – pokud je true, automaticky dojde k publikaci url obrázku (blogspotu) na twitter.
ITwitterPost newPost = m_twitterAccount.CreatePost(null,
                                                               null, true); 

            newPost.AppendMedium(“c:\pic.jpg”);
            newPost.SaveChanges();
 

Rozhraní ITwitterPost.

public interface ITwitterPost : IEntityWithClientState
    {
        string Url { get; }
        string Title{ get; }
        string Body { get; }
        string MediaId { get; }
        void AppendMedium(string filePath);
        bool AutopostToTwitter { get; }
        ITwitterAccount TwitterAccount { get; }
        IEnumerable<String> MediaNames { get; }
        ISinglePostInfo GetPostInfo();
    }

Další API vám dovoluje získat informace o libovolném blogpostu, u kterého znáte Bit.ly  adresu – Bit.Ly adresa je každému blogpostu přiřazena při vytvoření  - vlastnost IPosterousPost.Url.

Tento blogpost nemusí pocházet z vašeho blogu (Site).

Opět přes objekt PosterousApplication získáte odkaz na IBitLyService.

 public interface IBitLyService : IApplicationHolder
    {
        ISinglePostInfo GetSinglePost(string bitLySuffix);
        void GetSinglePostAsync(string bitLySuffix);
        event EventHandler<EventArgsValue<ISinglePostInfo>>  SinglePostLoaded;
    }
 
Ukázka získání jednoduchého blogspotu z této služby.
Uri uri = new Uri(Url);
//Extenzní metoda GetBitLySuffix pro snadné získání suffixu
ISinglePostInfo post = bitLyService.GetSinglePost(uri.GetBitLySuffix())
 

Rozhraní ISinglePostInfo

 public interface ISinglePostInfo : IExtensionInterface
    {
        string Link { get; }
        string Title{get;}        
        string Url { get; }
        int Id { get;}
        string Body{get;}                    
        DateTime PostDate {get;}
        int Views { get; }
        bool Private { get; }
        IAuthor Author { get; }
        bool AreCommentsEnabled { get; }                        
        IEnumerable<IPosterousComment> Comments { get;}
        IEnumerable<IPosterousMedium> Media { get;}
        IEnumerable<String> Tags{ get;}        
    }
 

Pokročilejší nastavení, která by se vám mohla hodit.

Blogspoty mohou být označeny jménem aplikace, která je vytvořila, a odkazem na aplikaci.

PosterousApplication.Current.ApplicationName = "Moje cool aplikace";
PosterousApplication.Current.ApplicationUrl = http://renestein.net;

Chcete pracovat přímo s objekty HttpWebRequest a HttpWebResponse? Potřebujete doplnit autentizaci k proxy, nebo chcete změnit maximální dobu, po kterou bude trvat požadavek? S pomocí rozhraní IRawRequestResponsePublisher  je to jednoduché.

    public interface IRawRequestResponsePublisher : IExtensionInterface
    {
        event EventHandler<EventArgsValue<WebRequest>> WebRequestCreated;
        event EventHandler<EventArgsValue<WebResponse>> WebResponseCreated;
    }
 

Stačí zaregistrovat obslužné metody pro události a poté všechny objekty HttpWebRequest a HttpWebResponse, které interně knihovna používá,  můžete upravit dle libosti.

Ukázka změny vlastnosti Timeout.

IRawRequestResponsePublisher publisher =
                PosterousApplication.Current.GetInterface<IRawRequestResponsePublisher>();

            Debug.Assert(publisher != null);
            publisher.WebRequestCreated += ((_, e) => e.Value.Timeout = WEB_TIMEOUT);

C# Posterous API toho zvládne ještě více, ale myslím, že pro dnešek už bylo kódu dost. Užijte si to. :-)



Friday, 15 January 2010 17:49:51 (Central Europe Standard Time, UTC+01:00)       
Comments [1]  .NET Framework | C# Posterous API | Compact .Net Framework | Silverlight