Monday, March 21, 2011
Prezentace Moderní trendy ve vývoji aplikací
Přibližně před rokem jsem u dvou firem začínal sérii technologických kurzů subjektivním shrnutím změn (nejen) v aplikacích psaných v .Net Frameworku. Nedávno jsme ji s kolegou náhodou otevřeli a pobavili jsme se nad tím, jak je rok v IT stále dlouhá doba a že zde dvojnásobně platí “tempus fugit”. Napadlo mě, že se nad prezentací možná se pobaví i někdo další, hlavně v pasážích, kde jemně naznačuju zálibu Microsoftu v zařezávání technologií.
U prezentace je třeba mít na paměti:
- Jedná se jen o osnovu “přehledové“ a cca dvouhodinové přednášky.
- Témata, typy projektů a technologie jsou v přednášce voleny podle zájmu zákazníka.
- Snažil jsem se nebýt hned v této úvodní přednášce příliš ostrý a konfliktní.
- Zvolená témata se týkala oblastí, které jsme v dalších dnech probíraly detailněji na konkrétních projektech vytvořených na návazných kurzech. Po pár zkušenostech si myslím, že jediný smyslupný kurz zabývající se technologií či programovacím jazykem je ten, na kterém píšete před účastníky kód. Tato přednáška byla koncipována jako motivační úvod k dalším tématům.
Monday, March 21, 2011 1:11:41 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7
Tuesday, February 15, 2011
FAQ k WP7 a pár specialitek navíc – aneb co jste vždy chtěli vědět o WP7, ale v Microsoftu se vám báli odpovědět
Na twitteru i v emailu se opakuje stále několik dotazů od lidí, kteří koketují s myšlenkou pořídit si WP7 telefon nebo chtějí pro WP7 vyvíjet. Účelem tohoto článku není suplovat uživatelský informační servis o WP7, který u nás skvěle dělá Smartmania a kterému se i v zahraničí věnuje mnoho webů, ale nabídnout snad trochu jiné informace než ty, které různí nájemní pisálci stále kopírují z tiskových zpráv a oficiálních prezentací a které další sociálně-altruističtí trotlové bezmyšlenkovitě linkují, sdílejí a lajkují.
Je Windows Phone 7 zcela nový operační systém, který Microsoft skutečně vyvinul “na zelené louce” a který nemá NIC společného s Windows Mobile?
Je i není. Neuspokojivá odpověď ve stylu hárající chytré horákyně, já vím. Windows Phone 7 jsou postaveny na operačním systému Windows CE, na kterém běžely i Windows Mobile. Dnešní oficiální název zní Windows Embedded CE a aktuální verze má pořadové číslo 6. I Windows Mobile v poslední verzi 6.5 byly stále založeny na Windows CE 5. Jak vidíte, Microsoft nám to moc neulehčuje a číslo verze Windows Mobile není nijak závislé na verzi použitých Windows CE. WP7 dle všech indicií běží na buildu Windows CE 6.
Windows CE 6 mají oprotí Windows CE 5 několik výhod. A z toho plyne, že i WP7 využívají nových rysů v systému a netrpí hlavními problémy Windows Mobile aplikací.
- Ve Windows CE 5 mohlo běžet najednou maximálně 32 procesů. Ano, celých 32 procesů musí stačit každému, ale s tím, jak se smartphony/PDA/MDA vyvíjely a výrobci ihned po startu zařízení obsazovali stále více procesních slotů pro své a se zařízením dodávané aplikace, se tento limit kupodivu ukázal nedostačující. Podpora cca 32 000 procesů ve Windows CE 6 je pořádné zlepšení.
- Ve Windows CE 5 měl každý proces 32 MB virtuální paměti a všechny procesy sdílely 4 GB adresní prostor. Nechci zabíhat do detailů, ale způsob nahrávání dynamických knihoven a jejich sdílení mezi procesy mohl vést velmi rychle k fragmentaci paměti a k chybě “nedostatek paměti”, i když fyzické paměti bylo dostatek. V CE 6.0 jsou 2 GB virtuálního paměťového prostoru vyhrazeny pro kernel a 2 GB pro každý proces. Správa paměti v CE 6 má některé nepříjemné dopady na vývojáře kernel/user driverů, ale pro uživatele i vývojáře aplikací je tento paměťový model mnohem lepší.
Mobilní Silverlight ve WP7 interně využívá Compact .Net Framework, který můžete znát z Windows Mobile. Jestliže pro WP7 vyvíjíte, poznáte to i podle toho, že při ladění aplikace a pokusu editovat zdrový kód vám Visual Studio hlásí, že Compact .Net Framework aplikace nemohou být při ladění upravovány.
Shrnuto – Windows Phone 7 mají nový kabátek (UI Metro), nová běhová prostředí pro uživatelské aplikace (Silverlight, XNA Framework) a vylepšený podvozek (CE 6). Psát a hlavně v článcích zdůrazňovat, že WP7 nemají se staršími Windows Mobile NIC společného, mi přijde v lepším případě naivní, v tom horším okázale neprofesionální. Už minulý rok jsem psal několikrát , že Microsoft by těžko za pár měsíců vyvinul zcela nový mobilní operační systém.
Mohu v ČR nakupovat aplikace v marketplace?
Stručná odpověď: Ne.
Složitější odpověď. Microsoft podle neoficiálních informací uvažuje, že marketplace v ČR bude dostupný “v létě 2010 2011”.
Neoficiálně to jde – stačí mít zahraniční platební kartu, založit nějaký Live účet, kde nastavíte, že pocházíte třeba z USA/UK. Ale musíte být asi velcí blázni nebo nadšenci, abyste si přes tyto obstrukce kupovali dnes (psáno 15.2. 2011) v ČR WP7 telefon. Počítejte také s tím, že když Microsoft zpřístupní marketplace pro ČR, a vy budete chtít používat svůj běžný Live účet, zakoupené aplikace mezi různými Live účty nepřevedete.
Mohu dnes dát svou WP7 aplikaci do marketplace?
Stručná odpověď: Ne.
Složitější odpověď. Pokud chcete dát aplikaci do marketplace, vyvíjejte se zahraničním partnerem. Je možné také se s trochou kreativity při tvorbě vývojářského účtu (v komentářích tohle sousloví vysvětlovat raději nebudu) registrovat jako vývojář z jiné země, pokud máte zahraniční platební kartu, ale riskujete, že Microsoft na tento “trik” přijde a zablokuje vám prodej aplikací, protože porušujete jeho podmínky.
Dobře, jsem smířen s tím, že se do market place prozatím nedostanu, ale přesto chci svou aplikaci otestovat na reálném zařízení, a ne jen v emulátoru. Jak to udělám, když mi WP7 telefon nedovolí nahrát vlastní aplikace?
Nejjednodušší je dnes použít Chevron WP7 k neoficiálnímu odemknutí telefonu. Poté co telefon odemknete, můžete nahrávat své vlastní aplikace. Podrobný návod, jak odemknout telefon, je na XDA-Developers, takže jej zde nebudu opakovat. Autoři Chevronu používají na odemčení WP7 telefonu jednoduchý trik. Požadavek na odemčení telefonu cílený na servery Microsoftu přesměrují na lokální počítač a odemčení telefonu povolí. Výhodou je, že nejde o vážný zásah do zařízení, nevýhodou, že pro Microsoft bude snadné tuhle “díru” (dá se pro tohle amatérské zabezpečení najít nějaký jiný eufemismus?) uzavřít další plánovanou aktualizací. Microsoft ale po setkání s autory Chevronu přislíbil, že sám umožní “co nejdříve” vývojářům nahrání a ladění vlastních aplikací v telefonu, aniž by bylo nutné otevírat a platit za vývojářský účet. Neznámou proměnnou je, které jednotky pro měření času používá Microsoft, kde, soudě podle stále odkládaného data první aktualizace pro WP7, korporátní čas evidentně plyne pomaleji, a jestli optimistická fráze “co nejdříve” zahrnuje alespoň předpokládanou dobu životnosti WP7 na trhu smartphonů.
Mohu pro WP7 vyvíjet v Compact .Net Frameworku?
Ne. I když Microsoft Compact .Net Framework používá a při nahrání Silverlight aplikace je patrné z debug logů, že se nahrávají CNF assembly, vlastní CNF aplikaci nenapíšete. Ale – v poslední části tohoto FAQ o prozatím hypotetické možnosti spouštět CNF aplikace na WP7 zařízení znovu píšu.
Mohu pro WP7 vyvíjet nativni (C/C++) aplikace? Slyšel jsem o nějakém Microsoftem vyvinutém nativním frameworku Iris.
Stručná odpověď: Nativní aplikace nejsou podporovány, můžete vyvíjet jen Silverlight aplikace nebo aplikace využívající XNA Framework. Na Iris v klidu zapomeňte.
Složitější odpověď: Nejprve vyřídíme IRIS, což je framework, ke kterém jsem se také vyjadřoval na twitteru.
Twitter: Objevily se bajky o tajném nativním frameworku IRIS pro #WP7. IRIS ale připomíná spíš mix CNF/WPF + P/Invoke. Nativní kód =stále WIN CE API |
Zde jen dodám, že IRIS si můžete představit jako framework, který v MS vyvinuli vývojáři, kteří asi nesnášejí technologie WPF/XAML/Silverlight vytvořené jejich kolegy, a proto si navrhli vlastní značkovací jazyk mixovaný s řízeným kódem a množstvím API (P/Invoke) volání. IRIS Microsoft pravděpodobně používá v shellu a ve svých WP7 aplikacích. Jestliže jste se ptali, jak to, že u MS aplikací třeba prvky Pivot a Panorama podporují plynulejší skrolování než stejné prvky v Silverlightu, v použití IRIS frameworku leží pravděpodobně odpověď. V Microsoftu buď občas neví levice, ce dělá pravíce, anebo mobilní Silverlight není zas tak odladěn a vyšperkován, jak by se mohlo z prezentací demo aplikací u MS evangelistů zdát, a teprve IRIS je tím zázrakem, který rozanimuje výchozí uživatelské rozhraní a dovede recenzenty WP7 systému k stále opakované větě o “ďábelské rychlosti WP7 aplikací”. Věta “o ďábelské rychlosti WP7 aplikací” platí paradoxně do té doby, než začnete další aplikace skutečně instalovat, k čemuž se nadšení recenzenti už asi nedokopou. Trochu sarkasticky dodejme, že v bajtech zhmotněná IRIS snad není ve WP7 proto, aby poslala brzy bezduché WP7 do křemíkového pekla, stejně jako kdysi mytologická Iris pomohla Dídó uvolnit duši z těla.
Nativní kód ale některé aplikace napsané v Silverlightu používají. Jestliže si prohlédnete XAP soubory (XAP soubor – distribuční soubor Silverlight aplikace) aplikací od výrobcu zařízení, zjistíte, že používají knihovnu GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll. Jak název assembly napovídá, měla by sloužit jako most k nativnímu kódu.
Aplikace od výrobce tuto assembly používají pro dynamické volání metod COM objektů. Zkusil jsem si napsat jednoduchou COM assembly ve VS 2008 pro Windows CE, protože VS 2010 z rozhodnutí nějaké moudré hlavy ve vývojářské centrále Microsoftu podporu pro vývoj CE aplikací již neobsahuje. COM knihovnu jsem nazval SL_COM2.dll.
Poté jsem založil projekt v Silverlightu, přidal do něj soubor nazvaný WPInteropManifest.xml, který obsahují i aplikace od výrobců. Soubor WPInteropManifest.xml má jednoduchý obsah.
Do projektu jsem přidal knihovnu GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll. i svou COM knihovnu SL_COM2.dll a u obou knihoven jsem nastavil build akci na content, aby obě knihovny byly přidány do výsledného XAP souboru. Žádné COM Interop knihovny nevygenerujete, COM typy musíte naimportovat z COM knihovny “ručně”.
Pomocí reflexe zaregistrujeme COM knihovnu na cílovém zařízení s využitím metod v assembly GAC_Microsoft.Phone.InteropServices_v7_0_0_0_cneutral_1.dll.
Tento kód projde, ale na posledním řádku dostanete výjimku, která znamená, že vaše COM knihovna je nekompatibilní s aktuální verzí operačního systému. Musíte si na internetu “najít” a nainstalovat správný Platform builder, což udělal asi Ch. Welsh, nebo si pohrát s PE hlavičkami knihoven, což jsem udělal já. COM knihovna by mohla být po odstranění všech překážek i branou ke všem API funkcím, které si vystavíte v COM objektech. Nepředpokládejte ale, že vaše aplikace bude při využívání COM knihovny přijata do marketplace.
Když jsou problémy s umístěním aplikací do marketplace, je naděje, že brzy bude pro WP7 existovat “jailbreak” a my budeme mít obchod s neoficiálními aplikacemi? Dají se WP7 “hacknout”?
Všechno jde, jen to stojí čas a peníze. A musíte mít přitom víru, že WP7 z trhu rychle nezmizí.
Hlavní problém je podle mě v tom, že Silverlight aplikace běží ve striktním bezpečnostním modelu, který není snadné překonat. Takže možnosti, které mě napadají:
Metoda pokus-omyl – začneme hledat slabiny v zabezpečení celé CE platformy a doufat, že Microsoft někde udělal další školáckou chybu podobnou té, kterou využil Chevron, jen teď někde v mnohem nižších vrstvách systému. Třeba kdyby Microsoft ponechal i byť jen trochu pootevřen přístup k RAPI, to by byla paráda. Sice se dá “získat” a použít knihovna SmartDevice.Connectivity, která RAPI interně využívá, ale neodemčený telefon nespolupracuje.
Ukázka volání:
Daleko nadějnější mi přijde pokračovat tak, že se do CE registrů přidá vlastní driver, který po startu zařízení shodí výchozí WP7 shell a dovolí vám nainstalovat a spouštět nativní aplikace i aplikace psané v Compact .Net Frameworku. V dalším kroku by to chtělo nového hostitele Silverlight aplikací, který bude vstřícnější k neoficiálním aplikacím. Na některých HTC zařízeních se průnik do registru již podařil… Takže nezbývá než zopakovat: Všechno jde, jen to stojí čas a peníze.
Snad jste v tomhle článků nalezli alespoň něco zajímavějšího než v konfekčních článcích o WP7.
Tuesday, February 15, 2011 3:23:05 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky | Silverlight | WP7
Monday, January 24, 2011
Tipy pro Windows Phone 7 aplikace V – vytváříme prvni aplikaci (a stavíme ji na vytvořených základech)
V předchozích článcích jsme si vytvořili miniframework pro view modely a ukázali si hostitele našich view modelů. Přišel čas naše znalosti, idiomy a návrhové vzory zakódované ve formě aplikační infrastruktury v našem miniframeworku využít při tvorbě konkrétní aplikace. Pro účely tohoto článku i následujících článků jsem se rozhodl, že vytvoříme aplikaci, která nám dovolí spravovat vlastní blog na doméně Posterous s využitím Posterous API.
Hlavní případy užití, které budeme v aplikaci podporovat.
- Přihlášení uživatele ke svému účtu - téma dnešního článku.
- Zobrazení seznamu blogů, které patří přihlášenému uživateli.
- Zobrazení seznamu příspěvků na vybraném blogu.
- Zobrazení detailu příspěvku na blogu.
- Úprava stávajícího příspěvku na blogu.
- Zadání nového příspěvku na blog.
Jako vždy nás tato prozatím letmo načrtnutá témata dovedou k dalším podivným zákoutím vývoje WP7 aplikací a my se z nich s úsměvem záludnostmi WP7 poučeného idiota zoceleného harcovníka pokusíme dostat..
Skutečně vás nechci urážet popisem klikání ve Visual Studiu ani popisem základů XAMLu, “data bindingu”, “behaviors”, takže jen napíšu, že byste před vytvářením aplikace měli:
- Stáhnout si a nainstalovat Windows Phone Developer Tools.
- Ve Visual Studiu založte nový projekt Windows Phone Application – nejlepší bude, když ho pojmenujete jako já RStein.PosterousReader.WP, abyste nebyli zmateni názvy jmenných prostorů dále v článku.
- Vývojářský život je na tomto projektu jednodušší o to, že k práci s Posterous API použijeme můj C# Posterous API Wrapper pro WP7. Po stažení přidejte referenci na knihovnu RStein.Posterous.API.SLM do svého projektu.
A i když to dnes ještě není tak nutné, zřiďte si na Posterous vlastní účet, abyste mohli aplikaci později testovat na reálných datech. C# Posterous API pro nás v triádě Model-View-ViewModel bude představovat model, což má pro nás výhodu, že se můžeme stále soustředit na view a view modely, o které šlo i v předchozích článcích, a model můžeme považovat za černou skříňku.
- V novém projektu vytvořte hlavně dvě nové složky View a ViewModels. Na obrázku jsou červeně podtrženy další složky, které si již dnes doporučuju přidat do projektu - Behaviors, Extensions, HostServices, Icons, SpecialTypes, UI, UIServices a hlavně ViewModels a Views.
- Předpokládám, že jste schopni do svého projektu vložit kód tříd, které jsem popisoval v předchozích článcích. Seznam předchozích článků s výpisy kódu naleznete na konci tohoto článku.
Dnes vytvoříme přihlašovací obrazovku do naší aplikace. Pro lepší představu je zde obrazovka, kterou byste měli mít hotovou na konci dnešního článku.
Nejprve určíme, které funkce musí obrazovka a její podkladový kód zvládat:
- Přihlašovací obrazovka se zobrazí ihned po startu aplikace.
- Uživateli dovolíme zadat přihlašovací jméno a heslo.
- Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.
- Jestliže dojde k “tombstoningu” stránky, bude jméno i heslo po návratu z “tombstonovaného” stavu zachováno – ověříme si tak poprvé in vivo, že naše třída ViewModelBase podporuje “tombstoning” přesně dle našich požadavků.
- Po návratu na přihlašovací obrazovku z jiné části aplikace bude zachován jen obsah textového pole “uživatelské jméno”, textové pole “heslo” bude prázdné.
- Po kliknutí na tlačítko Přihlásit nebudeme zadané jméno a heslo ihned ověřovat, ale uložíme oba údaje pro použití na dalších stránkách aplikace. Poté přesměrujeme uživatele na další stránku se seznamem blogů, kde bude jméno a heslo využito k získání seznamu blogů uživatele. Důvodem je to, že v Posterous API se jméno a heslo zasílá při každém webovém požadavku, žádné jednorázové přihlášení neexistuje a nemá smysl generovat nějaký “dummy” požadavek jen pro ověření hesla. Jestliže se na stránce se seznamem blogů, na kterou z přihlašovací obrazovky přesměrováváme, data kvůli neplatným přihlašovacím údajům získat nepodaří, aplikace nás vrátí na přihlašovací obrazovku. Vytvoření stránky se seznamem blogů bude téma dalšího článku.
A začínáme:
Do složky Views vložte nové View pro přihlášení uživatele. V dialogu Add new item vyberte Windows Phone Portrait Page a pojmenujte ji MainLoginView.xaml.
Jedná se o úvodní stránku aplikace, a proto v deskriptoru WP7 aplikace nazvaném WAMppManifest.xml, který naleznete v projektové složce Properties, změníme startovací stránku na View/mainLoginView.xaml.
I když to není u přihlašovacího dialogu nutné, v dnešním článku si ukážeme, jak můžeme jednoduše složit jedno view z dalších nezávislých view a také to, že view nemusí být jen stránka (Page), ale i “User Control”. Naše MainLoginView bude zobrazovat titulek aplikace (Posterous klient), titulek stránky (Přihlášení) , ale textová pole “heslo”, “uživatelské jméno” a tlačítko “Přihlásit" se” bude obsahovat “user control” LoginView, který můžeme použít jako součást i zcela jiného view (stránky) v aplikaci. Do složky Views přidejte Windows Phone User Control a nazvěte jej LoginView.xaml.
Zkompilujte (Build) “solution”.
Do view MainLoginView vložte tento kód:
Jak jsem zmiňoval v jednom z předešlých článků, všechny stránky v aplikaci by měly být potomkem naší třídy PageBase, která je hostitelem pro view modely. Proto je naše stránka uzavřena v elementu <controlex:PageBase>. XML jmenný prostor controleex je u mě namapován na xmlns:controlex="clr-namespace:RStein.PosterousReader.WP.UI". V projektovém adresáři UI musíte mít tedy třídu PageBase.
Také v “code behind souboru” musíte třídu MainLoginView učinit potomkem PageBase.
Hlavní xaml pro přihlášení uživatele obsahuje “User Control” LoginView, na který se v MainLoginView odkazujeme. (<views:LoginView Grid.Row="1"></views:LoginView>). XML jmenný prostor views je mapován na jmenný prostor RStein.PosterousReader.WP.Views v C# (xmlns:views="clr-namespace:RStein.PosterousReader.WP.Views) – v našem případě tedy na projektový adresář Views.
Do view s názvem LoginView vložte následující XAML:
Jak bylo vidět na snímku obrazovky na počátku článku, LoginView obsahuje hlavně textové pole pro zadání jména uživatele (txtName) a prvek pro zadání hesla uživatele (txtPassword) a tlačítko “Přihlásit se”. U prvků pro zadání uživatelského jména a hesla vás mohou zarazit jen tagy <i:Interaction.Behaviors> a atributy jako behaviors:TextboxPasswordAttachedProperties.TextBoxChangedAction, jejichž význam vysvětlím za chvíli. Také vás hned upozorním, že tlačítko “Přihlásit se” je speciální tlačítko ((<controlex:ButtonEx) dovolující reagovat na stisknutí tlačítka uživatelem tak, že spustí metodu Execute objektu ICommand, což standardní WP7 tlačítko nezvládá. Znovu připomínám, že Silverlight ve WP7 je bohužel založen na poměrně staré verzi 3 desktopového Silverlightu. Kód třídy ButtonEx uvidíte také za chvíli.
Máme view, ve view používáme “data binding”, ale nemáme ještě view modely, které fungují pro view jako zdroj dat.
V každém view modelu naší aplikace budeme používat objekt IPosterousApplication ke komunikaci s Posterous, budeme chtít z view modelů navigovat na další view v aplikaci a také bychom měli být schopni v každém view modelu získat uživatelské jméno a heslo zadané na na námi vytvářené přihlašovací obrazovce. To znamená, že společné vlastnosti a služby view modelů pro naši aplikaci můžeme soustředit v bázové třídě PosterousViewModelBase. Do projektové složky Views vložte novou třídu PosterousViewModelBase a do ní zkopírujte následující kód.
PosterousViewModelBase je potomkem naší staré známé třídy ViewModelBase
V konstruktoru třída PosterousViewModelBase přijímá odkaz na objekt IPosterousApplication, který je základním rozcestnikem pro přístup k Posterous API, navigační službu INavigationService, která view modelu dovolí navigovat na další view v aplikaci (např. ze seznamu článků na detail vybraného článku), a titulek zobrazené stránky. Titulek převezme a nabízí poté ve vlastnosti PageTitle bázová třída ViewModelBase.
Navigační služba je představována rozhraním INavigationService.
Základní implementace rozhraní INavigationService pro WP7 je dostupná ve třídě DefaultWPNavigationService a pouze obaluje navigační služby dostupné v samotných WP7 aplikacích.
Rozhraní INavigationService i třídu DefaultWPNavigationService vložte do projektové složky HostServices.Více si o navigaci mezi různými view povíme v dalších článcích.
Vraťmě se k PosterousViewModelBase. V PosterousViewModelBase máme vždy uloženo ve statických vlastnostech LastUsedUserName a LastUsedPassword poslední zadané přihlašovací údaje, které může každý view model využít při získání nebo úpravě dat z Posterous.
Poznámka: Kdyby někoho z vás pohoršovali statické vlastnosti, klidně si jako domácí úkol napište “CredentialsManager”, který bude injektován stejně jako již zmíněné dvě další služby do view modelů. Prozatím nechci komplikovat kód víc, než je nutné.
My máme dvě view, MainLoginView a LoginView, a proto vytvoříme i dva view modely. Opět zdůrazňuju, že tato volba je na vás a díky “dědění view modelů” bychom mohli ponechat třeba dvě view a vytvořit pro ně jen jeden společný view model.
Do projektové složky ViewModels přidejte třídu nazvanou MainLoginViewModel – view model pro MainLoginView. Název view modelu nyní musí odpovídat konvenci “NázevView +ViewModel”. Zdůrazňuju i zde, že jde jen o konvenci a že si můžete jednoduše napsat jiný IViewModelResolver, který dohledá dle vašich zcela jiných projektových konvencí k view vhodný view model.
MainLoginViewModel je sympaticky jednoduchý objekt. Má jen delegující konstruktor, ve kterém předá své bázové třídě PosterousViewModelBase vyžadované povinné argumenty posterousApplication, navigationService a titulek stránky.
Texty v aplikaci nejsou prozatím lokalizovány a titulky stránek jsou uloženy ve třídě GlobalConstants. Do “rootu” projektu přidejte třídu GlobalConstants.cs
Název aplikace (APP_MAIN_TITLE) , podobně jako titulek stránky, vydává ve vlastnosti AppTitle opět ViewModelBase
Jen o trochu složitější bude view model pro LoginView. Do projektové složky ViewModels přidejte třídu LoginViewModel.
V LoginViewModelu máme opět delegující konstruktor, jedinou změnou oproti MainLoginViewModelu je to, že jako titulek stránky předáváme prázdný řetězec, protože předpokládáme, že titulek na stránku dodá “nadřízené” view.
Vlastnosti a jejich význam
Název vlastnosti | Popis |
TextChangedAction | Akce, která má být vyvolána, když se ve view změní text přihlašovacího jména nebo hesla. Tato akce je v konstruktoru inicializována tak, že si vynutíme opětovné vyhodnocení toho, zda může být proveden LoginCommand. V článku popíšu, proč je to (prozatím?) řešeno takto. |
LoginCommand | Objekt podporující rozhraní ICommand, který v metodě Execute zavolá metodu handleLogin. Metoda handleLogin uloží zadané jméno a heslo do statických vlastností LastUsedPassword a a LastUsedUserName a přesměruje uživatele na stránku se seznamem blogů. Instanční vlastnost UserPassword je při každém pokusu o navigaci na stránku se seznamem blogů “vyčištěna” tím, že je do ní uložen prázdný řetězec, a při návratu na přihlašovací obrazovku tedy není automaticky předvyplněno heslo, což byl jeden z našich požadavků na přihlašovací dialog. LoginCommand může být proveden jen tehdy, když bylo ve view zadáno a do view modelu přes oboustranný (two way) binding zpropagováno uživatelské jméno a heslo. Pokud vlastnosti UserName a UserPassword mají hodnotu null nebo obsahují prázdný řetězec, LoginCommand nemůže být proveden. |
UserName | Zadané přihlašovací jméno na službu Posterous. V metodě DoInternalInit, která je volána na začátku životního cyklu view modelu předvyplníme uživatelské jméno posledním zadaným uživatelským jménem, které jsme dříve uložili do statické vlastnosti LastUsedUserName. |
UserPassword | Heslo na službu Posterous. |
Co potřebujeme, abychom mohli náš LoginViewModel zkompilovat?
Do třídy GlobalConstants přidejte URL dalšího view se seznamem blogů a článků.
Stránku PostsListView.xaml vytvoříme v dalším článku, nyní ji celou vytvářet nemusíte. Postačí do složky Views dát novou stránku (Page) s názvem PostsListView.xaml.
Jak jsem již psal, ve WP7 nemáme bohužel podporu pro objekty ICommand. Třída DelegateCommand, jejíž instancí je LoginCommand, je minimalistickou implementací rozhraní ICommand.
Pokud podobnou třídu nemáte, vložte si do projektu třídu DelegateCommand. U mě je v jmenném prostoru RStein.PosterousReader.Common.
Třídě DelegateCommand můžete předat dva delegáty. Delegát executeAction (“co má být vykonáno”) je spuštěn v metodě Execute z rozhraní ICommand. Delegát canExecuteAction představuje implementaci metody CanExecute (“může být nyní command vykonán?”). Minimalistická implementace je to proto, že nijak nepoužívám událost CanExecuteChanged, a namísto toho jsem si pro “binding” vystavil speciální vlastnost CanExecuteCommand, která v get akcesoru deleguje na metodu CanExecute.
Vlastnost TextChangedAction v LoginViewModelu je delegát typu Action, který nám pomáhá vyřešit jeden z požadavků na přihlášení.
“Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.”
To znamená, že potřebujeme po každém přidání nebo smazání znaku v textboxu pro zadání jména uživatele i v textboxu pro zadání hesla zjišťovat, jestli můžeme ve view zpřístupnit tlačítko pro přihlášení. Když je alespoň jeden textbox prázdný, tlačítko pro přihlášení není dostupné, když je vyplněno alespoň jedním znakem jméno i heslo, tlačítko pro přihlášení je dostupné.
Jak je tento požadavek ve view a view modelu splněn?
Ve view LoginView je vlastnost IsEnabled tlačítka “Přihlásit se“ z třídy ButtonEx “nabindována” na vlastnost CanExecuteCommand objektu LoginCommand ve view modelu.
Do projektové složky UI si přidejte třídu ButtonEx, která doplňuje standardní WP7 tlačítko o jednoduchou podporu objektů ICommand.
Vlastnost CanExecuteCommand musí vracet true, pokud je v každém textboxu alespoň jeden znak, jinak false. Jak ale ve view modelu zjistíme, že uživatel zadal nebo smazal v některém textovém poli další znak? Do view modelu se při oboustranném bindingu zpropaguje hodnota z textového pole až po opuštění textového pole uživatelem, ale my přitom musíme reagovat ve view modelu na zadání každého znaku.
Takové chování textových polí ve WP7 skutečně nemáme a musíme si ho dopsat, a to nejlépe za pomoci ”attached” vlastností a objektů Behavior<T>. Nejprve se podívejme, jak vypadají objekty TextBox a PasswordBox v XAMLu, když jsou rozšířeny o “attached” vlastnost TextboxPasswordAttachedProperties.TextBoxChangedAction, která je “nabindována” na nám již známou vlastnost TextChangedAction typu Action ve view modelu. “Attached” vlastnost TextBoxChangedAction tedy říká: “Hej, kdykoli se změní u prvku zadaný text, musí NĚKDO ochotný zavolat delegáta TextChangedAction, abychom ve view modelu nebyli odříznuti od novinek ve světě view.”
A ten někdo bude náš objekt Behavior , který zase světu sděluje: “Mám dobré vychování, a když mi dovolíte vstoupit do bran tagu PasswordBox nebo TextBox, delegáta TextChangedAction zavolám, i když to sám WP7 TextBox a PasswordBox nezvládne.”
Další nepříjemností ve WP7 je, že i když objekt Behavior má mít pro TextBox a PasswordBox stejné chování, musíme napsat dva objekty Behavior pro každý prvek, protože TextBox ani PasswordBox spolu kupodivu nemají moc společného. My se alespoň pokusíme kód v obou objektech Behavior neduplikovat.
Do projektové složky Behaviors přidejte třídu TextboxPasswordAttachedProperties a v ní vytvořte attached vlastnost TextBoxChangedAction.
Do složky Behaviors přidejte abstraktní třídu TextChangedBehaviorBase, která bude fungovat jako základ pro dva podobné objekty Behavior určené pro TextBox (TextBoxTextChangedUpdateSourceBehavior) a PasswordBox (PasswordTextChangedBehavior). Do projektu musíte přidat referenci na knihovnu System.Windows.Interactivity, kterou naleznete většinou ve složce c:\Program Files\Microsoft SDKs\Expression\Blend\Windows Phone\v7.0\Libraries\System.Windows.Interactivity.dll. Bez této knihovny není dostupná bázová třída Behavior<T>.
Metoda RunTextChangedAction se pro objekt Textbox a PasswordBox asociovaný s tímto objektem Behavior pokusí dohledat a spustit delegáta v “attached” vlastnost TextBoxChangedAction. Metoda UpdateSource požádá odvozené třídy o vydání "objektu BindingExpression” voláním metody GetBindingExpression. Vrácený objekt “BindingExpression“ by měl zapouzdřovat propojení vlastnosti view modelu (např UserName) s textem v textovém poli. Po kontrole, že se jedná o oboustranný (two way) binding, metoda UpdateSource zavolá na objektu BindingExpression UpdateSource, což způsobí přenesení hodnoty zadané uživatelem v textovém poli ve view do zdrojové vlastnosti (např. UserName) ve view modelu.
Potomek TextBoxTextChangedUpdateSourceBehavior v metodě GetBindingExpression vrátí BindingExpression pro svou vlastnost Text. V přepsané metodě OnAttached, která je volána vždy, když je objekt Behavior asociován s textboxem, si přihlásíme odběr události TextChanged TextBoxu a při každé změně textu voláme zděděné a výše popsané metody UpdateSource a RunTextChangedAction. V metodě OnDetaching si nezapomeneme odběr události TextChanged odhlásit.
Pro PasswordBox máme dalšího potomka PasswordTextChangedBehavior, který se od třídy TextBoxTextChangedUpdateSourceBehavior liší jen tím, že v metodě GetBindingExpression vrací “BindingExpression” pro vlastnost Password a zadávaný text sleduje přes událost PasswordChanged.
V LoginView si nezapomeňte zkontrolovat, že máte namapovány správně xml prefixy behaviors a i na správné jmenné prostory v C#, abyste mohli využívat novou attached vlastnost TextBoxChangedAction a objekty TextBoxTextChangedUpdateSourceBehavior a PasswordTextChangedBehavior.
prostoryxmlns:behaviors="clr-namespace:RStein.PosterousReader.WP.Behaviors"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Dodejme, že náš delegát TextChangedAction ve view modelu simuluje změnu objektu LoginCommand, který je přidružen k tlačítku Přihlásit se a tlačítko Přihlásit tedy po každém zadání znaku zjistí, jestli má být dostupné. Zopakujme, že vlastnost IsEnabled tlačítka je “nabindována” na vlastnost LoginCommand.CanExecuteCommand.
Aplikaci můžete spustit.
Poznámka: Ještě si ale nezapomeňte do složky UIServices dát kód rozhraní IViewModelResolver a třídy ViewModelResolver, které jsem popisoval v článku propojení view modelu s view (stránkou), protože bez třídy ViewModelResolver by aplikace nebyla schopna pro naše MainLoginView a LoginView dohledat právě vytvořené view modely. A v aplikaci musíte mít samozřejmě všechny další třídy z předchozích článků, které jsou odpovědné za “tombstoning” apod.
U aplikace si můžete zkontrolovat, že:
- Přihlašovací dialog plní všechny požadavky, které jsme vypsal na začátku článku.
- Nemusíme se nijak starat o “tombstoning” aplikace. Zmáčkněte tlačítko Win, poté se tlačítkem Back vraťte do aplikace a všechny hodnoty v textových polích zůstanou zachovány.
- Tlačítko pro přihlášení je dostupné jen tehdy, když textová pole pro zadání jména a hesla jsou vyplněna. Pokud alespoň jedno pole vyplněno není, tlačítko je neaktivní.
- I naše minimalistická podpora rozhraní ICommand vede k tomu, že ve View nemáme žádný “code behind”. Logika stojící za view je jen ve view modelech a XAML tahá data z view modelů přes “data binding”. Pokud přesně tohle to považujete za důležité, budete určitě nadšeni. Jak uvidíte v dalších článcích, já jsem vůči strategii “vše do XAMLu” hodně skeptický, ale XAML puristé
si mohou jít po dnešku jistě ožrat držku slavit.
- I když šlo jen o přihlašovací formulář, vytvořili jsme si další skládací kostky (attached vlastnosti, Behavior, PosterousViewModelBase), které se nám budou hodit při psani dalších view a view modelů (nejen) v této aplikaci.
Zauvažujte nad tím, jestli by se nám také nehodila nějaká podpora ve view modelech pro ukládání nejen tranzientního stavu, ale i pro ukládání “trvalejšího” stavu, který bude dostupný i v další nezávislé instanci aplikace. Možná by stálo za to, aby si aplikace pamatovala poslední zadané přihlašovací jméno nejen při “tombstoningu”, ale aby přihlašovací jméno bylo nabídnuto i při novém spuštění aplikace. A z hlediska vývojáře bezbolestná podpora pro ukládání perzistentního stavu (data s “delší záruční lhůtou”) bude námětem dalšího WP7 intermezza. Doufám, že se těšíte.
Předchozí články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Monday, January 24, 2011 5:48:26 PM (Central Europe Standard Time, UTC+01:00)
C# Posterous API | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Monday, January 3, 2011
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Update 4. 1. 2011 - upraven kód objektu UIHelper a spolupracující metody tak, aby byl klíč generovaný pro tranzientní stav každého view modelu skutečně unikátní. Když rozkliknete GISTy, můžete se podívat i na původní verzi kódu.
V předchozím článku jsme se podrobně věnovali vytvoření bázové třídy pro view modely včetně podpory ukládání tranzientního stavu během “tombstoningu” a na konci jsem slíbil, že další díl bude věnován hlavně propojení našich view modelů s view.
View pro nás bude hostitel view modelu a v souladu s WP7 modelem navigace mezi jednotlivými stránkami budeme za hlavního hostitele považovat potomka třídy PhoneApplicationPage. Uvidíme ale, že při vytváření View nejsme omezeni na potomky PhoneApplicationPage, protože budeme schopni vytvořit i view jako potomka třídy UserControl.
Zrekapitujme hlavní funkce, které by měl náš hostitel view modelů zvládat:
- Při vytvoření stránky je třeba najít ke stránce přidružený view model. Dodejme již nyní, že povinností hostitele bude dohledat i vnořené (dceřiné) view modely, jestliže je jedna stránka složena z více nezávislých view a každé view může být asociováno s jiným view modelem.
- Při vytvoření stránky je nutné u view modelu volat metodu Init pro základní inicializaci view modelu, jestliže view model podporuje naše rozhraní IInittialize. Znovu připomínám, co zaznělo v minulém článku, že si implementaci žádných rozhraní u view modelů nevynucujeme.
- Jestliže se stránka stane poprvé nebo po návratu uživatele “aktivní” (zobrazenou), je nutné volat na view modelu metodu Activate, když view model podporuje rozhraní IActivated.
. - Jestliže uživatel ze stránky odchází, hostitel na view modelu volá metodu Deactivate z rozhraní IDeactivated.
- Hostitel musí být schopen při “tombstoningu” uložit tranzientní stav view modelu.
- Po návratu z “tombstoningu” hostitel dovolí view modelu obnovit tranzientní stav.
- Hostitel se nás bude snažit ve view modelech odstínit od všech WP7 nedomyšleností, záludností, nesmyslných omezení a programátorských vrtochů, které jsou nedílnou a z pohledu mobilní MS divize jistě i zábavnou součástí celkově kapriciozního životního cyklu třídy PhoneApplicationPage.
Nyní již můžeme přístoupit k vytvoření hostitele. Našim hostitelem bude třída PageBase, která je potomkem PhoneApplicationPage z WP7 frameworku. Nakonec jsem pro účely článku zvolil toto řešení, i když není problém v dalších dílech ukázat adaptér, který nás nutnosti vlastní stránky v aplikaci odvozovat z bázové třídy PageBase zbaví. Myslím si ale, že navigační model WP7 aplikací neposkytuje sám o sobě mnoho prostoru pro kreativitu, a proto nutnost dědit z PageBase místo třídy PhoneApplicationPage z WP7 za nějak zvlášť restriktivní nepovažuju.
Třída PageBase:
Kdykoli dojde k aktivaci stránky, čímž míním první navigaci na stránku, nebo návrat na stránku pomocí tlačítka Back, je volána metoda OnNavigatedTo. My jsme přepsali metodu OnNavigatedTo a v ní vždy nastavíme svou vlastnost WasLayoutUpdateCalled na false. Vlastnost WasLayoutUpdateCalled použijeme za chvíli, zde jen řekněme, že tuto vlastnost potřebujeme k tomu, abychom poznali, že již je vytvořen celý vizuální strom ovládacích prvků a že můžeme s ovládacími prvky bez obav pracovat. Bohužel v metodě OnNavigatedTo vizuální strom prvků vytvořen být nemusí. O některých problémech, které je nutné řešit, když není zcela vytvořen vizuální strom prvků, jsem již mluvil u ovládacího prvku WebBrowser v první části tohoto seriálu.
Metoda OnNavigatedTo také dá příkaz k vyzvednutí dříve uloženého tranzientního stavu pomocí metody loadSavedTransientState z vlastnosti PhoneApplicationPage.State. Tranzientní stav, pokud existuje, je pouze uložen do vlastnosti LastSavedTransientState a zatím se s ním nijak nepracuje. Klíčem, pod kterým je uložen stav celé stránky včetně tranzientního stavu view modelů, je plně kvalifikované jméno aktuální stránky (string stateKey = GetType().FullName). Měli bychom si být vědomi, že při této implementaci nesmíme mít jednu stránku v aplikaci nahranou vícekrát, protože by různé instance stejné stránky mezi sebou sdílely stav. Když budete trvat na tom, že jedna stránka může mít v systému několik instancí, není problém změnit generování klíče, pod kterým bude stav pro každou unikátně identifikovanou instanci uložen.
Pokračujme v našem scénáři. V konstruktoru si přihlašujeme odběr události LayoutUpdated (LayoutUpdated += PageBase_LayoutUpdated;), abychom byli notifikováni, že již můžeme bezpečně pracovat s ovládacími prvky.
Obslužná metoda handleLayoutUpdated je po každé navigaci na stránku spouštěna jen jednou, a proto se podíváme na hodnotu vlastnosti WasLayoutUpdateCalled – jestliže má hodnotu true, nic dalšího neděláme. Jinak metoda zkontroluje, zda instance PageBase je nově vytvořena, k čemuž dojde při první navigaci na stránku a také po obnovení z “tombstonovaného" stavu. Jinak řečeno – když je volán konstruktor třídy PageBase, znamená to, že máme stránku v “panenském” stavu, protože v ní nejsou žádná data a dokonce ani nebyly připojeny view modely. A proto bezparametrický konstruktor PageBase nastavuje vlastnost IsNewInstance na true, abychom v metodě handleLayoutUpdated věděli, že máme co do činění s novou instancí stránky. U nové stránky je třeba získat view modely, a pokud je stránka obnovena po “tombstoningu”, je třeba do view modelů nahrát tranzientní stav. Při variantě “nová instance stránky” metoda handleLayoutUpdated volá metodu LoadState.
Jestliže nebyl volán konstruktor a stránka obsahuje veškerý stav, k čemuž většinou dojde, když se vrátíme bez “tombstoningu” pomocí tlačítka Back na stránku, metoda handleLayoutUpdated volá pouze pomocnou metodu handleAllActivated, která proiteruje všechny dříve nahrané view modely, a když podporují rozhraní IActivated, tak na nich zavolá metodu Activate.
Metoda LoadState ihned deleguje na metodu restoreTransientState, jíž předá v objektu ElementIndexPair odkaz na aktuální stránku (this), která představuje “root” všech prvků, a relativní index nastavený na 0 (první prvek na této úrovni). V dalším argumentu jako prvek, pro který má být obnoven stav, předá opět odkaz na aktuální stránku (this) a poslední argument, ve kterém metoda restoreTransientState očekává naposledy použitý view model, je null, protože se žádnými view modely se ještě nepracuje.
Metoda restoreTransientState nejprve zkontroluje, jestli předaný ovládací prvek představuje View. K tomu použije objekt podporující rozhraní IViewModelResolver, který je uložen ve statické vlastnosti ViewModelManager. Přesněji řečeno, ve vlastnosti ViewModelManager můžete uložit delegáta (Func), který vrací objekt realizující rozhraní IViewModelResolver. Ve statickém konstruktoru třídy PageBase ukládám do vlastnosti ViewModelManager funkci, která vytváří v mé aplikaci výchozí IViewModelResolver s názvem ViewModelResolver (ViewModelManager = () => new ViewModelResolver();)
Rozhraní IViewModelResolver
Metodě IsView předáte vybraný objekt a ona vám vrátí true, jestliže objekt považuje za view, pro které by měl existovat view model. Metoda ResolveViewModel přijme objekt představující view a dohledá k němu view model.
Pro lepší představu vám mohu bez podrobnějšího komentáře ukázat třídu, která v aplikaci používající Posterous API dohledá k view view model na základě jmenné konvence. Za View se považuje každá instance z třídy, jejíž název končí znaky “View”, a k tomuto View je vrácen view model, který se jmenuje stejně jako view, ale končí znaky “ViewModel”. K view s názvem LoginView je tedy vrácen view model s názvem LoginViewModel.
Vraťme se k metodě restoreTransientState. Když metoda zjistí, že objekt představuje view, pokusí se dohledat dříve uložený tranzientní stav. Stav je uložen pro každé view v objektu IDictionary, konkrétně v třídě KnownTypesDictionary, pod klíčem, kterým je úplné jméno ovládacího prvku – úplným jménem se rozumí jméno třídy ovládacího prvku + jména tříd všech jeho vizuálních předků (ancestors). Ke jménu třídy každého prvku je přidán pořadový indexu prvku mezi prvky na stejné úrovni vizuálního stromu. Klíč generuje třída UIHelper v metodě GetTransientStateKey.
Metoda restoreTransientState se poté s s využitím metody prepareViewModel pokusí získat přes ViewModelManager view model (ViewModelManager().ResolveViewModel(obj), dále zjistí, jestli view model podporuje rozhraní ITransientStateManager, IInitialize a a IActivated a pokračuje takto:
- Jestliže máme uložen tranzientní stav (došlo k “tombstoningu”) a view model podporuje rozhraní ITransientStateManager, zavoláme metodu LoadState z rozhraní ITransientStateManager (stateManager.LoadState(LastSavedTransientState[stateKey]); a view model tak dostane šanci načíst dříve uložená tranzientní data.
- Když nemáme uložen tranzientní stav a view model podporuje rozhraní IInitialize, zavoláme metodu IInitialize.Init. View model tak dostane šanci nahrát data, ať už se souboru, z webové služby nebo jiného datového zdroje, která mají být zobrazena v aktuálním view.
Poté, co view model obnoví nebo inicializuje svůj stav, je volána metoda activateAndSetDataContext, která na view modelech podporujících rozhraní IActivated zavolá metodu Activate a nastaví view model jako datový zdroj (“DataContext”) view.
Nakonec pro všechny dceřiné prvky, které mohou představovat vnořená view (např. user control), rekurzivně zavoláme opět metodu restoreTransientState. K získání dceřiných prvků a jejich relativního indexu mezi prvky na stejné úrovni zanoření ve vizuálním stromu je použita metoda UIHelper.GetChildren.
Metoda LoadState, restoreTransientState a další pomocné metody, o kterých jsme mluvili v předchozích odstavcích:
Za povšimnutí stojí ve scénáři metody restoreTransientState ještě několik dalších věcí:
- Když zjišťujeme, jestli objekt podporuje rozhraní IInitialize (metoda getInitObjectWithSyncContext), ihned do view modelu zpropagujeme synchronizační kontext (initObject.SynchContext = SynchronizationContext.Current;), aby například událost PropertyChanged mohla být vyvolávána vždy v UI vláknu.
- Pomocná metoda selectViewModel(frameworkElement, lastViewModel, currentViewModel); odpovědná za výběr view modelu zajistí, že když IViewModelResolver nenalezne pro view žádný view model, automaticky view přiřadí view model použitý u předchozího view. Kdy to potřebujete? Třeba když stránku rozdělíte na několik nezávislých “user controls”, tak se můžete rozhodnout, jestli bude mít každý user control (view) svůj view model, nebo view model vytvoříte jen pro celou stránku a “user controls” view modely automaticky “zdědí”. To byl jen jeden příklad - úroveň zanoření view je zcela ve vaší režii. Slovo “zdědí” je v uvozovkách, protože nejde o klasické dědení DataContextu, ale k předání posledně použitého view modelu dojde i tehdy, když dvě view nemají společné “nadview”.
Poznámka: Nejprve jsem chtěl volání metod Init a LoadState na view modelech a následné nastavení DataContextu u view dát do jiného vlákna. Problém je v tomto případě s dědením view modelů. Když přiřadíte view model, na kterém jste zavolali asynchronně metodu Init, dalšímu vnořenému View, tak se může stát, že metoda Init ještě nedoběhla, view model u prvního view nastaven není, ale u vnořeného (druhého) view již nastaven je, což může vést k podivným a kvůli použití více vláken obtížně reprodukovatelným chybám. Za asynchronní zpracování časově náročných scénářů zodpovídají samotné view modely, které by měly přepisovat virtuální metodu DoInternalAsyncInit z ViewModelBase. Metodu DoInternalAsyncInit jsem popisoval v předchozím článku.
Viděli jsme, kdy hostitel volá metody Init, Activate a kdy voláním metody LoadState obnoví tranzientní stav ve view modelech. Ještě nám zbývá projít, kdy ukládáme tranzientní stav a kdy notifikujeme view modely voláním jejich metody Deactivate o tom, že k nim přidružené view (stránka) není již aktivní.
Nebudeme při ukládání tranzientního stavu nijak pátrat, jestli došlo k “tombstoningu”, ale stav view modelů do PhoneApplicationPage.State uložíme vždy, když dojde k opuštění stránky. Hlavním důsledkem tohoto rozhodnutí je, že s obsluhou metod Application_Launching, Application_Activated, Application_Deactivated a Application_Closing, kterou jsem popisoval minule, se nemusíte kvůli “tombstoningu” trápit a zmíněné metody můžete většinou zcela ignorovat.
O opuštění stránky jsm informováni v metodě OnNavigatedFrom, kterou přepíšeme. Metoda OnNavigatedFrom voláním metody SaveState uloží stav všech view modelů a poté metoda handleAllDeactivated na všech modelech podporujících rozhraní IDeactivated zavolá metodu Deactivate.
Popisovat kompletně logiku v metodě SaveState nemá smysl, protože tato metoda je reverzní k metodě LoadState. Pro všechny view rekurzivně vyhledá jejich view modely a dovolí jim uložit pod unikátním klíčem view, který je vygenerován třídou UIHelper, tranzientní stav. Ten samý tranzientní stav view modelů, jehož obnovení v metodě LoadState jsem popisoval výše.
Za komentář stojí, že podmínka if (stateManager != null && stateManager != lastDataContext) ošetřuje, abychom neukládali trazientní stav view modelu, který slouží jako DataContext u více View, opakovaně, ale vždy pouze jednou.
Tranzientní stav všech view modelů je ukládán do speciálního objektu Dictionary s názvem KnownTypesDictionary.
var statebag = new KnownTypesDictionary();
Připomínám, že při tombstoningu je možné uložit jen serializovatelné objekty. Představme si nyní, že KnownTypesDictionary je potomek třídy Dictionary<string, object>. Do takového objektu Dictionary můžeme vložit kteroukoli instanci z třídy, která je přímo či nepřimo potomkem třídy Object, ale při pokusu o serializaci dostaneme chybu, protože naše potomky DataContractSerializer nezná a očekává, že v kolekci budou jen instance třídy Object, a ne instance odvozených tříd. Pomocí atributu KnownType, který je aplikován na KnownTypesDictionary, můžeme DataContractSerializer informovat, které všechny třídy má v objektu Dictionary očekávat. Jednou z možností, jak to udělat, je předat atributu KnownType název statické metody, která seznam “známých” tříd vrací.
Dnes si ukážeme jednoduchou verzi třídy KnownTypesDictionary, která v metodě GetKnownTypes vrátí fixní seznam "známých” tříd. Příště si ukážeme, jak budou “známé” třídy dohledány a registrovány automaticky bez nutnosti vytvářet seznam známých tříd vždy znovu a “natvrdo” v každé aplikaci. V dalších dílech také zjistíme, jak právě vytvořená infrastruktura nám dovolí vytvářet WP7 aplikace příslovečným lusknutím prstu, tedy spíš nečetným lusknutím prstů o klávesnici.
.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Monday, January 3, 2011 3:46:50 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Friday, December 17, 2010
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Již v prvním dílu seriálu o vývoji WP7 aplikací jsem zmiňoval nejen to, že mobilní verze Silverlightu ve WP7 je založena na Silverlightu 3, ale také, že mobilní Silverlight má své unikátní rysy, které v žádné desktopové verzi Silverlightu nenalezneme. Jednou ze změn je životní cyklus aplikace, včetně tzv. tombstoningu. Termín ”tombstoning”, kterého se i v tomto článku budu držet, abych nemusel zavádět nějaké směšně znějící české ekvivalenty, má asi naznačovat, že WP7 podporují nejen tradiční spuštění a ukončení aplikace, ale i jakýsi hybridní stav, v němž instance naší aplikace může být dočasně ukončena (“umrtvena”) například tím, že uživatel spustí jinou aplikaci, a poté může být z hlediska uživatele WP7 telefonu naše původní aplikace navrácena k životu, a to dokonce ve stavu, v jakém ji předtím uživatel zanechal. O smysluplnosti “tombstoningu” mám své pochybnosti a raději bych ve WP7 viděl tradiční multitasking, ale vývojářův boj s chováním “zombie” aplikací ve stavu “tombstoningu” má také něco do sebe. V tomto článku bych rád posunul boj se “zombie-tombstonovanými” aplikacemi do dalšího levelu a nabídnul pár “cheatů” .
Přechody mezi stavy WP7 aplikace nám nejlépe objasní metody pro obsluhu životního cyklu aplikace, které jsou po založení projektu automaticky vygenerovány v souboru App.xaml.cs.
Metoda Application_Launching je provedena jednou po spuštění aplikace. Jde o metodu, ve které můžete načíst dříve uložená data z “isolated storage”. Tato metoda NENÍ volána při obnovení “tombstonované” aplikace.
Metoda Application_Closing je provedena jednou při ukončení aplikace. Jde o metodu, ve které typicky uložíte data do “isolated storage”. Jde o stejná data, která načtete při dalším spuštění aplikace v již popsané metodě Application_Launching a můžeme tedy říci, že jde o data s delší záruční lhůtou, která mají význam pro různé instance aplikace. Když stáhnete z webové služby nějaké číselníky (typy zákazníků, kategorie objednávek) a nechcete je po startu aplikace stahovat vždy znovu, uložíte si je a při příštím spuštění aplikace rychleji naběhne, protože nemusí stahovat všechna data z webových služeb ihned po startu.
Další metody se týkají již tombstoningu. Metoda Application_Deactivated je volána vždy, když je vaše aplikace “tombstonována” a v této metodě byste měli uložit všechna data, která potřebujete k tomu, abyste mohli po návratu z “tombstonovaného” stavu aplikaci zobrazit uživateli tak, jako kdyby běžela celou dobu a k žádnému “tombstoningu” nedošlo. Pro vývojáře ale “tombstoning” ve skutečnosti znamená, že aktuálně běžící instance aplikace je zlikvidována! Můžeme tedy říci, že v této metodě hlavně ukládáme data s kratší záruční lhůtou, která mají význam jen pro další instanci aplikace po návratu z “tombstonovaného” stavu. Data, která chcete mít k dispozici i po obnovení z “tombstoningu” můžete ukládat pod vámi zvolenými identifikátory v objektu typu IDictionary ve vlastnosti PhoneApplicationService.Current.State. Data, která zde uložíte, musí být serializovatelná, protože se nedrží jen v paměti telefonu, ale jsou ukládána i do souboru.
Je ale nutné si uvědomit, že když je aplikace “tombstonována”, nemáte garantováno, že se uživatel do vaší aplikace vrátí a že budete obnoveni z “tombstonovaného” stavu. I v této metodě je proto vhodné ukládat data, která ukládáte v metodě Application_Closing popsané výše.
Uživatel může také spustit vaší aplikaci znovu, tedy spustit novou instanci aplikace, aniž by se vrátil k dříve “"tombstonované” aplikaci a vy svůj stav - data, o nichž jsem říkal, že mají kratší záruční lhůtu - dříve uložený v metodě Application_Deactivated nikdy nepoužijete. Když se uživatel vrátí do vaší aplikace, což většinou nastane po stisknutí tlačítko “Back”, kdy se v zásobníku dříve spuštěných aplikací, který je spravován přímé operačním systémem, stane aktivním vaše aplikace, musíte obnovit dříve uložený stav v metodě Application_Activated. Napsal jsem, že jde o zásobník aplikací, ale je potřeba si uvědomit, že jde spíš o metaforu, protože aplikace a jejich stránky nejsou nikdy fyzicky uloženy, ale při “tombstoningu” většinou bez milosti zlikvidovány, a metoda Application_Activated je volána v nové instanci naší aplikace. WP7 si jen v zásobníku pamatují, jaké aplikace a v jakém pořadí byly spuštěny a která stránka v konkrétní aplikaci byla aktivní.
Dále je potřeba si osvojit tato pravidla:
- Metoda Application_Activated NENÍ volána po spuštění nové (“netombstonované”) instance aplikace. Po spuštění nové (“netombstonované”) instance aplikace je volána pouze metoda Application_Launching.
- Metoda Application_Deactivated NENÍ volána při úplném ukončení aplikace. Při úplném ukončení aplikace je volána pouze metoda Application_Closing.
Zjednodušeně bychom mohli odpovědnosti metod v souboru App.xaml.cs popsat tímto fragmentem kódu:
Měli bychom ale vědět, že k tombstoningu může dojít kdykoli a podle mých testů na emulátoru i reálném zařízení může být aplikace “tombstonována”, i když se zrovna obnovuje z předchozího “tombstonovaného” stavu a je v metodě “Application_Activated“. Autoři WP7 se s “tombstoningem” moc nepatlají a bez skrupulí po určité době zlikvidují vlákna aplikace, což poznáte podle výjimky ThreadAbortException.
Také se mi nelíbí, že bych měl v tomto jediném souboru ukládat a obnovovat data pro všechny stránky (“formuláře”) aplikace. Rozhodl jsem se, že na obsluhu těchto metod rezignuju a místo životní cyklu aplikace se budu zajímat jen o životní cyklus view modelu (presentation modelu, chcete-li) u každé stránky ve WP7 aplikaci.
Dnes si ukážeme bázovou třídu pro view modely a další podpůrné třídy se službami, která nás svou spoluprací zbaví nutnosti při psaní každé stránky myslet na to, že autoři WP7 aplikací se vyžívají v recidivě nekrofilního chování u WP7 aplikací. Drahý bratr Sigmund Freud by se po prohlídce stavového automatu “tombstonované“ aplikace od architektů WP7 na své pohovce jistě tetelil radostí nad předaným šokujícím materiálem.
Nejdříve si ale navrhneme minimální množinu vlastností, kterou by měl každý view model splňovat, abychom se již nemuseli životním cyklem WP7 aplikace příliš zabývat při návrhu každé stránky.
- Musíme být schopni nahrát data při vytvoření nového view modulu. Také bychom měli zajistit, že když dojde k “tombstoningu” aplikace ještě před získáním všech dat, nezůstane view model v nějakém nekonzistentním stavu s třetinou nahraných dat, ale po obnovení z “tombstoningu” dostane šanci nahrát data znovu.
- Budeme schopni při “tombstoningu” automaticky perzistovat dočasný stav každého view modelu. Od této chvíle začneme říkat dočasnému stavu stav tranzientní. V tomto článku se zabývám jen tranzientním stavem aplikace, o perzistování “trvanlivějších” dat do “isolated storage” se pobavíme v některém z dalších článků.
- Po obnovení aplikace z “tombstonovaného” stavu každý view model automaticky nahraje svůj tranzientní stav.
Pro uložení a nahrání tranzientního stavu nadefinujeme rozhrani ITransientStateManager.
- Měli bychom dát šanci view modelům zareagovat na to, že stránka, ke které jsou přidruženy, se stala aktivní stránkou, i na to, že k nim přidružená stránka aktivní už není, ať už proto, že došlo k “tombstoningu” nebo uživatel přešel na jinou stránku v aplikaci.
Pro tyto účely máme rozhraní IActivated a IDeactivated.
- Budeme mít sice bázovou třídu pro view modely, ale její použití si nebudeme vynucovat. View model může být v aplikaci kterákoli třída. I když tato třída nebude potomkem bázové třídy pro view modely, bude moci volitelně využít většinu služeb, které jsou popsány v přechozích bodech.
- Jedná se o view modely, měli bychom tedy na úrovní bázové třídy podporovat rozhraní INotifyPropertyChanged, které nám dovoluje notifikovat o změně hodnoty ve vlastnostech view modelu. Rozhraní INotifyPropertyChanged je ve WPF i v Silverlight aplikacích všudypřítomné, proto si navrhneme další bázovou třídu PropertyChangedBase, která nám kromě implementace rozhraní INotifyPropertyChanged přinese další užitečné služby.
Jaké další užitečné služby má třída PropertyChangedBase?
Nemusíme vyvolávat událost PropertyChanged zadáním jen zadáním názvu vlastnosti (RaisePropertyChanged(“UserName”)), což může vést k chybě za běhu aplikace, když uděláme překlep v názvu vlastnosti ((RaisePropertyChanged(“UseName”)), ale můžeme předat název vlastnosti ve formě lambda výrazu, jehož syntaxe je zkontrolována již kompilátorem. K tomu nám slouží metoda RaisePropertyChangedEvent(Expression> propertyDefinition). Potomci třídy PropertyChangedBase mohou o změně hodnoty vlastnosti informovat takto:
U potomků třídy PropertyChangedBase, kterými budou i naše view modely, si můžeme také zavoláním metody RaiseAllPropertiesChanged vynutit vyvolání události o změně hodnoty pro každou vlastnost. Další metodě s názvem RaisePropertiesChanged(params string[] properties) můžeme předat názvy vlastností, pro které má být vyvolána metoda PropertyChanged.
A teď si již můžeme vytvořit slibovanou třídu ViewModelBase, která je potomkem třídy PropertyChangedBase a podporuje všechna rozhraní zmíněná dříve.
V konstruktoru třída ViewModelBase přijímá titulek zobrazované stránky, který je uložen do vlastnosti PageTitle.
Metoda Init z rozhraní Initialize by měla být volána vždy, když je view model vytvořen, nebo když je po obnovení aplikace z “tombstonovaného” stavu zřejmé, že view model nenahrál všechna data, což se zjistí voláním virtuální metody IsAllInitDataLoaded, která v této abstraktní třídě vždy vrací true a čeká na to, až odvozené konkrétní view modely dosadí svou logiku, kdy je považován view model za nahraný. Metoda Init by měla být taky volána, když odvozený view model obnovená tranzientní data považuje za špatná / zastaralá a nemůže je používat, což nám dá najevo nastavením vlastnosti IsInvalidModel na true. K rychlému zjištění, jestli je možné view model dále používat slouží derivovaná vlastnost CanUseModel, která spojuje logiku obsaženou ve vlastnosti IsInvalidModel a metodě IsAllInitDataLoaded.
Metoda Initialize.Init
V metodě Init nejdříve nastavíme vlastnost IsInvalidModel na true, protože naše metoda Init probíhá a kdyby došlo k “tombstoningu”, nemáme všechna data a je potřeba volat metodu Init znovu. Poté jen načteme titulek aplikace do vlastnosti AppTitle, inicializujeme vlastnost SuppressValidating, která slouží k dočasnému potlačení ověřování platnosti data zadaných uživatelem ve view modelu, na hodnotu false. O vlastnosti SuppressValidating budeme více mluvit v dalším článku včetně vysvětlení významu metody ValidateData a vlastnosti HasValidData.
Dále ViewModelBase nastavením vlastnosti IsInvalidModel na false sděluje, že jeho inicializace proběhla, všechna data v modelu považuje za platná a dá šanci odvozeným třídám, aby inicializovaly svá data voláním chráněné virtuální metody DoInternalInit, která má ve ViewModelBase prázdnou implementaci a do které odvozené třídy dají svou specifickou logiku pro načtení dat. Vlastnost IsInvalidModel mohou samozřejmě odvozené třídy nastavit v metodě DoInternalInit opět na true, ale my ve vlastnosti IsInvalidModel hodnotu true nenecháváme, protože si přepsání metody DoInternalInit v odvozených třídách nevynucujeme a v bázové třídě ViewModelBase nemůžeme vědět, kdy odvozené třídy považují svá data za neplatná.
V metodě Init nakonec také voláním metody další chráněné virtuální metody s názvem DoInternalAsyncInit v samostatném vlákně šanci odvozeným třídám asynchronně inicializovat svá data. Metodu DoInternalAsyncInit by měly odvozené třídy používat k časově náročné inicializaci, kterou není vhodné vhodné dávat do synchronně volané metody DoInternalInit a blokovat tak hlavní thread aplikace.
Mohlo by vás také zaujmout, že v sekci catch má speciální zacházení výjimka ThreadAbortException – tato výjimka není propagována výše, protože ji vyvolá samotné běhové prostředí Silverlightu při násilném “tombstoningu”, jak jsem poznamenal na začátku tohoto článku.
Rozhraní IActivated s metodou Activate a rozhraní IDeactivated s metodou Deactivate ve ViewModelBase mají jen prázdnou implementaci a čekají na to, jakou logiku do nich vloží odvozené třídy. Mimochodem – na tomto místě bychom měli poprvé vytušit, že budeme potřebovat “hostitele” našich view modelů, který bude vědět, kdy volat metody Activate, Deactivate, Init a další.
Třída ViewModelBase explicitně implementuje rozhraní ITransientStateManager, které jsme si zavedli pro podporu automatického ukládání a nahrávání tranzientního stavu. Metody LoadState a SaveState ale po svém vyvolání jen předají řízení chráněným virtuálním metodám DoInternalLoadTransientState a DoInternalSaveTransientState, aby dali šanci i odvozeným třídám změnit způsob uložení a nahrání tranzientního stavu, i když odvozené třídy jsou většinou spokojeni s tím, že to za ně zvládne předek ViewModelBase.
Třída ViewModelBase ve svém statickém konstruktoru dosazuje výchozí objekt podporující rozhraní ITransientStateHelper, které je klíčové pro uložení a obnovení tranzientního stavu view modelů a na které delegují i metody DoInternalLoadTransientState a DoInternalSaveTransientState v předchozím výpisu.
Všimněte si, že metoda DoInternalLoadTransientState po obnovení tranzientního stavu zkontroluje, jestli se dá view model používat, a pokud ne, zavolá i v této fázi metodu Init.
if (!CanUseModel)
{
Init();
}
A nyní se podíváme na mocné rozhraní ITransientStateHelper
Je asi zřejmé, že metoda GetTransientState vrátí v objektu Dictionary tranzientní stav objektu, který jí byl předán v argumentu obj. Metoda RestoreTransientState naopak obnoví tranzientní stav objektu v argumentu obj hodnotami v argumentu savedState.
Metoda IsTransientStateEnabledForObject zjistí, jestli je možné z předaného objektu získat tranzientní stav. Jak uvidíme, odvozené view modely mohou odmítnout uložení tranzientního stavu a kdekoli v aplikaci můžeme jejich rozhodnutí jednoduše zjistit předáním instance view modelu této metodě.
Zde je jedna z možných realizací rozhraní IStateTransientStateHelper, kterou používá i naše třída ViewModelBase.
Odpovědnosti metod GetTransientState a RestoreTransientState jsem již popsal, nyní jen zmíním pár specialitek v kódu třídy TransientStateHelper.
Metoda GetTransientState zjistí, jestli předaný objekt má tranzientní stav tak, že zavolá metodu IsTransientStateEnabledForObject(obj).
Metoda IsTransientStateEnabledForObject kontroluje, jestli třída, ze které objekt pochází, nezakázala vydání tranzientního stavu tím, že je na ní aplikován atribut [NonTransientState].
Atribut NonTransientState
Atribut NonTransientState může být aplikován nejen na celou třídu, ale i na jednotlivé vlastnosti objektu, které nemají být součástí tranzientního stavu. Metoda GetTransientState neukládá celé view modely, ale s využitím reflexe jen hodnoty jejich vlastností. Atribut NonTransientState dovoluje vyřadit vlastnosti, které v tranzientním stavu view modelu nemají co dělat. To ale není vše – přímo metoda GetTransientState dle jmenné konvence vyřadí všechny vlastnosti, jejichž suffix je v poli IGNORE_METHOD_SUFFIX_LIST
public static readonly IEnumerable IGNORE_METHOD_SUFFIX_LIST = new[] { "Command", "Action", "Helper", "Service", "SynchContext"};
Do tranzientního stavu se tak nedostanou ve view modelech se často nacházející, ale s tranzientním stavem view modelu nic nemající a navíc většinou neserializovatelné objekty jako jsou objekty ICommand (vlastnost SelectCommand, SaveCommand), delegáti TextboxTextChangedAction, služby (ILoggerService) a další.
Metoda RestoreTransientState obnoví tranzientní stav objektu. Za zmínku stojí, že když je předaný objekt potomkem PropertyNotificationBase, tak nastavením vlastnosti notificationBase.SuppressPropertyChangedNotification na true potlačíme dočasně vyvolání události OnPropertyChanged, protože je vhodné, abychom událost nevyvolávali, když view model neobsahuje všechna data a nevíme, kdo všechno na událost reaguje a jaká další, nyní ve view modelu se nenacházející data, by chtěl načíst. Jak jsem ale psal v požadavcích na začátku článku, view model nemusí podporovat žádná rozhraní, nemusí být potomkem ViewModelBase ani PropertyNotificationBase, a proto si ani dědění z PropertyNotificationBase v této metodě nevynucujeme. Poté, co metoda RestoreTransientState obnoví hodnoty všech vlastností, má TransientStateHelper povinnost notifikovat okolí o změně hodnot všech vlastností view modelu. Jestliže je objekt s obnoveným tranzientním stavem potomkem PropertyNotificationBase, zavoláme metodu RaisePropertiesChanged(propertyNames.ToArray()), jinak se metoda RestoreTransientState pokusí dohledat opět za pomoci reflexe na objektu metodu nazvanou RaisePropertyChangedEvent, která přijímá název vlastnosti a kterou použije pro hromadnou distribuci událostí OnPropertyChanged.
Ovládací prvky obsažené (nejen) v control toolkitu jsou přecitlivělé na pořadí vyvolávání události, a proto jsou vlastnosti seřazeny nyní tak, aby vlastnosti začínající slovem Selected vyvolávaly událost OnPropertyChanged jako poslední. Máte-li ve view modelu kolekci nazvanou Orders (všechny objednávky) a SelectedOrder (vybraná objednávka z této kolekce), je zaručeno, že událost OnPropertyChanged pro vlastnost SelectedOrder bude vyvolána až po události OnPropertyChanged pro vlastnost Orders.
Prefixy vlastností, které mají vyvolávat události jako poslední, jsou v proměnné LAST_SET_VALUE_METHOD_PREFIX. Tyto vlastnosti jsou označeny příznakem LastInit z enumerace PropertyType.
public static readonly IEnumerable LAST_SET_VALUE_METHOD_PREFIX = new[] { "Selected" };
To by pro dnešek k třídě ViewModelBase a jejím pomocníkům stačilo:
Příště se podíváme hlavně na “hostitele” view modelů, který by měl být schopen:
- Volat na view modelech ve “správnou dobu” metody Init, LoadState, SaveState, Activate, Deactivate.
- Připojit view modely k view (stránce).
- Náš hostitel bude podporovat i více view modelů na jedné stránce, včetně “dědění” a sdílení použitých view modelů mezi různými view na stránce.
A také si příště vysvětlíme, proč při získání tranzientního stavu ve ViewModelbase místo objektu Dictionary používáme vlastní třídu KnownTypesDictionary. Jak možná tušíte, i název “KnownTypes” odkazuje k tomu, že mají-li být hodnoty uložené v tranzientním stavu serializovatelné, tak DataContractSerializer, používaný infrastrukturou WP7 k serializaci tranzientního stavu, musíme přesvědčit, že v objektu Dictionary jsou jen objekty z jemu “známých” tříd.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Friday, December 17, 2010 7:25:14 PM (Central Europe Standard Time, UTC+01:00)
C# | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Monday, December 6, 2010
Tipy pro Windows Phone 7 aplikace I
První slíbený článek na téma vývoje Windows Phone 7 aplikací je venku. Dnes si ukážeme většinou kód, který řeší některé od CTP se vlekoucí chyby ve WP7 SDK nebo řeší některé nedomyšlenosti a omezení mobilní verze Silverlightu. Mobilní verze Silverlightu vychází totiž ze Silverlightu 3, i když se objevují náznaky, že v lednu přijde aktualizace, která z WP7 udělá zase o něco lepší systém. Nezbývá než doufat, že sousloví “pořádná aktualizace” neoznačuje jen zvukem slavnostních fanfár doprovázené antré funkce “Copy-Paste”, což se lehce sfoukne instalací nové verze běhového prostředí, která bude vycházet ze Silverlightu 4. Dělám si legraci, i když mě samotného mrzí, že kromě několika bodů stále platí to, co jsem napsal o WP7 v nadsázce už v březnu.
V dalších článcích bych rád ukázal, jak si můžete navrhnout aplikaci, která podporuje automaticky “tombstonning”, MVVM a nepotřebuje k tomu žádné těžkotonážní frameworky ani zbytečnými funkcemi obtěžkané DI kontajnery.
Tip 1 – listbox a a jeho náhlá indispozice pri pokusu o skrolování obsahu
Když vám najednou přestane listbox na emulátoru i v zařízení skrolovat obsah, důvodem je pravděpodobně to, že jste pozměnili layout stránky a že listbox na to odpoví “neskroluji”, Jedná se o to, že když listbox je zanořen v gridu, dalším gridu a poté ještě ve stack panelu, tak nedokáže skrolovat přes všechny položky. Sice je v dokumentaci k WP7 upozornění, že layout stránky by měl být co nejjednodušší, ale netušil jsem, že autoři listboxu si to přímo vynutí. Nedám vám taxativní výčet layoutů, při kterých listbox přestane skrolovat, ale jednoduché pravidlo zní: Když listbox neskroluje, nehledám chybu ve svém kódu, ale zjednoduším layout stránky.
Listbox zanořený jen v gridu funguje bez problémů
Tip 2: Když u prvku WebBrowser použijete metodu NavigateToString, stránka zobrazuje chybně (nejen) české znaky.
I když prvek webbrowser zobrazuje stránky s různým kódováním bez problémů, metoda NavigateToString nikdy nezobrazí cizí znaky správně. Nepomáhá ani nastavení encoding na stránce. Po mnoha pokusech jsem napsal extenzní metodu, která “enkóduje” všechny non ascii znaky.
Na řetězci předávaném metodě NavigateToString stačí zavolat jen myHtmlString.EncodeUnicodeChars()
Tip3: Stále jsme u prvku WebBrowser. Prvek WebBrowser má sice metodu NavigateToString, ale mnohem lepší je připojit “nabindovat” (“načíst") z view modelů rovnou html řetězec.
I u mobilního Silverlightu lze použít “dependency” a “attached” vlastnosti, na které lze “bindovat” data z view modelů.
Poznámka: I když to zde není nutné, třída WebBrowserBehavior dědí z třídy Behavior, kterou naleznete v assembly System.Windows.Interactivity.dll. Tato assembly je většinou nainstalována ve složce c:\Program Files\Microsoft SDKs\Expression\Blend\Windows Phone\v7.0\Libraries\
K prvku webbrowser jsem přidal “attached” vlastnost NavigateText.
Poté již mohu "bindovat" ve view na vlastnost z view modelum, ale tím problémy teprve začínají.:)
Jak asi tušíte, "attached" vlastnost NavigateText je jen speciální "proxy vlastnost" k metodě NavigateToString prvku WebBrowser.
Bohužel rozmarní vývojáři WP7 přišli s podivným životním cyklem stránky, o kterém budu více mluvit v článku zaměřeném na podporu MVVM, ale už dnes nás zajímá jeden jeho důsledek a tím je, že když zavoláte metodu NavigateToString ještě před tím, než je dokončeno přidání prvku webbrowser do vizuálního stromu (visual tree) UÏ prvků, tak je vyvolána výjimka InvalidOperationException. To konkrétně pro nás znamená, že nová attached “vlastnost” NavigateText může volat metodu NavigateToString po načtení dat z view modelu, ale stále ve chvíli, kdy prvek webbrowser ještě není plně inicializován. Obešel jsem to špinavým způsobem tak, že při vyhození výjimky InvalidOperationException prvkem webbrowser zaregistruji lambda handler k události WebBrowser.Loaded, ve kterém se pokusím zavolat metodu NavigateToText znovu, protože prvek WebBrowser by měl být při vyvolání události Loaded již plně inicializován a také součástí vizuálního stromu UI prvků na stránce.
Výpis kódu upravujícího chování třídy WebBrowser:
To myslím pro dnešek stačí.
Monday, December 6, 2010 10:57:19 AM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Silverlight | WP7
Friday, February 12, 2010
Doplnění metod FillPie a DrawPie do objektu Graphics v Compact .Net Frameworku
Nedávno se na fóru vývojáře objevil dotaz, jak nahradit chybějící metodu FillPie v objektu Graphics na Compact .Net Frameworku, protože prý ani tradiční zuřivé googlování žádné výsledky nepřineslo. Zkusil jsem napsat implementaci metody FillPie, a protože se podobných dotazů na internetu dá najít více, dávám kód obohacený nyní i o metodu DrawPie na blog, aby nezůstal utopen jen v diskuzním fóru.
Compact .Net Framework sice nemá metodu FillPie ani DrawPie, ale má obecné metody DrawPolygon a FillPolygon, se kterými nakreslíte, co se vám zlíbí. Zhýrale jsem kód opět trochu zlinqovatěl, asi začínám být na LINQu a extenzních metodách závislý. Inu, jak říkáme my C# vývojáři, původně odříkané extenzní metody plný zásobník volání.
static class GraphicsExtensions
{
public static readonly float ANGLE_MULTIPLY = (float) Math.PI / 180;
public static void FillPie(this Graphics g, SolidBrush brush, int x, int y, int width, int height, float startAngle, float sweepAngle)
{
var tempPoints = GetTempPoints(sweepAngle, startAngle, x, y, width, height);
g.FillPolygon(brush, tempPoints);
}
public static void DrawPie(this Graphics g, Pen pen, int x, int y, int width, int height, float startAngle, float sweepAngle)
{
var tempPoints = GetTempPoints(sweepAngle, startAngle, x, y, width, height);
g.DrawPolygon(pen, tempPoints);
}
private static Point[] GetTempPoints(float sweepAngle, float startAngle, int x, int y, int width, int height)
{
const float HALF_FACTOR = 2f;
const int TEMP_POINT_FIRST = 0;
const int TEMP_POINT_LAST= 100;
const int TOTAL_POINTS = TEMP_POINT_LAST - TEMP_POINT_FIRST;
float angleInc = (sweepAngle - startAngle) / TOTAL_POINTS;
float halfWidth = width / HALF_FACTOR;
float halfHeight= height / HALF_FACTOR;
return (new[] {new Point
{
X = x,
Y = y
}
})
.Union(
(from i in Enumerable.Range(TEMP_POINT_FIRST, TOTAL_POINTS)
let angle = i == TEMP_POINT_LAST - 1? sweepAngle : startAngle + (i * angleInc)
select new Point
{
X = (int) (x + (Math.Cos(angle*(ANGLE_MULTIPLY))*(halfWidth))),
Y = (int) (y + (Math.Sin(angle*(ANGLE_MULTIPLY))*(halfHeight)))
})).ToArray();
}
}
Použití metod:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var redBrush = new SolidBrush(Color.Red))
using (var blueBrush = new SolidBrush(Color.Blue))
using (var greenBrush = new SolidBrush(Color.ForestGreen))
{
e.Graphics.FillPie(redBrush, Width / 2, Height / 2, Width / 2, Height / 2, 0, 35f);
e.Graphics.FillPie(blueBrush, Width / 2, Height / 2, Width / 2, Height / 2, 35f, 80f);
e.Graphics.FillPie(greenBrush, Width / 2, Height / 2, Width / 2, Height / 2, 80f, 360f);
}
using (var redPen = new Pen(Color.IndianRed))
{
e.Graphics.DrawPie(redPen, Width / 5, Height / 5, Width / 5, Height / 5, 0, 60f);
}
}
}
A zde je náhled na formulář:
Friday, February 12, 2010 1:17:54 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | LINQ
Friday, January 15, 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í:
- 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.
- 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.
- 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.
- 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, January 15, 2010 5:49:51 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Compact .Net Framework | Silverlight
Wednesday, November 18, 2009
Vynucení si překreslení celé obrazovky v Compact .Net Frameworku
Při snaze kreslit grafické objekty na celou obrazovku PDA, a ne pouze na vlastní formulář, se můžete velmi často setkat s dotazem, jak zajistím, že předchozí nakreslené dílko, rozprostřené většinou přes vícero formulářů a mimo naši přímou kontrolu, smažu před vykreslením dalšího dílka. Následující příklad je reakcí na takový dotaz. Za “celou obrazovkou” budeme v článku považovat grafický kontext vrácený voláním metody GetDC s argumentem NULL. Po celé obrazovce PDA je postupně vykreslován kruh (s velkou fantazií míč) pohybující se z levé strany displeje na pravou. Před vykreslením “míče” na další pozici musí být míč vykreslený v předcházejícím kroku smazán. Následující kód je ukázkou použití “brutální síly”, protože žádné slečinkovské, sexy ani elegantní konstrukce s překreslováním pouze části obrazovky k výsledku nevedly. Hlavní trik, ke kterému jsem dospěl po sundání bílých vývojářských rukaviček, spočívá v rekurzivním vynucení si překreslení všech oken v metodě DoWork s využitím přímého volání mnoha nativních API funkcí, které mají paradoxně v aplikacích cílených na Microsoftem macešsky spravovaný a rozvíjený Compact .Net Framework (i ve verzi. 3.5) stále privilegovanou pozici.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace RedrawScreenTest
{
partial class Form1 : Form
{
#region constants
public const int POSITION_INCREMENT = 10;
public const int MAX_POSITION = 200;
public const int MAX_THREAD_WAIT = 5000;
public const int CIRCLE_RADIUS = 30;
#endregion constants
#region delegates
public delegate void InvokeDelegate();
#endregion delegates
#region Properties
public IntPtr WindowsHDC { get; set; }
public Thread Worker { get; set; }
public bool ShouldWork { get; set; }
public bool IsWorking { get; set; }
#endregion Properties
#region Constructors
public Form1()
{
InitializeComponent();
WindowsHDC = IntPtr.Zero;
ShouldWork = false;
IsWorking = false;
}
#endregion Constructors
#region methods
public void DoWork()
{
IsWorking = true;
int x = 0;
int y = 0;
bool firstDraw = true;
while (ShouldWork)
{
WindowsHDC = ApiWrapper.GetDC(IntPtr.Zero);
if (WindowsHDC == IntPtr.Zero)
{
break;
}
if (!firstDraw)
{
Invoke((InvokeDelegate) (() =>
{
IntPtr hwnd = ApiWrapper.GetForegroundWindow();
hwnd = ApiWrapper.GetWindow(hwnd, (int)ApiWrapper.GWConstants.GW_HWNDFIRST);
while (hwnd != IntPtr.Zero)
{
ApiWrapper.RedrawWindow(hwnd, IntPtr.Zero, IntPtr.Zero,
ApiWrapper.RDW_ERASE | ApiWrapper.RDW_INVALIDATE | ApiWrapper.RDW_ALLCHILDREN);
EnumChild(hwnd, 0);
hwnd = ApiWrapper.GetWindow(hwnd, (int)ApiWrapper.GWConstants.GW_HWNDNEXT);
}
}));
}
else
{
firstDraw = false;
}
using (Graphics g = Graphics.FromHdc(WindowsHDC))
using (Brush b = new SolidBrush(Color.Yellow))
{
g.FillEllipse(b, new Rectangle(x, y, CIRCLE_RADIUS, CIRCLE_RADIUS));
}
ApiWrapper.ReleaseDC(IntPtr.Zero, WindowsHDC);
Thread.Sleep(1000);
x += POSITION_INCREMENT;
y += POSITION_INCREMENT;
if (x > MAX_POSITION || y > MAX_POSITION)
{
x = 0;
y = 0;
}
}
IsWorking = false;
}
private void EnumChild(IntPtr hwnd, int level)
{
if ((hwnd == IntPtr.Zero))
{
return;
}
IntPtr childHwnd = ApiWrapper.GetWindow(hwnd, (int)ApiWrapper.GWConstants.GW_CHILD);
while (childHwnd != IntPtr.Zero)
{
EnumChild(childHwnd, level + 1);
ApiWrapper.RedrawWindow(childHwnd, IntPtr.Zero, IntPtr.Zero,
ApiWrapper.RDW_ERASE | ApiWrapper.RDW_INVALIDATE | ApiWrapper.RDW_ALLCHILDREN);
childHwnd = ApiWrapper.GetWindow(childHwnd, (int)ApiWrapper.GWConstants.GW_HWNDNEXT);
}
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void Form1_Closing(object sender, CancelEventArgs e)
{
buttonStop_Click(this, new EventArgs());
if (WindowsHDC != IntPtr.Zero)
{
ApiWrapper.ReleaseDC(IntPtr.Zero, WindowsHDC);
}
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (IsWorking)
{
return;
}
ShouldWork = true;
Worker = new Thread(DoWork);
Worker.Start();
}
private void buttonStop_Click(object sender, EventArgs e)
{
if (!IsWorking)
{
return;
}
ShouldWork = false;
Worker.Join(MAX_THREAD_WAIT);
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
}
#endregion methods
}
}
Zde jsou potřebné deklarace API funkcí, konstant a struktur
using System;
using System.Drawing;
using System.Runtime.InteropServices;
namespace RedrawScreenTest
{
class ApiWrapper
{
[DllImport("coredll.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate,
IntPtr hrgnUpdate, uint flags);
public const int RDW_INVALIDATE = 0x0001;
const int RDW_INTERNALPAINT = 0x0002;
public const int RDW_ERASE = 0x0004;
const int RDW_VALIDATE = 0x0008;
const int RDW_NOINTERNALPAINT = 0x0010;
const int RDW_NOERASE = 0x0020;
const int RDW_NOCHILDREN = 0x0040;
public const int RDW_ALLCHILDREN = 0x0080;
const int RDW_UPDATENOW = 0x0100;
const int RDW_ERASENOW = 0x0200;
const int RDW_FRAME = 0x0400;
const int RDW_NOFRAME = 0x0800;
[DllImport("coredll.dll", SetLastError = true)]
public static extern IntPtr GetWindow(IntPtr hWnd, int uCmd);
public enum GWConstants
{
GW_HWNDFIRST = 0,
GW_HWNDLAST = 1,
GW_HWNDNEXT = 2,
GW_HWNDPREV = 3,
GW_OWNER = 4,
GW_CHILD = 5,
GW_ENABLEDPOPUP = 6
}
[DllImport("coredll.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("coredll.dll", EntryPoint = "GetWindowDC")]
public static extern IntPtr GetDC(IntPtr ptr);
[DllImport("coredll.dll", EntryPoint = "ReleaseDC")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);
[DllImport("coredll.dll", EntryPoint = "GetDesktopWindow")]
public static extern IntPtr GetDesktopWindow();
[DllImport("coredll.dll", EntryPoint = "UpdateWindow")]
public static extern bool UpdateWindow(IntPtr hWnd);
[DllImport("coredll.dll", EntryPoint = "UpdateWindow")]
public static extern int SendMessage(IntPtr hWnd, uint msg, int wparam, int lparam);
[DllImport("coredll.dll")]
public extern static void InvalidateRect(IntPtr handle, Rectangle dummy, bool erase);
[DllImport("coredll.dll", EntryPoint = "FindWindowW", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}
}
Wednesday, November 18, 2009 3:02:51 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework
Monday, July 27, 2009
Odchytnutí zprávy WM_KEYDOWN v dialogu – Windows Mobile
V jednom předchozím článku jsem slíbil, že na blog dám i kód, který umožní ve Windows dialogu zachytit všechny stisknuté klávesy. Jak možná víte, dialog ve Windows je běžné okno (Window) s třídou (class) WC_DIALOG. K dialogu je přiřazena speciální funkce WNDPROC, která zajišťuje výchozí zpracování zpráv zaslaných formuláři (např. přechod mezi prvky dialogu pomocí klávesy TAB) a volá vývojářem aplikace určenou obslužnou funkci dialogu (DLGPROC). Jedním z nepříjemných důsledků tohoto modelu chování pro dialogy je, že nejsme schopni v DLGPROC odchytit a zpracovat zprávu o stisknutí tlačítka na klávesnici (WM_KEYDOWN).
Nechceme-li reimplementovat všechny vychytávky dialogů v našem vlastním “okně” (Window) a současně chceme i v dialogu odchytit zprávu WM_KEYDOWN, musíme výchozí WNDPROC obslužnou funkci při vytváření dialogu nahradit naší vlastní “proxy” WNDPROC funkcí.
BOOL CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_INITDIALOG)
{
ChangeDialogWndProc(hWnd);
}
return true;
}
void ChangeDialogWndProc(HWND hwnd)
{
g_oldDlgdProc = (WNDPROC)GetWindowLong(hwnd, GWL_WNDPROC);
SetWindowLong(hwnd, GWL_WNDPROC, (LONG)&DlgWindowsProc);
}
V naší obslužné proceduře dialogu DlgProc při inicializaci dialogu (zpráva WM_INITDIALOG) voláme funkci ChangeDialogWndProc, která zaregistruje naší “proxy” WINDPROC funkci pomocí API SetWindowLong. Ještě předtím si uložíme do proměnné g_oldDlgdProc pointer na předchozí WNDPROC funkci, která je návratovou hodnotou API funkce GetWindowLong, když jí ve druhém argumentu předáme konstantu GWL_WNDPROC.
V naší “proxy” funkci WNDPROC odchytneme všechny potřebné zprávy a když chceme zachovat výchozí chování dialogu, předáme zprávu ke zpracování v předchozím kroku uložené výchozí Windows proceduře pro dialogy.
RESULT CALLBACK DlgWindowsProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_KEYDOWN)
{
//Do something with key
int key = (int) wParam;
}
return g_oldDlgdProc(hWnd, message, wParam, lParam);
}
Následuje jednoduchý příklad založený na standardní šabloně WM projektu.
// HookDialog.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include "HookDialog.h"
#define MAX_LOADSTRING 100
// Global Variables:
HINSTANCE g_hInst; // current instance
HWND g_hWndMenuBar; // menu bar handle
WNDPROC g_oldDlgdProc;
// Forward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE, LPTSTR);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ChangeDialogWndProc(HWND hwnd);
BOOL CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK DlgWindowsProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
// Perform application initialization:
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
DialogBox(g_hInst, MAKEINTRESOURCE(IDD_POCKETPC_PORTRAIT), NULL, &DlgProc);
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
// COMMENTS:
//
ATOM MyRegisterClass(HINSTANCE hInstance, LPTSTR szWindowClass)
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HOOKDIALOG));
wc.hCursor = 0;
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = szWindowClass;
return RegisterClass(&wc);
}
//
// FUNCTION: InitInstance(HINSTANCE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
TCHAR szTitle[MAX_LOADSTRING]; // title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // main window class name
g_hInst = hInstance; // Store instance handle in our global variable
// SHInitExtraControls should be called once during your application's initialization to initialize any
// of the device specific controls such as CAPEDIT and SIPPREF.
SHInitExtraControls();
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_HOOKDIALOG, szWindowClass, MAX_LOADSTRING);
//If it is already running, then focus on the window, and exit
hWnd = FindWindow(szWindowClass, szTitle);
if (hWnd)
{
// set focus to foremost child window
// The "| 0x00000001" is used to bring any owned windows to the foreground and
// activate them.
SetForegroundWindow((HWND)((ULONG) hWnd | 0x00000001));
return 0;
}
if (!MyRegisterClass(hInstance, szWindowClass))
{
return FALSE;
}
hWnd = CreateWindow(szWindowClass, szTitle, WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_OK:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_CREATE:
SHMENUBARINFO mbi;
memset(&mbi, 0, sizeof(SHMENUBARINFO));
mbi.cbSize = sizeof(SHMENUBARINFO);
mbi.hwndParent = hWnd;
mbi.nToolBarId = IDR_MENU;
mbi.hInstRes = g_hInst;
if (!SHCreateMenuBar(&mbi))
{
g_hWndMenuBar = NULL;
}
else
{
g_hWndMenuBar = mbi.hwndMB;
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
CommandBar_Destroy(g_hWndMenuBar);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
BOOL CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_INITDIALOG)
{
ChangeDialogWndProc(hWnd);
}
return true;
}
LRESULT CALLBACK DlgWindowsProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_KEYDOWN)
{
//Do something with key
int key = (int) wParam;
}
return g_oldDlgdProc(hWnd, message, wParam, lParam);
}
void ChangeDialogWndProc(HWND hwnd)
{
g_oldDlgdProc = (WNDPROC)GetWindowLong(hwnd, GWL_WNDPROC);
SetWindowLong(hwnd, GWL_WNDPROC, (LONG)&DlgWindowsProc);
}
Monday, July 27, 2009 11:29:07 AM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Nativní kód
Friday, April 24, 2009
Drobná poznámka ke kontravariancí delegátů v C#
Předpokládám, že se stejně jako já těšíte na lepší podporu kovariance a kontravariance u rozhraní a delegátů v připravované verzi C# 4.0. Už dnes se ale dá s existující podporou kovariance a kontravariance u delegátů pěkně kouzlit – pro ty s exaktnějším přístupem ke kódu a vytříbenou terminologií se slovo “kouzlit” v knihách zásadně překládá jako “psát elegantnější kód”. Opakovat základy kovariance a kontravariance u delegátů zde nebudu a všechny ty, kteří sem zabloudili při svém ahashverovském “googlování” nějakého problému volně souvisejícího s probíraným tématem, odkážu na článek v MSDN.
Kovarianci i kontravarianci delegátů používám rád, ale dnes se mi podařilo narazit na potíž, o které si nejsem jistý, že je všeobecně známa. Alespoň já jsem se po chvíli údivu a narůstajícího rozčilení nad tím, že můj dokonalý kód nechce vybíravý kompilátor přijmout a neustále protestuje, musel zbaběle uchýlit ke specifikaci C# 3.0.
Takže zde je popis “problému”.
Tento kód asi nikoho nepřekvapí
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> del = Test;
}
public static void Test(Base p)
{
Console.WriteLine(p);
}
}
Máme generického delegáta MyAction, jehož instanci s názvem del vytvoříme v metodě Main. Za generický parametr T dosadíme generický argument typu “Derived” , přičemž delegát ukazuje na metodu Test, která přijímá argument Typu Base. Kontravariance zajistí, že tento kód bez problémů projde.
Zkusme udělat mírnou úpravu. Místo metody Test přiřadíme do delegáta del lambda výraz, u kterého explicitně určíme typ argumentu.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ContravarianceTest
{
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> a = (Base b) => Console.WriteLine(b) ;
}
}
}
Kompilátor se tentokrát razantně ohradí proti podobné manipulaci.
Cannot convert lambda expression to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs
A v další chybě své výhrady upřesní.
Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived' C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs
Shrneme-li to, je zřejmé, že při použití lambda výrazu kompilátor na nějakou kontravarianci argumentů zapomene a vyžaduje, aby typ argumentu v lambda výrazu byl identický s typem argumentu u delegáta. Ještě podotknu, že stejné chování s projeví i u negenerického delegáta - public delegate void MyAction(Derived d);.
Použijeme-li anonymní metodu, kompilátor stále protestuje jako u lambda výrazů.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ContravarianceTest
{
public delegate void MyAction<T>(T t);
public class Base
{
}
public class Derived : Base
{
}
class Program
{
static void Main(string[] args)
{
MyAction<Derived> a = delegate(Base b)
{
Console.WriteLine(b);
};
}
}
}
Cannot convert anonymous method to delegate type 'ContravarianceTest.MyAction<ContravarianceTest.Derived>' because the parameter types do not match the delegate parameter types C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs 27 35
Parameter '1' is declared as type 'ContravarianceTest.Base' but should be 'ContravarianceTest.Derived' C:\Documents and Settings\STEIN\Dokumenty\CovarianceTest\Program.cs 27 44
U anonymních metod a lambda výrazů tedy kontravarianci nehledejme. Ve specifikacci C# 3.0 (C# 3.0 specification – sekce 7.14.1) nalezneme popis omezení pro anonymní metody a lambda výrazy.
“If an anonymous function has an explicit-anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have the same parameter types and modifiers in the same order. In contrast to method group conversions (§6.6), contra-variance of anonymous function parameter types is not supported. [zvýraznil R.S.] If an anonymous function does not have an anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have no out parameters.”
Většina programátorů v C# asi nikdy na podobné omezení nenarazí, a když už ano, vezmou jako fakt, “že to asi z nějakého důvodu nejde”. Mně přijde podobné chování neintutivní a vsadil bych se, že vetšina vývojářů, ať už vědomě či podvědomě, na C# oceňuje, že jde o jazyk, ve kterém na ně nečíhá mnoho záludností nebo nepříjemných překvapení. S přidáváním dalších a dalších rysů do jazyka vzrůstá pravděpodobnost, že některá nová vlastnost začne ovlivňovat způsob použití starší vlastnosti v jazyce a také vzrůstá počet rozdílů mezi konstrukcemi, které na první pohled vypadají stejně, nebo alespoň od nich poučenější vývojář, ačkoli si je vědom některých rozdílů například mezi delegátem ukazujícím na tradiční funkci, anonymní metodu, lamda výraz a složenou lambdu (lambda statement), očekává podobné chování.
Celou poznámku bychom mohli uzavřít dotazem: “Kolik nových vlastností programovací jazyk snese bez šrámů a posléze pořádných zářezů na pověsti “jednoduchého” jazyka? Tipnul bych si, že empiricky si to budeme moci ověřit, až se začnou na fórech množit zoufalí vývojáři naříkající nad složitostí jazyka, jako se to děje dnes, když nějaká lama poté, co zbastlila při svých hrátkách “skorofunkčnípůlaplikaci” ve VB, Javě či C#, je nucena programovat v C++”? Jde samozřejmě o hyperbolu, vždyť vím, že budoucnost na krásném IT úhoru je otevřena právě pro všechny ty pilné dělníky nové éry, kteří ochotně vygooglují různé nesouvislé fragmenty kódu, jež považují za společný komunitní majetek k instantnímu užití, dále zkombinují několik z nebe spadlých frameworků dohromady a jsou patřičně hrdi na to, že po dlouhé praxi znají alespoň přibližný význam poloviny klíčových slov v C# či Javě. Tedy pro ty, kterým třeba kontravariance v programovacím jazyce nikdy chybět nebude.
Friday, April 24, 2009 11:57:02 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework
Tuesday, April 14, 2009
Windows Mobile formulář přes celý displej - v nativním kódu
Na MSDN fórech jsem si všiml, že se vícekrát objevil dotaz, jak v nativním kódu vytvořit okno přes celou obrazovku, které se bude chovat jako formulář v Compact .Net Framework aplikaci při nastavení vlastnosti WindowState = Maximized.
API SHFullScreen sice přepne okno do celoobrazovkového režimu, ale při zobrazení SIPu se opět objeví taskbar. Při skrytí SIPu se okno vrátí do celoobrazovkového režimu. První, co mě napadlo, je skrýt samotný taskbar. Idea dobrá, mohli jsme mít jednoduché řešení, ale autoři Windows Mobile jako již tradičně řekli ne.
Po několika pokusech jsem zjistil, že jediné použitelné řešení představuje změna pozice a velikosti formuláře vždy, když odchytím zprávu WINDOWPOSCHANGED. To celé je korunováno opakovaným voláním SHFullScreen. Na řešení v Compact .Net Frameworku jsem se nedíval, abych si nekazil radost z vyřešeného úkolu, takže netuším, zda autoři CNF používají ještě nějaký další trik.
Níže naleznete příklad, který je založen na výchozí šabloně Smart Device Windows API projektu. Zajímavé části jsou zvýrazněny tučně. Tento postup lze samozřejmě jednoduše použít ve WTL nebo MFC.
Tady se ještě zeptám:
1) Jsou alespoń pro někoho z Vás tyto tipy/FAQ zajímavé? Já sám miluji přecházení mezi nativním a “managed” kódem, ale asi nemá smysl, abych tyto tipy psal na blog, jestliže o nativní kód (již) nikdo nestojí.Potom by stačílo, abych je nechal utopeny ve fóru o mobilních zařízeních, kde poslouží podobným individuím jako jsem já. V zásobě mám například často kladený dotaz, jak ve Windows Mobile dialogu zachytit WM_KEY zprávy. I když sám si stále programování v (Compact) .Net frameworku bez dobré znalosti nativního kódu nedovedu představit - což je v roce 2009 možná tristní a nečekaná zpráva.
2) A obecnější dotaz – zajímají někoho z vás tipy pro Windows Mobile/Compact .Net Framework? Pro mě, jak asi tušíte, je programování pro WIN Mobile zařízení potěšení, a proto se podobné tipy objevují i na blogu, který píšu hlavně pro zábavu. I když většinu času jsem nyní strávil vývojem v Silverlightu, WPF, WCF a léčením roztomilých neštovic na zpočátku krásné tváři Linq2SQL, což znamená, že se na blogu se objeví i další témata, která se budou točit kolem návrhu různých typů aplikací a jako bonus odhalíme nejčastější průšvihy spojené s anemickými modely (i model-view-viewmodely )aplikací.
// FullScreen.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include "FullScreen.h"
#define MAX_LOADSTRING 100
// Global Variables:
HINSTANCE g_hInst; // current instance
HWND g_hWndMenuBar; // menu bar handle
RECT usedRect;
// Forward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE, LPTSTR);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
void MakeFullScreen();
void MakeFullScreen(HWND hWnd)
{
SetRect(&usedRect, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
LONG windowWidth = usedRect.right - usedRect.left;
LONG windowHeight = usedRect.bottom - usedRect.top;
MoveWindow(hWnd,
usedRect.left,
usedRect.top,
windowWidth,
windowHeight,
FALSE);
//SIPINFO info;
//info.cbSize = sizeof(info);
//ZeroMemory(&info, sizeof(info));
//info.rcVisibleDesktop = usedRect;
//
//if (!::SipSetInfo(&info))
//{
// int error = GetLastError();
// return FALSE;
//}
SHFullScreen(hWnd, SHFS_HIDETASKBAR | SHFS_HIDESTARTICON);
}
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
// Perform application initialization:
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable;
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FULLSCREEN));
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
// COMMENTS:
//
ATOM MyRegisterClass(HINSTANCE hInstance, LPTSTR szWindowClass)
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_FULLSCREEN));
wc.hCursor = 0;
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = szWindowClass;
return RegisterClass(&wc);
}
//
// FUNCTION: InitInstance(HINSTANCE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
TCHAR szTitle[MAX_LOADSTRING]; // title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // main window class name
g_hInst = hInstance; // Store instance handle in our global variable
// SHInitExtraControls should be called once during your application's initialization to initialize any
// of the device specific controls such as CAPEDIT and SIPPREF.
SHInitExtraControls();
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_FULLSCREEN, szWindowClass, MAX_LOADSTRING);
//If it is already running, then focus on the window, and exit
hWnd = FindWindow(szWindowClass, szTitle);
if (hWnd)
{
// set focus to foremost child window
// The "| 0x00000001" is used to bring any owned windows to the foreground and
// activate them.
SetForegroundWindow((HWND)((ULONG) hWnd | 0x00000001));
return 0;
}
if (!MyRegisterClass(hInstance, szWindowClass))
{
return FALSE;
}
hWnd = CreateWindow(szWindowClass, szTitle, WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
// When the main window is created using CW_USEDEFAULT the height of the menubar (if one
// is created is not taken into account). So we resize the window after creating it
// if a menubar is present
if (g_hWndMenuBar)
{
RECT rc;
RECT rcMenuBar;
GetWindowRect(hWnd, &rc);
GetWindowRect(g_hWndMenuBar, &rcMenuBar);
rc.bottom -= (rcMenuBar.bottom - rcMenuBar.top);
MoveWindow(hWnd, rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top, FALSE);
}
ShowWindow(hWnd, nCmdShow);
//MakeFullScreen(hWnd);
UpdateWindow(hWnd);
return TRUE;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
LPWINDOWPOS pos;
static SHACTIVATEINFO s_sai;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_HELP_ABOUT:
DialogBox(g_hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, About);
break;
case IDM_OK:
SendMessage (hWnd, WM_CLOSE, 0, 0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_CREATE:
SHMENUBARINFO mbi;
memset(&mbi, 0, sizeof(SHMENUBARINFO));
mbi.cbSize = sizeof(SHMENUBARINFO);
mbi.hwndParent = hWnd;
mbi.nToolBarId = IDR_MENU;
mbi.hInstRes = g_hInst;
if (!SHCreateMenuBar(&mbi))
{
g_hWndMenuBar = NULL;
}
else
{
g_hWndMenuBar = mbi.hwndMB;
}
// Initialize the shell activate info structure
memset(&s_sai, 0, sizeof (s_sai));
s_sai.cbSize = sizeof (s_sai);
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
CommandBar_Destroy(g_hWndMenuBar);
PostQuitMessage(0);
break;
case WM_WINDOWPOSCHANGED:
pos = (LPWINDOWPOS) lParam;
if ((pos->cx != usedRect.right - usedRect.left) ||
(pos->cy != usedRect.bottom - usedRect.top))
{
MakeFullScreen(hWnd);
}
break;
case WM_ACTIVATE:
// Notify shell of our activate message
SHHandleWMActivate(hWnd, wParam, lParam, &s_sai, FALSE);
break;
case WM_SETTINGCHANGE:
SHHandleWMSettingChange(hWnd, wParam, lParam, &s_sai);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
{
// Create a Done button and size it.
SHINITDLGINFO shidi;
shidi.dwMask = SHIDIM_FLAGS;
shidi.dwFlags = SHIDIF_DONEBUTTON | SHIDIF_SIPDOWN | SHIDIF_SIZEDLGFULLSCREEN | SHIDIF_EMPTYMENU;
shidi.hDlg = hDlg;
SHInitDialog(&shidi);
}
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK)
{
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break;
case WM_CLOSE:
EndDialog(hDlg, message);
return TRUE;
}
return (INT_PTR)FALSE;
}
Tuesday, April 14, 2009 3:04:43 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Nativní kód
Tuesday, March 3, 2009
LINQ a logování na příkladu logování kroků Dijsktrova algoritmu
Na LINQu je pěkné, jak jednoduše můžeme LINQ výraz upravit nebo jej bezbolestně rozšířit o další části. Nedávno jsem publikoval článek Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů a nyní si ukážeme drobnou úpravu v kódu, která způsobí, že se před každým rekurzivním voláním vždy vypíšou i prozatímní výsledky hledání nejkratší cesty.
Abychom mohli zalogovat výsledek, vytvoříme si vlastní extenzní metody pro výpis informací z předaného libovolného generického IEnumerable<T> do konzole.
static class MiscExtensions
{
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector, string beginString, string endString)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if(logDataSelector == null)
{
throw new ArgumentNullException("logDataSelector");
}
return innerLogToConsole(source, logDataSelector, beginString, endString);
}
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector)
{
return LogToConsole(source, logDataSelector, null, null);
}
public static IEnumerable<T>LogToConsole<T>(this IEnumerable<T> source)
{
return LogToConsole(source, (obj => obj.ToString()), null, null);
}
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, string beginString, string endString)
{
return LogToConsole(source, (obj => obj.ToString()), beginString, endString);
}
private static IEnumerable<T> innerLogToConsole<T>(IEnumerable<T> source, Func<T, String> selector, string beginString, string endString)
{
if (beginString != null)
{
Console.WriteLine(beginString);
}
foreach (var obj in source)
{
String val = selector(obj);
Console.WriteLine(val);
yield return obj;
}
if (endString != null)
{
Console.WriteLine(endString);
}
}
}
Metod pro logování máme více, abychom nemuseli pokaždé předat všechny argumenty. Prvním argumentem je vždy zdrojová sekvence, o jejíchž prvcích budou logovány informace. Argument logDataSelector nese odkaz na funkci, která umí z objektu ve zdrojové sekvenci získat jeho textovou reprezentaci. Jestliže delegát logDataSelector není předán, je k získání textové reprezentace objektu použita metoda ToString() zdrojového objektu. Další nepovinné argumenty beginString a endString jsou řetězce, které má extenzní funkce zapsat do konzole předtím, než jsou vypsána data o prvním objektu v zdrojové sekvenci (beginString), a po zalogování všech objektů v sekvenci (endString). V našem případě argumenty beginString a endString použijeme k vypsání řetězců, které ohraničí jednolivé kroky algoritmu. Naše extenzní funkce je “neinvazivní”, což znamená, že nefiltruje ani nekonvertuje objekty ve zdrojové sekvenci, ale po vypsání informace o zdrojovém objektu je nezměněný objekt příkazem yield return předán k dalšímu zpracování. Předchozí věta obsahuje varování, že nechcete-li se dočkat nepříjemných překvapení, delegát předaný v argumentu logDataSelector by neměl žádným způsobem měnit data zdrojového objektu, ale pouze je pasivně číst.
Celý algoritmus i s podrobným popisem už zde nebudu opakovat, vložím sem jen pro nás zajímavou rekurzivní metodu getShortestPathInner. Podpora logování je jednoduchou úpravou, protože pouze na námi vybraném neuralgickém místě v LINQ výrazu, které chceme špehovat, zavoláme naši extenzní funkci LogToConsole. Pro lepší orientaci je přidaný kód v následujícím výpisu zvýrazněn tučným červeným písmem.
private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
where A1 : IGraphEdge<A0>
{
var candidates = (from node in edges
where !processed.Contains(node.From)
select node.From).Distinct();
if (candidates.Count() == 0)
{
return initialGraphPath;
}
var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);
var minimumGPath = (from gPath in initialGraphPath
where candidates.Contains(gPath.Current) &&
gPath.TotalDistance == minimum
select gPath).First();
var newGraphPath = from cNode in edges
where cNode.From.Equals(minimumGPath.Current)
select new GraphPath<A0>
{
Current = cNode.To,
Previous = minimumGPath.Current,
TotalDistance = cNode.Distance + minimumGPath.TotalDistance
};
var newGraphResult =
(initialGraphPath.Concat(newGraphPath).Where(obj =>
!initialGraphPath.Any(
obj2 => obj2.Current.Equals(obj.Current) &&
(obj2.TotalDistance < obj.TotalDistance))))
.LogToConsole(obj => String.Format("{0} - {1} - {2}",
obj.Previous, obj.Current, obj.TotalDistance),"--Další kolo algoritmu--", "--Konec kola--")
.ToArray();
var newProcessed = processed.Union(new[] { minimumGPath.Current });
return getShortestPathInner(newGraphResult, newProcessed, edges);
}
}
A zde je ukázka, jak vypadá výstup.
Logovat nemusíte jen do konzole, ale můžete si přidat další extenzní metody, které zohlední vaše speciální nároky, kam a jak se mají informace o objektech v sekvenci logovat. Cílem článku bylo jen ukázat, jak bezbolestné a hlavně elegantní je přidání logování do stávajících LINQ výrazů.
Tuesday, March 3, 2009 4:34:35 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Monday, March 2, 2009
Náhrada ParametrizedThreadStart delegáta v Compact .Net Frameworku
Na fórech o Compact .Net Frameworku (CNF) se často objevují stesky, že v CNF třída Thread nemá konstruktor, který by přijímal delegáta ParametrizedThreadStart. Metodě, na kterou ukazuje delegát ParametrizedThreadStart a která bude spuštěna v novém threadu, můžeme předat jeden argument typu object .
public delegate void ParametrizedThreadStartDelegate(Object obj);
Ty nářky jsou liché, protože můžeme předat do konstruktoru odkaz na instanční metodu bez argumentů ve vlastním objektu, který má ve svých proměnných nebo vlastnostech stavové informace, které použije instanční metoda poté, co je zavolána z metody Start threadu.
Pomocí anonymních metod či lambda výrazů se ale zbavíme nutnosti deklarovat vlastní třídu. Lambda výraz funguje jako adaptér, který převede metodu s jedním argumentem na metodu bez argumentů, kterou očekává konstruktor třídy Thread.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
int myArg = 10;
Thread myThread = new Thread(() => MyThreadMethodWithArgument(myArg));
myThread.Start();
}
void MyThreadMethodWithArgument(Object obj)
{
Console.WriteLine(obj.ToString());
}
}
Jestliže chcete použít syntaxi velmi podobnou použití delegáta ParametrizedThreadStartDelegate ve “velkém” .Net Frameworku, můžete si napsat vlastní třídu ParametrizedThreadStart, která umožňuje konverzi na delegáta ThreadStartDelegate a tedy opět funguje jako adaptér, který můžeme bez problémů předat do konstruktoru třídy Thread.
using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace ParametrizedThread
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
int myArg = 10;
Thread myThread = new Thread(new ParameterizedThreadStart(MyThreadMethodWithArgument, myArg));
myThread.Start();
}
void MyThreadMethodWithArgument(Object obj)
{
Console.WriteLine(obj.ToString());
}
}
public delegate void ParametrizedThreadStartDelegate(Object obj);
public class ParameterizedThreadStart
{
private ParametrizedThreadStartDelegate InnerDelegate { get; set; }
private object Param { get; set; }
public ParameterizedThreadStart (ParametrizedThreadStartDelegate del, object param)
{
InnerDelegate = del;
Param = param;
}
public static implicit operator ThreadStart(ParameterizedThreadStart instance)
{
return (() => instance.InnerDelegate(instance.Param));
}
}
}
Třída ParameterizedThreadStart vyžaduje, abyste do konstruktoru předali argument pro delegáta. Jestliže do konstruktoru argument ihned předat nechcete, ale chcete ve třídě Thread předat argument pro delegáta přetížené metodě Start, tak jako je tomu opět v NF, nezbývá než se na CNF uchýlit k extenzním metodám.
using System;
using System.Collections;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace ParametrizedThread
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Thread myThread = new Thread(new ParameterizedThreadStart(MyThreadMethodWithArgument));
myThread.Start();
}
void MyThreadMethodWithArgument(Object obj)
{
Console.WriteLine(obj.ToString());
}
}
public delegate void ParametrizedThreadStartDelegate(Object obj);
public class ParameterizedThreadStart
{
public EventHandler<EventArgs> MyEvent;
private ParametrizedThreadStartDelegate InnerDelegate { get; set; }
private object Param { get; set; }
private bool IsParamSetInConstructor { get; set; }
public ParameterizedThreadStart(ParametrizedThreadStartDelegate del, object param)
{
InnerDelegate = del;
Param = param;
IsParamSetInConstructor = true;
}
public ParameterizedThreadStart(ParametrizedThreadStartDelegate del)
: this(del, null)
{
IsParamSetInConstructor = false;
}
public static implicit operator ThreadStart(ParameterizedThreadStart instance)
{
return (() =>
{
ThreadExtensions.SetThreadData();
var delArg = instance.IsParamSetInConstructor
? instance.Param
: Thread.GetData(Thread.GetNamedDataSlot(ThreadExtensions.THREAD_DATA));
instance.InnerDelegate(delArg);
});
}
}
public static class ThreadExtensions
{
public const string THREAD_DATA = "MethodData";
private static Hashtable _threadDatahashTable = Hashtable.Synchronized(new Hashtable());
public static void Start (this Thread thread, object val)
{
if (thread == null)
{
throw new ArgumentNullException("thread");
}
_threadDatahashTable[thread.ManagedThreadId] = val;
thread.Start();
}
internal static void SetThreadData()
{
object val = null;
val = _threadDatahashTable[Thread.CurrentThread.ManagedThreadId];
_threadDatahashTable.Remove(Thread.CurrentThread.ManagedThreadId);
Thread.SetData(Thread.GetNamedDataSlot(THREAD_DATA), val);
}
}
}
Operátor ThreadStart ve třídě ParametrizedThreadStartDelegate vrací složitější lambda výraz, ve kterém dojde k rozhodnutí, zda bude metodě, na kterou ukazuje InnerDelegate předán argument z konstruktoru, nebo argument, který byl předán extenzní metodě Start. Data specifická pro thread jsou v metodě SetThreadData vyzvednuta z objektu Hashtable a uložena v pojmenovaných datových slotech threadu. Extenzní metoda Start používá pro účely tohoto příkladu objekt Hashtable, protože pro Hashtable je narozdíl od generické třídy Dictionary možné rychle získat její threadově bezpečnou (tedy z větší části threadově bezpečnou ) verzi - Hashtable.Synchronized(new Hashtable()); a náš delegát ParametrizedThreadStartDelegate, přijímající typ object, si stejně na typovou bezpečnost moc nepotrpí. Tyto nevýhody by vás měly přesvědčit, že nejlepší, přímočaré a hacků prosté řešení jsem zmínil na začátku – vytvořte svoji vlastní třídu s instanční metodou a typovými vlastnostmi, které ponesou stavové informace. Další možností může být vytvoření vlastního wrapperu nad nativními API CreateThread a CreateFiber. Chcete-li ale v CNF použít ve třídě Thread delegáta ParametrizedThreadStartDelegate ve stylu NF, znáte nyní více způsobů, jak to provést.
Monday, March 2, 2009 1:48:05 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework
Wednesday, February 11, 2009
Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů
Pokusil jsem se napsat Dijsktrův algoritmus pomocí LINQ konstrukcí. Pokud někdo z vás tápe, k čemu je Dijsktrův algoritmus dobrý a k čemu slouží, odkážu jej na podrobný článek na Wikipedii. Zde jen připomenu, že Dijsktrův algoritmus slouží k nalezení nejkratších cest v grafu z jednoho zdrojového uzlu ke všem ostatním uzlům. Je tak možné najít například nejkratší cestu z jednoho města do druhého. Na internetu jsem našel jeden graf, který budeme mít stále před očima a na který budeme Dijkstrův algoritmus napsaný v LINQu aplikovat.
Uzly A, B atd. reprezentují města, ohodnocení hrany vyjadřuje počet kilometrů (město A je od města B vzdáleno 5 km). My budeme chtít spočítat nejkratší cestu z města H do města A a nebude nás zajímat jen počet ujetých kilometrů, ale také jak vypadá celá trasa - jinými slovy, přes jaká města naše nejkratší cesta vede.
Nejprve si vytvoříme potřebné třídy:
public interface IGraphEdge<T>
{
T From { get; }
T To { get; }
int Distance { get; }
}
Rozhraní IGraphEdge reprezentuje hranu grafu - vlastnost From nám říká, odkud hrana vede (z města A), vlastnost To sděluje, kam hrana vede (město B). Vlastnost Distance nese vzdálenost mezi uzly (města A a B jsou vzdálena 5 km).
My pracujeme s vzdálenostmi mezi městy, a proto si vytvoříme jednoduchou třídu reprezentující město. Z města nás zajímá jen název.
class City : IEquatable<City>
{
private Guid Id;
public City(string name)
{
Id = Guid.NewGuid();
Name = name;
}
public string Name
{
get;
set;
}
public override bool Equals(object obj)
{
City secondCity = obj as City;
if (secondCity == null)
{
return false;
}
return Equals(secondCity);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public bool Equals(City other)
{
if (other == null)
{
return false;
}
if (other.GetType() != this.GetType())
{
return false;
}
return (other.Id == Id);
}
public override string ToString()
{
return Name;
}
}
Hrany grafu představují silnice mezi městy, a proto si vytvoříme třídu CityEdge, která implemetuje rozhraní IGraphEdge a za generický parametr T dosadí třídu City.
class CityEdge : IGraphEdge<City>
{
#region Implementation of IGraphNode<City>
private City m_from;
private City m_to;
private int m_distance;
public CityEdge(City from, City to, int distance)
{
if (from == null)
{
throw new ArgumentNullException("from");
}
if (to == null)
{
throw new ArgumentNullException("to");
}
if (distance <= 0)
{
throw new ArgumentException("value must be greater than zero", "distance");
}
m_from = from;
m_distance = distance;
m_to = to;
}
public City From
{
get
{
return m_from;
}
}
public City To
{
get
{
return m_to;
}
}
public int Distance
{
get
{
return m_distance;
}
}
#endregion
public override string ToString()
{
return String.Format("From: {0}, To {1}, Distance{2}", From, To, Distance);
}
}
Dále potřebujeme třídu, která ponese informaci o nalezené nejkratší cestě do daného bodu a o předchozím městu, přes které musíme cestovat. Na konkrétním příkladu - z města A (námi zvolený jediné výchozí město, z nějž se počítá nejkratší cesta ke všem ostatním městům) vede nejkratší cesta do města C (vlastnost Current), která má délku 7 Km (vlastnost TotalDistance) a současně nám vlastnost Previous vrátí přechozí město (vlastnost Previous), přes které musíme jet (město B).
public class GraphPath<T> : IEquatable<GraphPath<T>>
{
private const int DUMMY_HASH_PLACEHOLDER = 100;
public T Current
{
get;
set;
}
public T Previous
{
get;
set;
}
public int TotalDistance
{
get;
set;
}
public override bool Equals(object obj)
{
GraphPath<T> second = obj as GraphPath<T>;
if (second == null)
{
return false;
}
return Equals(second);
}
public override int GetHashCode()
{
var prevHash = (EqualityComparer<T>.Default.Equals(Previous, default(T)) ? DUMMY_HASH_PLACEHOLDER : Previous.GetHashCode());
return (Current.GetHashCode() ^ prevHash ^ TotalDistance.GetHashCode());
}
#region Implementation of IEquatable<T>
public bool Equals(GraphPath<T> other)
{
if (other == null)
{
return false;
}
if (other.GetType() != this.GetType())
{
return false;
}
return (EqualityComparer<T>.Default.Equals(Current, other.Current) &&
EqualityComparer<T>.Default.Equals(Previous, other.Previous) &&
TotalDistance.Equals(other.TotalDistance)
);
}
#endregion
}
Přípravu máme za sebou, nyní se můžeme podívat na kostru celého algoritmu a poté rozpitvat jeho jednotlivé kroky. Pamatujme - důsledně se snažíme používat, kde to jen jde, LINQ konstrukce.
static void Main(string[] args)
{
var cities = (from i in Enumerable.Range(0, 8)
let key = ((char)(i + 'A')).ToString()
select new
{
Key = key,
Value = new City(key)
}).ToDictionary(el => el.Key, el => el.Value);
var cityNodes = new List<CityEdge>
{
new CityEdge(cities["A"], cities["B"], 5),
new CityEdge(cities["A"], cities["F"], 3),
new CityEdge(cities["B"], cities["C"], 2),
new CityEdge(cities["B"], cities["G"], 3),
new CityEdge(cities["C"], cities["H"], 10),
new CityEdge(cities["C"], cities["D"], 6),
new CityEdge(cities["D"], cities["E"], 3),
new CityEdge(cities["E"], cities["H"], 5),
new CityEdge(cities["E"], cities["F"], 8),
new CityEdge(cities["F"], cities["G"], 7),
new CityEdge(cities["G"], cities["H"], 2),
};
var allCityNodes = cityNodes.Concat(from cn in cityNodes
select new CityEdge(cn.To, cn.From, cn.Distance)).ToArray();
var resultPath = getShortestPath(cities["H"], allCityNodes);
var pathFromTo = resultPath.EnumerateShortestPathTo(cities["H"], cities["A"]).Reverse();
Array.ForEach(pathFromTo.ToArray(), myP =>
Console.WriteLine("Přes město {0}, Ujetá vzdálenost: {1}", myP.Current, myP.TotalDistance));
Console.ReadLine();
}
Na začátku metody vygenerujeme objekt Dictionary (proměnná cities), kde klíčem je název města a hodnotou objekt City. Do proměnné cityNodes uložíme hrany grafu (existující silnice mezi městy) s jejich ohodnocenim (kilometry). Silnice ale nevede jen z města A do města B, ale také z města B do A, proto do proměnné allCityNodes uložíme i zpáteční cesty mezi městy. Poté voláme metodu getShortestPath, které předáme výchozí město (zde H), pro které chceme spočítat nejkratší cesty ke všem ostatním městům, a veškeré hrany-silnice. Metoda getShortestPat vrátí nejkratší cesty, nově vytvořené extenzní metodě EnumerateShortestPathTo řekneme, že chceme vypsat nejkratší cestu z města H (první argument) do města A (druhý argument) - metoda vrátí pouze města, přes která musíme jet z města H do města A a my metodou Reverse otočíme jejich pořadí, abychom viděli cestu od H do A a nezačínali koncovým městem A. Výsledek vypíšeme s využitím metody Array.ForEach na konzoli.
Skeleton algoritmu je hotov, čas začít nabalovat extenzní maso (doslova... ) a LINQ svaly.
Zde je metoda getShortestPath.
private static IEnumerable<GraphPath<A0>> getShortestPath<A0, A1>(A0 startPoint, IEnumerable<A1> nodes)
where A1 : IGraphEdge<A0>
{
var initialGraphPath = (from cNode in nodes
where !cNode.From.Equals(startPoint)
select new GraphPath<A0>
{
Current = cNode.From,
Previous = default(A0),
TotalDistance = int.MaxValue
}).Distinct();
initialGraphPath = (initialGraphPath.Concat(from cNode in nodes
where cNode.From.Equals(startPoint)
select new GraphPath<A0>
{
Current = cNode.To,
Previous = startPoint,
TotalDistance = cNode.Distance
}));
return getShortestPathInner(initialGraphPath, new[] { startPoint }, nodes);
}
V metodě getShortestPath vygenerujeme objekty GraphPath, které po všech výpočtech ponesou nejkratší cestu v grafu. První dotaz vybere nejdříve ze seznamu hran pouze ty hrany, v nichž vlastnost From (Odkud) nepředstavuje počáteční město (where !cNode.From.Equals(startPoint) ). Jediné, co víme, je že vlastnost TotalDistance nového objektu GraphPath po ukončení algoritmu ponese informaci o nejkratší cestě z výchozího města do konkrétního města (vlastnost Current). U těchto uzlů tedy nejkratší vzdálenost neznáme, a proto vlastnost TotalDistance inicializujeme konstantou int.MaxValue, která říká "nejkratší cestu do města Current neznám a ještě ke všemu může být hodně dlouhá" . Z jednoho města může vést více silnic, což by vedlo k duplicitním objektům GraphPath, a proto pro každé město ponecháme pouze jeden objekt GraphPath voláním metody Distinct.
K objektům GraphPath z prvního dotazu připojíme objekty GraphPath komplementárním dotazem, který vybere pouze ty hrany, v nichž vlastnost From (Odkud) představuje počáteční město (where !cNode.From.Equals(startPoint) ). Každý objekt GraphPath z druhého dotazu má tedy vlastnost TotalDistance rovnu kilometrům z fixně stanoveného počátečního města (vlastnost Previous - v našem příkladu město H) do města (Vlastnost Current), k němuž vede z H silnice přímo. Pro počáteční město H tedy druhý dotaz vrátí dva objekty GraphPath naplněné takto - {1 - Current=C, Previous=H, TotalDistance=10} {2-Current=E, Previous=H, TotalDistance=5}.
Poté zavoláme metodu getShortestPathInner, které předáme vygenerované objekty GraphPath, seznam objektů, pro něž jsme zjišťovali nejkratší cestu v grafu (zatím jen počáteční město - město H), a dříve vytvořený seznam hran.
private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
where A1 : IGraphEdge<A0>
{
var candidates = (from node in edges
where !processed.Contains(node.From)
select node.From).Distinct();
if (candidates.Count() == 0)
{
return initialGraphPath;
}
var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);
var minimumGPath = (from gPath in initialGraphPath
where candidates.Contains(gPath.Current) &&
gPath.TotalDistance == minimum
select gPath).First();
var newGraphPath = from cNode in edges
where cNode.From.Equals(minimumGPath.Current)
select new GraphPath<A0>
{
Current = cNode.To,
Previous = minimumGPath.Current,
TotalDistance = cNode.Distance + minimumGPath.TotalDistance
};
var newGraphResult =
(initialGraphPath.Concat(newGraphPath).Where(obj =>
!initialGraphPath.Any(
obj2 => obj2.Current.Equals(obj.Current) &&
(obj2.TotalDistance < obj.TotalDistance)))).ToArray();
var newProcessed = processed.Union(new[] { minimumGPath.Current });
return getShortestPathInner(newGraphResult, newProcessed, edges);
}
}
V rekurzivní metodě getShortestPathInner je soustředěno jádro Dijkstrova algoritmu. Do proměnné candidates uložíme nejprve veškerá města, která jsme ještě nezpracovali - nezjišťovali jsme, jak daleko je to od nich k jejich přímým sousedům. Do proměnné minimumGPath uložíme objekt GraphPath, který reprezentuje prozatímní nejkratší zjištěnou cestu z výchozího města a jehož vlastnost Current nese město, pro něž jsme výpočet nejkratší cesty ještě neprovedli.
Do proměnné newGraphPath uložíme množinu objektů GraphPath, které jsou sestaveny tak, že jejich předchůdcem (vlastnost Previous) je vždy právě zpracovávané město. Vlastnost To nese město, do něhož se můžeme dostat z města Previous. Vlastnost TotalDistance je inicializována součtem hodnoty TotalDistance z procházeného objektu minimumGPath (s prozatímní nejkratší cestou) s vzdáleností z města v Previous do města ve vlastnosti Current.
Do proměnné newGraphPathResult jsou uloženy jen prozatím nejkratší cesty - jestliže tedy v předchozím kroku vypočítáme, že z města H se do města D můžeme dostat po ujetí 16 km, ale v proměnné initialGraphPath máme spočítáno, že do města H se můžeme dostat i po ujetí 8 km, je ponechána pouze lepší, tedy pro naše účely kratší cesta.
Poté na konci metody do seznamu již zpracovaných měst přidáme právě zpracované město (minimumGPath.Current) a jdeme na další kolo. Rekurzivně zavoláme metodu getShortestPathInner s vypočítanými mezivýsledky. Rekurze skončí po zpracování všech měst - nejsou nalezeni další kandidáti na zpracování (viz podmínka if (candidates.Count() == 0 )).
Ještě nám zbývá extenzní metoda, která enumeruje přes města z výchozího bodu do cílového. První argument jsou vypočítané nejkratší cesty (objekty GraphPath), argument start je zvolené počáteční město a argument end je koncové město, ke kterému chceme vypsat nejkratší cestu.
static class DijkstraExtensions
{
public static IEnumerable<A0> EnumerateShortestPathTo<A0, A1>(this IEnumerable<A0> paths, A1 start, A1 end)
where A0 : GraphPath<A1>
{
var pathDictionary = paths.ToDictionary(obj => obj.Current);
var currentNode = pathDictionary[end];
while (currentNode != null)
{
yield return currentNode;
if (currentNode.Previous.Equals(start))
{
yield break;
}
currentNode = pathDictionary[currentNode.Previous];
}
}
}
Metoda si vytvoří další objekt Dictionary a postupně vrací veškerá objekty GraphPath na ceste z jednoho města (start) do druhého (end) tak, že v cyklu while z objektu Dictionary vyzvedává město-předchůdce (vlastnost Previous), přes které musíme jet, dokud se nedostaneme do výchozího města.
A zde je výsledek - nejkratší cesta z H do A.
Použité algoritmy, LINQ konstrukce a extenzní metody by šlo určitě vylepšit a vytunit. Nemyslím si a nikdy jsem si narozdíl od některých rozjuchaných "VsechnoSLinqemJeTedCoolTyRetarde" MSDN blogů nemyslel, že LINQ a extenzní metody jsou univerzální kladivo na každý problém universa, včetně precizní analýzy finální odpovědi '42' , ale jako příklad, co lze s těmito nástroji dělat, mi Dijkstrův algoritmus přišel zajímavý.
Update 12. 2.:
Petr poptával v komentářích zdrojové kódy. Zde jsou.
Wednesday, February 11, 2009 7:13:14 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Wednesday, February 4, 2009
Textbox nepodporující výběr textu a další specialitky
Někdy se hodí mít textbox, u kterého je skrytý "caret" (netuší někdo, jak se termín caret překládá - pouze kurzor?) a současně nepodporuje označování textu. Také můžete chtít, aby se textbox choval podobně jako při nastavení vlastnosti ReadOnly na true, ale bez "zašedlého" zobrazení textboxu, což je většinou nechtěný průvodní jev textových polí označených pouze pro čtení.
Kód je pro Compact .Net Framework, nic vám ale nebrání přenést jej na "velký" NF. Ke zpracování Windows zpráv je použita třída NativeWindow, která je součástí OpenNetCF frameworku. Opravdu jen tato podivná sekvence zpracování Windows zpráv byla v CNF ta pravá.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using OpenNETCF.Windows.Forms;
namespace HideCaret
{
public class NativeTxtWrapper : NativeWindow
{
[DllImport("CoreDll.dll")]
private static extern bool ShowCaret(IntPtr hWnd);
[DllImport("CoreDll.dll")]
private static extern bool HideCaret(IntPtr hWnd);
private TextBox m_txtBox;
private bool m_hasFullFocus;
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_KEYDOWN = 0x0100;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_CHAR = 0x0102;
private const int WM_COMMAND = 0x0111;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_PAINT = 0x000F;
private const int WM_KILLFOCUS = 0x0008;
private const int WM_SETFOCUS = 0x0007;
public NativeTxtWrapper(TextBox txtBox)
{
init(txtBox);
m_hasFullFocus = false;
}
private void init(TextBox txtBox)
{
if (txtBox == null)
{
throw new ArgumentNullException("txtBox");
}
if (txtBox.Handle != IntPtr.Zero)
{
AssignHandle(txtBox.Handle);
}
txtBox.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
txtBox.HandleDestroyed += ((sender, e) => ReleaseHandle());
m_txtBox = txtBox;
}
protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
{
if (m.Msg == WM_CHAR)
{
m_hasFullFocus = true;
}
if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_LBUTTONDBLCLK)
{
return;
}
if (((m.Msg == WM_LBUTTONUP) ||
(m.Msg == WM_LBUTTONDOWN)) && m_hasFullFocus)
{
return;
}
if (m.Msg == WM_SETFOCUS)
{
base.WndProc(ref m);
HideCaret(m_txtBox.Handle);
return;
//m_hasFocus = false;
}
if (m.Msg == WM_KILLFOCUS)
{
m_hasFullFocus = false;
}
base.WndProc(ref m);
}
}
}
Jestliže chcete textbox pouze pro čtení, stačí nepředat ke zpracování bázové třídě (base.WndProc) zprávu WM_CHAR.
Objektem NativeTxtWrapper lze oddekorovat jakýkoli textbox, nebo můžete vytvořit potomka třídy Textbox, který před klienty třídy skryje použití objektu NativeTextWrapper
private void Form1_Load(object sender, EventArgs e)
{
m_wrapper = new NativeTxtWrapper(textBox1);
textBox1.Focus();
}
Podobnou třídu mám i pro nativní projekty psané v MFC, možná se někomu z vás bude hodit. Přepsat kód do Windows API z MFC je také trivální.
#pragma once
#include "afxwin.h"
class CTextBoxEx :
public CEdit
{
private:
bool m_hasFullFocus;
public:
CTextBoxEx(void);
~CTextBoxEx(void);
virtual LRESULT WindowProc(UINT message, WPARAM wparam, LPARAM lparam);
};
#include "StdAfx.h"
#include "TextBoxEx.h"
CTextBoxEx::CTextBoxEx(void) : m_hasFullFocus(false)
{
}
CTextBoxEx::~CTextBoxEx(void)
{
}
LRESULT CTextBoxEx::WindowProc(UINT message, WPARAM wparam, LPARAM lparam)
{
//Readonly textbox
if (message == WM_CHAR)
{
return 1;
}
//end Readonly textbox
POINT mousePoint;
GetCursorPos(&mousePoint);
ScreenToClient(&mousePoint);
RECT clientRect;
GetClientRect(&clientRect);
BOOL isMouseInRect = PtInRect(&clientRect, mousePoint);
if (((message == WM_MOUSEMOVE) || (message == WM_LBUTTONDBLCLK)) && isMouseInRect)
{
return 1;
}
if (((message == WM_LBUTTONUP) ||
(message== WM_LBUTTONDOWN)) && m_hasFullFocus && isMouseInRect)
{
return 1;
}
if (message == WM_SETFOCUS)
{
m_hasFullFocus = true;
HideCaret();
return 1;
}
if (message == WM_KILLFOCUS)
{
m_hasFullFocus = false;
}
return CEdit::WindowProc(message, wparam, lparam);
}
Wednesday, February 4, 2009 4:52:26 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Nativní kód | Windows Forms
Monday, February 2, 2009
Lehká imitace některých rysů windows forms aplikací v non-windows forms aplikacích
Omluvte prosím trochu kryptický název, ale lepší a hlavně výstižnější pojmenování článku mě nenapadlo. Název je stejně jen vábnička na čtenáře, proto se podívejme, co je jím míněno.
Již několikrát mně různí vývojáři tvrdili, jak nepříjemná je pro ně práce s konzolí (windows službou, dosaďte další typy aplikací dle libosti...), protože musí řešit, aby aplikace po svém spuštění ihned neskončila, a také je pro ně problematické zajistit, aby byly některé události zpracovány vždy ve stejném threadu.
Převedeme-li emocionální stížnost do věcného jazyka, zjistíme, že to, co v těchto typech aplikací chybí, jsou následující rysy běžné windows forms aplikace:
- Windows aplikace spustí smyčku Windows zpráv (message loop) a vývojář pouze obsluhuje události formuláře. V (Compact) .Net Frameworku nám stačí zavolat Application.Run(new Form1()) a aplikace neukončí svůj běh, dokud není uzavřen poslední formulář nebo dokud ti drsnější z nás nezavolají Application.Exit. O životní cyklus aplikace, její spuštění a ukončení, se většinou nemusíme nijak starat.
- Při obsluze formuláře máme po volání metody Invoke garantováno, že předaný delegát bude vykonán v takzvaném UI threadu. Hlavním účelem metody Invoke (a sesterských metod BeginInvoke a EndInvoke) je threadově bezpečná komunikace s ovládacími prvky. Ovládací prvky ve stylu windows prvků v konzolových aplikacích (windows službách) nenajdeme, ale přesto bychom i v těchto typech aplikací občas chtěli mít nástroj, který garantuje, že všechny nebo vybrané události budou zpracovány v jednom výkonném threadu.
V tomto článku se objeví návrh, který pro non-windows forms aplikace přinese výše zmíněné rysy a přidá pár věcí navíc.
Pár vysvětlujících poznámek na úvod . Kód (přesněji řečeno draft k dalšímu rozpracování), který za chvíli uvidíte, má běžet na .Net frameworku a na Compact .Net Frameworku. Vím, že existují synchronizační kontexty pro thready, ale metodu Invoke jsem zmiňoval proto, že představuje společný jmenovatel pro obě prostředí, protože Compact .Net Framework je stále tím strýčkem - beznadějným sociálním případem, co nám nikdy nepřiveze žádné úhledně zabalené dárky, v nichž se skrývá třeba nádherná vlastnost SynchronizationContext.Current. Se znalostí tohoto omezení je také jasné, proč jsem nepoužil i další metody/vlastnosti dostupné jen ve "velkém" .Net Frameworku.
Dále v kódu jsou třídy obsahující ve svém názvu slovo *Console*. Nenechte se zmást, že mluvím dále jen o konzolových aplikacích, stejné třídy lze použít ve windows službě a dalších typech aplikací.
Zaveďme si nejdříve abstraktní třídu ConsoleTask, která je předkem všech zpracovávaných úloh v aplikaci. Zjednodušeně si můžeme třídu ConsoleTask a její potomky představit jako výchozí stavební prvky zapouzdřující chování analogické k vybraným a pro nás zajímavým rysům windows formulářů.
/// <summary>
/// Základní rozhraní pro položky zpracovávané v jedné frontě
/// </summary>
internal interface IExecuteWorkItem
{
/// <summary>
/// Implementace metody spustí úlohu
/// </summary>
void Execute();
}
/// <summary>
/// Bázová třída pro všechny úlohy
/// </summary>
abstract class ConsoleTask : IDisposable
{
#region Inner classes
/// <summary>
/// Výchozí implementace rozhraní <see cref="IExecuteWorkItem"/>
/// </summary>
private class WorkThreadItem : IExecuteWorkItem
{
#region Private variables
private Delegate m_del;
private object[] m_vals;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát, který má být spuštěn ve frontě nadřazeného objektu <see cref="ConsoleTask"/></param>
/// <param name="vals">Argumenty delegáta</param>
public WorkThreadItem(Delegate del, params object[] vals)
{
if (del == null)
{
throw new ArgumentNullException("del");
}
m_del = del;
m_vals = vals;
}
/// <summary>
/// Metoda iniciuje vykonání předaného delegáta
/// </summary>
public virtual void Execute()
{
m_del.Method.Invoke(m_del.Target, m_vals);
}
}
#endregion Inner classes
#region private variables
private ManualResetEvent m_event;
private Thread m_innerWorkingThread;
private Queue<IExecuteWorkItem> m_workQueue;
private AutoResetEvent m_workingThreadEvent;
private object m_lockQueueRoot;
private bool m_continue;
private bool m_disposed;
#endregion private variables
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
protected ConsoleTask()
{
m_lockQueueRoot = new object();
m_workQueue = new Queue<IExecuteWorkItem>();
m_event = new ManualResetEvent(false);
m_workingThreadEvent = new AutoResetEvent(false);
m_innerWorkingThread = new Thread(processWorkerThread);
m_continue = true;
m_disposed = false;
}
#endregion constructors
#region Properties
/// <summary>
///<see cref="WaitHandle"/> běžící úlohy
/// </summary>
public WaitHandle TaskWaitHandle
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
return m_event;
}
}
/// <summary>
/// Metoda vrátí true, jestliže volající thread je odlišný od threadu, který vyřizuje položky zpracovávané v jedné frontě
/// </summary>
public virtual bool InvokeRequired
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
return SlaveWorkingTask.InvokeRequired;
}
return (Thread.CurrentThread.ManagedThreadId != m_innerWorkingThread.ManagedThreadId);
}
}
/// <summary>
/// Volitelná instance <see cref="ConsoleTask"/>, která převezme odpovědnost za vyřizování položek zpracovávaných v jedné frontě
/// </summary>
public ConsoleTask SlaveWorkingTask
{
get;
set;
}
#endregion Properties
#region Methods
/// <summary>
/// Metoda garantuje, že dojde k vykonání předaného delegáta v threadu, ktrerý vyřizuje položky zpracovávané v jedné frontě
/// </summary>
/// <remarks>Metoda pouze zařadí položky ke zpracování a nečeká na výsledek volání delegáta. </remarks>
public virtual void Invoke(Delegate del, params object[] vals)
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
SlaveWorkingTask.Invoke(del, vals);
return;
}
lock (m_lockQueueRoot)
{
m_workQueue.Enqueue(new WorkThreadItem(del, vals));
m_workingThreadEvent.Set();
}
}
/// <summary>
/// Metoda spustí úlohu
/// </summary>
/// <remarks>Spuštěním úlohy se rozumí spuštění kódu v přepsané metodě <see cref="DoInternalRun"/> v samostatném threadu. Metoda Run nevrátí řízení, dokud není úloha dokončena.</remarks>
public void Run()
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
m_innerWorkingThread.Start();
ThreadPool.QueueUserWorkItem(obj => DoInternalRun());
m_event.WaitOne();
}
/// <summary>
/// Metoda ukončí úlohu
/// </summary>
public virtual void CloseTask()
{
m_continue = false;
m_workingThreadEvent.Set();
m_event.Set();
}
/// <summary>
/// Metoda pro explicitní uvolnění veškerých nepoužívaných zdrojů - součást implementace "Disposable" vzoru
/// </summary>
public void Dispose()
{
if (m_disposed)
{
return;
}
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// "Destruktor" - součást implementace "Disposable" vzoru
/// </summary>
~ConsoleTask()
{
Dispose(false);
}
/// <summary>
/// Interní implementace "Disposable" vzoru
/// </summary>
/// <param name="disposing">true - jestliže je metoda volána z metody Dispose, false, pokud je volána z destruktoru - metody Finalize</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
((IDisposable)m_workingThreadEvent).Dispose();
((IDisposable)m_event).Dispose();
m_disposed = true;
}
catch (Exception e)
{
Trace.WriteLine(e);
}
}
}
/// <summary>
/// Metoda, která musí být přepsána v odvozených třídách a která obsahuje logiku specifickou pro každou úlohu
/// </summary>
protected abstract void DoInternalRun();
/// <summary>
/// Obsluha fronty položek, které mají být zpracovány ve stejném threadu
/// </summary>
private void processWorkerThread()
{
const int EXPECTED_MINIMUM_ITEMS = 1;
while (m_continue)
{
m_workingThreadEvent.WaitOne();
if (!m_continue)
{
continue;
}
int m_count = EXPECTED_MINIMUM_ITEMS;
IExecuteWorkItem nextItem = null;
while (m_count > 0)
{
lock (m_lockQueueRoot)
{
m_count = m_workQueue.Count();
if (m_count != 0)
{
nextItem = m_workQueue.Dequeue();
}
}
try
{
if (nextItem != null)
{
nextItem.Execute();
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
m_count--;
}
}
}
#endregion Methods
}
Abstraktní třída ConsoleTask obsahuje ve svém veřejném rozhraní metodu Run, kterou spustíme úlohu. Metoda Run je šablonovou metodou (Template method), protože obsahuje závazný scénář pro veškeré odvozené úlohy. Potomci třídy ConsoleTask do scénáře vstupují na přesně vymezeném místě - v metodě DoInternalRun, která je deklarována jako abstraktní a všechny konkrétní odvozené třídy ji musí přepsat a doplnit vlastní logiku. Třída ConsoleTask tedy garantuje, že je vždy nejprve spuštěn thread vyřizující požadavky, které mají být vykonány ve stejném threadu (podrobný popis viz níže), poté je třída ThreadPool použita ke spuštění kódu v metodě DoInternalRun v jiném threadu a nakonec aktuální thread pozastavíme čekáním na signalizaci instance synchronizačního objektu ManualResetEvent (proměnná m_event). Ve vlastnosti TaskWaitHandle vydáváme stejný objekt ManualResetEvent, který může jiný thread využít k synchronizaci svého běhu s instancí třídy odvozené od třídy ConsoleTask. Tím simulujeme pro uživatele objektů odvozených z třídy ConsoleTask spuštění smyčky zpráv, protože aplikace není ukončena po zavolání metody Run. Za ukončení běhu úlohy zodpovídá metoda CloseTask - metoda uvolní pracovní thread vyřizující frontu požadavků nastavením proměnné m_continue na false a signalizací synchronizační primitivy workingThreadEvent. Dále metoda CloseTask přes signalizaci synchronizačního objektu v proměnné m_event informuje o dokončení celé úlohy - thread pozastavený v metodě Run bude uvolněn.
Třída ConsoleTask dále obsahuje definici privátní třídy WorkThreadItem, která implementuje rozhraní IExecuteWorkItem a má roli adaptéru. Instance třídy WorkThreadItem jsou jednotlivé položky, které mají být vykonány v jednom pracovním threadu. Adaptérem je třída WorkThreadItem proto, že převádí rozhraní jakéhokoli předaného delegáta na rozhraní IExecuteWorkItem. Po volání metody Execute objektu WorkThreadItem je vykonána metoda, na kterou ukazuje delegát.
Jméno vlastnosti InvokeRequired by mělo znít povědomě - metoda vrátí true, jestliže thread, který zjišťuje hodnotu vlastnosti , je odlišný od threadu, který zpracovává položky typu IExecuteWorkItem. Thread poté může použít metodu Invoke, která zajistí, že předaný delegát bude vykonán v pracovním threadu. Je to zmíněno i v dokumentaci metody Invoke, ale zde zdůrazním, že metoda Invoke zařadí pouze novou položku do fronty ke zpracování a dá signál pracovnímu threadu, že je dostupná další položka voláním metody Set na proměnné m_workingThreadEvent, což je instance synchronizační primitivy AutoResetEvent. Metoda Invoke nečeká na výsledek volání delegáta a ani není zaručeno, že po návratu z metody Invoke byl již předaný delegát vykonán. Samotná obsluha fronty položek, které mají být vykonány v jednom threadu, je soustředěna do metody processWorkerThread.
U metody Invoke a vlastnosti InvokeRequired si můžete všimnout podmíněné delegace volání na instanci ve vlastnosti SlaveWorkingTask. Jestliže vlastnost SlaveWorkingTask není null, je odpovědnost za zpracování položek přenesena na jinou instanci třídy ConsoleTask. Jednotlivé tasky mohou tvořit zárodečný řetězec odpovědnosti (Chain of responsibility) a za chvíli uvidíme, k čemu můžeme toto předávání odpovědnosti na jiné instance ConsoleTask využít.
Třída ConsoleTask také implementuje běžný .Net "Disposable" vzor pro uvolňování prostředků (rozhraní IDisposable, chráněná metoda Dispose a destruktor - metoda Finalize).
Mimikry konzolové aplikace, která se v rámci námi vykolíkovaného seznamu požadavků snaží vydávat za windows forms aplikaci, vylepšíme zavedením jednoduché fasády (vzor facade), která bude simulovat metodu Application.Run.
/// <summary>
/// Facade s rozhraním pro spuštění úkolu
/// </summary>
class ConsoleApplication
{
/// <summary>
/// Metoda spustí předaný úkol (Fasáda ke spuštění úloh napodobující známou metodu Application.Run z Windows Forms aplikací)
/// </summary>
/// <param name="task">Úkol, který má být spuštěn</param>
public static void Run(ConsoleTask task)
{
if (task == null)
{
throw new ArgumentNullException("task");
}
task.Run();
}
}
Jak vidno, metoda Run zcela deleguje vykonání na předaný ConsoleTask.
Jak se prozatím s naším modelem pracuje? Nejlepší bude napsat si potomka třídy ConsoleTask a zjistit to. Zkusme vytvořit úlohu, která na Compact .Net Frameworku zpracuje příchozí SMS.
/// <summary>
/// Třída pro zpracování přijatých SMS
/// </summary>
class SMSTask : ConsoleTask
{
#region private variables
private MessageInterceptor m_interceptor;
#endregion private variables
#region Methods
/// <summary>
/// Metoda začne sledovat SMS
/// </summary>
protected override void DoInternalRun()
{
m_interceptor = new MessageInterceptor(InterceptionAction.Notify, false);
ThreadPool.QueueUserWorkItem(
(obj) =>
m_interceptor.MessageReceived += m_interceptor_MessageReceived);
}
/// <summary>
/// Obslužná metoda uálosti <see cref="MessageInterceptor.MessageReceived"/>
/// </summary>
/// <param name="sender">Odesílatel události</param>
/// <param name="e">Argument události</param>
private void m_interceptor_MessageReceived(object sender, MessageInterceptorEventArgs e)
{
if (InvokeRequired)
{
Invoke((Action<SmsMessage>)(handleMessage), e.Message as SmsMessage);
}
else
{
handleMessage(e.Message
as SmsMessage); }
}
public override void CloseTask()
{
m_interceptor.Dispose();
base.CloseTask();
}
/// <summary>
/// Zpracování SMS
/// </summary>
/// <param name="message">SMS zpráva ke zpracování</param>
private void handleMessage(SmsMessage message)
{
Console.WriteLine(message.Body);
CloseTask();
}
#endregion methods
}
Autor tříd odvozených z bázové třídy ConsoleTask má lehkou práci, protože se soustředí jen na úkol (příjem SMS) a ne na to, že jeho kód bude vykonán v konzolové aplikaci. V přepsané metodě si přihlásíme odběr události MessageReceived - zde je událost přihlášena přes ThreadPool, ale není to nutné. Obslužná metoda události MessageReceived (m_interceptor_MessageReceived) po příjmu SMS zaručí, že SMS budou vždy zpracovány ve stejném pracovním vlákně použitím vlastnosti Invoke Required a Invoke. Jestliže je událost vyvolána v jiném než pracovním threadu obsluhujícím frontu položek ke zpracování, zavoláme metodu Invoke, které předáme delegáta ukazujícího na metodu handleMessage. K vytvoření delegáta jsme použili standardního generického delegáta Action<T>, kde jsme za generický parametr T dosadili třídu SmsMessage, jejíž instanci přijímá jako argument metoda handleMessage. Přepsali jsme i metodu CloseTask, která uvolní interceptora pro příjem zpráv a poté vyvolá implementaci metody CloseTask z bázové třídy. Zde je úloha ukončena po příjmu první zprávy voláním CloseTask z metody handleMessage, ale způsob ukončení úlohy je zcela v rukou vývojáře konkrétní úlohy.
Poznámka na okraj: U naší třídy SMSTask by bylo vhodné, když chceme přijmout jen jednu SMS, ihned si odhlásit odběr dalších zpráv, nebo si v interní proměnné nastavit, že již zpráva byla přijata a další zprávy nepředávat ke zpracování.
Novou úlohu spustíme tímto nezáludným a pro vývojáře windows forms aplikací povědomým kódem:
class Program
{
static void Main(string[] args)
{
SMSTask smsTask = new SMSTask();
ConsoleApplication.Run(smsTask);
}
}
Na vývojářské práci je nejlepší, že poté, co máte nějaký nosný nápad, můžete jej rozvíjet ad libitum. Co když chceme ve stejné aplikaci nejen přijímat SMS, ale také reagovat na události v objektu, který nás informuje o spuštěných aplikacích uživatele. Nebo chceme sledovat přes třídu SystemState informace o příchozích hovorech? Napráskat vše do jedné instance potomka třídy ConsoleTask "ResimVzdyckyVsechnoNaJednomMisteAJsemTotalneVPohodeVoe" je sice řešením, ale i jen laxním zastáncům vágně formulovaného principu jedné odpovědnosti třídy (zdravím Aleši :) ) se právě teď nasucho aktivoval podmíněný reflex, protože vědí, že při správě takové aplikace po kolegovi-pohodářovi je vztekem podmíněné zoufalecké uslintávání a hlasité nadávání to nejmenší.
Chceme určitě zachovat stávající strukturu aplikace, chceme spouštět libovolné množství různorodých úloh a navíc chceme mít možnost zpracovávat položky napříč jednotlivými úlohami ve stejném threadu - pracovní frontě. Úkol jako stvořený pro jednu z možných nenásilných inkarnací návrhového vzoru Composite v aplikaci.
/// <summary>
/// Třída reprezentující kompozitní úlohu - viz návrhový vzor Composite
/// </summary>
class CompositeTask : ConsoleTask
{
#region private variables
private ICollection<ConsoleTask> m_tasks;
#endregion private variables
#region Constructors
public CompositeTask(ICollection<ConsoleTask> tasks)
{
if(tasks == null)
{
throw new ArgumentNullException("tasks");
}
if (tasks.Count == 0)
{
throw new ArgumentException("One or more tasks are required");
}
m_tasks = tasks;
}
#endregion Constructors
#region Methods
/// <summary>
/// Spuštění všech úkolů
/// </summary>
protected override void DoInternalRun()
{
foreach (var task in m_tasks)
{
ConsoleTask task1 = task;
task1.SlaveWorkingTask = this;
ThreadPool.QueueUserWorkItem((obj) => task1.Run());
}
}
/// <summary>
/// Metoda ukončí všechny úkoly
/// </summary>
/// <remarks>Metoda pouze zavolá metodu CloseTask na všech předaných objektech <see cref="ConsoleTask"/>, ale nestará se o výsledek volání</remarks>
public override void CloseTask()
{
foreach (var task in m_tasks)
{
task.CloseTask();
}
base.CloseTask();
}
#endregion Methods
}
Metoda CompositeTask je také potomkem třídy ConsoleTask, a proto můžeme ve zbytku aplikace pracovat se stejným rozhraním, na které jsme zvyklí. Jednoduchá i složená úloha mají stejné rozhraní, takže si klient tříd nemusí být skládání úloh vědom, což je mimochodem jedna z hlavních motivací pro zavedení návrhového vzoru Composite. V konstruktoru očekáváme odkaz na kolekci dceřiných úkolů. V metodě DoInternalRun zavoláme v cyklu metodu Run všech předaných úkolů. Ještě před voláním metody Run ale nastavíme u každé úlohy vlastnost SlaveWorkingTask na aktuální objekt CompositeTask, což nám zaručí, že veškeré položky ze všech jednotlivých úloh vložené do pracovní fronty voláním metody Invoke budou zpracovány v jediném pracovním vlákně CompositeTask. Zde vidíme jeden z důvodů, proč máme vlastnost SlaveWorkingTask a proč třída ConsoleTask ve členech Invoke a InvokeRequired nejprve kontroluje, jestli má zpracovat požadavek ve své pracovní frontě, anebo existuje jiný vhodný objekt - "otrok" (SlaveWorkingTask), který se o položky postará sám. Metoda CloseTask opět nejprve zavolá metodu CloseTask na všech objektech ConsoleTask, ze kterých je aktuální instance třídy CompositeTask složena.
Opět poznámka: Snad si rozumíme v tom, že navržená třída CompositeTask není jediná možná. Jiná třída CompositeTask2 nemusí přesměrovávat pracovní frontu na sebe, další po uzavření úloh nejprve vyčká na ukončení všech dceřiných úloh. Další scénáře jistě nalezne laskavý čtenář sám.
Než třídu CompositeTask vyzkoušíme, vytvoříme si dalšího potomka Consoletask, který bude zpracovávat pravidelně vyvolávanou událost z našeho objektu.
Zde je jednoduchá "demo" třída, jejíž srdce tiká v rytmu události AliveEvent.
class MyEventClass
{
public event EventHandler<EventArgs> AliveEvent;
private bool m_continue;
private const int INTERVAL = 1000;
private object m_lockObj;
public MyEventClass()
{
m_continue = true;
m_lockObj = new object();
}
public void Start()
{
ThreadPool.QueueUserWorkItem((state) =>
{
while (m_continue)
{
Thread.Sleep(INTERVAL);
AliveEvent(this, new EventArgs());
}
});
}
public void Stop()
{
lock (m_lockObj)
{
m_continue = false;
}
}
protected void OnAliveEevent(EventArgs e)
{
if (AliveEvent != null)
{
AliveEvent(this, e);
}
}
}
Naše nová konkrétní úloha zpracovává události instance MyEventClass
class ConcreteTask : ConsoleTask
{
private const int HEART_BEAT_LIMIT = 10;
private MyEventClass m_evClass;
private int m_heartBeatcount;
private bool m_processEvent;
protected override void DoInternalRun()
{
m_evClass = new MyEventClass();
m_heartBeatcount = 0;
m_processEvent = true;
m_evClass.Start();
m_evClass.AliveEvent += evClass_AliveEvent;
}
private void evClass_AliveEvent(object sender, EventArgs e)
{
Action myAction = (Action) (
() =>
{
if (!m_processEvent)
{
return;
}
Console.WriteLine("Event fired");
m_heartBeatcount++;
if (m_heartBeatcount >= HEART_BEAT_LIMIT)
{
m_evClass.Stop();
m_processEvent = false;
CloseTask();
}
}
);
if (InvokeRequired)
{
Invoke(myAction);
}
else
{
myAction();
}
}
}
Jenom pro zajímavost je ukázáno, že metodě Invoke můžeme předat složenou (statement) lambdu (nebo anonymní metodu).
Spuštění více úloh pomocí třídy CompositeTask není odlišné od spuštění jedné úlohy.
class Program
{
static void Main(string[] args)
{
var compTask = new CompositeTask(new List<ConsoleTask>
{
new SMSTask(),
new ConcreteTask()
});
ConsoleApplication.Run(compTask);
}
}
Znovu opakuji, že článek měl za cíl ukázat, jak transponovat do jiného aplikačního rámce postupy, které Windows Forms vývojáři dobře ovládají a o kterých mi tvrdili, že jsou "přirozené". Další rozpracování těchto draftů napsaných v půlnoční chvilce nespavosti je už jen variací předvedených postupů.
Monday, February 2, 2009 2:51:15 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms
Monday, January 12, 2009
Odhlášení uživatele z aplikace po uplynutí nastavené doby
Jedním z požadavků na Pocket PC (Windows Mobile ) aplikace je odhlášení uživatele po uplynutí stanovené doby, kdy s aplikací nepracoval, aby se zmenšilo riziko, že uživatel někde PDA položí, někdo jiný PDA najde a ochutná z gurmánského menu položku "co nám dnes servírují za citlivé údaje" nebo se vrhne na žertovné a bezrizikové mazání dat na serveru pod identitou nenáviděného kolegy. Tedy alespoň nějak takto si představuji důvody, kvůli kterým zákazníci na automatickém odhlašování tak lpějí. :)
Jestliže jste někdy zkusili automatické odhlašování napsat, víte, že kvůli podivnému chování modálních dialogů ve Windows Mobile se nejedná o příjemný úkol. Mezi podivné chování modálních dialogu počítám to, že ve správci procesů vždy vidíte všechny formuláře, ne pouze poslední otevřený, a navíc např. třída v Symbol SDK pro práci se čtečkou čárových kódů při zobrazení více modálních formulářů tiše a nehrdinsky zhyne.
Podívejme se na jedno z možných řešení.
Všechny formuláře v aplikaci budou skryté za rozhraním IStackForm.
public interface IStackForm
{
void BeginShowErrors();
void EndShowErrors();
void CloseForm();
void NotifyInactivityTimeout();
void SetLastTitle();
void ResetTitle();
}
Metoda BeginShowErrors notifikuje formulář, že je v aplikaci aktivním formulářem, který má zobrazovat případné uživatelské chyby. Metoda EndShowErrors sděluje formuláři, že nyní nemá na nastalé chyby sám reagovat. Jak uvidíme, metoda EndShowErrors se nám bude hodit proto, abychom potlačili chybová hlášení při odhlašování uživatele a místo odhlášení tak nezůstali v nějakém, nyní pro uživatele zcela nezajímavém, dialogu.
Metoda CloseForm uzavře formulář. Metoda NotifyInactivityTimeout slouží k informování formuláře, že uživatel bude odhlášen z aplikace, a formulář může uložit změny nebo uvolnit používané prostředky .
Metoda Reset Title přikazuje formuláři, aby nastavil svůj titulek (vlastnost Text) na prázný řetězec. Vlastnost SetlastTitle nutí formulář, aby obnovil svůj titulek na hodnotu, kterou měl před voláním metody ResetTitle. Tyto na první (a možná i na druhý) pohled podivné kejkle slouží k simulaci modálního formuláře - přesněji řečeno, využívám toho, že ve správci úloh se zobrazí pouze formuláře s nastaveným titulkem. Jestliže máte v aplikaci pouze jeden formulář s neprázdným titulkem, tak jen tento formulář je zobrazen ve správci úloh a další formuláře s prázdným titulkem nejsou viditelné a ani běžným způsobem pro uživatele dostupné.
Takto může vypadat bázová třída FormBase (Layer Supertype pro UI) s výchozí implementací rozhraní IStackForm.
public class FormBase : Form, IStackForm
{
[DllImport("coredll", EntryPoint = "SetForegroundWindow")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
#region protected variables
protected bool m_InactivityTimeout;
protected bool m_CloseRequired;
protected string m_LastTitle;
protected FormNativeWrapper m_NativeWrapper;
protected bool m_InError;
#endregion protected variables
#region IStackForm Members
public delegate void MyInvokeDelegate();
public FormBase()
{
InitializeComponent();
m_LastTitle = String.Empty;
KeyPreview = true;
m_NativeWrapper = new FormNativeWrapper((Form)this);
}
private static void updateLastUpdate()
{
FormNativeWrapper.LastFormUpdate = DateTime.Now;
}
public virtual void BeginShowErrors()
{
ErrorInfo.NewError += new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
}
void ErrorInfo_NewError(object sender, ErrorInfoEventArgs e)
{
if (InvokeRequired)
{
Invoke((EventHandler<ErrorInfoEventArgs>)ErrorInfo_NewError, sender, e);
return;
}
m_InError = true;
MessageBox.Show(e.Error.Description,
Resource1.General_AppCaption,
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation,
MessageBoxDefaultButton.Button1);
}
public virtual void ClearErrorFlag()
{
m_InError = false;
ErrorInfo.ClearErrors();
}
public virtual void EndShowErrors()
{
ErrorInfo.NewError -= new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
}
public virtual void CloseForm()
{
if (InvokeRequired)
{
Invoke((MyInvokeDelegate)CloseForm);
return;
}
Close();
}
public virtual void NotifyInactivityTimeout()
{
if (InvokeRequired)
{
Invoke((MyInvokeDelegate)NotifyInactivityTimeout);
return;
}
m_InactivityTimeout = true;
ResetTitle();
try
{
if (User.CurrentUser != null)
{
User.CurrentUser.Logout();
Session.Current.EndSession();
}
}
catch (Exception e)
{
Debug.WriteLine(e);
}
//CloseForm();
}
public virtual void SetLastTitle()
{
if (InvokeRequired)
{
Invoke((MyInvokeDelegate)SetLastTitle);
return;
}
Text = m_LastTitle;
SetForegroundWindow(Handle);
}
public virtual void ResetTitle()
{
if (InvokeRequired)
{
Invoke((MyInvokeDelegate)ResetTitle);
return;
}
m_LastTitle = Text;
Text = String.Empty;
}
protected override void OnClosed(EventArgs e)
{
if (!m_InactivityTimeout)
{
FormStack.Instance.Pop();
}
base.OnClosed(e);
}
private void FormBase_Load(object sender, EventArgs e)
{
FormStack.Instance.Push(this);
updateLastUpdate();
}
}
Kvůli souvislostem jsem v kódu ponechal i ukázku implementace metod BeginShowErrors a EndShowErrors. Již na úrovni FormBase zajistíme, že chyby jsou uživateli zobrazovány vždy tak, aby rodičem dialogu s chybovým hlášením byl aktuální formulář. Jak jsem už zmínil, metody SetLastTitle a ResetTitle pouze pracují s titulkem formuláře. U každé metody zajistíme, že je volána v UI threadu - proto se na počátku metod objevuje vždy kontrola vlastnosti InvokeRequired a jestliže je metoda spuštěna v jiném threadu, metodou Invoke si vynutíme opětovné a nyní již bezpečné volání stejné metody v UI threadu.
Pro podporu automatického odhlášení uživatele jsou pro nás důležité tyté části kódu:
- Ihned v konstruktoru je každý formulář oředáh instanci třídy FormNativeWrapper.
m_NativeWrapper = new FormNativeWrapper((Form)this);
Třídu NativeWrapper uvidíme za chvíli, zde jen řeknu, že FormNativeWrapper sleduje vstupy od uživatele (stisknutí tlačítka, zadání textu...) a zajišťuje, že kdykoli uživatel pracuje s nějakým prvkem na formuláři, uloží se aktuální datum a čas do proměnné, která nese informaci, kdy naposledy uživatel pracoval s aplikací. Chceme sledovat aktivity uživatele a proto si požádáme o zasílání všech stisknutých kláves (KeyPreview = true;).
- Při nahrání formuláře (FormBase_Load) je každý formulář vložen do zásobníku (LIFO kontajner) používaných formulářů.
FormStack.Instance.Push(this);
Kód třídy FormStack projdeme za chvíli. Současně také pomocí metody updateLastUpdate(); při nahrání formuláře uložíme informaci, že uživatel je aktivní. Metoda updateLastUpdate uloží aktuální datum a čas do statické vlastnosti FormNativeWrapper.LastFormUpdate.
- Při zavření formuláře (metoda OnClosed) vyjmeme formulář ze zásobníku, ale jen tehdy, jestliže je formulář zavřen uživatelem - pokud je formulář zavřen aplikací, protože je právě odhlašován uživatel, již se zásobníkem formulářů nepracujeme.
- V metodě NotifyInactivityTimeout, která je vyvolána vždy, když má být uživatel automaticky odhlášen, na úrovni předka zajistíme odhlášení uživatele voláním metody User.CurrentUser.Logout(); . V kódu jsem ponechal také řádky, které ukazují, že zde ukončíme například "session" uživatele a uložíme změny.
Následuje třída FormNativeWrapper.
public class FormNativeWrapper : NativeWindow
{
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_KEYDOWN = 0x0100;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_CHAR = 0x0102;
private const int WM_COMMAND = 0x0111;
private static DateTime m_lastUpdate;
private static Object _lockObj = new object();
public static DateTime LastFormUpdate
{
get
{
lock (_lockObj)
{
return m_lastUpdate;
}
}
set
{
lock(_lockObj)
{
m_lastUpdate = value;
}
}
}
public FormNativeWrapper(Form form)
{
init(form);
}
public FormNativeWrapper(IStackForm form)
{
init(form as Form);
}
private void init(Form form)
{
if (form == null)
{
throw new ArgumentNullException("form");
}
if (form.Handle != IntPtr.Zero)
{
AssignHandle(form.Handle);
}
form.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
form.HandleDestroyed += ((sender, e) => ReleaseHandle());
}
static FormNativeWrapper()
{
LastFormUpdate = DateTime.Now;
}
protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
{
if ((m.Msg == WM_KEYDOWN) ||
(m.Msg == WM_LBUTTONDOWN)||
(m.Msg == WM_MOUSEMOVE)||
(m.Msg == WM_CHAR) ||
(m.Msg == WM_COMMAND)
)
{
LastFormUpdate = DateTime.Now;
}
base.WndProc(ref m);
}
}
- Datum poslední zaregistrované aktivity uživatele - vlastnost LastFormUpdate - máme pro jednoduchost přímo ve třídě FormNativeWrapper. Přistupovat k vlastnosti může kód běžící v různých threadech, a proto jsou přístupové metody obaleny sekcí lock.
- Třída FormNativeWrapper dědí z NativeWindow. Tato třída je součástí OpenNetCF frameworku. Třída zpracovává Windows zprávy zaslané formuláři, který jí byl předán do konstruktoru. V přepsané metodě WndProc nastavíme datum poslední aktivity uživatele, jestliže uživatel píše na klávesnici, používá stylus, stiskl tlačítko či vyvolal položku v menu.
S datem poslední aktivity musí pracovat další třída, která bude zodpovědná za informování dalších tříd v aplikaci, že již uživatel po stanovenou dobu s aplikací nepracoval a že by tedy měl být odhlášen. Takovou třídou je třída InactivitySimpleGuard.
static class InactivitySimpleGuard
{
public static event EventHandler<EventArgs> UserInactivity;
private const int INACTIVITY_CHECK = 10000;
private static Timer _timer;
private static int _inactivityInterval = Timeout.Infinite;
public static void Start(int inactivityCheck)
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
if (inactivityCheck != Timeout.Infinite)
{
_inactivityInterval = inactivityCheck;
_timer = new Timer(onTimer, null, INACTIVITY_CHECK, INACTIVITY_CHECK);
}
}
public static void Stop()
{
if (_timer != null)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
_timer = null;
}
}
private static void onTimer(object state)
{
if (User.CurrentUser != null &&
checkUserInactivity())
{
OnUserInactivity(new EventArgs());
}
}
private static void OnUserInactivity(EventArgs e)
{
if (UserInactivity != null)
{
UserInactivity(null, e);
}
}
private static bool checkUserInactivity()
{
TimeSpan span = (DateTime.Now - FormNativeWrapper.LastFormUpdate);
if (span.TotalSeconds > _inactivityInterval)
{
return true;
}
return false;
}
}
- Třída InactivitySimpleGuard je statická, protože jsem měl své důvody v konkrétním řešení, proč ji udělat statickou. Nikdo vám samozřejmě nebrání udělat z třídy InactivitySimpleGuard běžnou instanční třídu, která bude singletonem, nebo budete mít (testovatelné) rozhraní, za něž bude dosazena konkrétní třída přes DI za běhu aplikace. V CNF je ale občas lepší ve jménu vyššího principu microsoftího zatratit své vzletné myšlenky. :)
- Rozhraní třídy je jednoduché. Metodě Start předáme počet sekund, které představují časový interval, po jehož uplynutí je uživatel odhlášen, jestliže s aplikací nepracoval. Metoda Stop zruší objekt Timer, což znamená, že uživatel nebude nikdy automaticky odlogován, protože se jeho aktivita nebere v úvahu. Událost UserInactivity je vyvolána vždy, když uplyne zadaný časový interval, v němž uživatel s aplikací nepracoval, a aplikace tak může uživatele odhlásit. Kontrola doby neaktivity uživatele je v privátní metodě checkUserInactivity, která je volána metodou onTimer. Metoda onTimer je "callback" metodou, která je vyvolána objektem _timer vždy po uplynutí zadaného časového intervalu (délku časového intervalu intervalu představuje konstanta INACTIVITY_CHECK).
Událost UserInactivity si přihlásí zásobník všech formulářu v aplikaci (třída FromStack) a po vyvolání události zavře všechny otevřené formuláře a zobrazí přihlašovací obrazovku. Následuje kód třídy FormStack.
class FormStack
{
private Stack<IStackForm> m_forms;
private IStackForm prevForm;
private bool m_noSetErrors;
protected FormStack()
{
m_forms = new Stack<IStackForm>();
InactivitySimpleGuard.UserInactivity += new EventHandler<EventArgs>(InactivitySimpleGuard_UserInactivity);
m_noSetErrors = false;
}
void InactivitySimpleGuard_UserInactivity(object sender, EventArgs e)
{
try
{
InactivitySimpleGuard.Stop();
//Konvence, že poslední formulář je vždy přihlašovací formulář
m_noSetErrors = true;
while (FormsCount() > 1)
{
IStackForm frm = Pop();
frm.EndShowErrors();
frm.NotifyInactivityTimeout();
frm.CloseForm();
}
Debug.Assert(FormsCount() == 1);
IStackForm loginFrm = Peek();
try
{
loginFrm.EndShowErrors();
loginFrm.SetLastTitle();
loginFrm.NotifyInactivityTimeout();
}
finally
{
loginFrm.BeginShowErrors();
m_noSetErrors = false;
//loginFrm.BeginShowErrors();
}
}
catch (Exception ex)
{
//Ignorovat?
Debug.WriteLine(ex);
}
}
public virtual void Push(IStackForm form)
{
if (form == null)
{
throw new ArgumentNullException();
}
if (FormsCount() >= 1)
{
prevForm = m_forms.Peek();
prevForm.ResetTitle();
prevForm.EndShowErrors();
}
form.BeginShowErrors();
m_forms.Push(form);
}
public virtual IStackForm Pop()
{
IStackForm retForm = m_forms.Pop();
retForm.EndShowErrors();
if (FormsCount() >= 1)
{
IStackForm currentForm = m_forms.Peek();
if (!m_noSetErrors)
{
currentForm.BeginShowErrors();
}
currentForm.SetLastTitle();
}
return retForm;
}
public virtual IStackForm Peek()
{
return m_forms.Peek();
}
public virtual int FormsCount()
{
return m_forms.Count;
}
public static FormStack Instance
{
get
{
return FormStackInternal.Stack;
}
}
private class FormStackInternal
{
public static FormStack Stack = new FormStack();
}
}
Ve třídě FormStack jsou veškeré formuláře uloženy v pořadí, v němž byly zobrazeny. Za přidání a odebrání formulářů ze zásobníku je odpovědná třída FormBase, která používá veřejné metody Push a Pop.
Nás nyní hlavně zajímá obsluha události UserInactivity třídy InactivitySimpleGuard.
- V okamžiku, kdy je událost vyvolána, požádáme třídu InactivitySimpleGuard voláním její metody Stop, aby přestala aktivitu uživatele vyhodnocovat, protože uživatel má být odhlášen a je zbytečné a nesmyslné, aby byla událost UserInactivity vyvolávána, když není uživatel přihlášen.
- Postupně zavřeme všechny formuláře kromě přihlašovacího formuláře. Platí konvence, že poslední formulář v zásobníku, tedy první přidaný formulář, je přihlašovací formulář. Každý uzavíraný formulář nejprve dostane příkaz, aby nezobrazoval chybová hlášení, poté formuláři dovolíme zareagovat na vypršení doby neaktivity a nakonec formulář zavřeme.
frm.EndShowErrors();
frm.NotifyInactivityTimeout();
frm.CloseForm();
- S přihlašovací formulářem zacházíme jinak. Také nechceme,aby prozatím zobrazoval chyby, dále u něj obnovíme titulek, aby byl viditelný i ve správci úloh jako aktivní formulář, a poté i jemu dáme šanci zareagovat na automatické odhlášení uživatele. Nakonec se přihlašovací formulář stane formulářem, který při novém přihlášení uživatele opět může zobrazovat nastalé chyby. Jak si můžete všimnout, v této fázi se snažíme nereagovat na výjimky - naším hlavním cílem je zobrazení přihlašovacího formuláře.
try
{
loginFrm.EndShowErrors();
loginFrm.SetLastTitle();
loginFrm.NotifyInactivityTimeout();
}
finally
{
loginFrm.BeginShowErrors();
m_noSetErrors = false; //loginFrm.BeginShowErrors();
}
Chceme-li odhlásit uživatele po uplynutí stanovené doby nečinnosti, stačí po startu aplikace zavolat metodu InactivitySimpleGuard.Start.
int timeOut = ServerConfiguration.Instance.GetPdaTimeout();
InactivitySimpleGuard.Start(timeOut);
Kód se dá dále vylepšit. FormNativeWrapper sleduje události na formuláři, ale když máte v aplikaci panel, tak některé notifikace pro rodiče okna pohltí panel a vlastnost LastFormUpdate není aktualizována. Potom by mohlo dojít k tomu, že uživatel je odhlášen, i když s aplikací pracuje. Je ale jednoduché ve tříde FormBase vyhledat všechny panely a obalit je nativním wrapperem. Vlastnost LastFormUpdate můžete také modifikovat při načtení libovolného čárového kódu - jinými slovy, sami jste zodpovědni za to, co bude aplikace považovat za aktivitu uživatele.
Monday, January 12, 2009 1:56:25 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework
Sunday, December 21, 2008
Extenzní metoda - binární operace And pro enumerace
V diskuzním fóru se (po dlouhé době ) objevil jeden zajímavější dotaz, který se netýká ani toho, jak zobrazit druhý formulář v aplikaci, ba ani autor nebojuje s mizením dat po postbacku v ASP.NET aplikaci.
Ale vážně - autor dotazu by chtěl mít lepší syntaxi pro binární operaci And v enumeracích označených metaatributem Flags. Mně stávající C syntaxe (Rights & Rights.Add == Rights.Add) zcela vyhovuje a žádný další syntaktický cukřík hltat nechci, ale přesto mě zaujalo, jak by se dal problém, tedy spíš estetická preference náročného tazatele , řešit.
Tazatel přeposlal svůj dotaz i do konference na vývojáři, kde bylo nabídnuto řešení přes dočasné přetypování na typ Object a poté z typu Object na typ int. Jak zaznělo v kritice na vývojáři - "dirty" řešení je funkční, ale opakovaný boxing a unboxing hodnot není zrovna ta pravá vývojářská slast.
Zkusil jsem vymyslet jiné řešení - přiznám se, že IL jsem nezkoumal a žádné výkonnostní testy nedělal.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace BinaryOpGenericTest
{
[Flags]
enum MyFlags
{
A = 1,
B = 2,
C = 4
}
static class EnumExtensions
{
private static Dictionary<Type, Delegate> m_operations = new Dictionary<Type, Delegate>();
public static bool Contains<T>(this T firstOperand, T secondOperand)
where T : struct
{
Type enumType = typeof(T);
if (!enumType.IsEnum)
{
throw new InvalidOperationException("Enum type parameter required");
}
Delegate funcImplementorBase = null;
m_operations.TryGetValue(enumType, out funcImplementorBase);
Func<T, T, bool> funcImplementor = funcImplementorBase as Func<T, T, bool>;
if (funcImplementor == null)
{
funcImplementor = buildFuncImplementor(secondOperand);
}
return funcImplementor(firstOperand, secondOperand);
}
private static Func<T, T, bool> buildFuncImplementor<T>(T val)
where T : struct
{
var first = Expression.Parameter(val.GetType(), "first");
var second = Expression.Parameter(val.GetType(), "second");
Expression convertSecondExpresion = Expression.Convert(second, typeof(int));
var andOperator = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(
Expression.And(
Expression.Convert(first, typeof(int)),
convertSecondExpresion),
convertSecondExpresion),
new[] { first, second });
Func<T, T, bool> andOperatorFunc = andOperator.Compile();
m_operations[typeof(T)] = andOperatorFunc;
return andOperatorFunc;
}
}
class Program
{
static void Main(string[] args)
{
MyFlags flag = MyFlags.A | MyFlags.B;
Console.WriteLine(flag.Contains(MyFlags.A));
Console.WriteLine(EnumExtensions.Contains(flag, MyFlags.C));
Console.ReadLine();
}
}
}
Pár poznámek ke kódu.
- Nejdůležitější v kódu je generická extenzní metoda Contains. Nelze použít negenerickou metodu rozšiřující třídu Enum, protože poté začneme mít další problémy s přetypováváním hodnot z obecné Enum na konkrétní enumeraci. Řešením není ani negenerická metoda, v níž pracujeme pouze s třídou Enum. Enumerace jsou hodnotové typy, a proto je na generický parametr T aplikováno alespoň omezení struct. Jestliže bude zájem, mohu tato rozhodnutí, zde jen zběžně zmíněná, vysvětlit v nějakém dalším spotu.
- Když si nejsme jisti, že nám byla za generický typ předána enumerace, musíme provést v metodě Contains další kontrolu za běhu aplikace.
- Binární operace And je reprezentována delegáty. Delegáty dynamicky vytvářím za běhu aplikace pro každou enumeraci v metodě buildFuncImplementor. Delegát je vytvořen pomocí abstraktního syntaktického stromu (Expression tree) a poté je na výsledném výrazu (Expression) volána metoda Compile, která vrátí delegáta typu Func<T, T, bool>.
- Delegáty Func<T, T, bool> ukládáme do objektu Dictionary jako obecný typ Delegate - důvodem je to, že třída EnumExtensions není generická. Místo přetypovávání delegáta z předka Delegate na typ Func<T, T, bool> by bylo možné také použít pozdní vazbu voláním metody DynamicInvoke dostupné přímo ve třídě Delegate.
- Jestliže byste chtěli operaci And rozšířit i na další typy, které lze konvertovat na typ Int, mohla by se Vám hodit moje extenzní metoda pro detekci konverze generické proměnné na typ Int za běhu aplikace.
Sunday, December 21, 2008 12:22:20 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Monday, December 1, 2008
Běh aplikace na Windows Mobile při vypnutém displeji a opětovné probuzení zařízení
Jedním z problémů, se kterými se vývojáři často potýkají, je, že PDA (přesněji 7Windows Mobile Professional a Classic zařízení - zařízení s dotykovým displejem) na rozdíl od Smartphonu (Windows Mobile Standard) po uplynutí doby nastavené v ovládacích panelech přecházejí do stavu "Off" (přesněji do stavu "Suspended" dle oficiální terminologie, stav Off má pouze Smartphone - my ale budeme dále ve spotu používat termín "off") , v němž je provádění kódu aplikace zcela "zmrazeno". Aplikace jsou "hibernovány" a můžeme je považovat za dočasné mrtvolky.
Občas potřebujeme, aby sice došlo k vypnutí displeje zařízení a my tak drasticky a navíc zbytečně neredukovali výdrž baterie, ale aby naše aplikace na pozadí stále běžela. To je první požadavek. Dalším z častých požadavků je schopnost aplikace probudit celé zařízení ze stavu "off".
Jestliže nám jde pouze o probuzení aplikace po uplynutí námi nastaveného času či při nějaké události (připojení k síti, změna proxy apod.), je řešení jednoduché - v našem arzenálu bude hrát prim omnipotentní funkce CeSetUserNotificationEx.
Když chceme v naší aplikaci pracovat i při vypnutém displeji (např. odpočítáváme sekundy v aplikaci "minutka") a současně probudit zařízení poté, co uběhla nastavená doba, musíme se začít přátelit s dalšími mocnými, avšak ke škodě samotné platformy Windows Mobile mizerně dokumentovanými API.
Co musíme zajistit:
- Zařízení nesmí přejít do "off" módu, ale musíme jej ponechat ve speciálním stavu, kdy je vypnutý displej, ale aplikace, které potřebují běžet, nejsou "zmrazeny". Námi požadovaný speciální stav se na Windows Mobile nazývá "unattended mód". Z hlediska uživatele je při "unattended módu" zařízení vypnuto, ale naše aplikace, která si o tento mód požádala, pokračuje ve své činnosti.
K tomu nám poslouží API PowerPolicyNotify.
PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_ON).
Funkci PowerPolicyNotify předáme v prvním argumentu požadavek na Unattended mód a ve druhém argumentu konstantu UNATTENDED_ON s hodnotou 1 - TRUE.
- V unattended módu chceme zařízení zcela probudit. Opět použijeme API PowerPolicyNotify.
PowerPolicyNotify(PowerMode.AppButtonPressed, 0)
Tentokrát ale předáme konstantu PowerMode.AppButtonPressed, která simuluje stisknutí tlačítka "On/Off" na zařízení. Druhý argument musí být 0, funkcí s ním při tomto volání nepracuje.
- Jestliže již nepotřebujeme běžet v "unattended módu", měli bychom dovolit zařízení, aby naši aplikaci hibernovalo. Nejpozději při ukončení aplikace se tedy musíme vzdát "unattended módu". Použijeme opět API PowerPolicyNotify s prvním argumentem PowerMode.UnattendedMode a ve druhém argumentu předáme konstantu UNATTENDED_OFF s hodnotou 0 - FALSE.
PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_OFF).
Sice jsem si ověřil, že při ukončení procesu je "unattended mód" zrušen samotnými Windows, ale je vždy příjemné pracovat s kódem, v němž nejsou obvyklé "prasácké" zlozvyky v managed kódu, a nenechat tedy v naší situaci uklízet jen Garbage Collector nebo správce procesů ve Windows.
V následujícím příkladu naleznete také tento zakomentovaný kód:
powerSettings = SetPowerRequirement("BKL1:", (int)DeviceState.FullOn, POWER_FORCE, IntPtr.Zero, 0);
if (powerSettings == IntPtr.Zero)
{
MessageBox.Show("Failed");
}
K zapnutí displeje je použito API SetPowerRequirement. Toto API ale v "unattended" módu selhává - vždy mi na zařízeních, která mám doma, vrací false. Dále musíte znát zkratku zařízení, pro které chcete nastavit nový režim. Na většině zařízení je displej identifikován zkratkou BKL1: - abyste si ale byli jisti, musíte název najít v registrech - klíče pod HKLM\Drivers\Active\*. API PowerPolicyNotify je tedy pro naše účely mnohem vhodnější.
Pozor: Některá zařízení (pokud se dobře pamatuji, tak např. MIO) nemají od výrobce podporu "unattended" módu a aplikace je stejně vždy zmrazena.
Zde je slíbený příklad - na formuláři je pouze listbox, do kterého je každých 5 vteřin přidáno další číslo. Můžete si ověřit, že čísla přibývají i v "unattended" módu. Po uplynutích každých 75 vteřin (konstanta WAKE_UP_COUNTER (15) * 5) je zařízení opakovaně probuzeno k "plnému vědomí", což se u něj projeví rozsvíceným displejem.
V ovládacích panelech (záložka Systém, položka Napájení, ikona Upřesnit) nastavte vypnutí displeje i zařízení po uplynutí jedné minuty neaktivity, abyste si příklad užili.
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
namespace PowerPDAProject
{
public delegate void MethodInvoker();
public partial class Form1 : Form
{
public const int UNATTENDED_ON = 1;
public const int POWER_FORCE = 0x1000;
public const int UNATTENDED_OFF = 0;
public const int WAKE_UP_COUNTER = 15;
private int m_secondsCount;
private IntPtr powerSettings;
public enum PowerMode
{
ReevaluateStat = 0x0001,
PowerChange = 0x0002,
UnattendedMode = 0x0003,
SuspendKeyOrPwrButtonPressed = 0x0004,
SuspendKeyReleased = 0x0005,
AppButtonPressed = 0x0006
}
[DllImport("CoreDll.dll")]
public static extern int PowerPolicyNotify(PowerMode powerMode, int flags);
public enum DeviceState : int
{
Unspecified = -1,
FullOn = 0,
LowOn,
StandBy,
Sleep,
Off,
Maximum
}
[DllImport("CoreDll.DLL")]
public static extern IntPtr SetPowerRequirement(String pvDevice, int DeviceState, int DeviceFlags, IntPtr pvSystemState, int StateFlags);
[DllImport("CoreDll.DLL")]
public static extern uint ReleasePowerRequirement(IntPtr hPowerReq);
public Form1()
{
InitializeComponent();
m_secondsCount = 0;
}
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
}
private void Form1_Load(object sender, EventArgs e)
{
bool result = (PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_ON) != 0);
MessageBox.Show(result.ToString());
timer1.Enabled = true;
}
private void timer1_Tick(object sender, EventArgs e)
{
m_secondsCount++;
listBox1.Invoke((MethodInvoker)(
() => listBox1.Items.Add(m_secondsCount.ToString())));
if (m_secondsCount % WAKE_UP_COUNTER == 0)
{
PowerPolicyNotify(PowerMode.AppButtonPressed, 0);
//powerSettings = SetPowerRequirement("BKL1:", (int)DeviceState.FullOn, POWER_FORCE, IntPtr.Zero, 0);
//if (powerSettings == IntPtr.Zero)
//{
// MessageBox.Show("Failed");
//}
}
}
private void Form1_Closing(object sender, CancelEventArgs e)
{
timer1.Enabled = false;
timer1.Dispose();
PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_OFF);
ReleasePowerRequirement(powerSettings);
}
}
}
Monday, December 1, 2008 6:23:53 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky
Friday, November 7, 2008
Změna nastavení aplikace pro stahování TV programu
Po opakovaných dotazech v mailu, proč se posledních 14 dní nestahuje TV program automaticky do MDA, dávám odpověď sem, abych nemusel všem odpovídat individuálně.
Pravděpodobně se opět trochu změnilo generování TV programu a je nutné upravit kofiguraci aplikace.
V konfiguračním souboru RStein.SimpleWMDownloader.exe.config se ujistěte, že máte nastaveny následující klíče takto:
<add key="StartDate" value="20081104"/>
<add key="StartDateValue" value="1225753200"/>
<add key="DayOffsetValue" value="86400"/>
Vzorový konfigurační soubor může vypadat takto:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="StartDate" value="20081104"/>
<add key="DayQueryKey" value="dny[]"/>
<add key="StartDateValue" value="1225753200"/>
<add key="DayOffsetValue" value="86400"/>
<add key="NumberOfDays" value="8"/>
</appSettings>
<system.diagnostics>
<trace autoflush="true" indentsize="0">
<listeners>
<add name="myListener"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="c:\Rstein.SimpleWMDownloader.log" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
Podrobné informace o aplikaci a instalace
A na okraj dodám, že aplikaci nemusíte použivat jen pro stahování TV programu, ale lze s ní stáhnout jakoukoli stránku na webu, kterou chcete při synchronizaci přes AS uložit do (M|P)DA. Stačí do souboru DownloadConfig.txt přidat další řádky v tomto tvaru:
URL;cesta k souboru na pda
Příklad:
http://www.example.com/vzor.txt;\Storage card\example\vzor.txt
http://www.example.com/dalsi2.txt;\Storage card\example\dalsi2.txt
Friday, November 7, 2008 11:29:27 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Mobilitky | Ostatní
Sunday, October 5, 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#
V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.
binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);
V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je funktor equal_to binárním funktorem, poznáme z jeho deklarace.
template<class Type>
struct equal_to : public binary_function<Type, Type, bool>
{
bool operator()(
const Type& _Left,
const Type& _Right
) const;
};
Na druhém řádku příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type& _Right) s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right (konkrétně v našem případě jde o podmínku 10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude představovat při každém volání "adaptovaného" funktoru equal_to adaptérem equalPredicate hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.
Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").
static class ComparerEx
{
public static bool GreaterThan<T>(T a, T b)
{
return Comparer<T>.Default.Compare(a, b) > 0;
}
public static bool LessThan<T>(T a, T b)
{
return Comparer<T>.Default.Compare(a, b) < 0;
}
}
Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func< >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.
Nejprve se podívejme na použití adaptérů.
class Program
{
static void Main(string[] args)
{
Random rand = new Random();
//Vygenerování náhodných čísel v rozsahu 1..100
List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));
//Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
predicate = FuncExtension.Not(predicate);
//LINQ dotaz - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
var result = myList
.Where(predicate)
.Select((elem, index) => new {elem, index});
//Výpis LINQ dotazu
foreach (var res in result)
{
Console.WriteLine("{0}:{1}", res.index, res.elem);
}
//Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
//Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
foreach (var val in vals)
{
Console.WriteLine(val);
}
Console.ReadLine();
}
}
V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.
Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než 10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná při volání funkce Bind2nd.
V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".
Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)
Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.
Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.
Jak adaptéry pracují? Podívejme se na funkci Bind2nd.
public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(arg, bindValue));
}
Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument typu T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.
Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.
public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
{
return arg => originalFunc(arg);
}
Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.
Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy".
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FunctionExtensions
{
static class FuncExtension
{
public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(bindValue, arg));
}
public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(arg, bindValue));
}
public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
{
return (arg => !originalFunc(arg));
}
public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
{
return ((arg1, arg2) => !originalFunc(arg1, arg2));
}
public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
{
return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
}
public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
{
return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
}
public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
{
return (arg1 => originalFunc(arg1) && originalFunc2(arg1));
}
public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
{
return ((arg1, arg2) => originalFunc(arg1, arg2) || originalFunc(arg1, arg2));
}
public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
{
return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
}
public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
{
return arg => originalFunc(arg);
}
}
}
Sunday, October 5, 2008 5:34:45 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ | Návrhové vzory
Monday, September 15, 2008
Několik poznámek k přetypovávání generických kolekcí
Při práci s generickými kolekcemi asi každy občas zatouží převést generickou kolekci s objekty typu B na generickou kolekci s objekty typu A, přičemž instinktivně očekává, že když je typ A předkem typu B, žádný problém při konverzi nenastane a navíc půjde o konverzi implicitní - automatickou. Instinkty, intuice a další feminní rysy jsou ale při programování spíš zátěží (že by jeden z hlavních důvodů, proč je stále tak málo programátorek? ) )
Konkrétně - mějme tyto dvě třídy.
class Test
{
public override string ToString()
{
return "Test";
}
}
class SpecialTest : Test
{
public override string ToString()
{
return "SpecialTest";
}
}
Vytvoříme si kolekci (List) odvozených tříd SpecialTest.
List<SpecialTest> srcList = new List<SpecialTest> { new SpecialTest(), new SpecialTest() };
Při pokusu přetypovat List<SpecialTest> na kolekci objektů typu Test (List<Test>) neuspějeme.
List<Test> invalidAttemptList = srcList;
Cannot implicitly convert type 'System.Collections.Generic.List<CollectionInheritance.SpecialTest>' to 'System.Collections.Generic.List<CollectionInheritance.Test>'). Důvod je zřejmý - dva generické objekty List, jeden s generickým argumentem Test a druhý s generickým argumentem SpecialTest, jsou dvě zcela rozdílné a nezávislé třídy a skutečnost, že třída SpecialTest je potomkem třídy Test, neznamená, že by stejný vztah platil mezi třídami List<SpecialTest> a List<Test>. Autoři jazyka C# (dle svých vyjádření prozatím?) zavedli toto omezení kvůli typové bezpečnosti.
Možností, jak konverzi provést, je mnoho. Vyjmenujme alespoň ty, které nově přinesl LINQ.
LINQ nám nabízí pro daný účel metodu Cast, která zkonvertuje prvky ze zdrojové kolekce (IEnumerable) na (generický) typ předaný metodě.
IEnumerable<Test> ieList = srcList.Cast<Test>();
Jestliže by nebylo možné všechny prvky v kolekci převést na 'Test', bude vyvolána výjimka. Když máme v kolekci směs objektů z různých tříd nebo podporujících různá rozhraní, můžeme přetypovat pomocí dalšího standardního LINQ operátoru OfType, který do výsledné kolekce vloží jen ty objekty, které se pomocí operátoru as podaří přetypovat na cílový typ. Objekty, které přetypovat na cílový typ (v následujícím kódu tedy na typ Test) nelze, jsou ignorovány.
IEnumerable<Test> ieList2 = srcList.OfType<Test>();
Jestliže nechceme mít jako výslednou kolekci typ IEnumerable, můžeme použít další LINQ operátor ToList() a výsledek přetypování nám bude vrácen v instanci List<T>.
List<Test> testList = srcList.OfType<Test>().ToList();
Alternativou k předchozímu zápisu může být využití konstruktoru třídy List.
List<Test> testList2 = new List<Test>(srcList.OfType<Test>());
Jestliže nechcete vždy pracovat jen s kolekcí typu List a chcete přetypovávat například na typové kolekce, využijete mé extenzní metody.
Collection<Test> trgList = srcList.WideningConvert<SpecialTest, Test, Collection<Test>>();
Metodě WideningConvert předáte jako typové argumenty aktuální typ v generické kolekci (SpecialTest), cílový-výsledný typ, na který má být zdrojový typ převeden (Test) a typ kolekce, která má být vrácena. Kolekce musí mít bezparametrický konstruktor a také musí podporovat rozhraní ICollection<T>.
Pomocí extenzní metody lze přetypovávat i z kolekce objektů typu "předek" na kolekci "potomků".
Collection<SpecialTest> nextList = trgList.NarrowingConvert<Test, SpecialTest, Collection<SpecialTest>>();
Metoda WideningConvert svým názvem dává najevo, že je určena pro implicitní ("bezpečnou") konverzi, kdy převádíte kolekci potomků na kolekci předků. Obdobně, metoda NarrowingConvert přetypovává kolekci objektů typu "předek" na kolekci objektů typ "potomek (explicitni konverze). Metoda NarrowingConvert se pokusí převést každý objekt ve zdrojové kolekci na cílový typ ("Potomek") pomocí operátoru as. Jestliže přetypování selže, je zdrojový objekt ignorován, a proto může výsledná kolekce vrácená metodou NarrowingConvert obsahovat méně prvků než kolekce zdrojová.
Zde je kompletní výpis metod. Bylo by samozřejmě možné začít uvažovat nad zjednodušením kódu pro přetypovávání, ale to si necháme "napříště".
public static class ConvertExtensions
{
/// <summary>
/// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "potomky" na generickou kolekci s "předkem"
/// </summary>
/// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
/// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
/// <typeparam name="R">Typ cílové kolekce</typeparam>
/// <param name="source">Zdrojová kolekce</param>
/// <returns>Cílovou kolekci s převedenými elementy</returns>
public static R WideningConvert<T0, P, R>(this IEnumerable<T0> source)
where R : ICollection<P>, new()
where T0: P
{
if (source == null)
{
throw new ArgumentNullException();
}
R retCol = new R();
foreach (T0 srcElem in source)
{
retCol.Add(srcElem);
}
return retCol;
}
/// <summary>
/// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "předkem" na generickou kolekci "potomků"
/// </summary>
/// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
/// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
/// <typeparam name="R">Typ cílové kolekce</typeparam>
/// <param name="source">Zdrojová kolekce</param>
/// <returns>Cílovou kolekci s převedenými elementy</returns>
///<remarks>Počet prvků v cílové kolekcí může být menší než počet prvků v zdrojové kolekci, protože veškeré objekty, které nelze zkonvertovat na <typeparamref name="R"/>, jsou ignorovány</remarks>
public static R NarrowingConvert<T0, P, R>(this IEnumerable<T0> source)
where R : ICollection<P>, new()
where P : class, T0
{
if (source == null)
{
throw new ArgumentNullException();
}
R retCol = new R();
foreach (T0 srcElem in source)
{
P retValue = srcElem as P;
if (retValue != null)
{
retCol.Add(retValue);
}
}
return retCol;
}
}
Monday, September 15, 2008 1:29:48 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Monday, August 11, 2008
Dobře utajený nástroj - Interní Spy
Dnes jsem náhodou našel užitečný "Easter Egg" skrytý ve Windows Mobile (určitě ve verzích 5 a 6). Když držíte tlačítko "Action" a současně přidržíte stylus - jako kdybyste chtěli zobrazit kontextové menu - v horní liště s notifikačními ikonami přesně mezi popiskem formuláře a první notifikační ikonou, zobrazí se okno Internal Use Spy, ve kterém najdete informace o hwnd aktivního okna, třídě (class), pozici - než bych se rozepisoval, podívejte se sami na obrázky, jaké informace o okně jsou zobrazeny. Docela užitečná věc, když nemám po ruce notebook s nástrojem Remote Spy.
Monday, August 11, 2008 2:57:08 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky
Wednesday, May 28, 2008
Friday, May 9, 2008
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly
V předchozím spotu jsem byl schopen pracovat s anonymními datovými typy, i když byly dotazy a výsledné sady dat vytvořeny v jiné assembly. Odstranění vrozené xenofobie v praxi.:)
Náš kód ale vygeneruje výjimku, jestliže anonymní datový typ z jiné assembly obsahuje další vnořené anonymní datové typy jako v následujícím upraveném příkladu. Vlastnost InnerAT vrací další anonymní datový typ, který pro zajímavost obsahuje odkaz ještě na další anonymní datový typ.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LINQTEST
{
public class TestAT
{
public static object GetResult()
{
string[] rows = { "Toyota", "Lexus", "Audi" };
var test = from row in rows
select new
{
FirstLetter = row[0],
Index = 110,
Original = row,
InnerAT = new { X = row[1], B = new {A=1}}
};
return test;
}
}
}
Řešení spočívá v úpravě extenzí a to tak, že přidáme privátní metodu GetTypeInstance a přeneseme do ní většinu kódu z extenze ToAnonymousType. Metoda GetTypeInstance při neshodě datového typu očekávaného parametrem "našeho - v naší assembly dostupného" konstruktoru anonymního datového typu a datového typu vlastnosti anonymního datového typu z "cizí" assembly rekurzivně přenese data z "cizího" anonymního datového typu do "našeho".
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;
namespace LINQAnonymous
{
/// <summary>
/// Rozšíření pro LINQ
/// </summary>
static class RSLinqExtensions
{
/// <summary>
/// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static T ToAnonymousType<T>(this object obj, T prototype)
where T: class
{
T atiObj = obj as T;
if (atiObj == null)
{
atiObj = GetTypeInstance(obj, prototype.GetType()) as T;
}
return (atiObj);
}
private static object GetTypeInstance(object obj, Type expected)
{
object atiObj = null;
ConstructorInfo constructorInfo = expected.GetConstructors()[0];
if (constructorInfo == null)
{
return null;
}
ParameterInfo[] paramInfos = constructorInfo.GetParameters();
PropertyInfo[] origProperties = obj.GetType().GetProperties();
if (paramInfos.Count() != origProperties.Count())
{
return null;
}
object[] paramArgs = new object[paramInfos.Count()];
for (int i = 0; i < paramArgs.Length; i++)
{
PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
if (origProperty == null)
{
return null;
}
object val = origProperty.GetValue(obj, null);
if (origProperty.PropertyType != paramInfos[i].ParameterType)
{
val = GetTypeInstance(val, paramInfos[i].ParameterType);
}
paramArgs[i] = val;
}
atiObj = constructorInfo.Invoke(paramArgs);
return atiObj;
}
/// <summary>
/// Metoda vrátí
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static List<T> CastToList<T>(this object obj, T prototype)
where T : class
{
List<T> list = new List<T>();
IEnumerable<T> enumerable = obj as IEnumerable<T>;
if (enumerable != null)
{
list.AddRange(enumerable);
}
else
{
IEnumerable enumObjects = obj as IEnumerable;
if (enumObjects == null)
{
return null;
}
foreach (object enumObject in enumObjects)
{
T currObject = ToAnonymousType(enumObject, prototype);
if (currObject == null)
{
//K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
list.Clear();
return list;
}
list.Add(currObject);
}
}
return list;
}
}
Při přetypovávání stačí stále jen zadat prototyp anonymního datové typu.
//Anonymní typ z jiné assembly!
var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char),
Index =default(int),
Original = default(string),
InnerAT = new { X = default(char), B = new { A = default(int) } }
})
;
foreach (var res in result2)
{
Console.WriteLine(res.FirstLetter);
Console.WriteLine(res.Original);
}
Console.WriteLine(TestAT.
GetResult().
CastToList(new
{
FirstLetter = default(char),
Index = default(int),
Original = default(string),
InnerAT = new { X = default(char), B = new { A =default(int)} }
}
).
Where(car => car.FirstLetter == 'T')
.FirstOrDefault()
.ToString());
Console.ReadLine();
Friday, May 9, 2008 9:09:26 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Thursday, May 8, 2008
LINQ - anonymní typ deklarovaný v jedné assembly dostupný v metodách další assembly?
Anonymní datové typy v LINQu nelze použít jako návratový typ z metody a jediný způsob, jak anonymní typ z metody předat, je použít jako návratovou hodnotu typ object, protože v .Net Frameworku - jak je všeobecně známo - všechny třídy přímo či nepřímo dědí z třídy Object. Navíc platí, že anonymní typ je kompilátorem vždy deklarován jako internal a jeho použití je tak striktně omezeno na jednu assembly.
Jde o rozumné omezení a anonymní datové typy bychom neměli zneužívat k nesmyslům typu "hezká syntaxe pro generování objektů Dictionary", které si našly cestu i do připravovaného (a už dnes "přehypovaného") MVC frameworku pro ASP.NET.
V různých diskuzích se ale stále dokola objevuje dotaz, jak anonymní typ z metody vráti. A každé omezení se dá samozřejmě obejít - když nefunguje ani bodový systém na silnicích, proč nenajít hrubý trik ve stylu "osoby blízké" i pro erozi různých omezení u anonymního datového typu. :) Znovu alibisticky varuji všechny před zařazením následujících nehezkých triků do svého arzenálu běžných postupů při vývoji, protože všechny postupy spoléhají na chování kompilátoru C#, které není garantováno a které se může v další verzi nebo i jen při vydání service packu .Net Frameworku bez varování změnit.
Pro vrácení anonymního datového typu z metody použijeme hezký hack od Tomáše, který se ujal pod názvem "Cast By Example". Zjednodušeně řečeno - sice nemůžeme používat při přetypovávání názvy anonymních datových typů (tříd), protože anonymní datové typy jsou generovány až při kompilaci, ale můžeme kompilátoru dát při přetypování "vzor", jaký anonymní datový typ nám bude vyhovovat. Podrobnosti si můžete najít v odkazovaném článku Tomáše Petříčka = zde jen připomenu, že technika využívá současného chování kompilátoru, který pro různé deklarace anonymních datových typů se stejnými vlastnostmi generuje v jedné assembly vždy právě jednu třídu.
Napsal jsem jednoduše použitelné extenze, které vám dovolí nejen přetypovat jednu instanci "object" na anonymní datový typ, ale můžete přetypovat množiny záznamů na (anonymně ) typovou kolekci List<NějakýAnonymniTyp>, a dokonce je možné jednoduše použít anonymní datové typy z jiné assembly.
/// <summary>
/// Rozšíření pro LINQ
/// </summary>
static class RSLinqExtensions
{
/// <summary>
/// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static T ToAnonymousType<T>(this object obj, T prototype)
where T: class
{
T atiObj = obj as T;
if (atiObj == null)
{
ConstructorInfo constructorInfo = typeof(T).GetConstructors()[0];
if (constructorInfo == null)
{
return null;
}
ParameterInfo[] paramInfos = constructorInfo.GetParameters();
PropertyInfo[] origProperties = obj.GetType().GetProperties();
if (paramInfos.Count() != origProperties.Count())
{
return null;
}
object[] paramArgs = new object[paramInfos.Count()];
for (int i = 0; i < paramArgs.Length; i++)
{
PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
if (origProperty == null)
{
return null;
}
paramArgs[i] = origProperty.GetValue(obj, null);
}
atiObj = constructorInfo.Invoke(paramArgs) as T;
}
return (atiObj);
}
/// <summary>
/// Metoda vrátí
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static List<T> CastToList<T>(this object obj, T prototype)
where T : class
{
List<T> list = new List<T>();
IEnumerable<T> enumerable = obj as IEnumerable<T>;
if (enumerable != null)
{
list.AddRange(enumerable);
}
else
{
IEnumerable enumObjects = obj as IEnumerable;
if (enumObjects == null)
{
return null;
}
foreach (object enumObject in enumObjects)
{
T currObject = ToAnonymousType(enumObject, prototype);
if (currObject == null)
{
//K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
list.Clear();
return list;
}
list.Add(currObject);
}
}
return list;
}
}
Komentáře u metod by měly dostatečně popisovat funkci extenzí. Metoda ToAnonymousType předpokládá, že chcete přetypovat na instanci anonymního typu (např. při použití metody Single v LINQu), metoda CastToList pracuje s množinou (IEnumerable<T>) instancí anonymního datového typu. Většina kódu v obou metodách ošetřuje situaci, kdy pracujete s anonymním datovým typem z jiné (referencované) assembly, jehož data je potřeba přenést do instance anonymního datového typu v aktuální assembly.
Použití extenzí - nejprve u anonymního datového typu deklarovaného v assembly, ve které je také náš LINQ dotaz.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;
class Program
{
//Anonymní typ deklarovaný v této (exe) assembly
private static object GetLetters()
{
string[] names = {"Rene", "Petra", "Kamilka"};
var test = from name in names
select new {FirstLetter = name[0], Index=1};
return test;
}
static void Main(string[] args)
{
var result = GetLetters().CastToList(new {FirstLetter = default(char),
Index =default(int)}
);
foreach (var res in result)
{
Console.WriteLine(res.FirstLetter);
}
}
Metodě CastToList jsme predali "vzor" anonymího datového typu (new {FirstLetter = default(char), Index =default(int)}) a hodnoty vlastností jsme u prototypu inicializovali s využitím klíčového slova default. V metodě Main v cyklu foreach je funkční intellisense a můžeme pracovat zcela typově s proměnnou res. Jenom zdůrazním, že nyní žádná reflexe nebyla použita! Metoda CastToList s využitím automatické typové inference kompilátoru pouze zkopírovala prvky v IEnumerable<T> do našeho typového generického Listu.
if (enumerable != null)
{
list.AddRange(enumerable);
}
Reflexe je využita při konverzi anonymního typu deklarovaného v jiné assembly. Předpokládejme, že v jiné assembly nazvané např. LINQTest máme další metodu vracející množinu dat skrytou opět za obecným rozhraním "služebníka zcela neužitečného" neboli třídy object.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LINQTEST
{
public class TestAT
{
public static object GetResult()
{
string[] rows = { "Toyota", "Lexus", "Audi" };
var test = from row in rows
select new { FirstLetter = row[0],
Index=110,
Original = row
};
return test;
}
}
}
Zkompilovanou assembly LINQTest zareferencujeme v našem projektu. Kód pro práci s anonymní datovým typem v jiné assembly se z pohledu uživatele LINQ extenze nijak nezměnil od předchozího příkladu.
class Program
{
static void Main(string[] args)
{
//Anonymní typ z jiné assembly!
var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char),
Index =default(int),
Original = default(string)}
);
foreach (var res in result2)
{
Console.WriteLine(res.FirstLetter);
Console.WriteLine(res.Original);
}
Console.WriteLine(TestAT.
GetResult().
CastToList(new
{
FirstLetter = default(char),
Index = default(int),
Original = default(string)
}).
Where(car => car.FirstLetter == 'T')
.FirstOrDefault()
.ToString());
Console.ReadLine();
}
}
Jak si můžete všimnout, po cyklu foreach si požádám o data z jiné assembly znovu a poté nad vrácenou typovou kolekci vytvořím další projekci. A ani mě nemusí zajímat, že se mi pod rukama zcela změnil typ používaných objektů.
Docela zábavná záležitost ne?
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly
Thursday, May 8, 2008 3:00:43 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Monday, May 5, 2008
Thursday, April 24, 2008
Vlastní reakce na podrženi stylu uživatelem (zobrazení kontextového menu)
Další z triků pro Compact .Net Framework, na který se lidé často ptají. Chcete sami zareagovat na podržení stylu místo výchozího zobrazení kontextového menu, což zajišťuje výchozí obsluha události přímo v CNF? Přes P/Invoke je to možné.
private const uint GN_CONTEXTMENU = 1000;
private const uint SHRG_RETURNCMD = 0x00000001;
[StructLayout(LayoutKind.Sequential)]
public class SHINFO
{
public uint cbSize = 0;
public IntPtr hwndClient = IntPtr.Zero;
public int x = 0;
public int y = 0;
public uint dwFlags = 0;
}
[DllImport("aygshell", SetLastError = true)]
private static extern uint SHRecognizeGesture(SHINFO shrg);
//Obsluha udalosti MouseDown formulare/ovladaciho prvku
private void mouseDown(object sender, MouseEventArgs e)
{
SHINFO shinfo = new SHINFO();
shinfo.cbSize = (uint)(Marshal.SizeOf(shinfo));
shinfo.hwndClient = this.Handle; //handle formulare/ovladaciho prvku
shinfo.x = e.X;
shinfo.y = e.Y;
shinfo.dwFlags = SHRG_RETURNCMD;
if (SHRecognizeGesture(shinfo) == GN_CONTEXTMENU)
{
//nase reakce
}
}
V nativním kódu můžeme vlastním zpracováním gesta GN_CONTEXTMENU odstranit nepříjemnou a snad od počátku přítomnou chybu v MFC, která způsobí, že se animace ("tečky opisující kruh") zobrazí 2x. Ukázka z Today pluginu (čisté API, ne MFC).
SHRGINFO shrg;
HMENU hMenu;
shrg.cbSize = sizeof(shrg);
shrg.hwndClient = m_hWnd;
shrg.ptDown.x = point.x;
shrg.ptDown.y = point.y;
shrg.dwFlags = SHRG_RETURNCMD ;
POINT screenPoint = point;
ClientToScreen(m_hWnd, &screenPoint);
if (!tabControl.HasFocus())
{
:PostMessage(GetParent(), TODAYM_TOOKSELECTION, (WPARAM)m_hWnd, 0);
}
if (SHRecognizeGesture(&shrg) == GN_CONTEXTMENU)
{
//Nas kod
}
Thursday, April 24, 2008 12:11:30 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky
Sunday, October 29, 2006
Kontrola duplicitního spuštění aplikace v Compact .Net Frameworku - Windows CE
I když samotný Compact .Net Framework v souladu s doporučeními Microsoftu pro vývoj Windows Mobile aplikací zajišťuje, že je vždy spuštěna nanejvýš jedna instance aplikace, v Compact .Net Frameworku spuštěném na "čistých" Windows CE již zmíněné pravidlo neplatí a kontrolu na opakované spuštění aplikace musíme doplnit sami.
Tento kód funguje pro Compact .Net Framework od verze 1.x a byl otestován na Windows CE 4.2 a vyšších.
private const int ERROR_ALREADY_EXISTS = 183;
[DllImport("CoreDll.dll")]
private static extern int GetLastError();
[DllImport("CoreDll.dll", EntryPoint="CreateMutexW")]
private static extern int CreateMutex( IntPtr
lpMutexAttributes, bool InitialOwner, string MutexName );
public static bool AppAlreadyStarted()
{
string myID =
System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
if( CreateMutex(IntPtr.Zero, true, myID) != 0 )
{
return (GetLastError() == ERROR_ALREADY_EXISTS);
}
return false;
}
Můžeme také najít formulář již spuštěné aplikace podle titulku, přenést jej do popředí a duplicitní instanci aplikace ukončit.
namespace DeviceApplication1
{
static class Program
{
[DllImport("coredll", EntryPoint="FindWindow")]
private static extern IntPtr FindWindow(
string lpClassName,
string lpWindowName);
[DllImport("coredll", EntryPoint="SetForegroundWindow")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[MTAThread]
static void Main()
{
IntPtr mainWindowHwnd = FindWindow(null, "DeviceMain");
if (mainWindowHwnd.Equals(IntPtr.Zero))
{
Application.Run(new Form1());
}
else
{
SetForegroundWindow(mainWindowHwnd);
Application.Exit();
}
}
}
Sunday, October 29, 2006 8:52:11 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework
Monday, May 1, 2006
Odstranění problému(ů) s instalací Compact .NET Frameworku 2.0 na Windows Mobile 5.0
Již několikrát jsem se setkal s tím, že na zařízení s Windows Mobile 5.0 byl problém nainstalovat Compact .Net Framework. Pár lidí si mi teď v poslední době v mailu nebo na ICQ také stěžovalo, že jim CNF nejde nainstalovat a že při pátrání po příčinách potíží dostanou nanejvýš jen hlášku, že došlo k problému a že by bylo dobré resetovat zařízení a zkusit instalaci znovu. Tomu říkám skvělé design guidelines v praxi - "nikdy neobtěžujte uživatele zbytečnými technickými podrobnostmi".
Odstranění problému není složité - z důvodu známému asi jen vývojářskému týmu CNF v Microsoftu a mystérióznímu pro běžného smrtelníka ActiveSync při instalaci CNF z počítače přes Application Manager zkopíruje do zařízení soubor nazvaný NETCFv2.wce5.armv4i.cab místo správného NETCFv2.wm.armv4i.cab. NETCFv2.wm.armv4i.cab patří na všechna současná zařízení používající procesor s ARM V4 kompatibilní instrukční sadou.
Takže selže-li vám instalace, najděte na počítači soubor NETCFv2.wm.armv4i.cab, a zkopírujte jej "ručně" (v Exploreru nebo třeba v v Total Commanderu s pluginem pro CE zařízení) do PDA. V PDA pak jen cab soubor spusťte a VŽDY zvolte instalaci CNF do zařízení - instalace na kartu (SD, CF) je sice možná, ale při prvním použití CNF dojde stejně k nakopírování všech knihoven do storage v zařízení.
Pokud máte stále problémy s instalací:
- Resetujte (soft-reset) zařízení a zkuste nainstalovat CNF znovu. Teď už má reset smysl.
- Nezabere-li ani to, vypněte po dobu instalace všechny Today pluginy, restartujte (soft reset) a znovu instalujte.
Přinejhorším bod 2 zabere podle mých zkušeností vždy - ještě se mi nestalo, že bych na nějaké PDA s WM 5 CNF 2 nenainstaloval
Monday, May 1, 2006 9:50:26 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky
Sunday, April 9, 2006
Bázová třída pro typové kolekce v .NET Frameworku 2.0
V souvislosti s uvedením generiky v .NET Frameworku se v různých článcích dočtete, jak generika usnadní vytváření a použití typových kolekcí. To je sice pravda, ale v článku se kromě zjednodušených příkladů a frikulínských hlášek o dokonalosti .NETu 2.0 z úst (respektiva pera) excitovaných jedinců po právě dokonaném intimním styku se softwarovou emanací božstva Microsoftu málokdy dovíte, jak by taková typová kolekce měla vypadat v běžné aplikaci.
Proč nevyhovuje obyčejné použití generického typu List? (deklarace typové kolekce objednávek ve tvaru List<Order> orderCollection = new List<Order>()).
- Jednou z dobrých praktik u generik je co nejvíce před uživateli (dalšími vývojáři) skrývat informaci, že pracují s generickým typem. Ač mně syntaxe pro práci s generickými typy připadá intuitivní, nemusí si to myslet všichni, a mnoho vývojářů stále asi raději používá důvěrně známý kód OrderCollection orderCollection = new OrderCollection() místo výše zmíněného kódu List<Order> orderCollection = new List<Order>()). Tento požadavek by ale List<T> splnil - typová kolekce může být potomkem List<T>.
- Ve třídě List nejsou metody Add a Remove virtuální. To znamená, že nemůžete po přidání nebo odebrání položky z/do kolekce vyvolávat vlastní události. A to je problém, protože po přidání/odebrání položek z kolekce můžeme chtít nastavit/zrušit rodiče nebo přepočítát sumární hodnoty za položky v kolekci apod.
Bázová třída pro všechny kolekce, kterou používám, je otevřeným generickým typem a jejím předkem je třída Collection<T> z jmenného prostoru System.Collections.ObjectModel. Třída Collection<T> nabízí virtuální chráněné metody InsertItem a RemoveItem, ve kterých můžete vyvolávat potřebné události. Jestliže používate návrhový vzor Layer SuperType a máte tedy jednu bázovou třídu pro všechny business objekty (BusinessObjectBase), je vhodné, aby bázová třída pro kolekce kladla na generický typ T omezení, že musí být potomkem třídy BusinessObjectBase. Omezení slouží k tomu, abyste ve svých kolekcích mohli intuitivně používat všechny atributy a metody deklarované na úrovni společného předka BusinessObjectBase.
Kód kolekce:
public class BusinessCollectionBase<T> : Collection<T>
where T : BusinessObjectBase
{
#region Events
public event EventHandler<CollectionChangeEventArgs> ItemAdded;
public event EventHandler<CollectionChangeEventArgs> ItemRemoved;
#endregion Events
#region Protected methods
/// <summary>
/// Přidání položky do kolekce
/// </summary>
/// <param name="index">Index položky</param>
/// <param name="item">Vkládaná položka</param>
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
OnItemAdded(new CollectionChangeEventArgs(item));
}
/// <summary>
/// Přidání položky do kolekce
/// </summary>
/// <param name="index">Index položky</param>
protected override void RemoveItem(int index)
{
T item = this[index];
base.RemoveItem(index);
OnItemRemoved(new CollectionChangeEventArgs(item));
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemRemoved
/// </summary>
/// <param name="e">Argumenty události</param>
protected void OnItemRemoved(CollectionChangeEventArgs e)
{
if (ItemRemoved != null)
{
ItemRemoved(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemAdded
/// </summary>
/// <param name="e"></param>
protected void OnItemAdded(CollectionChangeEventArgs e)
{
if (ItemAdded != null)
{
ItemAdded(this, e);
}
}
/// <summary>
///
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual T FindById(Guid id)
{
List<T> mylist = (List<T>) Items;
T objectMeetsCriteria = null;
objectMeetsCriteria = mylist.Find(delegate(T iterObject)
{
if (iterObject.Id == id)
{
return true;
}
else
{
return false;
}
});
return objectMeetsCriteria;
}
/// <summary>
/// Nalezení všech objektů splňujících zadanou podmínku
/// </summary>
/// <param name="criteria">Podmínka výběru</param>
/// <returns>List s objekty, které splňují zadanou podmínku</returns
public virtual List<T> Find(Predicate<T> criteria)
{
List<T> mylist = (List<T>) Items;
return (mylist.FindAll(criteria));
}
/// <summary>
/// Spuštění akce nad všemi elementy v kolekci
/// </summary>
/// <param name="action">Akce, která se má provést</param>
public virtual void ForEach(Action<T> action)
{
List<T> mylist = (List<T>)Items;
mylist.ForEach(action);
}
#endregion Protected methods
}
}
Jak vidíme:
- Třída BusinessCollectionBase je potomkem třídy Collection<T> a vyžaduje, aby typ T byl vždy potomkem BusinessObjectBase. Motivace pro toto rozhodnutí jspu popsány výše.
- Nadeklarovali jsme dvě události ItemAdded a ItemRemoved, které jsou vyvolávany v přepsaných metodách InsertItem (ItemAdded) a RemoveItem (ItemRemoved). Pokud bych měl zájem, mohu jednoduše přidat i události vyvolávané před přidáním/odebráním položky z kolekce.
- Do rozhraní BusinessCollectionBase jsem také přidal několik zajímavých metod.
- Metoda FindById nalezne podle předaného Id (unikátní identifikátor instance) objekt v kolekci. V této metodě opět používáme nové konstrukce z .Net Frameworku 2.0. Implementační objekt kolekce (starý známý List<T> ) vystavuje metodu Find, která očekává generického delegáta Predicate z jmenného prostoru System.
public delegate bool Predicate( T obj);Delegát Predicate je "ukazatelem" na metodu, která očekává jeden generický argument T a vrací true nebo false. Delegát Predicate tedy zastupuje metodu s podmínkou, která je pro předaný argument obj pravdivá nebo nepravdivá. My pro vytvoření podmínky použijeme anonymní metodu, která vrátí true pouze tehdy, když se Id objektu v kolekci shoduje s předaným Id. Atribut Id u generického typu T kolekce můžeme používat právě proto, že jsme zavedli pro typ T omezení (musíš být potomkem BusinessObjectBase) a atribut Id je deklarován ve třídě BusinessOBjectBase.
- Pro pokročilejší operace s elementy kolekce jsme z objektu List<T> zveřejnili metody FindAll A ForEach. Metoda FindAll podle předané podmínky (delegát Predicate) nalezne a vrátí všechny objekty, které jí vyhovují. Metoda ForEach spustí pro všchny elementy v kolekci "akci - činnost" implementovanou v metodě, na níž "ukazuje" další užitečný delegát Action<T>.
public delegate void Action<T> ( T obj);
Když tedy budete chtít všechny objekty v kolekci zrušit, místo psaní cyklu foreach napíšete kód podobný tomuto:
myCol.ForEach(delegate(OrderItem item)
{
item.Discard();
});
Vytváření vlastních typových kolekcí je jednoduché:
/// <summary>
/// Kolekce objektů OrderItem
/// </summary>
public class OrderItemCollection : BusinessCollectionBase<OrderItem>
{
}
Pro úplnost sem dávám triviální kód třídy pro argumenty události CollectionChanged.
/// <summary>
/// Objekt v kolekci
/// </summary>
public class CollectionChangeEventArgs : EventArgs
{
#region Private variables
private BusinessObjectBase m_collectionObject;
#endregion Private variables
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="collectionObjekt">Objekt v kolekci, kterého se událost týká</param>
public CollectionChangeEventArgs(BusinessObjectBase collectionObject)
{
BasicValidations.AssertNotNull(collectionObject, "collectionObject");
m_collectionObject = collectionObject;
}
/// <summary>
/// Objekt v kolekci, kterého se událost týká
/// </summary>
public BusinessObjectBase CollectionObject
{
get
{
return m_collectionObject;
}
}
#endregion Constructors
}
Související články:
Bázová třída pro business objekty - návrhový vzor Layer Supertype
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Ukázka použití třídy BusinessObjectBase
>
Sunday, April 9, 2006 2:34:33 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory
Tuesday, January 31, 2006
Sunday, January 8, 2006
Program na import oblíbených položek z Pocket IE do Opery Mobile 8.5
Download programu.
Operu 8.5 Mobile pro Pocket PC je možné stáhnout zde - verzi určenou původně jen pro Willcom W-ZERO3 lze "opatchovat" ;) tak, aby byla použitelná i na dalších zařízeních. Crack ode mě ale nečekejte. ;) Další informace o Opeře naleznete na msmobiles.
Protože se dá očekávat, že i kvůli tomuto poněkud trapnému úniku ;), který těžko zbagatelizuje i vášnivý apologet a nekritický fanoušek Opery Radek Hulán, poněvadž jde o takové diletanství, jež dle mnoha indicií vede jedině k podezření na infiltraci Opery neschopnými zaměstnanci Apple ;), společnost Opera nabídne na svých stránkách mobilní Operu oficiálně pro všechny zařízení s Windows Mobile 5.0, mohl by se vám hodit prográmek, který jednorázově přenese všechny oblíbené položky z Pocket Internet Exploreru do Opery. Program jsem napsal (link na stažení je v prvním řádku tohoto spotu) - prosím vás tedy o otestování a o reportování nalezených chyb.
Jak program funguje?
1) Stažený soubor RStein.ImportOperaFavorites.exe nakopírujte do zařízení.
2) Soubor spusťte - pokud import proběhne úspěšně, jste informováni, že import byl dokončen, jinak je zobrazen text výjimky.
- Program kopíruje oblíbené položky ze složky, jejíž cesta je uložena v registry pod klíčem System\Explorer\Shell Folders\Favorites. Pokud klíč neexistuje, kopíruje se ze složky \Windows\Favorites.
- Program rekurzivně kopíruje i všechny složky a podsložky.
- Data pro Operu jsou ukládána do souboru Application Data\opera6.adr
- Původní oblíbené položky v Opeře jsou zazálohovány do souboru Application Data\opera6.adr.bak
- Otestováno na MDA Vario (HTC Wizard).
Update: Dle prvnich ohlasů jsem vytvořil novou verzi:
1) Pokud neexistuje soubor opera6.adr, program pokračuje dál i bez zálohy původních oblíbených položek.
2) Při chybě by měl program nahlásit, jaká položka mu dělá potíže. Jestliže máte v IE položku s nějakým "velmi dlouhým názvem", zkraťte název - vypada to, ze v CNF 1.x je nějaká chyba, kvůli které nelze otevřít stream, pokud je předán "dlouhý" název souboru.
Sunday, January 8, 2006 5:25:19 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky
Wednesday, July 27, 2005
Dotazy na vývoj aplikací pro Pocket PC
Na serveru CE4YOU můžete v v diskuzi Programování klást své dotazy, které se týkají problémů s vývojem pro Pocket PC a SmartPhone miláčky . Nezáleží na tom, s jakým vývojovým nástrojem pracujete - takže se můžete ptát na vývoj v Compact .Net Frameworku 1.0 (2.0), na vývoj nativních aplikací v C++ nebo i na bastlení ve stařečkovi eVB.
Specializovaná konference na Emwacu není a otázky kolem Pocket PC platformy zůstávají v obecných konferencích většinou bez odpovědi, proto si myslím, že tématicky úzce zaměřená konference bude pro všechny vývojáře pro platformu Windows Mobile příjemným a postupně znalostmi nabytým koutkem. :)
Informace poslední: Jsem moderátorem tohoto fóra, takže jej aktivně sleduji a pokud znám odpověď, nenechávám si ji pro sebe.
Wednesday, July 27, 2005 2:49:46 PM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Mobilitky | Ostatní
Sunday, October 10, 2004
Dva tipy k vývoji aplikací pod compact .Net Frameworkem, které možná neznáte
- I v PDa zařízeních s nainstalovaným compact .Net Frameworkem můžeme umísťit podepsané (strong name) assembly umístit do globálního úložiště (Global Assembly Cache - GAC). DO GAC by měly být umísťovány assembly, které používá více CF aplikací a které tedy není vhodné pokaždé instalovat v samostatné kopii. Postup umístění assembly do GAC se podstatně liší od umístění assembly do GAC na desktopu.
- Nakopírujte podepsanou assembly do jakéhokoli adresáře na PDA (například \Program Files\SharedDll\MyAssembly.dll)
- Vytvořte soubor s příponou gac, do kterého vložíte cestu k assembly (soubor se jmenuje MyDll.gac a obsahuje řádek \Program Files\SharedDll\MyAssembly.dll). V jednom souboru může být libovolné množství registrovaných dll a na jméně souboru nezáleží.
- Soubor s příponou gac umístěte do adresáře Windows.
- Při vývoji aplikace pod compact .Net Frameworkem nám chybí základní statistiky o běhu aplikace, respektive o výkonnosti běhového prostředí. Alespoň základní výkonnostní ukazatele (Performance Counters) zjistíme po přidání klíče HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETCompactFramework\PerfMonitor do registrů. Pod nově vytvořený klíčem přidáme novou položku typu DWORD, nazveme ji Counters a její hodnotou bude číslo 1.
Pak můžete spustit jednu CF aplikaci, simulovat běžnou práci uživatele a po ukončení aplikace analyzovat nově vygenerovaný soubor mscoree.stat v kořenovém adresáři PDA.
Sunday, October 10, 2004 11:24:00 AM (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework