Tuesday, 28 June 2022
Školení C++, C#, .NET, OOP je opět možné objednat
V animovaném gifu je ukázka z jednoho příkladu, který jsem ukazoval minulý týden.
Předpokládám, že se dá snadno poznat, čeho se příklad týká.
A dávám to sem, protože mám vyřízeny všechny resty, které se týkají inhouse kurzů objednaných v době covidové a přesunutých na lepší časy. Lepší časy sice kvůli válce na Ukrajině nepřišly, ale přesto jsem stihnul za poslední 4 měsíce dokončit více kurzů než za celé předchozí dva roky.
A proto i tady píšu, že jestli jste ode mě poslední rok obdrželi email "inhouse kurzy se prozatím objednat nedají a neřeknu vám přesný termín, kdy je zase bude možné objednat", tak už tato informace neplatí.
Příklad dole je napsán v C++ 20. A jestli jste se zaradovali, že v C++ 20 jsou coroutines, ale pak jste zjistili, že ve standardní knihovně si "co_awaitování" moc neužijete, ukážu vám svou knihovnu, kde jsou všechny typy potřebné pro psaní paralelizovaného a asynchronního kódu.
A jestli jste ještě třeba nepřešli z C++ 98 na C++ 11 (C++ 14, C++ 17) a potřebujete do nové spletité syntaktické džungle průvodce, který vám proseká cestu k jednoduššímu jazyku, který se za tím starým bordelem skrývá, můžeme se také domluvit.
Objednat se dají samozřejmě ale i kurzy, kde hlavní roli hraje C# a třeba .NET 6.
A pokud nechcete řešit nějaké triviality, které si může člověk, který není líný, přečíst a nastudovat v každém druhém tutorialu a přežvýkají vám je na každém školení, určitě se domluvíme.
Tuesday, 28 June 2022 16:49:57 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | C++
Thursday, 08 April 2021
Nová .NET Standard 2.0 knihovna RStein.HDO (Hromadné dálkové ovládání).
TL;DR
Jestli někdo chcete/potřebujete pracovat s HDO rozpisem, můžete použít mou novou .NET Standard 2.0 knihovnu pro snadné získání rozpisu HDO (hromadné dálkové ovládání - laicky řečeno, chcete znát a ve svém programu pracovat s časy, kdy je aktivní takzvaný nízký tarif elektřiny).
Nyní se data stahují z ČEZu.
https://github.com/renestein/RStein.HDO
Konec TL;DR
Nedávno jsem psal na FB o chytrých zásuvkách, které bych chtěl automaticky synchronizovat s HDO rozpisem od ČEZu.
Přes Velikonoce jsem pokročil a nyní už mi stačí u "chytré" zásuvky (chytrého zařízení):
1) Po zakoupení a přidání zásuvky nastavit, že se řídí podle HDO rozpisu. Pokud takový příznak nastaven není, v zásuvce můžete mít jakákoli jiná ručně zadaná pravidla pro vypínání a zapínání zařízení a tato pravidla nebudou nikdy přepsána pravidly z HDO rozpisu.
2) Můj nový agent pro Smart Home pravidelně stáhne HDO data z Čezu, porovná stávající pravidla v zásuvkách, u kterých je nastaveno, že se řídí HDO rozpisem, s pravidly v HDO rozpisu u ČEZu, a jestliže se pravidla liší, změní pravidla v zásuvkách tak, aby byla shodná s aktuálním ČEZ HDO rozpisem.
Vedlejším výsledkem je .NET Standard 2.0 knihovna RStein.HDO, kterou jsem oddělil od hlavního a neveřejného projektu, protože jsem si říkal, že knihovnu já a možná i někdo jiný využijeme i pro jiné účely.
Základní funkce:
1) Stáhne data z ČEZu a vytvoří z nich rozpis (scheduli).
2) Schedule může být cachována (doporučuju, aby nás ČEZ neblokl, stahovat data maximálně jednou denně - rozpis HDO se zase tak často nemění).
3) Schedule se snadno dotážete, jestli je HDO v daném čase aktivní.
4) Kdyby vám nestačila vystavená strukturovaná data, dostanete se jednoduše i k nezpracovaným původním JSON datům z ČEZu a k objektovému modelu, který jim odpovídá.
Pár poznámek na závěr.
1) Pull requesty pro jiné distributory elektřiny jsou vítány.
2) Paskvily v enumeraci CezRegion - např. CezRegion.stred (čeština, neidiomatické malé písmeno na začátku názvu) jsou moje - název přesně odpovídá vyžadované reprezentaci hodnoty ve stringu.
3) Česko-anglické patvary v původním modelu (SAZBA, VALID_FROM) jsou dílem vývojářů v ČEZu.
A jeden jeden postřeh nakonec. GitHub actions pro CI/CD jsou mnohem lépe zdokumentovány než Azure pipelines. I když si odmyslím dokumentaci, přijdou mi GitHub actions intuitivnější a logičtější než Azure pipelines. Popularitu YAMLu ale stejně pořád nechápu.
Thursday, 08 April 2021 07:47:07 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C#
Tuesday, 19 August 2014
Veřejná přednáška pro WUG - TPL – konkurenční, paralelní a asynchronní kód pro náročné.
Rád bych vás pozval na svou přednášku, kterou pořádá WUG.
Název přednášky: TPL - konkurenční, paralelní a asynchronní kód pro náročné.
Datum konání: 2.10.2014 od 17:30 do 21:00
Místo konání: pobočka: BB centrum, budova Alfa (Aquarius), Vyskočilova 1461/2a, Praha 4
Registrace na přednášku: http://wug.cz/praha/akce/597-TPL-konkurencni-paralelni-a-asynchronni-kod-pro-narocne
Anotace přednášky:
Znáte alespoň trochu Task Parallel Library a přednášek slibujících další nenáročný „úvod do TPL“ jste už viděli dost? Myslíte si, že klíčová slova async/await v C# jsou magií kompilátoru, jejíž kouzlo pro vás už navěky pominulo po zhlédnutí triviálních a donekonečna opisovaných příkladů, jak zavolat asynchronně pár nudných webových služeb?
Na přednášce probereme, jak rozšířit knihovnu TPL o další užitečné konstrukce i jak odstranit některá omezení v současné verzi TPL. Podíváme se na různé způsoby psaní konkurenčního, paralelního a asynchronního kódu. U konkurenčního kódu se zaměříme (nejen) na aktory a porovnáme různé způsoby, jak můžeme aktory psát.
Nezapomeňte s sebou vzít i kolegy, kteří hlásají, že každou nebezpečnou hlavu konkurenčního kódu setne jeden pořádný „lock“, a to nejlépe rekurzivní, aby vás deadlock nebo livelock ve firmě zabavil i o dlouhých zimních večerech.
Tuesday, 19 August 2014 11:08:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Návrhové vzory
Saturday, 23 March 2013
Monday, 03 December 2012
Prezentace z přednášky na MS festu 2012 - DI v .NET bez pověr, iluzí a frikulínského nadšení
Tomáše Herceg & comp. opět po roce uspořádali další ročník konference MS Fest. A musím hned dodat, že z mého pohledu velmi povedený MS Fest, jehož organizace nikde neskřípala a na kterém jsme se cítil příjemně. Tímto organizátorům ještě jednou děkuju za skvělou organizaci konference a za veškerý servis, který poskytovali účastníkům konference i přednášejícím.
Na MS Festu jsme měl přednášku nazvanou Dependency injection v .Net Frameworku bez pověr, iluzí a frikulínského nadšení.
Sice jsem se jako každý rok po domluvě s organizátory na tématu přednášky dodatečně zděsil, že na přednášku mám jen 60 minut, a těsně před konferencí se stovkami účastníků jsme musel nahodit svůj přídavný a životní energii rychle spalující extrovertní pohon, ale samotná přednáška probíhala oproti minulému roku poklidně. Nemyslete si, já teprve po minulém ročníku MS Festu dovedu ocenit, jaké je to blaho, když s vámi v půlce probíhající přednášky nezačne zuřivě diskutovat oponent z Nokie.
Nabízel jsem tyto přednášky.
Tomáš Herceg mi původně v programu navrhl dvě přednášky, ale já jsme měl čas jen na přípravu jedné přednášky a vybral jsem tu, která dostala nejvíce hlasů.
Nevím, jaká je poptávka po pokročilejších/hard core přednáškách. Jak jsem psal na Twitteru, sám bych raději přednášel o “Task parallel library pro pokročilé”, ještě raději o skrytých pokladech v RX Frameworku, ale RX si již dříve zamluvil Jarda Jírava. Bavilo by mě také přednášet o specialitkách typu dynamic, mohli bychom se pobavit o klíčových slovech async/await v netradičních kontextech, nebo bychom mohli napsat dalšího hostitele .Net Frameworku. To je alespoň malý výběr z témat, která mi jsou blízká, protože jsem podobné vývojářské specialitky řešil pro různé firmy u nás i v zahraničí. Nevím, jaká by ale byla po těchto tématech na MS Festu poptávka, protože povídat si v potemnělé posluchárně jen pro sebe nebo prezentovat pro maximálně deset dalších lidí má své kouzlo, ale - při vší úctě - intimní atmosféru mám raději s jinými než vývojářskými kulisami a aktéry.
Slíbená prezentace:
Doprovodný kód je na Bitbucketu:
Díky za to, že jste na mou přednášku přišli. A děkuju za hodnocení přednášky, moc jste mě potěšili.
P .S. Ještě málá terminologická poznámka, proč používám slovo “kontajner”, a ne kontejner, na což se mě ptal už Tomáš Herceg po zaslání anotace.
Oficiálně [myšleno - slovo kontajner žádný speciální význam] nemá – dokonce myslím ÚJČ slovo kontajner ani neuznává.
Viděl jsem, že se ale v ČR slovo kontajner docela vžilo a snažím se jeho použitím odlišit od konotací „kontejneru“ – u kterého mnoho lidí vidí spojitost s odpadky.:)
P. P. S. A ještě děkuju Alešovi Roubíčkovi za to, že si ochotně pročetl mou prezentaci a upozornil mě na místa, která by si zasloužila nějaké upřesnění.
P.P.P.S. Mrzí mě jediná věc. Na svých přednáškách většinou nezvětšuju písmo, na této přednášce jsem písmo ve Visual Studiu zvětšil přesně tak, jako to měl Tomáš Herceg na první přednášce, a při výkladu mě dost mátlo, že na obrazovce je vidět málo kódu, i když jsem měl z domova vyzkoušeno, že by kód měl být vidět bez problémů. Místo toho, abych VS přepnul na celou obrazovku, tak jsem skroloval a v duchu se divil, proč je toho vidět tak málo. Takové momentální okno přednášejícího, který zapomněl, co udělal s fontem o přestávce. A ještě – poté, co mi spadl mikrofon a já si jej znovu nasadil, tak prý bylo slyšet hlavně v zadních řadách praskání a jiné pazvuky. Já jsem bohužel nic neslyšel a nikdo z posluchačů neprotestoval.
Monday, 03 December 2012 13:25:14 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Entity Framework | Návrhové vzory
Monday, 12 March 2012
Lazy loading (zpožděné nahrávání) objektů do kolekce i ve starší aplikaci s využitím dynamické proxy
Jestliže používáte i přes jeho nezralost Entity Framework, nebo jste si zvolili jiné ORM, které zvládá “lazy loading”, neboli zpožděné, či chcete-li dodatečné nahrání dat do typových kolekcí, možná jste přemýšleli, jak byste stejnou službu napsali ve starší aplikaci, která žádné ORM nepoužívá, nebo v hybridní aplikaci, pod kterou si představuju aplikaci, jejíž starší moduly ORM nepoužívají, ale novější moduly již s přístupem přes ORM počítají. I bez ORM byste ale často v aplikaci rádi využívali některé vychytávky, které s sebou přináší ORM. Dnes chci ukázat, že zpožděné nahrávání kolekcí není žádná magie, která by bez ORM byla v aplikaci zapovězena. Když budete mít zájem, můžeme v dalších článcích probrat například i automatickou detekci změn vlastností na objektech a s tím související ukládání objektů i odvolávání proběhlých změn, jestliže zákazník nechce změny uložit a třeba na formuláři stiskne po deseti minutách zuřivé editace objektu a po několika masivních business transakcích, které pozmění desítky objektů najednou, tlačítko Storno.
Co si představit pod zpožděným nahráním objektů v kolekci? Můžeme vyjít z již zlidovělé třídy Objednávka (Order), která má kolekci svých položek (kolekce Items). Místo nahrání všech položek objednávky z databáze ihned po vytvoření instance objednávky, odložíme nahrání položek až na dobu, kdy budou v aplikaci poprvé potřeba. Před klienty třídy Order ale tuto optimalizaci skrýváme tak, že kolekci Items naplníme položkami teprve při prvním přístupu ke kolekci přes veřejné rozhraní třídy Order.
Jde sice o triviální kód, kdy v get akcesoru kontrolujeme bool příznak m_itemsIsLoaded (byly položky nahrány?) a při prvním přístupu ke kolekci zavoláme metodu loadItems, ale představte si, že tento kód u starší aplikace zběsile doplňujete ke každé kolekci v každé třídě, kde teď stojíte o zpožděné nahrávání. Čitelnost kódu je citelně snížena a chrabří obhájci principu jedné odpovědnosti třídy (SRP) právě dopisují Kladivo na heretiky S.O.L.I.Dní víry pravé a připravují v zájmu lepší veřejné vývojářské morálky autodafé účtu dotyčného vývojáře na všech významných sociálních sítích.
Můžeme se rozhodnout, že nebudeme zatěžovat zpožděným nahráváním kolekcí přímo třídu Objednávka, ale že vytvoříme proxy třídu, do které odpovědnost za tuto “infrastrukturní službu” vložíme. Proxy objekt je, jak známo, objekt, který má z hlediska klienta stejné rozhraní jako původní objekt a klient si není vědom, že pracuje s instancí zástupce (surogátem) třídy Order, a ne s originální třídou Order.
Samotná třída Objednávka není nijak zatěžována znalostí, že její kolekce Items je nahrána až při prvním přístupu ke kolekci. Zpožděné nahrávání zvládne ale potomek třídy Order, třída OrderProxy, ve které je podobný kód, který se původně nacházel v objednávce. A protože platí, že potomek nějaké třídy může být v aplikaci používán na všech místech, kde je očekáván předek, můžeme například z repozitáře/identitní mapy objektů začít ihned vydávat klientům instance OrderProxy místo originální třídy Order.
Odpovědnosti už jsme rozdělili lépe, protože třída Order není zatěžována zpožděným nahráváním kolekce a jediným důvodem existence třídy OrderProxy je právě zpožděné nahrávání. Přesto stále platí, že budeme do úmoru psát další a další třídy Proxy, u nichž jediná kreativní vývojářská činnost spočívá v pojmenování bool proměnné, která nám sděluje, jestli kolekce byla, nebo nebyla nahrána. Je možné si práci zjednodušit tím, že například vytvoříme T4 šablonu, která proxy třídy vygeneruje, ale my se dnes zaměříme na to, jak vytvořit proxy třídu, aniž bychom museli psát proxy ručně nebo spoléhat na T4 šablony.
Použijeme takzvanou dynamickou proxy, kterou si lze představit jako nástroj, kterému řekneme, co má proxy dělat, a on pro každou třídu bez ohledu na unikátní rozhraní každé třídy sám vygeneruje proxy, která pro tuto třídu implementuje námi vyžadované chování. Slovo “dynamická” u proxy vyjadřuje hlavně to, že jde o proxy generovanou automaticky za běhu aplikace! V našem konkrétním scénáři se zpožděným nahráváním objektů vytvoříme i pro aplikaci, která má v business vrstvě stovky i tisíce tříd jen jednu další třídu, která představuje “deskriptor” pro každou proxy zajišťující zpožděné nahrávání kolekcí, přičemž platí, že tento “deskriptor“ bude schopen obsloužit všechny kolekce ve všech třídách. Sice se pro každou třídu vytvoří unikátní proxy (potomek originální třídy), ale tato proxy se bude odkazovat na obecný scénář zpožděného nahrávání v našem “deskriptoru”. Při čtení dalších částí článku mějte na paměti, že kód dělá stále to samé, jako námi vytvořená třída OrderProxy výše, jen jsme kód zobecnili tak, abychom si dokázali vynutit zpožděné nahrávání kolekcí ve všech třídách. Vedlejším a nepříjemným důsledkem tohoto zobecnění, jak je tomu asi u každé abstrakce, je samozřejmě snížená schopnost jen po letmém prolétnutí infrastrukturního kódu očima poznat, oč přesně usilujeme. Výhodou však bude to, že náš deskriptor oddálí zmiňované autodafé vývojáře už jen tím, že dodržuje princip DRY-Don't repeat yourself – místo psaní jen mírně obměněného kódu v každé konkrétní proxy jednorázově vyjádříme náš záměr zpožděně nahrávat kolekce v “deskriptoru” pro generování dynamické proxy.
Mějme třídy Customer, Order a OrderItem. Zajímá nás hlavně to, že třída Customer má kolekci objednávek a třída Order kolekci položek objednávky. Pro všechny kolekce chceme doplnit zpožděné nahrání kolekcí.
Všimněte si také toho, že vlastnosti s kolekcemi jsou virtuální. Stále platí, že dynamická proxy je potomkem naší třídy a musí být schopna přepsat implementaci a doplnit kód pro zpožděné nahrání kolekce stejně, jako jsme přepisovali get akcesor u “manuální” proxy výše.
I když je mnohem zábavnější napsat si podporu pro dynamicky generované proxy sám, ne vždy bychom měli vynalézat na projektu kolo, zvláště když jsme v časovém presu a navíc pod drábovou knutou nudných projektových manažerů surově lámajících naše vývojářská křídla , a alespoň v tomto článku použijeme výborný a ověřený nástroj pro generování dynamických proxy z projektu Castle. Nejjednodušší způsob přidání knihovny pro generování dynamických proxy spočívá v instalaci přes NuGet. V Powershell konzoli ve Visual studiu zadejte příkaz.
Vytvoření dynamické proxy v Castlu je kupodivu otázkou napsání jednoho řádku kódu. My služby Castlu zapouzdříme do naší vlastní třídy ProxyEngine.
Do konstruktoru třídy ProxyEngine dostáváme instanci třídy SimpleObjectFactory, kterou si můžete prozatím zjednodušeně představit jako velmi jednoduchou generickou továrnu na výrobu business objektů, která zároveň funguje jako identitní mapa. Její kompletní výpis naleznete níže v tomto článku.
Hlavní je pro nás metoda AddDefaultProxy, která dostává jako první argument typ, pro který má být vytvořena proxy. Tedy předáte-li typový deskriptor objednávky, metoda by měla vyrobit OrderProxy. Druhým argumentem jsou argumenty, které mají být předány konstruktoru naší třídy. Jestliže objednávka vyžaduje v konstruktoru odkaz na své id, vygenerovaná proxy třída garantuje, že jí id bude do konstruktoru předáno.
Teď přichází zajímavá část – vytvoření proxy:
Proměnná m_proxyGenerator je instancí třídy ProxyEngine z Castlu, která představuje výkonné jádro pro generování proxy. Prvním argumentem metody proxyGenerator.CreateClassProxy je typ, pro který chceme proxy vytvořit. Druhý argument typu ProxyGenerationOptions jsou různé volby, které dovolují jemně řídit, jak se bude vytvořená proxy chovat. My zatím potřebujeme jen sdělit, které metody originální (ne proxy) třídy chceme v proxy “přepsat”. Proto jsou ProxyGenerationOptions inicializovány hned v konstruktoru třídy ProxyEngine a je jim předána instance třídy ProxyGenerationHook, která, jak ihned ve výpise uvidíme, vybírá kolekce, u kterých má být podporováno zpožděné nahrání objektů.
Rozhraní IProxyGenerationHook je rozhraní Castlu. Metoda ShouldInterceptMethod z tohoto rozhraní je metoda, kterou Castle používá k rozhodnutí, jaké metody a vlastnosti mají být “přepsány” v dynamické proxy. Třída ProxyGenerationHook v metodě ShouldInterceptMethod říká - tedy vrací true - , že chceme zachytit všechny get akcesory ( methodInfo.IsSpecialName&& methodInfo.Name.StartsWith(GET_METHOD_NAME_PREFIX), jejichž návratovou hodnotou je kolekce. Přesněji řečeno každá kolekce podporující generické rozhraní ICollection<T> (methodInfo.ReturnType.GetInterface(COLLECTION_NAME) != null). Jiných metod ani vlastností si v tomto článku u tříd nevšímáme, a proto pro ně z metody ShouldInterceptMethod vrátíme false. Zajímavou metodou v rozhraní IProxyGenerationHook je i metoda NonVirtualMemberNotification, pomocí níž nás Castle informuje, že v originální třídě je nevirtuální metoda – my metodu NonVirtualMemberNotification nevyužíváme, ale mohli bychom do ní snadno doplnit kód, který vyhodí výjimku, jestliže jste Castlem notifikováni, že existuje nevirtuální vlastnost vracející ICollection<T>, protože vaše firemní konvence vyžadují, aby všechny kolekce podporovaly zpožděné nahrávání kolekce.
Nyní jsme již Castlu sdělili, že máme zájem “přepsat” get akcesory kolekcí, ale stále Castle neví, jakou logiku má do těchto get akcesorů doplnit. Vraťme se k metodě CreateClassProxy. Třetí argument je zřejmý, předáváme argumenty, se kterými má být zavolán konstruktor naší originální třídy. Posledním argumentem metody CreateClassProxy je objekt, který nás zajímá nejvíce – jedná se o tzv. interceptora, který bude použit vždy, když je na proxy použita metoda/vlastnost, kterou chceme “přepsat”. Stále píšeme obecné řešení zpožděného nahrávání kolekcí, a proto metodě CreateClassProxy předáme interceptora s výmluvným názvem LazyLoadInterceptor. LazyLoadInterceptor je ten “zázračný” typ, který jsem výše popisoval jako obecný deskriptor funkcí, které musí podporovat dynamická proxy pro instanci z každé třídy v business vrstvě.
Náš LazyLoadInterceptor, stejně jako každý jiný interceptor, musí podporovat rozhraní IInterceptor z Castlu. Rozhraní IInterceptor má jedinou metodu Intercept, kterou Castle zavolá vždy, když je volána metoda/vlastnost, kterou chceme “přepsat”.
Metodě Intercept je předán objekt Invocation, který nese základní informace o volané metodě.Kromě dalších vlastností je vhodné si zapamatovat, že v invocation.Method naleznete objekt MethodInfo (deskriptor metody) a v InvocationTarget zase konkrétní instanci, na které je metoda volána. Zdůrazním, že touto konkrétní instancí je v našem případě (dynamický) proxy objekt, ne instance originální třídy.
Zkusme si scénář v metodě Intercept projít. Mějme na paměti, že i když ten kód může vypadat na první pohled děsivě, neřeší nic jiného než ručně napsaná proxy výše. Zkusme se v našem popisu pro názornost zaměřit na konkrétní proxy objektu reprezentujícího zákazníka Josefa Nováka v momentě, kdy je poprvé přistoupeno k jeho kolekci Orders (seznam objednávek), i když kód v LazyLoadInterceptoru funguje analogicky ve všech dalších proxy business tříd v systému.
- Metoda Intercept nejprve na předaném objektu invocation volá metodu Proceed. Metoda Proceed vyvolá get akcesor originálního objektu a návratovou hodnotu (při prvním volání prázdnou typovou kolekci objednávek) nalezneme ve vlastnosti invocation.ReturnValue. Proč voláme nejprve invocation.Proceed? Protože potřebujeme v interceptoru kolekci, do které u zákazníka můžeme nahrát objednávky, a tuto kolekci stále spravuje instance originální třídy, jak si můžete ověřit ve výpisu třídy Customer.
- Jestliže se nejedná o první volání metody, nic neděláme, protože kolekce už musí být naplněna. V proměnné m_inspectedMethods máme názvy vlastností, které jsme již u daného objektu zpracovali.
- Jestliže invocation.ReturnValue je null, opět nic dalšího neděláme. Nemáme žádnou kolekci, do které bychom mohli nahrát objednávky.
- Do kolekce m_inspectedMethods přidáme název aktuální vlastnosti (Orders), protože jsme ji již začali zpracovávat.
m_inspectedMethods.Add(invocation.Method.Name);
- Nejprve potřebujeme zjistit, z jaké třídy pocházejí objekty, které budeme do kolekce, jejíž data nahráváme, přidávat. U objektu zákazník a kolekce Orders půjde samozřejmě o objekty z třídy Objednávka.
Type collectionItemType = invocation.Method.ReturnType.GetInterface(COLLECTION_INTERFACE_NAME).GetGenericArguments()[0];
- Dohledáme třídu z databázové vrstvy, která nám bude schopna vrátit seznam objednávek pro daného zákazníka.
Object dbComponent = findDbComponent(collectionItemType);
Pro účely článku je zvolena jednoduchá jmenná konvence - rozhraní pro přístup k databázi se jmenují vždy I<Název třídy>DbComponent. Pro objednávku tedy hledáme typ IOrderDbComponent. Pokud db komponentu nenalezneme, nic dalšího nemůžeme dělat.
- V nalezené db komponentě musíme najít metodu, která nám vrátí záznamy pro všechny objednávky zákazníka Josefa Nováka.
MethodInfo methodInfo = getDbCollectionMethodInfo(dbComponent, invocation.TargetType, collectionItemType);
Opět je zvolená jmenná konvence, kdy metoda má tvar Get{TypeInCollection}RecordsBy{ParentType}Id" a přijímá jeden argument typu int . V našem scénáři hledáme tedy na db komponentě metodu GetOrderRecordsByCustomerId, která přijímá id “rodičovského” zákazníka. V dalších článcích bych rád ukázal, jak se bez této i dalších dále zmíněných jmenných konvence obejdeme a budeme moci nakonfigurovat zpožděné nahrávání kolekcí přes jakkoli nazvané třídy a metody.
Stejně jako v předchozím bodě platí, že nenalezneme-li vyhovující metodu, nic dalšího nemůžeme dělat.
- Dále u objektu zákazník získáme hodnotu vlastnosti Id, kterou potřebujeme pro vyvolání metody na db komponentě nalezené v předchozím odstavci
int? objectId = getTargetObjectId(invocation.InvocationTarget);
- Zavoláme metodu GetOrderRecordsByCustomerId. Návratovou hodnotou je objekt DataTable, který v našem scénáři obsahuje záznamy všech objednávek patřících Josefu Novákovi.
DataTable retValues = methodInfo.Invoke(dbComponent, new object[] {objectId}) as DataTable;
- Přes pomocnou třídu SimpleObjectFactory, kterou LazyLoadInterceptor vyžaduje v konstruktoru, vytvoříme proxy objekty Objednávek a přidáme je do kolekce Orders zákazníka.
var targetCollection = invocation.ReturnValue;
addItemsToCollection(targetCollection, collectionItemType, retValues, invocation);
Navíc se u každé vytvořené objednávky pokusíme nastavit odkaz na “rodičovského zákazníka”, přesněji řečeno na proxy zákazníka. Opět je zvolena jmenná konvence, kdy objekt Order musí obsahovat vlastnost nazvanou Customer, jinak k nastavení “rodiče” nejde. Jak jsem již psal výše, v dalších článcích bychom si měli ukázat, jak tyto výchozí jmenné konvence rozšíříme a dovolíme i jejich úplné nahrazení.
- Hotovo, kolekce Orders u zákazníka Josefa Nováka je naplněna proxy objekty třídy Order a stejný scénář proběhne i při přístupu ke kolekci Items (položky objednávky) u každé objednávky.
Nyní můžeme vyzkoušet, jestli jsou proxy třídy generovány a hlavně jestli naše úsilí nebylo marné a proxy třídy podporují zpožděné nahrávání kolekce.
Výsledkem by měl být tento výpis, ze kterého je patrné :
- Místo originální třídy jsou používány proxy třídy.
- Kolekce jsou naplněny, i když ve třídách Customer ani Order žádný kód pro nahrání kolekce nemáme.
- Je naplněna kolekce Orders u zákazníka i kolekce Items u každé objednávky.
Následuje slibovaný výpis generické třídy SimpleObjectFactory.
Třída SimpleObjectFactory podporuje rozhraní SimpleObjectFactory a při vydání objektu:
- Funguje jako identitní mapa, takže každý objekt je nahrán jen jednou (nyní per process, což se dá snadno změnit).
- Používá záměrně služby “zastaralé” db vrstvy pro nahrání dat každého objektu – vytváření objektů ale deleguje na ProxyEngine.
- Pokusí se nastavit hodnoty jednoduchých vlastností u vytvořených proxy objektů – jestliže se název vlastnosti shoduje s názvem sloupce v datovém zdroji, je vlastnost objektu nastavena na hodnotu sloupečku, která je uložena v řádku vytaženém z databáze.
Co můžeme udělat dále v dalších článcích, jestliže budete mít zájem:
- Nebudeme spoléhat na jmenné konvence při plnění kolekci a “rodičovských” vlastností, ale dovolíme nakonfigurovat interceptora tak, abychom mohli spravovat asociace mezi třídami podle konvencí unikátních pro každý projekt, a přitom abychom nemuseli do těchto nízkoúrovňových proxy služeb moc zasahovat. Konfiguraci provedeme nejlépe pomocí fluentního API.
- LazyLoadInterceptor nebude používat stále dokola reflexi pro dohledání typů a metod, ale bude nalezené hodnoty cachovat.
- Budeme schopni podpořit i zpožděné nahrávání “rodičovských” vlastností. Prozatím je “rodičovská” vlastnost nastavena jen při nahrání kolekce – když vytáhnete z databáze jako první objednávku a sáhnete na její vlastnost Customer, vlastnost vám nyní vrátí null!
- Mohli bychom rozšířit proxy třídy o sledování změn vlastností a umožnit u každého objektu uložení změn nebo vrácení změn (undo).
- Zavedeme repozitáře (Repository), které i v hybridní aplikaci sjednotí ve vyšších vrstvách aplikace přístup k objektům, které jsou nahrány přes ORM i k objektům vytaženým z našich stávajících “old school” db/business služeb.
- Místo toho, abychom generovali dynamické proxy vždy po startu aplikace, umoříme jednorázově (pro většinu aplikací stejně zanedbatelnou) režii spojenou s tímto postupem vygenerováním a uložením assembly s dynamickými proxy při prvním spuštění nové verze aplikace. Při dalším spuštění aplikace se již použijí proxy ve vygenerované assembly.
Pro dnešek toho bylo ale myslím dost. Snad jen dodám, že jsem se v tomto článku chtěl vyhnout různým buzzwordům, ale fajnšmekrům potvrdím, co asi sami tuší, že jsme v tomto článku zavítali do hájemství AOP - aspektově orientovaného programování.
Celý projekt si můžete stáhnout, nejlépe přes Mercurial (hg). Součástí zdrojových kódů je i jednoduchá třída napodobující rozhraní tradičních “db komponent” pro přístup do databáze a zpřístupňující data v Datasetu.
Monday, 12 March 2012 14:06:49 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Entity Framework | Návrhové vzory
Friday, 24 February 2012
Entity Framework 4.3. Code First - (nechutný) problém s TPC mapováním?
Update 25. 2.2011: Tak chyba potvrzena EF týmem. Jedná se skutečně o chybu, která je částečně popsána v known issues.
Diego B Vega : @Rene Stein: Thanks for reporting this and for the repro. What you describe seems to be a bug in TPC mapping that we are already aware of and that we are planning to fix in the upcoming EF 4.3.1. Please take a look at the list of known issues above for more information.
Jedná se tedy o chybu, kterou někdo zmínil i v komentářích. Zarážející ale je, že k chybě “chybí sloupec v databázi” se dostanete teprve tehdy, kdy vygenerujete databázi s jiným než požadovaným schématem, odchytnete výjimku při dotazování a podíváte se na popisek vnořené výjimky. V “known issues” EF by spíš podle mě mělo být - ve verzi 4.3 se vám ani nepodaří vygenerovat databázi s TPC mapováním dědičnosti a volání metody MapInheritedProperties při konfiguraci entit je jen zbytečná dekorace v kódu a cvičení v marnosti.
Mohl by prosím někdo ověřit, že jsem buď udělal nějakou triviální chybu při mapování, anebo potvrdit mé podezření, že je EF Code First v poslední verzi 4.3 natolik prolezlý chybami, že v něm nefunguje ani tento triviální scénář.
Problém se snažím reprodukovat na tomto kódu.
Mám třídy Base a Derived. Jejich role asi vysvětlovat nikomu nemusím.
Snažím se pro mapování třídy Derived do databáze použít v db kontextu strategii TPC – table per (concrete) class (metoda MapInheritedProperties).
Po spuštění se aplikace vytvoří databáze se dvěma tabulkami. Struktura databáze ale odpovídá TPT strategii pro mapování dědičnosti:
Tabulka Base má sloupce Id a BaseProperty, tabulka Derived Id a Note. Volání MapInheritedProperties je tedy zcela ignorováno.
Jak popisuju i v kódu, matoucí je to, že Entity Framework sice mapuje třídy do databáze podle TPT strategie, ale dotazy klade, jako kdyby v databázi byly třída Derived namapována TPC strategií.
Vygenerovaný SQL dotaz do tabulky Derived vypadá takto:
SELECT '0X0X' AS [C1], [Extent1].[id] AS [id], [Extent1].[BaseProperty] AS [BaseProperty], [Extent1].[Note] AS [Note] FROM [dbo].[Derived] AS [Extent1] Schizofrenní Entity Framework se beze všech skrupulí snaží dohledat v tabulce Derived sloupec BaseProperty (TPC mapování), což pochopitelně skončí výjimkou při vykonávání dotazu, protože se jiná část jeho vícečetné osobnosti složené z nespolupracujících spoluautorů EF rozhodla při generování databáze, že TPT je pro každého aplikačního vývojáře vždycky jasná volba.
A protože perverzních projevů EF se při pátečním večeru nelze nabažit, tak tady je skript pro založení databáze, který jsem vytáhl z podkladového ObjectContextu a který by měl mapovat podle TPC, což se ale nestane, protože je proti databázi spuštěn skript zcela jiný.
Výsledek Trace.WriteLine(((IObjectContextAdapter) context).ObjectContext.CreateDatabaseScript());
Projekt s reprodukcí problému ke stažení.
Friday, 24 February 2012 22:06:21 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Entity Framework | LINQ
Monday, 21 March 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, 21 March 2011 13:11:41 (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7
Monday, 10 January 2011
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Dnešní článek je jen “intermezzem”, protože doplňuje předchozí článek o slíbenou informaci, jak můžeme automaticky registrovat serializovatelné třídy, jejichž instance nesou tranzientní stav, který je uložen v KnownTypesDictionary. Přechozí článek končil ukázkou, jak můžeme vrátit staticky definovaný seznam typů, dnes se poohlédneme po trochu dynamičtějším řešení. I když postup není omezen jen na WP7 aplikace, ale můžete ho použít v kterékoli aplikaci, která využívá (WCF) DataContractSerializer, my se zaměříme v článku opět hlavně na řešení WP7 specialitek.
Přečetli jste si první odstavec a nevíte, co je tranzientní stav? Hledáte marně v MSDN typ KnownTypesDictionary? Nedivte se, milostný akt sice můžete rozjet bez předehry, ale abyste rozuměli tomuto intermezzu, pečlivě a pomalu si přečtěte jako předehru k intermezzu tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”, i díl třetí, kde se bavíme o KnownTypesDctionary.
Předehru máte za sebou, takže víte, že KnownTypesDictionary je specializovaný objekt Dictionary, ve kterém je klíčem řetězec a hodnotou libovolný objekt a že v objektech KnownTypesDictionary ukládáme tranzientní stav view modelů při každém “tombstoningu”. I když KnownTypesDictionary může jako hodnotu nést libovolný objekt, my musíme instruovat DataContractSerializer, které objekty a hlavně z kterých odvozených tříd má v objektu Dictionary očekávat.
Když do KnownTypesDictionary ukládáme v jedné aplikaci instance z tříd OrderVO a InvoiceVO a v jiné aplikaci instance TwitterPost a ObservableCollection<TwitterPost >, musíme vždy znovu zmíněné třídy registrovat pomocí atributu KnownType.
Pro připomenutí následuje výpis kódu, kterým jsme končili a který registruje serializovatelné třídy v aplikaci “natvrdo” pomocí statické metody GetKnownTypes, jejíž název je předán do konstruktoru atributu KnownType.
Napevno zadrátované typy v KnownTypesDctionary nám nemusí vadit v jedné aplikaci, ale když chceme používat KnownTypesDctionary v mnoha různých aplikacích, musím řešení upravit.
Místo abychom třídy v metodě GetKnownTypes registrovali přímo, delegujeme odpovědnost za vrácení serializovatelných typů na specializovaného poskytovatele.
BTW: To víte, že první a okouzlující zákon vztahu mezi třídami zní: “Když je třída vyčerpána množstvím odpovědností, vždy si může přičarovat otroka, který špinavou práci udělá za ni”?. Občas se tomuto zákonu také říká SRP – princip jedné odpovědnosti třídy”.
Poskytovatel je objekt podporující rozhraní IKnownTypeProvider s jednou samopopisnou metodou.
Refaktorizujeme třídu KnownTypesDictionary, aby při vracení serializovatelných typů využívala objekt IKnownTypeProvider.
Do nové statické vlastnosti KnownTypeProvider je ve statickém konstruktoru přiřazen DefaultKnownTypesProvider, o kterém budeme mluvit za chvíli, ale do vlastnosti KnownTypeProvider můžete klidně vložit pro účely vaší aplikace lépe přizpůsobený IKnownTypeProvider.
Metoda GetKnownTypes vrátí všechny staticky registrované typy v poli KNOWN_TYPES společně s typy, které dodá IKnownTypeProvider. Typy jsou získány jen při prvním volání metody GetKnownTypes a jsou cachovány v proměnné _cachedKnownTypes, abychom opakovaným získáním typů z IKnownTypeProvider zbytečně neplýtvali výkonem WP7 telefonů ani trpělivostí uživatele. Předpokladem tohoto přístupu samozřejmě je, že v aplikaci je fixní množina serializovatelných typů, která se za běhu aplikace již nemění. Jestli vám to nevyhovuje, odstraníte cache z KnownTypesDictionary za 10 sekund.
Jaké odpovědnosti bude mít DefaultKnownTypesProvider?
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v hlavní assembly aplikace.
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v dalších assembly, ze kterých je složena aplikace.
- DefaultKnownTypesProvider nalezne další a pro aktuální aplikaci specifické objekty podporující rozhraní IKnownTypeProvider a vrátí seznam všech typů z těchto providerů.
- Pro všechny nalezené typy T zaregistruje i kolekci Observable<T>. To znamená, že pro třídu OrderVO označenou atributem DataContract je automaticky vrácen z poskytovatele nejen její Type (typof(OrderVO), ale i deskriptor kolekce typeof(ObservableCollection<OrderVO>).
Kód třídy DefaultKnownTypesProvider:
V metodě GetKnownTypes vyzvedneme všechny typy z “hlavní” (vstupní) assembly. Hlavní assembly získáme v metodě getExecutingAssembly přes vlastnosti třídy Deployment.
Typy z hlavní assembly sloučíme s typy v dalších assembly voláním metody getRefencedAssembliesTypes(). Ve WP7 jsem bohužel nenašel způsob, jak seznam dalších assembly získat, a proto další assembly poskytují objekty IAssemblyTypesProvider nalezené v hlavní assembly.
Chcete-li tedy automaticky vyhledat typy označené atributem DataContract v další assembly, vložte do hlavní assembly třídu podporující rozhraní IAssemblyTypesProvider.
Do proměnné collectionTypes v metodě GetKnownTypes vygenerujeme pro všechny nalezené typy jejich kolekce. Možná trochu složitě vypadající kód jen zabraňuje tomu, abyste nalezené deskriptory tříd typu ObservableCollection<T> v proměnné types balili znovu do kolekce ObservableCollection. Jinými slovy, když v proměnné types bude kolekce ObservableCollection<OrderVO>, do proměnné collectionTypes nebude generována kolekce ObservableCollection<ObservableCollection<OrderVO>>.
Na konci vracíme nalezené typy sloučené s vygenerovanými typy kolekcí ObservableCollection<T> a dalšími a pro aplikaci specifickými typy z ostatnich poskytovatelů . Za poskytovatele považujeme další objekty IKnownTypeProvider nalezené v hlavní assembly.
A tím máme hotovo.
Připomínám, že DefaultKnownTypesProvider můžete využít nejen ve WP7 aplikacích, ale ve všech aplikacích v .Net Frameworku, kde je používán DataContractSerializer a vy chcete automaticky registrovat odvozené serializovatelné typy.
Příště již začneme stavět aplikaci založenou na našem "mini frameworku”, abyste viděli, k čemu tyto počáteční díly seriálu včetně intermezza, které právě čtete, vůbec byly.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Monday, 10 January 2011 13:37:12 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Návrhové vzory | Silverlight | WP7
Monday, 15 November 2010
C# Posterous API Wrapper verze 0.0.3 – pro .NF 3.5+, Silverlight 4 a nově i s podporou pro Windows Phone 7
Ke stažení jsem uvolnil další verzi API wrapperu služby Posterous. Pracuji nyní na komerčním a delší dobu plánovaném projektu pro WP7, a proto jsem v rámci vývojářského ducha povznášejícího testování různých nedomyšleností a špeků tohoto nového nadělení od mobilní divize MS cvičně přidal už v Beta/RC verzi WP7 do Posterous API zkušební knihovnu pro WP7, kterou jsem nyní otestoval a doladil na “ostré”(sic! skutečně?) verzi WP7.
Další sarkasmy na adresu WP7 si dnes odpustím, těch už bylo dost na Twitteru , ale brzy sem dám nějaké postřehy, jaké to je vrátit se do časů Silverlightu 3, na kterém je mobilní verze Silverlightu založena, a hlavně tipy, jak si vývoj mobilních aplikací usnadnit.
Posterous tento měsíc uvolnil nové API založené na JSON formátu, wrapper pracuje zatím se starším XML formátem. I pro nové JSON API chci ale udělat wrapper.
Hlavní změny v této verzi
- Podpora pro Windows Phone 7.
- Odstraněny chyby při volání Posterous API, které byly způsobeny změnami v nastavení serveru na službě Posterous. Zvláště se jedná o http chybu 500 ihned při získávání Sites.
BTW: Příjemné je, že vývojáři Posterous reagují na připomínky rychle a sami přiznali, že upravovali zabezpečení služeb i u starého API.
- Mnoho drobných změn a opravy chyb.
Soubory ke stažení:
RStein.Posterous.API.dll - .Net Framework 3.5+
RStein.Posterous.API.dll - Silverlight 4
RStein.Posterous.API.dll - Windows Phone 7
Rozcestník:
- Úvod do mého Posterous API
- Ukázka práce s Posterous API – zálohování blogu
- Ukázka práce s Posterous API v Silverlightu (součást spotu s informací o uvedení verze 0.0.0.2)
- Posterous API a RX Framework.
Monday, 15 November 2010 16:25:38 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Silverlight | WP7
Monday, 23 August 2010
C# - kontrola existence vlastnosti u typu dynamic bez vyvolání výjimky RuntimeBinderException.
Dan Steigerwald mě na Facebooku upozornil na článek “Challenge: Dynamically dynamic” na blogu Ayende Rahiena. Jak se můžete sami podívat, celá výzva se týká toho, jak zjistit, jestli u dané instance typu dynamic existuje vlastnost se zadaným jménem, aniž byste museli odchytávat výjimku RuntimeBinderException, která vás na chybějící vlastnost sice drsně upozorní, ale zároveň vás nutí používat kód řízený výjimkami.
Jak vypadá kód detekující existenci vlastnosti s vy/zneužitím RuntimeBinderException?
private static bool HasPropertyNaive(IDynamicMetaObjectProvider dynamicProvider, string name)
{
try
{
var callSite =
CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
new[]
{
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.None, null)
}));
callSite.Target(callSite, dynamicProvider);
return true;
}
catch (RuntimeBinderException)
{
return false;
}
}
A použití:
static void Main(string[] args)
{
dynamic testDynamicObject = new ExpandoObject();
testDynamicObject.Name = "Testovaci vlastnost";
Console.WriteLine(HasPropertyNaive(testDynamicObject, "Name"));
Console.WriteLine(HasPropertyNaive(testDynamicObject, "Id"));
Console.ReadLine();
}
Stejně jako v zadání na blogu Ayende metoda HasPropertyNaive pracuje s každým objektem dynamic skrytým za rozhraním IDynamicMetaObjectProvider. V metodě napodobíme chování kompilátoru C# – vytvoříme “kontext operace”, tzv. CallSite, které předáme hlavně tzv. “Binder” voláním tovární metody metody Binder.GetMember. Binder, v našem případě binder pro get akcesor vlastnosti, jejíž přítomnost testujeme a jejíž název jsme předali metodě HasPropertyNaive v argumentu name, si lze zjednodušeně představit jako objekt, který je odpovědný za dohledání hodnoty vlastnosti u dynamického objektu za běhu aplikace.
U CallSite použijeme metodu Target, které předáme samotnou instanci callSite a objekt dynamic, u nějž chceme otestovat existenci vlastnosti. Jestliže vlastnost u objektu dynamic neexistuje, metoda Target vyvolá výjimku RuntimeBinderException a my vrátíme false, jinak ignorujeme návratovou hodnotu metody target a vracíme true, což je pro kód volající metodu HasPropertyNaive potvrzení, že vlastnost existuje.
Metoda HasPropertyNaive plní svůj účel, ale za cenu vyvolání výjimky RuntimeBinderException. A toho se týká právě “challenge”. Zkusme se výjimky zbavit.
Kdybychom měli testovat existenci vlastnosti jen u instancí “ExpandoObject”, měli bychom hned hotovo.
private static bool HasPropertyExpandOnly(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return ((IDictionary)dynamicProvider).ContainsKey(name);
}
ExpandoObject totiž podporuje rozhraní IDictionary a klíčem v objektu Dictionary jsou názvy vlastností.
Zadání ale vyžaduje, abychom zkontrolovali přítomnost vlastnosti u ktreréhokoli objektu dynamic typu IDynamicMetaObjectProvider. Když předáte metodě HasPropertyExpandOnly instanci dynamic, která dědí z DynamicObject nebo přímo implementuje rozhraní IDynamicMetaObjectProvider, při pokusu o přetypování instance na rozhraní IDictionary dojde k výjimce.
Problém s detekcí přítomnosti vlastnosti by také zcela zmizel, kdybychom měli zaručeno, že každá instance typu IDynamicMetaObjectProvider a s ní asociovaný “DynamicMetaObject” z metody GetDynamicMemberNames vrátí seznam s názvy všech dynamických členů.
private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return dynamicProvider
.GetMetaObject(Expression.Constant(dynamicProvider))
.GetDynamicMemberNames()
.Contains(name);
}
Bohužel ani to garantováno nemáme a metoda GetDynamicMemberNames u mnoha instancí dynamic bez skrupulí vrátí prázdné pole, i když vlastnosti existují.
Musíme si tedy poradit jinak.
Následuje kód metody HasProperty včetně podpůrných konstrukcí, která pracuje s libovolnou instanci typu IDynamicMetaObjectProvider a ke zjištění, zda je, či není vlastnost přítomna, nepotřebuje vyvolávat výjimku RuntimeBinderException.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace DynamicCheckPropertyExistence
{
class Program
{
private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
{
var defaultBinder = Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
new[]
{
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.None, null)
}) as GetMemberBinder;
var callSite = CallSite<Func<CallSite, object, object>>.Create(new NoThrowGetBinderMember(name, false, defaultBinder));
var result = callSite.Target(callSite, dynamicProvider);
if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
{
return false;
}
return true;
}
}
class NoThrowGetBinderMember : GetMemberBinder
{
private GetMemberBinder m_innerBinder;
public NoThrowGetBinderMember(string name, bool ignoreCase, GetMemberBinder innerBinder) : base(name, ignoreCase)
{
m_innerBinder = innerBinder;
}
public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
{
var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});
var noThrowVisitor = new NoThrowExpressionVisitor();
var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);
var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
return finalMetaObject;
}
}
class NoThrowExpressionVisitor : ExpressionVisitor
{
public static readonly object DUMMY_RESULT = new DummyBindingResult();
public NoThrowExpressionVisitor()
{
}
protected override Expression VisitConditional(ConditionalExpression node)
{
if (node.IfFalse.NodeType != ExpressionType.Throw)
{
return base.VisitConditional(node);
}
Expression<Func<Object>> dummyFalseResult = () => DUMMY_RESULT;
var invokeDummyFalseResult = Expression.Invoke(dummyFalseResult, null);
return Expression.Condition(node.Test, node.IfTrue, invokeDummyFalseResult);
}
private class DummyBindingResult {}
}
}
Proč se metoda HasProperty obejde bez vyvolání výjimky? Použil jsem trik, kdy objektu CallSite nepředávám přímo výchozí GetMemberBinder, ale vlastní NoThrowGetMemberBinder, který je potomkem bázové třídy GetMemberBinder z DLR. Můj NoThrowGetMember v kostruktoru přijímá další objekt GetMemberBinder, který interně použije pro zjištění hodnoty vlastnosti. Metoda HasProperty předává instanci NoThrowGetMember do konstruktoru tovární metodou Binder.CreateBinder vytvořený výchozí C# Binder, takže nemusíme v třídě NoThrowGetMember naštěstí duplikovat veškerou logiku pro přístup k vlastnosti, která je již součástí výchozího C# Binderu.
NoThrowGetBinderMember se spoléhá na to, že při pokusu o přístup k dynamickým metodám a vlastnostem u objektu IDynamicMetaProvider třída GetMemberBinder dovoluje odvozeným třídám, aby aplikovaly vlastní logiku pro práci s “dynamickými” členy v tzv. “fallback” metodách. NoThrowGetBinderMember tedy dostane šanci dohledat vlastnost v přepsané metodě FallbackGetMember.
Metoda FallbackGetMember pracuje takto:
1. Použije metodu Bind předaného Binderu (m_innerBinder) , které předá jako první argument “DynamicMetaObject” v argumentu target a druhým argumentem je prázdné pole objektů “DynamicMetaObject”. Výchozí Binder udělá svou práci a vrátí nám další DynamicMetaObject, který si uložíme do proměnné retMetaObject.
var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});
2. V retMetaObject je vyhodnocovací výraz (Expression tree) pro získání hodnoty vlastnosti, který pro vlastnost Name může vypadat zjednodušeně takto. Tučně je vyznačena část, která je odpovědná za vyvolání výjimky, jestliže vlastnost neexistuje.
IIF(ExpandoTryGetValue(Convert($arg0), value(System.Dynamic.ExpandoClass), 0, "Name", False, value), value, throw(new RuntimeBinderException("'System.Dynamic.ExpandoObject' does not contain a definition for 'Name'")))
IIF(ExpandoCheckVersion(Convert($arg0), value(System.Dynamic.ExpandoClass)), {var value; ... }, gotoCallSiteBinder.UpdateLabel)
My ale výjimku vyvolávat nechceme, a proto vlastním vizitorem NoThrowExpressionVisitor modifikujeme “expression tree” tak, že místo vyvolání výjimky, když vlastnost neexistuje, vrátíme hodnotu statické proměnné DUMMY_RESULT. Vlastnost Expression u proměnné retMetaObject je určena pouze pro čtení, proto vytvoříme nový DynamicMetaObject s upravenou “Expression” a původními restrikcemi a uložíme ho do proměnné finalMetaObject, která je také návratovou hodnotou metody FallbackGetMember.
var noThrowVisitor = new NoThrowExpressionVisitor();
var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);
var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
return finalMetaObject;
Úplný kód třídy NoThrowExpressionVisitor naleznete ve výpisu výše.
Metoda HasProperty vyvolá metodu Target na objektu CallSite a zkontroluje, zda její návratová hodnota je referenčně shodná s hodnotou v proměnné NoThrowExpressionVisitor.DUMMY_RESULT a pokud tomu tak je, vrátí false, protože nyní místo vyvolání výjimky byla vrácena zástupná hodnota signalizující “vlastnost u objektu dynamic neexistuje”, jinak vrátí true - “vlastnost existuje”.
var result = callSite.Target(callSite, dynamicProvider);
if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
{
return false;
}
Použití metody HasProperty.
static void Main(string[] args)
{
dynamic testDynamicObject = new ExpandoObject();
testDynamicObject.Name = "Testovaci vlastnost";
Console.WriteLine(HasProperty(testDynamicObject, "Name"));
Console.WriteLine(HasProperty(testDynamicObject, "Id"));
Console.ReadLine();
}
/*Výsledek:
True
False
*/
Zkoušel jsem metodu HasProperty použít i na zjišťování existence vlastnosti u potomků třídy DynamicObject pro zpracování rss a vše funguje dle očekávání.
“Challenge” pokořen. Obvyklá poznámka na závěr – za nic neručím, kód nemusí fungovat v dalších verzích DLR, C# a .Net Frameworku, ale to vy určitě víte.:)
Monday, 23 August 2010 14:33:03 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | LINQ | Programátorské hádanky
Sunday, 21 March 2010
Výhody a nevýhody softwarových továren
Emailem jsem dostal zajímavou otázku, jaký je můj názor na softwarové továrny a kde vidím výhody a nevýhody softwarových továren. Odpověď nakonec publikuji i zde – už jen proto, že jsem si při jejím psaní uvědomil, že na továrnu kladu stejné nároky jako na kteroukoli další knihovnu v systému a že výběr softwarové továrny se u mě moc neliší od výběru třeba ORM Frameworku. Nejde o taxativní výčet výhod a nevýhod, ale spíš o volně nahozená témata, která mě za 20 minut psaní příspěvku napadla.
SW továrny jsou jen pokračováním trendu, že nemá smysl vynalézat znovu kolo ani v případě, že by bylo o dvě setiny procenta krásnější než to staré, které je již navržené a hotové. SW továrna říká – namísto opakování chyb svých předchůdců použijte ověřené zkušenosti, osvědčené praktiky, doporučené postupy a již otestovaný kód. Namísto obecného návrhového vzoru nebo vágně popsaného doporučovaného postupu máte k dispozici osvědčenou knihovnu, základní průvodce a odborný polštář, který by měl zabránit, že napácháte v dané oblasti velké množství hrubých bezpečnostních nebo jen s duchem technologie neslučitelných chyb.
Takže výhody:
1) Osvědčená řešení ihned po ruce. Tím se cíl SW faktory moc neliší od obecných frameworků a návrhových vzorů.
2) Není nutné psát infrastrukturní kód přímo v SW firmě (ISV). Je možné se soustředit na problémovou doménu zákazníka a ne na to, jak centrálně publikovat události nebo jak propojit různé pohledy v aplikaci tak, aby spolu dokázaly komunikovat .
3) Pokud stejnou továrnu používáte napříč všemi projekty, je zaučení nového vývojáře na dalším projektu jednodušší a rychlejší.
4) SW továrna používá praktiky osvědčené v dané technologii. Takže odlišně jsou zakódovány původně platformně nezávislá doporučení a všeobecně zaměřené vzory v SW továrně pro ASP.Net a jinak pro Silverlight. Místo popisu obecných návrhových vzorů je již vzor adaptován na konkrétní cílové prostředí.
Nevýhody:
1) Kód nemáte pod kontrolou a musíte se spolehnout, že továrna sama neobsahuje příliš kritických chyb ani otravných chyb s nižší prioritou. To platí o jakékoli abstrakci (frameworku, knihovně), kterou na projektu používáte, ale myslím, že ani v Microsoftu stále nemají SW továrny stejnou podporu jako samotný .Net Framework. A také by měla být známa alespoň orientační roadmapa SW továrny, abyste věděli, kam autoři směřují a jestli jsou si vědomi nevýhod a omezení stávající verze SW továrny. Od továrny, která se ihned po svém uvedení honosí přídomky „experimentální, testovací, zkušební“, bych dal ruce pryč. Jen ti nejodvážnější z nás si dovolí svým zákazníkům za půl roku po zatracení SW továrny jako slepé uličky tvrdit, že musí trochu poštelovat kardiostimulátor výkonného srdce aplikace a že místo plánovaných dvou hodin za přidání dvou vlastností do objednávky zákazník zaplatí dva měsíce migrace na jinou SW továrnu. Nenechal bych se zmýlit tím, že některé firmy pro odvážlivce používající jejich experimentální SW továrny a frameworky razí lichotivý titul „early adopter“ – méně korektní a pravdě bližší překlad z marketingového slovníku totiž zní „natěšený všehoschopný blbec“.
2) Univerzální řešení jako je SW továrna může být pro jednoduché aplikace zabijákem. Více času strávíte integrací aplikace do rámce vynucovaného SW továrnou než psaním obchodní logiky specifické pro aplikaci a důležité pro zákazníka.
3) U některých složitějších aplikací zjistíte, že rámec SW továrny je příliš těsný a vy potřebujete SW továrnu pro svůj projekt upravit nebo rozšířit. Náklady na rozšíření služeb , „hackování“ a ohýbaní SW továrny pro danou problematiku mohou převážit nad výhodami SW továrny. Z velké radosti nad úsporou času v raných fázích projektu, kde byla továrna použita, můžete ještě před dodáním první verze projektu a po pořádném časovém skluzu, vynuceném třeba přepisem částí továrny, které pro výkonnostních testech nestačí stávajícím požadavkům na projekt, přejít k jadrným kletbám nad šílenou SW továrnou, v níž si úprava jednoho modulu kaskádově vynutí úpravy všech dalších modulů. Místo používání původně slibované elegantní černé skříňky s jednoduchými službami se prohrabujete nevábnými vyhřezlými vnitřnostmi mizerně navržené SW továrny.
Z výhod a nevýhod snad vyplývá můj postoj k SW továrnám. SW továrny jsou dalším evolučním stádiem na cestě, jejímž cílem je využít pro danou problematiku osvědčená řešení a postupy, nepsat stále se opakující kód nebo v každém řešení už jen změnou typu klienta (Web, Windows Forms, Silverlight, mobilní aplikace) neprocházet znovu a znovu všechny slepé vývojářské uličky specifické pro použitou technologii. Objevování objeveného ponechme těm, kterým stačí zařvat vítězoslavně heuréka, i když jsou v pořadí stí nebo tisící, kteří stejnou infrastrukturní nebo funkční trivialitu konečně zakódovali rozumným a v životním cyklu projektu dále udržovatelným způsobem.
A samozřejmě si nemyslím, že tohle evoluční stádium je konečné. :)
Sunday, 21 March 2010 11:57:10 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Analytické drobky | Návrhové vzory | UML
Wednesday, 24 February 2010
Podivné? chování při explicitním přetypování typu dynamic ve Visual Studiu 2010 RC
Na twitteru jsem psal, že si pohraju s implementací rozhraní ve třídě přes automatickou delegaci na privátní proměnnou s využitím nového typu dynamic v C# 4.0. Jestliže se dobře pamatuji, většinou se po nějakém takovém řešení pídí Delphisté. Z příkladu níže bude asi jasné i pro ostatni, co mám předchozími hutnými větami na mysli .
Při hraní si s typem dynamic jsem ale narazil na zvláštní chování při explicitním přetypování a chtěl bych poprosit někoho dalšího z mých čtenářů o vyzkoušení stejného chování ve Visual Studiu 2010 (nejlépe nejen na RC, ale i na starší Betě 2, kterou jsem už smazal). Příklad níže je jen jednoduchý “jednosměrný” prototyp, na kterém vynikne problém s explicitním přetypováním.
Zde je mnou zmiňovaná podivnost (problém):
Mějme rozhraní IWorker:
public interface IWorker
{
void DoWork();
}
A třídu Worker, která toto rozhraní implementuje.
class Worker : IWorker
{
#region Implementation of IWorker
public void DoWork()
{
Console.WriteLine(GetType().ToString());
}
#endregion
}
Dále máme třídu Order, která rozhraní IWorker neimplementuje, ale má privátní proměnnou m_worker implementující toto rozhraní, kterou předá své bázové třídě DirtyCastBase. DirtyCastBase je třída, která zajistí, že bude-li klient přetypovávat instanci Order na rozhraní IWorker, tak toto přetypování projde a klient dostane jako implementora instanci m_worker.
public class Order : DirtyCastBase
{
private IWorker m_worker;
public Order() : base()
{
m_worker = new Worker();
SetImplementors(m_worker);
}
}
Třída DirtyCastBase je potomkem třídy DynamicObject, která nám v .Net 4.0 dovoluje reagovat na “dynamická volání” a přidat jednoduše “dynamické chování” přepsáním metod začínajících písmeny Try (TryGetMember, TrySetMember ) apod. Já jsem přepsal metodu TryConvert, která se s využitím chráněné virtuální metody TryFindImplementor pokusí nalézt objekt, který podporuje rozhraní vyžadované uživatelem. Deskriptor rozhraní je předán ve vlastnosti Type argumentu binder.
public class DirtyCastBase : DynamicObject
{
private IEnumerable<Object> m_implementors;
private Dictionary<Type, object> m_castContext;
public DirtyCastBase()
{
m_castContext = new Dictionary<Type, object>();
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
Type requestedType = binder.Type;
Tuple<bool, Object> FindResult = TryFindImplementor(m_implementors, m_castContext, requestedType);
if (FindResult.Item1)
{
result = FindResult.Item2;
return true;
}
return base.TryConvert(binder, out result);
}
protected virtual Tuple<bool, Object> TryFindImplementor(IEnumerable<Object> implementors, Dictionary<Type, object> currentCastContext, Type requestedType)
{
if (implementors == null)
{
throw new ArgumentNullException("implementors");
}
if (currentCastContext == null)
{
throw new ArgumentNullException("currentCastContext");
}
if (requestedType == null)
{
throw new ArgumentNullException("requestedType");
}
object result = null;
bool found = m_castContext.TryGetValue(requestedType, out result);
if (!found)
{
result = (from implementor in implementors
where implementor != null
let type = implementor.GetType()
where requestedType.IsAssignableFrom(type)
select implementor).FirstOrDefault();
found = result != null;
}
if (found)
{
m_castContext.Add(requestedType, result);
}
return new Tuple<bool, object>(found, result);
}
protected void SetImplementors(params object[] implementors)
{
SetImplementors(implementors.AsEnumerable());
}
protected void SetImplementors(IEnumerable<object> implementors)
{
if (m_implementors != null)
{
throw new InvalidOperationException();
}
m_implementors = implementors ?? Enumerable.Empty<Object>();
}
}
V našem případě by tedy třída Order by měla dovolit přetypování na rozhraní IWorker, i když sama toto rozhraní neimplementuje. Zanedbejme nyní, že není zachována referenční identita při konverzi i že vydáváme jako implementora privátní objekt, protože pro demonstrovanou techniku to není příliš podstatné.
Tento kód ověří, že přetypování projde. Využíváme implicitní (“bez závorek”) konverzi. Samozřejmě že je nutné instanci Order přiřadit do proměnné typu dynamic.
class Program
{
static void Main(string[] args)
{
dynamic order = new Order();
IWorker worker = order;
worker.DoWork();
Console.ReadLine();
}
}
Implicitní konverze projde, a jak jsem očekával, je vyvolána naše metoda TryConvert.
Když ale projde implicitní konverze, proč explicitní konverze selže a metoda TryConvert vyvolá výjimku?
static void Main(string[] args)
{
dynamic order = new Order();
IWorker worker = (IWorker) order;
worker.DoWork();
Console.ReadLine();
}
Ihned je vyvolána výjimka InvalidcastException {"Unable to cast object of type 'DynamicCastTest.Order' to type 'DynamicCastTest.IWorker'."}
Z call stacku výjimky (at CallSite.Target(Closure , CallSite , Object ) se dá odvodit, že výjimku vyhodil C# DLR binder, který se ale ani nepokusil zavolat metodu TryConvert. Metoda TryConvert by ve vlastnosti Explicit argumentu binder měla dostat příznak, že šlo o explicitní konverzi, ale tato metoda evidentně není volána.
Přitom z dokumentace metody TryConvert se dá usoudit, že by metoda TryConvert měla být při explicitní konverzi volána.
Ještě jsem zcela do detailu nestudoval všechna pravidla pro typ dynamic ve specifikaci C# 4.0, ale tohle na mě působí jako bug. Pokud projde implicitní konverze, proč by byla zcela zakázána explicitní? To vůbec není v souladu s pravidly kompilátoru pro konverze v C#:
“The set of explicit conversions includes all implicit conversions. This means that redundant cast expressions are allowed.” (C# specification sekce 6.2 Explicit conversions)
A C# DLR binder se i za běhu snaží podle mých dosavadních zkušeností vždy co co nejvěrněji napodobit chování C# kompilátoru.
Anebo už jsem dnes utahaný, nejde o žádnou anomálii a něco triviálního ve svém kódu přehlížím?
Tedy znovu. Můžete někdo spustit projekt ve svém Visual Studiu (nejlépe i v Betě 2) a podělit se o výsledek?
Zde je projekt ke stažení.
Díky!
Wednesday, 24 February 2010 19:19:03 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C#
Tuesday, 02 February 2010
Hrátky s Reaktivním frameworkem (RX extenze)
V předchozím článku jsem ukazoval, jak volat asynchronně metody z C# Posterous API v Silverlightu. C# Posterous API nabízí asynchronní zpracování pomocí jednoho z doporučovaného přístupu k asynchronním operacím v .Net Frameworku – metoda s konvenčním sufixem Async (LoadPostsAsync) spustí vykonání operace v jiném vlákně a výsledky operace jsou nabídnuty v argumentech události, která je (opět) jen dle jmenné konvence spojena s asynchronní operací (událost LoadPostsCompleted). C# Posterous API nenabízí ve svém rozhraní metody pro podporu dalšího a již od verze 1.0 .Net Frameworku přítomného asynchronního vzoru, který je spojen s dvojicí metod začínajících prefixem Begin a End. (BeginGetRequest, EndGetRequest, BeginRead, EndRead apod.)
Dále předpokládám, že oba přístupy k vytváření asynchronních opreací znáte a že jste si vědomi i toho, jak se způsob práce s asynchronními API odlišuje od práce s běžnými synchronnními metodami.
V již odkazovaném článku bylo dobře patrné, jak je řízení toku asynchronních operaci odlišné od sady volání běžných synchronních operací.
Pro připomenutí:
posterousAccount.SitesLoaded += (o, e) =>
{
throwIfAsyncEx(e.Exception);
posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
{
throwIfAsyncEx(e2.Exception);
Posts = (from p in e2.Value
select new ViewPost
{
Title = p.Title,
Body = p.Body,
Url = p.Url
}).ToList();
};
posterousAccount.PrimarySite.LoadAllPostsAsync();
};
posterousAccount.LoadSitesAsync();
Jediné, co tento kód dělá, je, že nejprve (!) nahraje všechny blogy (příkaz k asynchronnímu nahrání posterousAccount.LoadSitesAsync(); je na posledním (!) řádku. Na prvním (!) řádku máme zpracování výsledku volání metody LoadSitesAsync, ve kterém opět nejdříve (!) lambdou přihlášenou k odběru události PostsLoaded (posterousAccount.PrimarySite.PostsLoaded += (_, e2)) řekneme, jak zpracujeme výsledek následného (!) volání další asynchronní metody (posterousAccount.LoadSitesAsync());. Tato “inverzní“ práce s asynchronními metodami a zpracováním jejich výsledku je na hony a možná ještě dále vzdálena intuitivní práci se synchronními metodami.
Zkusme se nyní podívat, jak by nám s “převrácením starších asynchronních metod z hlavy zpět na synchronní nohy” mohl pomoci RX Framework. Úplné základy v tomto článku nezazní a začátečníky odkazuji na sérii přednášek na Channel 9, kde dozvíte i zajímavé podrobnosti o genezi celého RX Frameworku a matematické dualitě rozhraní IEnumerable a IObservable (jinými slovy o společných rysech dobře známých GoF návrhových vzorů Iterátor a Observer).
Současné příklady jsou vytvořeny v aplikaci Windows Forms pro .Net 3.5. Silverlight má své zvláštnosti a a rozchození příkladů v SL si zaslouží další článek, protože teď by řešení problémů specifických pro SL zamlžovalo cíl příkladu. Aplikace je pro .Net 3.5, protože stejná aplikace pro .Net 4.0 hlásí konflikt (ambiguous reference) mezi NF typy a RX typy.
Upozornění: Nic z toho, co napíšu neberte ani jako dogmata ani, nedej bože, jako best practices. RX Framework je v Betě, zdokumentován je mizerně a z jednoho řádku u každé metody se dá jen těžko bez dalších experimetů vytušit, co přesně metoda dělá. Tento článek je výsledkem hraní si pro účely jednoho projektu, kam se RX extenze hodí a zjednodušují (alespoň to tak prozatím vypadá ) dost rutinních činností.
Zde j výsledek našeho snažení, abychom měli motivaci se RX Frameworkem zabývat.
var resultPosts = from sites in account.GetSites()
from site in sites.ToObservable()
from posts in site.GetPosts()
from post in posts.ToObservable()
where post.Private == false
select post;
Získání blogů (Sites) i blogpostů (post) je stále asynchronní, ale výsledný kód vypadá jako běžný LINQ (To Enumerable) dotaz. Žádné inverzní volání a práce s výsledkem, jen prostý dotaz, jehož zvláštností je pouze to, že v některých místech voláme metodu ToObservable.
Jak jsem dosáhl tohoto výsledku?
Podíváme-li se na první řádek, vidíme, že voláme metodu account.GetSites. Metoda GetSites součástí C# Posterous API není a jedná se o extenzní metodu. Tato extenzní metoda je zvláštní tím, že její návratovou hodnotou je je jedno z klíčových rozhraní v RX Frameworku – rozhraní IObservable<T>.
public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
Rozhrani IObservable má v RX Frameworku podobný význam jako rozhraní IEnumerable v celém .Net Frameworku. Zjednodušeně můžeme rozhraní IObservable popsat jako ceduli, kterou třída implementující rozhraní dává celému světu najevo: “Miluju voyery, jestliže chcete sledovat, co se ve mně děje, dejte mi sem pozorovatele a já na sebe všechno podstatné, co se od této chvíle stane, postupně vyzvoním ”.
Rozhraní IObservable je tedy příslib, že zainteresovaný pozorovatel dostane data, která třída podporující toto rozhraní nabízí. Svůj zájem pozorovatel deklaruje tak, že předá odkaz sám na sebe do metody Subscribe.
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
Pozorovatel (IObserver) reaguje (proto reaktivní framework) na informace, které jsou mu poskytnuty objektem podporujícím rozhraní IObservable.
public interface IObserver<T>
{
void OnCompleted();
void OnNext(T value);
void OnError(Exception exn);
}
Metoda OnNext je na IObserver volána vždy, když Observable objekt má k dispozici další data. Metodou OnError Observable objekt signalizuje chyby a metodou OnCompleted Observeru říká “jsem u konce, nic dalšího už pro tebe nemám”.
Naše metoda GetSites tedy říká – zavolejte mě a já vám nabídnu IObservable objekt, který, až budou data k dispozici, vašemu observeru (IObserver) vydá kolekci (IEnumerable) objektů IPosterousSite.
Extenzní metoda GetSites vypadá takto:
public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
{
checkAccountNotNull(account);
var sitesEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousSite>>>(handler => account.SitesLoaded += handler,
handler => account.SitesLoaded -= handler)
.Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
return sitesEvents.GetFinalObservableEvents(account.LoadSitesAsync);
}
Po kontrole, zda předaný IPosterousAccount není null, využijeme pomocnou metodu Observable.FromEvent z RX Frameworku, která nám vrátí IObservable objekt. Tento IObservable objekt notifikuje případného observera o každé nastalé události sites.Loaded. V našem případě Observera notifikuje o právě jedné události, protože jsme použili metodu Take (Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT)) a konstanta DEFAULT_TAKE_EVENTS_COUNT má hodnotu 1. Jak si můžete všimnout, metoda FromEvent nám dovoluje s událostmi, které postupně nastávají, zacházet jako (s potenciálně nekonečnou) kolekcí hodnot. Metodě FromEvent jsme pouze museli říct, jaká třída nese argumenty událost (EventArgsValue<IEnumerable<IPosterousSite>) a poskytli jsme ji dva delegáty pro registraci/deregistraci obslužných handlerů, které nám RX Framework předá (handler => account.SitesLoaded += handler, handler => account.SitesLoaded –= handler). U našeho volání metody Take bych ještě poznamenal, že po vyvolání první události dojde automaticky RX Frameworkem k deregistraci obslužného handleru.
Proměnná sitesEvents je IObserver tohoto typu.
IObservable<IEvent<EventArgsValue<IEnumerable<IPosterousSite>>>>
Argumenty události jsou vždy zabaleny do instance třidy IEvent, která je vydána zaregistrovanému observeru v jeho metodě OnNext. Všimněte si ale, že návratovou hodnotou metody GetSites je již Observable, který observeru předá hodnoty bez IEvent (IObservable<IEnumerable<IPosterousSite>>).
Vidíme, že na sitesEvents je volána další má extenzní metoda GetFinalObservableEvents, které je předán delegát Action ukazující na asynchronní metodu account.LoadSitesAsync, a výsledek volání GetFinalObservableEvents je vrácen klientovi.
Metoda GetFinalObservableEvents:
public static IObservable<TEventData> GetFinalObservableEvents<TEventData>(this IObservable<IEvent<EventArgsValue<TEventData>>> sourceEvents, Action runAction)
{
if (sourceEvents == null)
{
throw new ArgumentNullException("sourceEvents");
}
var retObservable = new DelegateObservable<TEventData>(
observer =>
{
var eventObserver = new EventObserver<TEventData>(observer);
var unsubScribe = sourceEvents.Subscribe(eventObserver);
runAction();
return unsubScribe;
});
return retObservable;
}
Metoda GetFnalObservableEvents vrací opět Observable, ale tentokrát jde o Observable typu IObservable<TEventData> - jinými slovy, v našem případě IObservable<IEnumerable<IPosterousSite>>. Jak je toho dosaženo? Zdrojový IObservable objekt nazvaný sourceEvents je předán instanci třídy DelegateObservable, což je v současném scénaři již ten hledaný Observable podporující rozhraní IObservable<IEnumerable<IPosterousSite>>. DelegateObservable je tedy adaptér, který převádí události zabalené do IEvent na “rozbalené” hodnoty očekávané observerem. DelegateObservable je můj pomocný IObservable, který dostává do konstruktoru lambdu představující tělo jeho metody Subscribe, abychom nemuseli reimplementovat rozhraní IObservable v různých třídách stále dokola.
Výpis třídy DelegateObservable
public class DelegateObservable<T> : IObservable<T>
{
private readonly Func<IObserver<T>, IDisposable> m_subscribeDel;
public DelegateObservable(Func<IObserver<T>, IDisposable> subscribeDelegate)
{
m_subscribeDel = subscribeDelegate;
if (m_subscribeDel == null)
{
throw new ArgumentNullException("subscribeDelegate");
}
}
#region Implementation of IObservable<out T>
public IDisposable Subscribe(IObserver<T> observer)
{
if (observer == null)
{
throw new ArgumentNullException("observer");
}
return m_subscribeDel(observer);
}
#endregion
}
Předaná lambda v našem případě vytvoří instanci třídy eventsObserver, což je observer, který bude zpracovávat přicházející události, a do konstruktoru mu podhodí observer předaný klientským kódem – eventsObserver je tedy další adaptér, který je zdopovědný za “rozbalení” dat z instance IEvent a za předání těchto dat klientskému (“konečnému”) observeru.
Třída EventObserver:
public class EventObserver<T> : IObserver<IEvent<EventArgsValue<T>>>
{
private readonly IObserver<T> m_innerObserver;
private bool m_exceptionOccured;
public EventObserver(IObserver<T> innerObserver)
{
if (innerObserver == null)
{
throw new ArgumentNullException("innerObserver");
}
m_innerObserver = innerObserver;
m_exceptionOccured = false;
}
#region Implementation of IObserver<T>
public virtual void OnNext(IEvent<EventArgsValue<T>> eventData)
{
if (eventData.EventArgs.Exception != null)
{
OnError(eventData.EventArgs.Exception);
return;
}
//Rozbalení a předání dat Observeru
m_innerObserver.OnNext(eventData.EventArgs.Value);
}
public virtual void OnError(Exception exception)
{
m_innerObserver.OnError(exception);
m_exceptionOccured = true;
}
public virtual void OnCompleted()
{
//Chyba ukončí sekvenci sama o sobě
if (!m_exceptionOccured)
{
m_innerObserver.OnCompleted();
}
}
Třída EventObserver implementuje rozhraní IObservable s těmito generickými argumenty - IObserver<IEvent<EventArgsValue<T>>> . V C# Posterous API všechny události předávají svá data v instanci třídy EventArgsValue<T>, což znamená, že pro naše účely je EventObserver univerzálně použitelný observer pro zpracování výsledků asynchronní operace.
Pro úplnost zde je výpis třídy EventArgsValue
public class EventArgsValue<T> : EventArgs
{
private readonly T m_value;
private readonly Exception m_exception;
internal EventArgsValue(T value, Exception exception)
{
m_value = value;
m_exception = exception;
}
public T Value
{
get
{
return m_value;
}
}
public Exception Exception
{
get
{
return m_exception;
}
}
}
V lambdě předané do objektu DelegateObservable také musíme spustit asynchronní operaci – to je volání Action delegáta runAction, který nám předala již metoda GetSites. Každý IObservable také z metody vrací objekt implementující rozhraní IDisposable – volání metody Dispose dovoluje klientovi odpojit se objektu IObservable. Lambda předaná do instance DelegateObservable vrátí IDisposable objekt, který je vydán po připojení EventObservera k “streamu události“ (sourceEvents).
Přidání extenzních metod k dalším třídám je triviální – zde je extenzní metoda pro IPosterousSite, která nahraje všechny blogposty.
public static IObservable<IEnumerable<IPosterousPost>> GetPosts(this IPosterousSite site)
{
throwIfSiteNull(site);
var postsEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousPost>>>(handler => site.PostsLoaded += handler,
handler => site.PostsLoaded -= handler)
.Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
return postsEvents.GetFinalObservableEvents(site.LoadAllPostsAsync);
}
A nyní se můžeme znovu podívat, jak naše API použijeme v klientském kódu – díky alternativní implementaci Query vzoru v RX frameworku můžeme používat staré dobré známé LINQ dotazy.
Metoda loadPosts:
private void loadPosts()
{
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;
IPosterousApplication app = PosterousApplication.Current;
IPosterousAccount account = app.GetPosterousAccount("<Posterous user name>", "Posterous password");
var syncContext = SynchronizationContext.Current;
var resultPosts = from sites in account.GetSites()
from site in sites.ToObservable()
from posts in site.GetPosts()
from post in posts.ToObservable()
where post.Private == false
select post;
resultPosts.Subscribe(post =>
{
lock (m_threadsSet)
{
m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
}
syncContext.Post(_ =>
{
var UCpost = new UC_Post
{
Title = post.Title,
Body = post.Body
};
flowLayoutPanel1.Controls.Add(UCpost);
},
null
);
},
ex => syncContext.Post(_ =>
{
throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
},
null),
() => syncContext.Post(_ =>
{
lock (m_threadsSet)
{
m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
}
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
},
null
));
}
V proměnné resultPosts jsou uloženy všechny blogposty (IPosterousPost) ze všech blogů (IPosterousSite). Blogy i blogposty jsou nahrány asynchronně, ale v klientském kódu nevidíme žádná specialitky kvůli asynchronnímu nahrávání dat. Na proměnných sites i posts v dotazu je volána další extenzní metoda z RX Frameworku ToObservable, protože jak víme, výsledkem volání asynchronních metod byly typy IEnumerable<IPosterousPost> a IEnumerable<IPosterousSite>.
Důležité je, že zpracování dotazu je opět “lazy” – to znamená, že k získání dat dojde až poté, co k resultsPosts zaregistruju svého Observera metodou Subscribe (analogie “Lazy” vyhodnocování v LINQ To IEnumerable a procházení dotazu v cyklu foreach). Metoda Subscribe má několik variant a jedna z nich nám dovoluje pro metody OnNext, OnError a OnCompleted předat delegáty, aniž bychom byli nuceni vytvářet svou třídu implementující rozhraní IObserver.
První delegát (OnNext) vezme předaný post a vytvoří pro něj UserControl, který vloží do FlowPanelu na formuláři. Ještě předtím pro zajímavost do Hashsetu ukládám identifikátory vláken, které se na zpracování dotazu podílí. S prvky na formuláři můžeme pracovat jen z UI threadu, a proto je vložení User controlu provedeno přes SynchronizationContext uložený do proměnné syncContext před spuštěním dotazu.
Druhý delegát (OnError) pouze přes SynchronizationContext zpropaguje výjimku, která nastala při asynchronním zpracování, do UI threadu. Všimněte si, jak je zpracování výjimek jednoduché – rozdíl vynikne při srovnání s opakovaným voláním metody throwIfAsyncEx v kódu na začátku tohoto článku.
Třetí delegát (OnCompleted) naplní listbox na formuláři ID použitých threadů a změní text ve status baru.
Zde je výsledný formulář. V listboxu nahoře si můžete všimnout, že u mě byly k vykonání dotazu použity celkem 3 thready.
Tím bychom mohli skončit, ale RX Framework má pro asynchronní operace ještě další zajímavou podporu. Pomocí metody Observable.FromAsyncPattern můžeme vytvořit IObservable rychle a bezpracně ze standardního a výše již zmíněného asynchronního Begin/End vzoru. V C# Posterous API metody Begin*/End* nejsou, proto je zkusme dodat pomocí extenzních metod.
Rozhraní IPosterousAccount bude obohaceno o extenzní metody BeginLoadSites a EndLoadSite.
Metoda BeginLoadSites
public static IAsyncResult BeginLoadSites(this IPosterousAccount account, AsyncCallback callback, object context)
{
checkAccountNotNull(account);
var loadSiteAction = new Action(account.LoadSites);
return RXEventsHelper.GetAsyncResultEx(loadSiteAction, callback, context);
}
Jak vidíte, přesně dle konvencí .Net vzoru metoda vrací odkaz na rozhraní IAsyncResult a přijímá callBack, což je tedy u tohoto vzoru metoda, která má být vyvolána po dokončení asynchronního zpracování, a jak také vzor vyžaduje, posledním argumentem je libovolný objekt reprezentující libovolný “stavový token” operace, který v metodě End* klient používá pro korelací mezi požadavkem a odpovědí.
Veškerá práce je přenesena na metodu GetAsyncResultEx v mém RXEventsHelperu – metoda vyžaduje, abyste ji poslali v delegátu Action metodu, která má být spuštěna asynchronně.
Metoda RXEventsHelper.GetAsyncResultEx.
public static IAsyncResult GetAsyncResultEx(Action runAction, AsyncCallback callback, object context)
{
if (runAction == null)
{
throw new ArgumentNullException("runAction");
}
Exception ex = null;
var proxyCallback = new AsyncCallback(ar =>
{
IAsyncResult proxyResult = new AsyncResultEx(ar, runAction);
callback(proxyResult);
});
return runAction.BeginInvoke(proxyCallback, context);
}
Hlavním trikem je využití možností delegátů – každý delegát v .Net Frameworku vždy obsahuje asynchronní metody BeginInvoke a EndInvoke, které splňují nároky asynchronního vzoru. My tedy na předaném delegátu runAction zavoláme metodu BeginInvoke, ale místo klientské callBack Funkce podhodíme svou proxy funkci (proxyCallback), která po dokončení asynchronního volání připraví pro naši End metodu vlastní IAsyncResult (AsyncResultEx).
Třída AsyncResultEx zapouzdřuje původní IAsyncResult (argument ar předaný do konstruktoru v předešlém výpisu) a navíc, když na její instanci zavoláme metodu EndAction, na předaném delegátovi (argument runAction v předešlém výpisu) je zavolána metoda EndInvoke, čehož využije naše metoda EndLoadSites.
Třída AsyncResultEx
public class AsyncResultEx : IAsyncResult
{
#region private variables
private IAsyncResult m_originalAsyncResult;
private readonly Action m_originaldelegate;
#endregion private variables
public AsyncResultEx(IAsyncResult origAsyncResult, Action originaldelegate)
{
if (origAsyncResult == null)
{
throw new ArgumentNullException("origAsyncResult");
}
m_originalAsyncResult = origAsyncResult;
m_originaldelegate = originaldelegate;
}
#region properties
public virtual IAsyncResult OriginalAsyncResult
{
get
{
return m_originalAsyncResult;
}
}
public virtual Action OriginalDelegate
{
get
{
return m_originaldelegate;
}
}
#endregion properties
#region methods
public virtual void EndAction()
{
if (OriginalDelegate != null)
{
OriginalDelegate.EndInvoke(OriginalAsyncResult);
}
}
#endregion methods
#region Implementation of IAsyncResult
public virtual bool IsCompleted
{
get
{
return m_originalAsyncResult.IsCompleted;
}
}
public virtual object AsyncState
{
get
{
return m_originalAsyncResult.AsyncState;
}
}
public virtual WaitHandle AsyncWaitHandle
{
get
{
return m_originalAsyncResult.AsyncWaitHandle;
}
}
public virtual bool CompletedSynchronously
{
get
{
return m_originalAsyncResult.CompletedSynchronously;
}
}
#endregion
}
Extenzní metoda EndLoadSites
public static IEnumerable<IPosterousSite> EndLoadSites(this IPosterousAccount account, IAsyncResult result)
{
checkAccountNotNull(account);
var exResult = result as AsyncResultEx;
if (exResult == null)
{
throw new ArgumentException("result");
}
exResult.EndAction();
return account.Sites;
}
Zde vidíme volání metody EndAction na podhozené instanci AsyncResultE. Poté metoda EndLoadSites jen vrátí kolekci Sites objektu account, protože ta nyní již musí být po asynchronním volání naplněna daty.
Se stávající infrastrukturou si opět si můžeme rychle připravit další Begin a End metody. Zde jsou extenzní metody BeginLoadPosts a EndLoadPosts pro IPosterousSite.
public static IAsyncResult BeginLoadPosts(this IPosterousSite site, AsyncCallback callback, object context)
{
throwIfSiteNull(site);
var loadPostsAction = new Action(site.LoadAllPosts);
return RXEventsHelper.GetAsyncResultEx(loadPostsAction, callback, context);
}
public static IEnumerable<IPosterousPost> EndLoadPosts(this IPosterousSite site, IAsyncResult result)
{
throwIfSiteNull(site);
var exResult = result as AsyncResultEx;
if (exResult == null)
{
throw new ArgumentException("result");
}
exResult.EndAction();
return site.Posts;
}
A metoda loadPosts2, která dělá to samé, co předchozí metoda loadPosts, ale používá naše nové extenzní Begin/End metody.
private void loadPosts2()
{
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;
IPosterousApplication app = PosterousApplication.Current;
IPosterousAccount account = app.GetPosterousAccount("posterousname", "posterouspassword");
var syncContext = SynchronizationContext.Current;
var resultPosts = from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
from site in sites.ToObservable()
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
from post in posts.ToObservable()
where post.Private == false
select post;
resultPosts.Subscribe(post =>
{
lock (m_threadsSet)
{
m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
}
syncContext.Post(_ =>
{
var UCpost = new UC_Post
{
Title = post.Title,
Body = post.Body
};
flowLayoutPanel1.Controls.Add(UCpost);
},
null
);
},
ex => syncContext.Post(_ =>
{
throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
},
null),
() => syncContext.Post(_ =>
{
lock (m_threadsSet)
{
m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
}
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
},
null
)
);
}
Upozornil bych jen na dvě specialitky či zrádná místa, která (alespoň v této BETA verzi RX) dělají kód méně intuitvním, než by bylo žádoucí:
Jedná se o tyto dva řádky:
from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
Metoda FromAsyncPattern přijímá delegáty na naše asynchronní metody, ale místo spolehnutí se na typovou inference jsem musel generický argument předat explicitně(Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>) – pokud argument nezadáte, kompilátor hlásí “ambigous reference”.
Dále je patrné, že výsledek funkce FromAsyncPattern, kterým je další funkce vracející IObservable, je předán jako argument metodě Observable.Defer. Metoda Observable.Defer zajistí, že k vyhodnocení předané funkce dojde až poté, co je k výsledkům dotazu přihlášen observer – jinými slovy, metoda Defer nám pomáhá zachovat “lazy” vyhodnocení dotazu.
Dotaz bude fungovat i v této podobě (bez Defer):
var resultPosts = from sites in Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()
from site in sites.ToObservable()
from posts in Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()
from post in posts.ToObservable()
where post.Private == false
select post;
Ale jeho vyhodnocení už není “lazy”. Všimněte si závorek na konci výrazu - FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)() - výsledné IObservable získám okamžitým zavoláním funkce vrácené z metody FromAsyncPattern. Vadit vám to začne v okamžiku, kdy zkonstruujete dotaz, ihned se odpálí asynchronní volání, dojde k chybě a vy budete mít v aplikaci neošetřenou výjimku v threadu na pozadí, protože IObserver ještě není přihlášen (není možné zavolat druhého delegáta - ex => syncContext.Post(_ => { throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex); }, null), ).
Snad se vám tato exkurze líbila. Já ještě na RX Framework konečný názor nemám, ale něco neodbytného ve mně říká, že by mohlo jít o další LINQ, který otřese programátorským světem. Některé extenze pravděpodobně zahrnu do samostatného jmenného prostoru v C# Posterous API.
Tuesday, 02 February 2010 07:43:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms
Thursday, 28 January 2010
C# Posterous API pro Silverlight 4 (a .Net Framework 3.5) – verze 0.0.0.2
Stáhnout C# Posterous API pro Silverlight 4.0
Stáhnout C# Posterous API pro .Net Framework 3.5
Vše podstatné k C# Posterous API naleznete v úvodním článku.
Poznámky ke změnám v této verzi:
- Kvůli verzi pro Silverlight přidány další asynchronní metody tak, aby bylo možné získat data z webu asynchronně, jak to Silverlight vyžaduje a jak je toto chování v aplikacích vynuceno asynchronními metodami v rozhraní platformních tříd - příkladem budiž rozhraní tříd WebRequest a WebResponse. Pokud se pokusíte zavolat synchronní verzi metody v SL z UI vlákna, měli byste z Posterous API dostat výjimku – to je lepší varianta, než skončit v paralyzovaném stavu, kdy celá aplikace na nic nereaguje.
Ukázka uložení změn v blogpostu – přidání komentáře. :
var comment = post.CreateNewComment(updateText);
post.SaveChangesCompleted += (_0, _1) =>
{
Assert.IsTrue(comment.Id > 0);
TestComplete();
};
post.SaveChangesAsync();
- V Silverlight verzi aplikace nedostanete v událostech rozhraní IRawRequestResponsePublisher k modifikaci objekty HttpWebRequest a HttpWebResponse, které jsou použity pro stažení obrázku autora (IAuthor.AuthorPicture) a stažení média (IPosterousMedium.Medium). Důvodem je, že pro stažení používám svého potomka třídy WebClient. Tento potomek přepisuje metody GetWebRequest a GetWebResponse, které jsou poté nabídnuty v událostech dostupných přes rozhraní IRawRequestResponsePublisher. V Silverlightu je ale třída WebClient označena jako kritický kód (Critical) a takzvaný transparentní (Transparent) kód, pod který patří i námi psaný kód, nemá právo z takto označené infrastrukturní třídy podědit. Kromě dvou výše zmíněných omezení ale IRawRequestResponsePublisher pracuje stejně jako v desktopové verzi a v další verzi API uvažuji, že místo WebClienta použiju na stažení obrázku autora i všech dalších médií přímo třídy WebRequest a WebResponse, které budou opět nabídnuty i v rozhraní IRawRequestResponsePublisher.
- Silverlight má omezený přístup k souborovému systému, a proto naleznete v blogpostu (IPosterousPost) a twitter postu (ITwitterPost) další dvě metody pro přidání médií. Ve verzi 0.0.0.1 bylo možné předat pouze cestu k souboru, nyní můžete předat jakýkoli stream – např. Stream načtený z IsolatedFileStorage.
Nové metody:
void AppendMedium(Stream stream, string mediaName, Action<Stream> releaseStreamFunction);
void AppendMedium(Stream stream, string mediaName);
Kromě streamu a názvu média můžete do jedné z variant funkce AppendMedium předat delegáta typu Action<Stream> , který bude vyvolán v okamžiku, kdy Posterous API již se streamem nepotřebuje pracovat. Nejasná zodpovědnost, kdo vlastní a hlavně uvolňuje zdroje alokované objektem stream, mě právě vedla k tomu, že v první verzi API pro .Net Framework 3.5 jste mohli pouze předat název souboru a životní cyklus FileStreamu byl zcela pod mou kontrolu. Jestliže delegáta releaseStreamFunction nepředáte, máte životní cyklus streamu zcela ve svých všemocných rukou. Posterous API ani jeho autor si nepřejí být vyzváni na souboj žádným šíleným omnipotentním vývojářem , a proto na předaném streamu Posterous API nikdy nevolá metodu Close ani Dispose.
Ukázka práce s metodou AppendMedium:
IPosterousPost newPost = site.CreatePost(DateTime.Now + " Media Silverlight",
"Príliš žlutoucký kun úpel dábelské ódy", true);
IsolatedStorageFileStream stream = new IsolatedStorageFileStream("AlbumArtSmall.jpg ", FileMode.Open, IsolatedStorageFile.GetUserStoreForSite());
newPost.AppendMedium(stream, "mojeSL.jpg", isoStream => isoStream.Close());
newPost.AddTag("Všechno a nic");
newPost.AddTag("C# omnia vincit");
newPost.SaveChangesCompleted += (_, __) =>
{
Assert.IsTrue(newPost.Id > 0);
TestComplete();
};
newPost.SaveChangesAsync();
Jednoduchá ukázka použití API v Silverlightu. V jednoduchém boxu na stránce zobrazíme titulek spotu a url spotu. Výsledek vypadá takto:
Ve Visual Studiu jsem založil projekt typu běžná navigační aplikace v Silverlightu. Zde je jednoduchý “ViewModel” pro stránku:
public class VM_Posts : INotifyPropertyChanged
{
public static readonly string ASYNC_EXCEPTION_TEXT = "Error while retrieving data. See inner exceptions for details";
private bool m_isBusy;
private IEnumerable<ViewPost> m_posts;
public VM_Posts()
{
m_posts = null;
IsBusy = false;
LinkClickCommand = new DelegateCommand<String>(link => Debug.WriteLine(link) /*m_navigationServices.HandleExternalLink(link)*/,
_ => true);
init();
}
private void init()
{
IsBusy = true;
IPosterousApplication posterousApp = PosterousApplication.Current;
IPosterousAccount posterousAccount = posterousApp.GetPosterousAccount("<PosterousUserName>", "PosterousPassword>");
posterousAccount.SitesLoaded += (o, e) =>
{
throwIfAsyncEx(e.Exception);
posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
{
throwIfAsyncEx(e2.Exception);
Posts = (from p in e2.Value
select new ViewPost
{
Title = p.Title,
Body = p.Body,
Url = p.Url
}).ToList();
};
posterousAccount.PrimarySite.LoadAllPostsAsync();
};
posterousAccount.LoadSitesAsync();
}
private void throwIfAsyncEx(Exception exception)
{
if (exception != null)
{
IsBusy = false;
throw new Exception(ASYNC_EXCEPTION_TEXT,exception);
}
}
public IEnumerable<ViewPost> Posts
{
get
{
return m_posts;
}
private set
{
m_posts = value;
if (m_posts != null)
{
IsBusy = false;
}
OnPropertyChanged(new PropertyChangedEventArgs("Posts"));
}
}
public bool IsBusy
{
get
{
return m_isBusy;
}
private set
{
m_isBusy = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsBusy"));
}
}
public ICommand LinkClickCommand
{
get;
private set;
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
#endregion
}
public class ViewPost
{
public string Title
{
get;
set;
}
public string Body
{
get;
set;
}
public string Url
{
get;
set;
}
}
}
V privátní metodě init, která je volána ihned po vytvoření instance třídy WM_Posts, nahrajeme blogy (Sites –> metoda LoadSitesAsync) a poté opět asynchronně z primárního blogu (PrimarySite) nahraju všechny blogposty (metoda LoadAllPostsAsync). Jestliže při některém asynchronním volání dojde k výjimce, metoda throwIfAsyncEx výjimku zpropaguje do UI vlákna.
Z nahraných blogpostů jsou vlastností Title, Url a Body přeneseny do instancí pomocné třídy ViewPost a kolekce objektů ViewPost je uložena do vlastnosti Posts. Třídu ViewPost vytvořit musíme, protože “Binding” dat, který budeme používat v XAMLu, nedokáže v Silverlightu z bezpečnostních důvodů pře reflection přistupovat ke člelnům “internal” třídy deklarované v jiné assembly. Třída PosterousPost, která v Posterous API implementuje rozhraní IPosterousPost, je označena jako internal. Ze stejného důvodu v Silverlightu nemohu použít místo explicitně definované třídy ViewPost anonymní datový typ – i anonymní datové typy jsou reprezentovány ve výsledném aplikačním dll třídami s modifikátorem viditelnosti “internal” a kód pro “binding” dat je ve zcela jiné platformní assembly.
Kromě vlastnosti Posts nabízí třída WM_Posts vlastnost IsBusy, kterou v XAMLu využijeme k zobrazení indikátoru informujícího uživatele, že právě získáváme data. Vystaven je také LinkClickCommand, který nůže být zavolán například v okamžiku, kdy je stisknuto nějaké tlačítko reprezentující hyperlink s URL blogpostu.
A zde je pro úplnost ještě XAML. Památeční XAML, protože mi při jeho psaní nejméně a bez jakékoli nadsázky 50x spadlo Visual Studio 2010 Beta 2 (s Resharperem).
<navigation:Page x:Class="SL_PosterousAPITest.Home"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:ct="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:local="clr-namespace:SL_PosterousAPITest;assembly=SL_PosterousAPITest"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
Title="Home"
Style="{StaticResource PageStyle}">
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<local:VM_Posts x:Key="PostsView"></local:VM_Posts>
<Style x:Name="PostTitleStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="IndianRed" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style x:Key="PostsRectangle" TargetType="Rectangle">
<Setter Property="RadiusX" Value="30" />
<Setter Property="RadiusY" Value="30" />
<Setter Property="Fill">
<Setter.Value>
<LinearGradientBrush>
<GradientStop Offset="0" Color="LightYellow" />
<GradientStop Offset="1" Color="LightBlue" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ItemsControl">
<Setter Property="Width" Value="500" />
<Setter Property="Height" Value="360" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer BorderThickness="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Rectangle Style="{StaticResource PostsRectangle}">
</Rectangle>
<ItemsPresenter Margin="15,15,0,0"/>
<ct:BusyIndicator IsBusy="{Binding Path=IsBusy}"
DisplayAfter="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
<StackPanel x:Name="ContentStackPanel" DataContext="{StaticResource PostsView}">
<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
Text="Home"/>
<ItemsControl Name="itemsPosts" ItemsSource="{Binding Mode=OneWay, Path=Posts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource PostTitleStyle}" Text="{Binding Path=Title}"></TextBlock>
</StackPanel>
<HyperlinkButton Content="{Binding Path=Url}"
Command="{Binding Source={StaticResource PostsView}, Path=LinkClickCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Content}"
FontSize="12"
></HyperlinkButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</navigation:Page>
Thursday, 28 January 2010 16:33:08 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Silverlight
Wednesday, 20 January 2010
Ukázka práce s Posterous API – zálohování blogu
Stáhnout výsledné exe -RSPosterousBackup.
Po jednoduchém přehledu možností mého C# Posterous API wrapperu se nyní podíváme, jak se dá API použít k zálohování vašeho blogu. Pro účely tohoto článku předpokládám, že jste úvodní článek o API wrapperu četli.
Zálohovač blogu (RSPosterousBackup.exe) je jednoduchá konzolová aplikace, které stačí předat uživatelské jméno (parametr –u) a heslo (parametr –p) vašeho účtu na Posterous a také adresář vašem počítači (parametr bd), do kterého chcete blog zazálohovat.
Jednoduchá ukázka:
RSPosterousBackup.exe -u:rene@renestein.net -p:mojeheslo -bd:c:\_Archiv\PosterousBackup
Blog uživatelského účtu reneATrenestein.net s heslem mojeheslo bude zazálohován do adresáře c:\_Archiv\PosterousBackup.
Program referencuje samozřejmě knihovnu RStein.Posterous.API a pro (své, uznávám ) pobavení jsem také použil RX for .Net Framework 3.5 SP1. Z RX Frameworku jsou referencovány assembly System.CoreEx, System.Interactive a System.Threading. Nejzajímavější na RX Frameworku je, že pro verzi 3.5 .Net Frameworku zpřístupňuje Parallel Linq – PLINQ, který je součástí připravovaného .Net Frameworku 4.0.
Ještě upozornění – v žádném případě nechci tvrdit, že kód, který uvidíte, využívá PLINQ správným způsobem. Program je jen pískovištěm, na kterém jsem si zkoušel a ověřoval, co RX a PLINQ umí a jak vypadá výsledný kód.
Po spuštění RSPosterousBackup.exe funkce Main pouze předá argumenty z příkazové řádky metodě BackupData v třídě BackupEngine, která představuje výkonný mozek celého zálohovače Posterous blogu.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace RSPosterousBackup
{
class Program
{
static void Main(string[] args)
{
var engine = new BackupEngine();
engine.BackupData(args);
Console.ReadLine();
}
}
}
Zde je třída BackupEngine
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using RStein.Posterous.API;
namespace RSPosterousBackup
{
public class BackupEngine
{
#region constants
public static readonly string POSTEROUS_USER = "-u";
public static readonly string POSTEROUS_PASSWORD = "-p";
public static readonly string BACKUP_DIRECTORY= "-bd";
public static readonly string HELP_KEY = "-?";
public static readonly string HELP_ALT_KEY = "-h";
public static readonly string POSTEROUS_FILE_EXTENSION = ".posterous";
public static readonly string POSTEROUS_MEDIA_FORMAT = "Media_{0}";
public static readonly string POSTEROUS_COMMENT_FORMAT = "Comment_{0}";
public static readonly string POSTEROUS_SITE_FORMAT = "Site_{0}";
#endregion constants
#region private variables
#endregion private variables
#region constructors
public BackupEngine()
{
}
#endregion constructors
public void BackupData(string[] commandLine)
{
if (commandLine == null)
{
throw new ArgumentNullException("commandLine");
}
var parser = new CommandLineParser();
Dictionary<string, string> cmSwitches = parser.Parse(commandLine);
Debug.Assert(cmSwitches != null);
if (cmSwitches.Keys.Any(key => (key.Equals(HELP_KEY, StringComparison.OrdinalIgnoreCase)) ||
(key.Equals(HELP_ALT_KEY,StringComparison.OrdinalIgnoreCase))))
{
logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
return;
}
if (!checkSwitches(cmSwitches))
{
logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
return;
}
backupDataInner(cmSwitches);
}
private void backupDataInner(Dictionary<string, string> cmSwitches)
{
string pUser = cmSwitches[POSTEROUS_USER];
string pPassword = cmSwitches[POSTEROUS_PASSWORD];
string backupDir = cmSwitches[BACKUP_DIRECTORY];
try
{
IPosterousAccount account = PosterousApplication.Current.GetPosterousAccount(pUser, pPassword);
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
var currDirBackupName = new StringBuilder(DateTime.Now.ToLocalTime().ToString());
currDirBackupName.ToSafeFileName();
string currentBackupDir = Path.Combine(backupDir, currDirBackupName.ToString());
account.Sites.Run(site =>
{
string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
Directory.CreateDirectory(sitePath);
});
var processedPosts = (from site in account.Sites.AsParallel()
from post in site.Posts.AsParallel()
.Do(postPost =>
Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
postPost.Title,
Thread.CurrentThread.ManagedThreadId)))
.Do(postPost =>
{
string siteDir = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
var titlBuilder = new StringBuilder(postPost.Title + postPost.Id.ToString());
titlBuilder.ToSafeFileName();
string postDirName = Path.Combine(siteDir,
titlBuilder.ToString()
);
Directory.CreateDirectory(postDirName);
string postFileName = Path.Combine(postDirName, titlBuilder.ToString() + POSTEROUS_FILE_EXTENSION);
using (var fileStream = File.Open(postFileName, FileMode.Create))
using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
{
stremWriter.Write(postPost.Body);
}
postPost.Media
.AsParallel()
.Run(media =>
{
var mediaName = new StringBuilder(String.Format(POSTEROUS_MEDIA_FORMAT, Guid.NewGuid()));
mediaName.ToSafeFileName();
string mediaFile = Path.Combine(postDirName, mediaName.ToString());
using (var fileStream = File.Open(mediaFile, FileMode.Create))
{
media.Content.CopyToStream(fileStream);
}
});
postPost.Comments
.AsParallel()
.Run(comment =>
{
var commentFileName = new StringBuilder(String.Format(POSTEROUS_COMMENT_FORMAT, Guid.NewGuid()));
commentFileName.ToSafeFileName();
string commentFile = Path.Combine(postDirName, commentFileName.ToString());
using (var fileStream = File.Open(commentFile, FileMode.Create))
using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
{
stremWriter.WriteLine(comment.Author.Name);
stremWriter.Write(comment.Body);
}
});
})
select post).ToList();
logMessage(String.Format(GlobalConstants.FINAL_BACKUP_MESSAGE_FORMAT, processedPosts.Count));
}
catch (Exception e)
{
Trace.WriteLine(e);
Console.WriteLine(e);
}
}
private bool checkSwitches(Dictionary<string, string> cmSwitches)
{
return ckeckIfSwitchNullOrEmpty(POSTEROUS_USER, cmSwitches) &&
ckeckIfSwitchNullOrEmpty(POSTEROUS_PASSWORD, cmSwitches) &&
ckeckIfSwitchNullOrEmpty(BACKUP_DIRECTORY, cmSwitches);
}
private bool ckeckIfSwitchNullOrEmpty(string switchKey, Dictionary<string, string> cmSwitches)
{
string val;
cmSwitches.TryGetValue(switchKey, out val);
if (String.IsNullOrEmpty(val))
{
logMessage(String.Format(GlobalConstants.INVALID_CML_SWITCH_FORMAT_STRING_EX, switchKey));
return false;
}
return true;
}
private void logMessage(string message)
{
Console.WriteLine(message);
}
}
}
Třída BackupEngine nejdříve v metodě BackupData pomocí instance třídy CommandLineParser rozpársuje příkazový řádek a voláním pomocné metody checkSwitches zkontroluje, zda byly předány všechny vyžadované parametry. Jestliže nějaký parametr chybí nebo byl zadán parametr pro zobrazení nápovědy (-h, –?), program zobrazí nápovědu a k zálohování nedojde.
Ihned po dokončení všech předběžných kontrol je volána privátní metoda backupDataInner, která je odpovědná za zálohování blogu. Metoda backupDataInner získá odkaz na Posterous účet (PosterousApplication.Current.GetPosterousAccount(pUser, pPassword); a poté pro každou Site (samostatný blog) založí nový podadresář v adresáři, jehož názvem je aktuální lokální datum a čas a který je vytvořen v adresáři předaném v parametru –bd uživatelem.
Adresářová struktura pro každý zálovaný blog:
<adresář určený –bd přepínačem>\<adresář - názvem je aktuální datum a čas>\Site_<Site Id>
Příklad adresářové struktury:
"c:\_Archiv\PosterousBackup\20.1.2010 15_57_07\Site_851694"
Zálohován tedy není jeden blog, ale všechny blogy, které jsou asociovány s daným posterous účtem.
Můžete si všimnout, že pro založení podaresáře používvám jednu z RX extenzních metod – metodu Run.
account.Sites.Run(site =>
{
string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
Directory.CreateDirectory(sitePath);
});
Metoda Run je náhradou za extenzní metodu ForEach, kterou jste si dříve museli sami dopsat nebo jste byli nuceni použít metodu ForEach ve třídě List takto.
account.Sites.ToList().ForEach(site => {//zbytek kódu lambdy identický s kódem výše…});
Zdůrazním, že metoda Run vykoná nějaký vedlejší efekt nad každým elementem v IEnumerable
Signatura metody Run:
public static void Run<TSource>(
this IEnumerable<TSource> source,
Action<TSource> action
)
Dále do proměnné processedPosts uložíme pomocí speciálního a pro naši kratochvíli jediného LINQ dotazu všechny zpracované blogposty (instance podporující rozhraní IPosterousPost). Jak vidíte, stačí se přes Posterous API dotázat do kolekce Sites (blogy) a poté z každého blogu zpracovat všechny blogspoty.
from site in account.Sites.AsParallel()
from post in site.Posts.AsParallel()…
Na více místech v dotazu si můžete všimnout volání extenzní metody AsParalllel, kterým dáváte najevo, že zpracování jednotlivých blogpostů a také médií (IPosterousMedium) a komentářů (IPosterousComment) může proběhnout ve více vláknech – o detaily se ale postará PLINQ, vy sami žádná nová vlákna nespouštíte ani nespravujete.
Můžete si také všimnout, že na několika místech volám extenzní metodu Do .Metoda Do pracuje podobně jako před chvílí zmiňovaná metoda Run. Na každý element v zdrojové kolekci aplikuje předanou funkci, ale poté na rozdíl od metody Run element předá k dalšímu zpracování.
Signatura metody Do:
public static IEnumerable<TSource> Do<TSource>(
this IEnumerable<TSource> source,
Action<TSource> action
)
Zde vypíšeme přes RX extenzní metodu Do titulek právě zpracovávaného blogpostu a Id vlákna, které blogspot zpracovává. Tato metoda je zde jen na ukázku, že každý element ve zdrojové kolekci je metodou Do předán dále ke zpracování
.Do(postPost =>
Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
postPost.Title,
Thread.CurrentThread.ManagedThreadId)))
Na metodu Do navazuje další metoda Do, ve které proběhne zpracování každého blogspotu. V adresáří každé Site (blogu) je vytvořen pro každý blogspot vytvořen nový podadresář, jehož názvem je titulek (vlastnost Title) společně s Id blogpostu. Do tohoto podadresáře je uložen text blogspotu. Soubor s textem blogspotu má příponu posterous a také jsou do podadresáře uložena média (zvukové, obrazové a video soubory) a všechny komentáře k blogspotu.
Hlavní část programu je za námi. Zde jsou ještě výpisy pomocných tříd.
Třída CommandLineParser pro pársování hodnot předaných uživatelem v příkazovém řádku.
using System;
using System.Collections.Generic;
using System.Linq;
namespace RSPosterousBackup
{
public class CommandLineParser
{
#region constants
public static readonly char COMMAND_PARTS_SEPARATOR = '-';
public static readonly char COMMAND_VALUE_SEPARATOR = ':';
public const int MIN_KEY_PARTS = 1;
public const int MAX_KEY_PARTS = 2;
#endregion constants
#region constructors
public CommandLineParser()
{
}
#endregion constructors
#region methods
public virtual Dictionary<string, string> Parse (string[] commandLine)
{
if (commandLine == null)
{
throw new ArgumentNullException("commandLine");
}
return parseInner(commandLine);
}
private Dictionary<string, string> parseInner(string[] commandLine)
{
var dict = (from part in commandLine
let keyValuePair = part.Split(new[] {COMMAND_VALUE_SEPARATOR}, MAX_KEY_PARTS)
select new
{
Key = keyValuePair.First().Trim().ToLower(),
Value = keyValuePair.Length > MIN_KEY_PARTS ? keyValuePair.Last().Trim() : String.Empty
}).ToDictionary(kv => kv.Key, kv => kv.Value);
return dict;
}
#endregion methods
}
}
Konstanty
namespace RSPosterousBackup
{
public static class GlobalConstants
{
public static readonly string INVALID_CML_SWITCH_FORMAT_STRING_EX = "Invalid switch {0}";
public static readonly char SAFE_FILE_PATH_CHAR = '_';
public static readonly string POST_BACKUP_MESSAGE_FORMAT = "Processing post: {0} - in thread {1}";
public static readonly string FINAL_BACKUP_MESSAGE_FORMAT = "Total posts: {0}";
public static readonly string CMDLINE_SHOW_USAGE =
@"Usage:
RSPosterousBackup.exe -u:<posterous user name> p:<posterous password> -bd:<backup directory>
Example:RSPosterousBackup.exe -u:GIC@Roma.com p:Rubicon -bd:c:\PosterousBackup
RSPosterousBackup.exe -? - show this help";
}
}
Třída StringBuilderExtensions s extenzní metodou ToSafeFileName, která v navrhovaném jménu souboru nahradí nepovolené znaky podtržítkem.
using System.IO;
using System.Linq;
using System.Text;
namespace RSPosterousBackup
{
public static class StringBuilderExtensions
{
public static void ToSafeFileName(this StringBuilder builder)
{
Path.GetInvalidFileNameChars().Run(ch => builder.Replace(ch, GlobalConstants.SAFE_FILE_PATH_CHAR));
}
}
}
Stáhnout výsledné exe -RSPosterousBackup.
Wednesday, 20 January 2010 17:54:21 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | LINQ | RX Extensions
Friday, 15 January 2010
Projekt C# Posterous API – verze 0.0.0.1 Alfa
Stáhnout knihovnu – download
Jak jsem avizoval minulý týden na twitteru, píšu C# wrapper webového API zajímavé služby Posterous.
Pár odkazů na začátek:
Jestliže nevíte, co je Posterous, přečtěte si článek na Živě.
Popis Posterous API. Hned na začátku zdůrazním, že autoři Posterous API nepovažují API za kompletní a za sebe dodám, že je to na mnoha místech vidět.:)
Několik důležitých informací:
- 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, 15 January 2010 17:49:51 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | Compact .Net Framework | Silverlight
Friday, 24 April 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, 24 April 2009 11:57:02 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework
Tuesday, 03 March 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, 03 March 2009 16:34:35 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Wednesday, 11 February 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, 11 February 2009 19:13:14 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Wednesday, 04 February 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, 04 February 2009 16:52:26 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Nativní kód | Windows Forms
Monday, 02 February 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, 02 February 2009 14:51:15 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms
Sunday, 21 December 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, 21 December 2008 12:22:20 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Friday, 07 November 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, 07 November 2008 11:29:27 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Mobilitky | Ostatní
Sunday, 05 October 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, 05 October 2008 17:34:45 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ | Návrhové vzory
Monday, 15 September 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, 15 September 2008 13:29:48 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ
Friday, 25 July 2008
GSM Net Monitor verze 0.6.0. Alfa Preview - !podpora lokalizace polohy bez GPS na vámi zvolených mapách! Místo Google Maps Mobile třeba Seznam...
Homepage aplikace.
Hlavní změny:
- Lokalizace polohy dle BTS, ke které jste přihlášeni. Přesný návod, jak zprovoznit tuto funkci, naleznete níže.
- Aplikace je určena pouze pro zařízení Windows Mobile 5 Professional (zařízení s dotykovou obrazovkou) a vyšší.
- Odstraněna spodní lišta ("telefon - indikátor zapnuté/vypnuté rádiové části") , která dle uživatelských ohlasů zbytečně zabírala místo. Lišta se vrátí v některé další verzi po přidání dalších indikátorů, kdy už bude mít větší smysl. V nastavení si budete vždy moci vybrat, zda má být lišta viditelná.
- V nastavení si můžete zvolit preferovanou výšku pluginu na Today obrazovce.
- Vylepšen instalační cab, který automaticky deaktivuje stávájící verzi GSM .Net Monitor na Today obrazovce a zapne po instalaci jeho novou verzi. Přesto ihned po instalaci doporučuji soft reset zařízení.
- Zobrazení mapy s lokalizovanou pozicí pod/nad Today pluginem nebo přes celou obrazovku. Pro lepší představu se podívejte na snímky obrazovek v tomto spotu.
- Zcela přepracován způsob práce s RIL vrstvou.
- Odstraněny chyby (a zcela jistě zaneseny nové) :).
- Změna grafiky.
Instalace
Protože se jedná o AlFA preview, doporučuji před instalaci Net Monitoru mít v zařízení např. SPB Pocket Plus a v něm aktivovaný safe boot - jestliže by vám "vytuhlo" zařízení, nebudete muset dělat HR (Hard Reset), protože můžete při startu zařízení dočasně deaktivovat Today pluginy.
Důležité: Pokud používáte předchozí verzi .Net Monitoru, pro jistotu ji sami deaktivujte na Today obrazovce a poté odinstalujte.
- Z adresy http://blog.renestein.net/__DOWNLOAD/NetMonitorTodayInstall.CAB si stáhněte instalační CAB.
- CAB spusťte a potvrďte případně hlásky zabezpečení.
- Resetujte (Soft reset) zařízení.
Návod na zprovoznění lokalizace polohy:
- Nainstalujte aplikaci GSM .Net Monitor.
- Na adrese http://gsmadmin.renestein.net/ si musíte vygenerovat unikátní přístupový kód. Stačí zadat platný email, opsat "captcha" kód a kliknout na tlačítko 'Vygenerovat přístupový kód". Pozor si dejte na to, že některé (zvláště freemailové ) schránky mohou email umístit do složky se spamem, nebo rovnou smazat.
- Z adresy gsmservicenoreply@renestein.net vám přijde email s přístupovým kódem, který musíte aktivovat kliknutím na odkaz v emailu. Pokud se aktivace podaří, zobrazí se vám následující informace.
- Přístupový kód a registrovaný email je nutné zadat v nastavení v GSM Net Monitoru. Z kontextového menu vyberete položku Nastavení a poté přejdete na záložku BT GPS souřadnice, kde vyplníte pole Email a Klíč (přístupový kód). Dbejte na to, abyste správně opsali i velikost písmen v přístupovém kódu. Potvrďte nové nastavení kliknutím na tlačítko OK.
- V nastavení, na záložce Základní nastavení, můžete změnit časový interval, v jakém jsou zjišťovány informace o GSM síti. Výchozí hodnota je 60 sekun, můžete nastavit např. 5 sekund.
- Jestliže jste vše zadali správně, spusťte sledování BTS v GSM Net Monitoru vybráním položky 'Sledovat informace o GSM síti' (to můžete udělat také kliknutím na zelené tlačítko v levém horním rohu aplikace). Poté vyberte z kontextového menu položku Zobrazovat mapu.
Důležité podrobnosti: Jestliže chcete mapu zobrazovat kdykoli po spuštění pluginu, můžete v nastavení (opět záložka BT GPS souřadnice - viz obrázek u bodu 4) zaškrtnout položku 'Zobrazovat mapu po spuštění'´. Samozřejmě při nedostupnosti WiFi Net Monitor používá placené GPRS/EDGE připojení. Mapa je viditelná po přepnutí na záložku GPS data v pluginu. Mapa je viditelná pouze, když jsou k dispozici platné GPS souřadnice. Jestliže jste na místě, kde GPS souřadnice BTS nejsou známy nebo jste bez GSM signálu, aplikace mapu skryje. Mapa NENÍ skryta, jestliže je zobrazena přes celou obrazovku (full screen).
- Na celou obrazovku mapu přepnete zobrazením kontextového menu na mapě a vybráním jediné položky ´Celá obrazovka´. Zobrazování mapy na celé obrazovce zrušíte opětovným zobrazením kontextového menu na mapě a vybráním (nyní zaškrtnuté) položky ´Celá obrazovka´.
Poznámky:
- Po instalaci je v aplikaci přednastaveno zobrazování pozice na wap verzi map Seznamu. Můžete ale použít jakoukoli další mapu, které stačí předat GPS souřadnice.
http://wap.mapy.cz/search?from=&query=LOC:{LAT} {LON}&mapType=base&zoom=14
V URL map stačí zadat, kam má aplikace doplnit GPS souřadnice - řetězec {LAT} bude nahrazen zeměpisnou šířkou a řetězec {LON} zeměpisnou délkou.
- Aplikace zjišťuje souřadnice na adrese http://gsm.renestein.net/NetMonitorService.Svc/GSMTEST/GsmQuery(...) . Toto URL je zadáno v textovém poli Služba na záložce GPS souřadnice a neměli byste ho měnit.
- Záložka Podrobnosti pracuje stejně jako v předchozích verzích jen s lokálními csv seznamy BTS ze serveru GSMWeb.
Komerční využití GSM Net Monitoru i přidružených služeb je striktně zakázáno. Pokud máte zájem o komerční služby, kontaktujte mě prosím.
!Za žádné přímé, nepřímé, reálné či domnělé škody způsobené aplikací nenesu žádnou odpovědnost!
Užijte si to.
Různé obrazovky:
Aktualizace 31.7.:
Ještě pro informaci:
Aplikace se snaží vypisovat chyby.
Pokud je chyba se znaménkem - (např. -1001), je chyba pravděpodobně na klientovi (není dostupné internetové připojení apod.).
Pokud je chyba bez znaménka -, požadavek odmítl můj server (např. chyba 1 znamená, že jste nebyli úspěšně ověřeni - nemáte platné uživatelské jméno, opsali jste špatně přístupový kód atd.).
Pokud budete mít nějaký problém, opiště mi hlavně prosím číslo chyby.
Pokud BTS není v databázi - aplikace vypisuje chyba na serveru 1000. Nic se neděje, pouze pro tuto BTS nemám ještě GPS souřadnice.
Friday, 25 July 2008 12:18:26 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Mobilitky | Net Monitor
Friday, 09 May 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, 09 May 2008 09:09:26 (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Thursday, 08 May 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, 08 May 2008 15:00:43 (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Wednesday, 07 May 2008
Stále neopravený bug při volání metody HttpWebRequest.BeginGetResponse?
Ještě při psaní jedné aplikace běžící na .Net Framework verze 2.0 jsem objevil podivnou chybu, kdy se při asynchronním stahování stránek pomocí třídy HttpWebRquest asynchronní přístup skoro nelišil od synchronní verze. Metoda BeginGetResponse vrátila řízení volajícímu kódu teprve poté, co došlo ke stažení celé stránky. Dle Microsoftu se problém objevoval i ve verzi 1.1 a příčinou jsou "antipatie" mezi metodou BeginGetResponse a konfigurací DNS.
Myslel jsem si, že tento bug je ve verzi 3.5 odstraněn, ale dnes jsem zjistil, že bugy stejně jako kočky mají minimálně devět životů. U mě stačí -někdy-zadat adresu začínající na "www" a z asynchronního volání je volání synchronní. Zvláštní a k vzteku je, že že nelze stoprocentně napsat návod na reprodukci bugu (zkoušel jsem vyčistit i DNS cache). Bug se ale projevuje při připojení přes ADSL, O2 HSDPA i TMO GPRS/EDGE.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
namespace WebTest
{
class Program
{
static void Main(string[] args)
{
bool done = false;
WebRequest req = WebRequest.Create("http://www.vyvojar.cz");
req.BeginGetResponse(delegate(IAsyncResult result)
{
done = true;
Console.WriteLine(String.Format("Done: {0}", DateTime.Now.ToLongTimeString()));
req.EndGetResponse(result).Close();
Console.WriteLine();
}
,null);
while(!done)
{
Console.WriteLine("Not done");
}
Console.WriteLine("Stop");
Console.ReadLine();
}
}
}
Jedinou obranou proti náhodným výkyvům v matrixu registrovaných služeb z předzjednaného řádu a Microsoftem sponzorované harmonie (Disclaimer: Tato obrana zabírá jen na popsaný bug, ale proti výkyvům univerza pocházejících od zeleného viru, a to i v jeho totálně dezorientované a nejméně hostilní mutaci "Džamila Stehlíková", je zcela bemocná ) je po mnoha mých pokusech nahrazení názvu domény v url adrese její IP adresou (jestliže máte pod kontrolou nastavení webového serveru, na kterém běží stránky-webové služby, protože jinak nemusí samozřejmě ip adresa v hlavičce HOST dostačovat...). Ip adresu zjistíme jednoduše voláním metod z třídy DNS. Volání metod třídy DNS kupodivu žádné zpoždění nevykazuje...
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Threading;
namespace WebTest
{
class Program
{
static void Main(string[] args)
{
bool done = false;
IPHostEntry address = Dns.GetHostEntry("www.vyvojar.cz");
HttpWebRequest req = WebRequest.Create(String.Format("http://{0}", address.AddressList[0].ToString())) as HttpWebRequest;
req.BeginGetResponse(delegate(IAsyncResult result)
{
done = true;
Console.WriteLine(String.Format("Done: {0}", DateTime.Now.ToLongTimeString()));
req.EndGetResponse(result).Close();
Console.WriteLine();
}
,null);
while(!done)
{
Console.WriteLine("Not done");
}
Console.WriteLine("Stop");
Console.ReadLine();
}
}
}
Wednesday, 07 May 2008 12:48:42 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Web Services
Monday, 09 October 2006
ASP.NET - jednoduchý přístup z kódu na webovém formuláři k prvkům deklarovaným v šabloně
V ASP.NET 1.x jsme při přístupu k prvkům v šabloně (šablonou rozumíme vlastnost serverového ovládacího prvku typu ITemplate) museli zavolat metodu FindControl a předat ji Id hledaného prvku. A protože návratovou hodnotou metody FindControl je pouze rozhraní třídy Control, byli jsme nuceni přetypovávat na odvozený ovládací prvek.
Label lblMess = LoginControl1.FindControl("lblMessage") as Label;
U prvků DataList, Repeater a dalších, kteří používají šablony pro opakování stejného obsahu pro každý řádek v datovém zdroji (ItemTemplate, AlternateItemTemplate) je použití metody FindControl odůvodněné. Ovládací prvky v šabloně jsou vytvořeny opakovaně pro všechny řádky v datovém zdroji a rodičovský prvek šablony implementací rozhraní INamingContainer zajišťuje, že každá instance šablony je v html formuláři složena z html elementů s unikátními hierarchickými názvy. Metoda FindControl na řádku Datalistu, Repeateru si můžeme zjednodušeně představit jako "překladač" dlouhého, hierarchického a automaticky generovaného Id na jednoduché Id zadané v šabloně. Vidíme-li na stránce Id LoginControl1_ctl00_lblMessage, můžeme se k prvku s Id 'lblMessage' dostat tak, jak jsme si ukázali v kódu výše.
Proč si ale komplikovat život, když máme na stránce vždy maximálně jednu instanci šablony? Šablona prvku WizardStep bude na stránce právě jednou, protože šablonu jednoho kroku v průvodci (asp:wizard) nepoužíváme pro opakovanou instanciaci stejného obsahu, ale jen pro vytvoření vlastního vzhledu jednoho kroku průvodce. Šablonu nám prvek wizard nabízí jen proto, abychom si mohli vytvořit pěkné vlastní uživatelské rozhraní a nebyli sešněrováni ve svém tvůrčím rozletu představami ASP.NET týmu. Pak ale není žádný důvod, abychom k prvkům šablony přistupovali přes metodu FindControl. Prvky jsou na stránce pouze jednou a je bezpečné na ně odkazovat přímo na úrovni stránky jako na každý jiný ovládací prvek v ASP.NET formuláři, protože nehrozí kolize jejich Id.
ASP.NET 2.0 dovoluje u každé vlastnosti typu ITemplate určit, jestli bude šablona instanciována na jedné stránce opakovaně, anebo zda se na šablona stránce vyskytne nanejvýš jednou. Informaci o tom, jak budete šablonu používat, nese nový atribut TemplateInstance, kterým dekorujete vlastnost ITemplate.
[TemplateInstance(TemplateInstance.Single)]
Hodnotu Single z enumerace TemplateInstance ASP.NET interpretuje jako příkaz k vygenerování typových proměnných na úrovni stránky pro všechny ovládací prvky v šabloně.
Když atribut TemplateInstance nepoužijeme nebo zvolíme režim TemplateInstance.Multiple, k vygenerování proměnných nedojde a ASP.NET 2.0 se k prvkům k šabloně chová stejně jako ASP.NET 1.x.
Zde je jednoduchý serverový ovládací prvek LoginControl, který obsahuje dvě šablony - jednu pro anonymní uživatele a druhou, asi nepřekvapivě, pro přihlášené uživatele. Obě šablony jsou dekorovány atributem TemplateInstance s hodnotou TemplateInstance.Single - jako autoři ovládacího prvku víme, že určitě nebudeme používat více instancí jedné šablony.
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Serverový ovládací prvek, který dovoluje definovat odlišné šablony pro anonymního a přihlášeného uživatele
/// </summary>
[DefaultProperty("AnonymousUserMessage")]
[DefaultEvent("LoginRequest")]
[ToolboxData("<{0}:LoginControl runat=server></{0}:LoginControl>")]
public class LoginControl : CompositeControl
{
#region Delegates
public delegate void LoginItemCommandEventHandler (object sender, CommandEventArgs e);
#endregion Delegates
#region Public Constants
/// <summary>
/// popisek na tlačítko pro událost <see cref="LoginRequest"/>
/// </summary>
public const string LOGIN_BUTTON_NAME = "Login";
/// <summary>
/// Konstanta reprezentuje popisek na tlačítko pro událost <see cref="LogoutRequest"/>
/// </summary>
public const string LOGOUT_BUTTON_NAME = "Logout";
#endregion Public Constants
#region Events Keys
/// <summary>
/// Klíč události <see cref="LoginRequest"/>
/// </summary>
public static readonly Object LoginRequestKey = new Object();
/// <summary>
/// Klíč události <see cref="LoginRequest"/>
/// </summary>
public static readonly Object LogoutRequestKey = new Object();
/// <summary>
/// Klíč události <see cref="ItemCommand"/>
/// </summary>
public static readonly Object ItemCommandKey = new Object();
#endregion Events Keys
#region Public Events
/// <summary>
/// Událost 'Přihlásit uživatele' je vyvolána po kliknutuí na tlačítko s popiskem <see cref="LoginButtonName"/>
/// </summary>
public event EventHandler LoginRequest
{
add
{
Events.AddHandler(LoginRequestKey, value);
}
remove
{
Events.RemoveHandler(LoginRequestKey, value);
}
}
/// <summary>
/// Událost 'Odhlásit uživatele' je vyvolána po kliknutuí na tlačítko s popiskem <see cref="LogoutButtonName"/>
/// </summary>
public event EventHandler LogoutRequest
{
add
{
Events.AddHandler(LogoutRequestKey, value);
}
remove
{
Events.RemoveHandler(LogoutRequestKey, value);
}
}
/// <summary>
/// Událost zprostředkovává události vnořených ovládacích prvků
/// </summary>
public event LoginItemCommandEventHandler ItemCommand
{
add
{
Events.AddHandler(ItemCommandKey, value);
}
remove
{
Events.RemoveHandler(ItemCommandKey, value);
}
}
#endregion Public Events
#region Private variables
private LoginControlContent m_content;
private ITemplate m_anonymousTemplate;
private ITemplate m_loggedInTemplate;
#endregion Private variables
#region Contructors
/// <summary>
/// Konstruktor
/// </summary>
public LoginControl()
{
}
#endregion Contructors
#region Public properties
/// <summary>
/// Vlastnost dovoluje přistupovat k prvkům šablony přes FindControl
/// </summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public LoginControlContent Content
{
get
{
EnsureChildControls();
return m_content;
}
}
/// <summary>
/// Šablona pro přihlášeného uživatele
/// </summary>
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(LoginControlContent))]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate LoggedInTemplate
{
get
{
return m_loggedInTemplate;
}
set
{
m_loggedInTemplate = value;
}
}
/// <summary>
/// Šablona pro nepřihlášeného uživatele
/// </summary>
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(LoginControlContent))]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate AnonymousTemplate
{
get
{
return m_anonymousTemplate;
}
set
{
m_anonymousTemplate = value;
}
}
/// <summary>
/// Informace zobrazená nepřihlášenému uživateli
/// </summary>
[Category("Behavior")]
[Bindable(true)]
[DefaultValue("")]
[Description("Informace zobrazená nepřihlášenému uživateli")]
public string AnonymousUserMessage
{
get
{
string message = (string) ViewState["AnonymousUserMessage"];
return (message == null ? String.Empty : message);
}
set
{
ViewState["AnonymousUserMessage"] = value;
}
}
/// <summary>
/// Informace zobrazená přihlášenému uživateli
/// </summary>
[Category("Behavior")]
[Bindable(true)]
[DefaultValue("")]
[Description("Informace zobrazená přihlášenému uživateli")]
public string LoggedInMessage
{
get
{
string message = (string) ViewState["LoggedInMessage"];
return (message == null ? String.Empty : message);
}
set
{
ViewState["LoggedInMessage"] = value;
}
}
#endregion Public properties
#region Protected properties
/// <summary>
/// Prvek bude uzavřen v HTML značce DIV
/// </summary>
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
#endregion Protected properties
#region Protected methods
/// <summary>
/// Vytvoření vnořených ovládacích prvků
/// </summary>
protected override void CreateChildControls()
{
Controls.Clear();
if (Page.User.Identity.IsAuthenticated)
{
m_content = new LoginControlContent(Page.User.Identity.Name, AnonymousUserMessage, LoggedInMessage);
ITemplate template = null;
if (m_loggedInTemplate != null)
{
template = m_loggedInTemplate;
}
else
{
template = new DefaultLoggedInTemplate();
}
template.InstantiateIn(m_content);
}
else
{
m_content = new LoginControlContent(String.Empty, AnonymousUserMessage, LoggedInMessage);
ITemplate template = null;
if (m_anonymousTemplate != null)
{
template = m_anonymousTemplate;
}
else
{
template = new DefaultAnonymousTemplate();
}
template.InstantiateIn(m_content);
}
this.Controls.Add(m_content);
}
/// <summary>
/// Přpsání metody, která zachycuje bublané události
/// </summary>
/// <param name="source">Zdroj bublané události</param>
/// <param name="args">Argumenty bublané události</param>
/// <returns><see cref="true"/> - Událost byla zpracována, <see cref="false"/> - Událost nebyla zpracována</returns>
protected override bool OnBubbleEvent(object source, EventArgs e)
{
CommandEventArgs ex = e as CommandEventArgs;
if (ex != null)
{
if (ex.CommandName.ToLower() == LOGIN_BUTTON_NAME.ToLower())
{
OnLoginRequest(new EventArgs());
}
else if (ex.CommandName.ToLower() == LOGOUT_BUTTON_NAME.ToLower())
{
OnLogoutRequest(new EventArgs());
}
else
{
OnItemCommand(ex);
}
return true;
}
return false;
}
/// <summary>
/// Metoda odpovědná za vyvolání události loginRequest
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnLoginRequest(EventArgs e)
{
EventHandler eh = (EventHandler) Events[LoginRequestKey];
if (eh != null)
{
eh(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události LogoutRequest
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnLogoutRequest(EventArgs e)
{
EventHandler eh = (EventHandler) Events[LogoutRequestKey];
if (eh != null)
{
eh(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemCommand
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnItemCommand(CommandEventArgs e)
{
LoginItemCommandEventHandler eh = (LoginItemCommandEventHandler) Events[ItemCommandKey];
if (eh != null)
{
eh(this, e);
}
}
#endregion Protected methods
}
}
LoginControl použijeme na testovací stránce a zadáme anonymní šablonu.
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="WebControlLibrary1" Namespace="RStein.Web.UI.WebControls"
TagPrefix="cc2" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Testovací stránka</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cc2:LoginControl ID="LoginControl1" runat="server" AnonymousUserMessage="Přihlašte se prosím" OnLoginRequest="LoginControl1_LoginRequest">
<AnonymousTemplate>
<asp:label runat="server" ID="lblMessage"><%#Container.AnonymousMessage%> </asp:label> <br />
<asp:LinkButton Id="Login" runat="server" Text="Přihlásit se" />
</AnonymousTemplate>
</cc2:LoginControl>
<
</div>
</form>
</body>
</html>
Tlačítko Login i popisek lblMessage deklarované v šabloně jsou přímo dostupné ze stránky, jak si můžeme ověřit, když v události PreRender stránky změníme barvu pozadí popisku lblMessage. Nemusíme používat metodu FindControl, ani přetypovávat, což jsou docela příjemná vylepšení kódu ;)
private void Page_PreRender(object sender, EventArgs e)
{
LoginControl1.DataBind();
if (lblMessage != null)
{
lblMessage.BackColor = Color.Cyan;
}
}
Abyste mohli ovládací prvek LoginControl zkompilovat, následuje kód používaných pomocných tříd.
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.ComponentModel;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Kontejner pro šablony
/// </summary>
[ToolboxItem(false)]
public class LoginControlContent : Control, INamingContainer
{
#region Private variables
private string m_userName;
private string m_anonymousMessage;
private string m_loggedInMessage;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="userName">Jméno uživatele</param>
/// <param name="anonymousMessage">Zpráva pro nepřihlášeného uživatele</param>
/// <param name="loggedInMessage">Zpráva pro přihlášeného uživatele</param>
internal LoginControlContent(string userName, string anonymousMessage, string loggedInMessage)
{
m_userName = userName;
m_anonymousMessage = anonymousMessage;
m_loggedInMessage = loggedInMessage;
}
/// <summary>
/// Jméno uživatele
/// </summary>
public string UserName
{
get
{
return m_userName;
}
}
/// <summary>
///Zpráva pro nepřihlášeného uživatele
/// </summary>
public string AnonymousMessage
{
get
{
return m_anonymousMessage;
}
}
/// <summary>
///Zpráva pro přihlášeného uživatele
/// </summary>
public string LoggedInMessage
{
get
{
return m_loggedInMessage;
}
}
}
}
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Standardní šablona pro nepřihlášeného uživatele
/// </summary>
public class DefaultAnonymousTemplate : ITemplate
{
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
internal DefaultAnonymousTemplate()
{
}
#endregion Constructors
#region public methods
#region ITemplate Members
/// <summary>
/// Implementace InstantiateIn - vytvoření ovládacích prvků anonymní šablony
/// </summary>
/// <param name="container">Ovládací prvek, do jehož kolekce Controls bzdou prvky šablony přidány</param>
public void InstantiateIn(Control container)
{
Label lblMessage = new Label();
lblMessage.ID = "lblMessage";
lblMessage.DataBinding += new EventHandler(lblMesssage_DataBind);
LiteralControl separator = new LiteralControl(" ");
LinkButton btnLogin = new LinkButton();
btnLogin.ID = "btnLogin";
btnLogin.Text = "Přihlásit";
btnLogin.CommandName = LoginControl.LOGIN_BUTTON_NAME;
container.Controls.Add(lblMessage);
container.Controls.Add(separator);
container.Controls.Add(btnLogin);
}
#endregion ITemplate Members
#endregion public methods
#region private methods
//Handler události DataBind prvku lblMessage
private void lblMesssage_DataBind(object sender, EventArgs e)
{
Label lblMessage = (Label) sender;
LoginControlContent currContent = lblMessage.NamingContainer as LoginControlContent;
lblMessage.Text = currContent.AnonymousMessage;
}
#endregion private methods
}
}
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Standardní šablona pro přihlášeného uživatele
/// </summary>
public class DefaultLoggedInTemplate : ITemplate
{
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
internal DefaultLoggedInTemplate()
{
}
#endregion constructors
#region public methods
#region ITemplate Members
/// <summary>
/// Implementace InstantiateIn - vytvoření ovládacích prvků anonymní šablony
/// </summary>
/// <param name="container">Ovládací prvek, do jehož kolekce Controls bzdou prvky šablony přidány</param>
public void InstantiateIn(Control container)
{
Label lblMessage = new Label();
lblMessage.ID = "lblMessage";
lblMessage.DataBinding += new EventHandler(lblMesssage_DataBind);
LiteralControl separator = new LiteralControl(" ");
LinkButton btnLogout = new LinkButton();
btnLogout.ID = "btnLogout";
btnLogout.Text = "Odhlásit";
btnLogout.CommandName = LoginControl.LOGOUT_BUTTON_NAME;
container.Controls.Add(lblMessage);
container.Controls.Add(separator);
container.Controls.Add(btnLogout);
}
#endregion ITemplate Members
#endregion public methods
#region private methods
//Handler události DataBind prvku lblMessage
private void lblMesssage_DataBind(object sender, EventArgs e)
{
Label lblMessage = (Label) sender;
LoginControlContent currContent = lblMessage.NamingContainer as LoginControlContent;
lblMessage.Text = currContent.LoggedInMessage + " " + currContent.UserName;
}
#endregion private methods
}
}
Monday, 09 October 2006 15:42:53 (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET
Wednesday, 13 September 2006
Windows Forms DataGridView - nevykreslování záhlaví sloupce
V .Net konferenci na serveru builder se objevil dotaz, jak skrýt záhlaví (header) sloupce v DataGridView. Kolegové, zvyklí pravděpodobně stále na poněkud nehrabaný DataGrid z verze 1.x .Net Frameworku, radili vytvořit nový sloupec a nové záhlaví.
Nejjednodušší řešení ale představuje odchycení události CellPainting DatagridView a a v ní zamezíte vykreslení sloupce nastavením vlastnosti Handled na true. DataGridView má ale mnohem lepší objektový model než DataGrid a jednoduchých změn ve vykreslování DataGridView dosáhneme rychle v obsluze událostí CellPainting, RowPrePaint, RowPostPaint a dalších.
const int hiddenRowIndex = -1; //záhlaví má index -1
const int hiddenColumnIndex = 0; //skryjeme první sloupec
private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (e.RowIndex == hiddenRowIndex && e.ColumnIndex == hiddenColumnIndex)
{
e.Handled = true;
}
}
Update : Samozřejmě nemusíte zcela rezignovat na vykreslení buňky a chcete vybarvit alespoň její pozadí.
Opět je to jednoduché - vyplníme pozadí a nastavíme e.Handled na true
e.Graphics.FillRectangle(myColor, e.CellBounds);
e.Handled = true;
Wednesday, 13 September 2006 17:17:52 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Windows Forms
Sunday, 09 July 2006
Chyba ve VS.NET 2005?
Včera a dneska jsem narazil na jedno "omezení-chybu" VS.NET 2005. Vždy, když jsem se snažil zkompilovat projekt, jsem dostal ve VS.NET pouze hlášku.
"An error has occured which the C# compiler is unable to report due to low memory or possible heap corruption. It is recommended that you save all your files, close and restart Visual Studio"
Restart VS.NET ale nezabral a dokonce ani nepomohl jindy všemocný restart počítače - měl jsem podezření, že po opakované hibernaci notebooku, která ve Windows XP na počítačích s více než jedním GB paměti není ani zdaleka dokonalá, má problémy VS.NET se správou paměti přesně tak, jak se snaží reportovat v chybové zprávě.
Zkusil jsem projekt zkompilovat v příkazovém řádku:
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Csc.exe /noconfig /nowarn:1701,1702 /errorreport:prompt
/warn:4 /define:DEBUG;TRACE /reference:"C:\Documents and Settings\Rene\My Documents\Visual Studio 2005\Projects\RStein.RS\RStein.RS\RStein.RS.DataStorage\bin\Debug\RStein.RS.DataStorage.dll"
/reference:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727
\System.configuration.dll /reference:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Data.dll /reference:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll /reference:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll /debug+ /debug:full /optimize- /out:obj\Debug\RStein.RS.BusinessEntities.dll /target:library Articles\Article.cs
Articles\ArticleCategory.cs Articles\ArticleChapter.cs Articles\ArticleKeyword.cs
Articles\ArticlePhotoCategory.cs Articles\ArticleSerial.cs Articles\ArticleState.cs Task.cs Articles\ArticleTheme.cs Attachment.cs AttachmenType.cs Collections\ArticleCategoryCollection.cs
Collections\ArticleChapterCollection.cs Collections\TaskCollection.cs Collections\ArticleThemeCollection.cs Collections\BusinessCollectionBase.cs
Collections\CollectionChangeEventArgs.cs Collections\HWInformationTypeCollection.cs
CommonInterfaces\IUIDataProvider.cs Helpers\BasicValidations.cs BusinessObjectBase.cs Category.cs
Collections\AttachmentCollection.cs Collections\CategoryCollection.cs Collections\DeviceInfoCollection.cs Collections\ObservationCollection.cs Helpers\CentralConfigData.cs
Helpers\GlobalConstants.cs Helpers\IdentityMap.cs HWInformationDataType.cs DeviceInfo.cs
Helpers\DataCacheHelper.cs Observation.cs CodeTableBase.cs
Collections\HWCategoryCollection.cs HWCategory.cs
HWInformationType.cs Language.cs Measurement.cs
Properties\AssemblyInfo.cs
CategoryObservation.cs Quantity.cs Unit.cs UnitType.cs UrlAttachment.cs
A zjistil jsem, že VS.NET svádí na kompilátor C# chybu, se kterou si nedokáže poradit samo. Kompilátor totiž bez reptání nareportoval chybu ve zdrojovém kódu.
Collections\TaskCollection.cs(10,18): error CS0309: The type
'RStein.RS.BusinessEntities.Task' must be convertible to
'RStein.RS.BusinessEntities.BusinessObjectBase' in order to use it as
parameter 'T' in the generic type or method
'RStein.RS.BusinessEntities.BusinessCollectionBase<T>'
Collections\BusinessCollectionBase.cs(8,18): (Location of symbol related to
previous error)
Task.cs(7,18): (Location of symbol related to previous error)
VS.NET si tedy evidentně neumí vypořádat s touto kombinací:
Máte bázovou třídu pro typové kolekce s generickým typem T, u kterého vyžaduji, že musí být potomkem třídy BussinessObjectBase.
public class BusinessCollectionBase<T> : Collection<T>
where T : BusinessObjectBase
{
}
Dále máte odvozenou typovou kolekci pro business objekt - např. pro třídu Task.
public class TaskCollection : BusinessCollectionBase<Task>
{
}
A Třída Task není potomkem třídy BusinessObjectBase.
public class Task
{
}
Poté dostanete ve VS.NET výše uvedenou docela zmatečnou hlášku.
Stačí doplnit bázovou třídu BusinessObjectBase k třídě Task a kompilace i ve VS.NET proběhne tak jak má.
public class Task : BusinessObjectBase
{
}
Můžete prosím někdo toto chování ověřit na svém počítači, abych mohl zaměstnat Lady Bug Microsoftu? :) Díky.
Sunday, 09 July 2006 16:43:43 (Central Europe Standard Time, UTC+01:00)
.NET Framework
Sunday, 02 July 2006
Řešení programátorské hádanky z 24.6.2006 - ThreadStartDelegate
Jak bylo patrné ze zadání "hádanky", snažil jsem se, abyste mi napsali, jaké postupy používáte pro předání argumentů a získání návratové hodnoty z metody, když jste omezeni signaturou delegátu ThreadStartDelegate. Třída Thread dokáže pracovat pouze s metodami bez návratových hodnot (void) a také:
a) Metoda nesmí mít žádné argumenty - delegát ThreadStartDelegate
b) Metody přijímající jeden argument typu object - delegát ParameterizedThreadStart (pouze v .Net Frameworku 2.0).
Jak jste se zmínili v komentářích - když chceme předat argumenty kódu vykonávanému v samostatném threadu, můžeme použít třídy s vlastním stavem a návratovou hodnotu získat třeba tak, že si zaregistrujeme "callback" funkci.
Mně by se ale líbilo, kdybych měl jen jednu "stavovou" třídu s argumenty pro metody s jakoukoli signaturou a tedy společnou pro všechny rozdílné typy delegátů a hlavně chci objekt z této třídy přímo předat do konstruktoru třídy Thread, aby se můj kód nijak nelišíl od přímého použití delegátů ThreadStart a ParameterizedThreadStart. Proto jsem v komentářích mluvil o eleganci zvoleného řešení :)
class ThreadStartDelegateWrapper
{
#region Private members
private Delegate m_innerDelegate;
private object[] m_methodParameters;
private object m_returnValue;
#endregion Private members
#region Construtors
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát ukazující na metodu, která poběží v jiném threadu</param>
public ThreadStartDelegateWrapper(Delegate del)
{
if (del == null)
{
throw new ArgumentNullException("del");
}
m_innerDelegate= del;
}
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát ukazující na metodu, která poběží v jiném threadu</param>
///<param name="methodParameters">Argumenty metody</param>
public ThreadStartDelegateWrapper(Delegate del, params object[] methodParameters) : this(del)
{
m_methodParameters = methodParameters;
}
#endregion Construtors
#region Public properties
/// <summary>
/// Argumenty metody
/// </summary>
public object[] MethodParameters
{
get
{
return m_methodParameters;
}
set
{
m_methodParameters = value;
}
}
/// <summary>
/// Návratová hodnota metody
/// </summary>
public object ReturnValue
{
get
{
return m_returnValue;
}
}
#endregion Public properties
#region Operators
/// <summary>
/// Implicitní konverzní operátor, který instanci třídy <see cref="ThreadStartDelegateWrapper"/> převede na delegáta <see cref="ThreadStartDelegate"/>
/// </summary>
/// <param name="originalWrapper">Instance třídy <see cref="ThreadStartDelegateWrapper"/></param>
/// <returns>Instanci delegáta <see cref="ThreadStartDelegate"/>, který ukazuje na metodu zapouzdřující zavolání jakéhokoli delegáta</returns>
public static implicit operator ThreadStart(ThreadStartDelegateWrapper originalWrapper)
{
if (originalWrapper == null)
{
throw new ArgumentNullException("originalWrapper");
}
return new ThreadStart(originalWrapper.InvokeDelegate);
}
#endregion Operators
#region private methods
//Metoda, jejíž signatura odpovídá signatuře deklarované delegátem ThreadStartDelegate
private void InvokeDelegate()
{
m_returnValue = m_innerDelegate.DynamicInvoke(m_methodParameters);
}
#endregion private methods
}
Třídě ThreadStartDelegateWrapper můžete do konstruktoru předat odkaz na jakéhokoli delegáta a také můžete předat všechny potřebné argumenty metodě, na kterou delegát ukazuje (argument methodParameters v konstruktoru). Předané argumenty můžete kdykoli upravit, protože jsou zveřejněny ve vlastnosti MethodParameters.
Abych mohl svoji třídu použít kdekoli je očekáván delegát ThreadStart, napsal jsem implicitní konverzní operátor třídy ThreadStartDelegateWrapper na delegát ThreadStartDelegate.
Návratovou hodnotu metody vykonané v jiném threadu nalezneme ve vlastnosti ReturnValue - nic nám také nebrání přidat událost, ve které získanou návratovou hodnotu budeme sami aktivně distribuovat všem zájemcům.
A tady je jednoduchý ukázkový kód.
class Test
{
public delegate string TestDelegate(string message);
public string WriteMessage(string message)
{
Console.WriteLine(message);
return "OK";
}
}
class Program
{
static void Main(string[] args)
{
Test myTestClass = new Test();
Test.TestDelegate myDelegate = myTestClass.WriteMessage;
ThreadStartDelegateWrapper wrapper = new ThreadStartDelegateWrapper(myDelegate, new object[] { "Hello world" });
Thread myThread = new Thread(wrapper);
myThread.Start();
myThread.Join();
Console.WriteLine(wrapper.ReturnValue);
Console.Read();
}
}
Je jednoduché rozšířit třídu ThreadStartDelegateWrapper o podporu dalších delegátů v .NET Frameworku (např. WaitCallback), takže na metody s různými signaturami mohou ukazovat typičtí delegáti v .Net Frameworku s využitím jedné jediné třídy fungující jako obecný adaptér mezi naším kódem a kódem v .Net Frameworku.
Sunday, 02 July 2006 14:40:08 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Programátorské hádanky
Saturday, 24 June 2006
Programátorská hádanka - jak metodě v delegátu ThreadStart předat argumenty?
Jak asi víte, delegát ThreadStart v .Net Frameworku může ukazovat pouze na metodu, která nemá žádné argumenty a vrací void.
Jak nejlépe metodě v delegátu ThreadStart předat argumenty? A navíc - dokázali byste, aby metoda spuštěná v delegátu ThreadStart vrátila hodnotu, nebo si teď už opravdu jen vymýšlím po dobrém sobotním bourbonu nějaké rozkošné .Net 3.0 pohádky? Napadlo mě totiž docela elegantní řešení, tak by mě zajímalo, jestli ho někdo z vás už zná a používá?
Saturday, 24 June 2006 22:28:13 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Programátorské hádanky
Friday, 19 May 2006
Sloupec v GridView s potvrzením vymazání záznamu (na klientovi)
Jestliže chcete mít v GridView sloupec, který uživatele požádá na klientovi (s využitím JavaScriptu) o potvrzeni smazání záznamu, je doporučeno používat TemplateField, jehož šablona ItemTemplate obsahuje ve vlastnosti OnClientClick tlačítka klientský kód.
<asp
:TemplateField ShowHeader="False">
<ItemTemplate>
<asp:ImageButton ID="ImageButton1" runat="server" CausesValidation="False" CommandName="Delete" ImageUrl="~/Images/delete.gif" OnClientClick="javascript:return confirm('Opravdu smazat záznam?');" />
</ItemTemplate>
</asp:TemplateField>
ButtonField by byl sice vhodnější, ale tvůrci ASP.NET zadání kódu v JavaScriptu pro ButtonField nepodporují. Třída ButtonField nenabízí ani přístup k vlastnostem prvku Button, který je vyrenderován na každém řádku GridView.
Chceme-li napsat svůj vlastní typový sloupec pro mazání záznamů, který budeme používat ve všech svých projektech, použijeme třídu ButtonField alespoň jako předka svého nového sloupce a lehce "dirty" kódem v v přepsané metodě InitializeCell přidáme tlačítku vytvořenému v "našem" sloupci pro každý řádek GridView jednoduchý kód v JavaScriptu, jenž bude vyžadovat od uživatele potvrzení smazání záznamu.
/// <summary>
/// Nový typ sloupce pro GridView obsahující tlačítko pro smazání záznamu s potvrzením na klientovi (JS)
/// </summary>
public class DeleteButtonField : ButtonField
{
#region Private constants
private const string DEFAULT_CONFIRM_MESSAGE = "Opravdu smazat záznam?";
private const string DELETE_COMMAND = "Delete";
private const string DELETE_JS = "javascript: if (!confirm('{0}')) return false;";
#endregion Private constants
public DeleteButtonField() : base()
{
}
#region Public properties
/// <summary>
///Text varování, které se má zobrazit uživateli při pokusu vymazat záznam
/// </summary>
public string ConfirmMessage
{
get
{
string confirmMessage = (string)ViewState["ConfirmMessage"];
if (confirmMessage == null)
{
return DEFAULT_CONFIRM_MESSAGE;
}
return (confirmMessage);
}
set
{
ViewState["ConfirmMessage"] = value;
}
}
#endregion Public properties
#region Public properties
/// <summary>
/// Metoda volaná při incializaci buňky Gridu
/// </summary>
/// <param name="cell">Inicializovaná buňka</param>
/// <param name="cellType">Typ buňky</param>
/// <param name="rowState">Stav řádku buňky</param>
/// <param name="rowIndex">Index řádku buňky</param>
public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
if (cell.Controls.Count > 0)
{
IButtonControl control = cell.Controls[0] as IButtonControl;
if (control != null)
{
control.CommandName = DELETE_COMMAND;
LinkButton lb = control as LinkButton;
Button btn = control as Button;
ImageButton iBtn = control as ImageButton;
string fullConfirmMessage = String.Format(DELETE_JS, ConfirmMessage);
if (lb != null)
{
lb.OnClientClick = fullConfirmMessage;
}
else if (btn != null)
{
btn.OnClientClick = fullConfirmMessage;
}
else if (iBtn != null)
{
iBtn.OnClientClick = fullConfirmMessage;
}
}
}
}
#endregion Public methods
}
Kód není nijak složitý. Kromě konstant, jejichž význam je dostatečně zřejmý z jejich názvu, máme v našem novém sloupci vlastnost ConfirmMessage, která obsahuje text zobrazovaný v dialogu pro potvrzení smazání záznamu. V metodě InitializeCell nejdříve zavoláme implementaci InitializeCell třídy ButtonField. Poté zjistíme, jestli buňka gridu (cell) obsahuje nějaké ovládací prvky. Jestliže ne, byla metoda volána pro řádek reprezentující záhlaví nebo zápatí gridu, a v ní žádné tlačítko nebude. Když buňka obsahuje alespoň jeden ovládací prvek, zjistíme zda první prvek v kolekci1 je typu IButtonControl (rozhraní IButtonControl implementují třídy LinkButton, Button i ImageButton) a nastavíme jeho vlastnost CommandName na text Delete. Jak asi víte, řetězec "Delete" je jedním z příkazů, které GridView rozeznává a speciálně ošetřuje, takže při bublání událost Click tlačítka s CommandName nastaveným na "Delete" je vyvolána událost RowDeleting (a případně další) místo generické události RowCommand.
Protože rozhraní IButtonControl nenabízí vlastnost OnClientClick pro zadání potvrzujícího kódu v Javascriptu, musíme přetypovat na konkrétní třídu Button a poté teprve můžeme použít vlastnost OnClientClick. Další "dirty" kód - byl bych raději, kdyby byla vlastnost OnClientClick přímo součástí rozhraní IButtonControl, anebo by mohl existovat společný abstraktní předek s vlastností OnClientClick pro třídy ImageButton, LinkButton a Button.
Nový sloupec ve svém projektu použijete takto:
Zaregistrujete nový sloupec na stránce jako běžný serverový ovládací prvek zadáním jména jeho assembly, jmenného prostoru a rezervací prefixu značek pro své prvky v direktivě @Register.
<%
@ Register Assembly="Rstein.Web.Ui" Namespace="Rstein.Web.Ui" TagPrefix="custom" %>
Sloupec přidejte do kolekce Columns prvku GridView a nastavte dle svých požadavků vlastnosti sloupce.
<asp
:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" OnRowDeleting="GridView1_RowDeleting">
<Columns>
<custom:DeleteButtonField Text="Smazat" ButtonType="Button" ConfirmMessage="Smazat záznam?"/>
</Columns>
</asp:GridView>
Příště si ukážeme, jak podobné sloupce pro Grid udělat lépe s využitím šablon vytvořených v kódu a také doplníme pro své sloupce kompletní podporu pro design time.
- Zde je ten zmiňovaný "dirty" postup. I když jsem si ověřil Reflectorem, že prvním prvkem kolekce je vždy IButtonControl vytvářený třídou ButtonColumn, jde o typický případ svazování svého kódu s kódem, jehož změny nemáte pod kontrolou a přitom se nespoléháte jen na jeho "typově bezpečné" veřejné API, takže se v dalších verzích můžete dočkat nepříjemných překvapení :(
Friday, 19 May 2006 17:44:04 (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET
Sunday, 09 April 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, 09 April 2006 14:34:33 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory
Tuesday, 28 March 2006
Další FAQ k .NET Remotingu
- Při použití CAO (Client Activated) objektu nebo při vrácení MarshalByRefObjectu ze SAO (Server Activated) objektu fungují volání metod jen v lokální síti. Na počítači přistupujícím přes internet (a komunikujícím tedy většinou přes adresu firewallu/routeru) volání metody vzdáleného CAO objektu vždy selže.
Důvodem je, že každý CAO objekt je identifikován unikátním dynamickým URI, které je vygenerováno na serveru. Server za firewallem/routerem ale vrátí název počítače nebo IP adresu (v závislosti na nastavení vlastnosti useIpAddress u přenosového kanálu), které jsou platné pouze v lokální síti. Nastavením vlastnosti machineName u kanálu můžeme zadat, jaký název serveru (IP adresa) mají být v URI pro CAO objekty vráceny - zadáme tedy externí a pro klienty viditelnou adresu (např. adresu firewallu).
V Konfiguračním souboru
<channel ref="tcp" machineName="serverExternalVisibleName">
</channel>
V kódu: IDictionary properties = new Hashtable();
properties["machineName"] = "serverExternalVisibleName";
TcpChannel tcpChannel = new TcpChannel(properties, null, null);
ChannelServices.RegisterChannel(tcpChannel);
- Při komunikaci se serverovým objektem dostanete hlášení, že selhalo přihlášení k proxy (The remote server returned an error: (407) Proxy Authentication Required).
Net Remoting podporuje přihlášení k webovému serveru, ale jeho autoři trestuhodně a pravděpodobně omylem (ale i ve verzi 2.0?) nezveřejnili pro zadání přihlašovacích údajů objekt IWebProxy. Jediným nepříliš čistým řešením (pomineme-li nespolehlivé zadání autentizačních údajů přes třídu GlobalProxySelection) je použití reflection a nastavení privátního člena _proxyObject v objektu HttpClientChannel.
//Vytvoření nového http komunikačního kanálu
HttpChannel channel = new HttpChannel();
//Získáme deskriptor člena _clientChannel (klientský http kanál) ve třídě HttpChannel
FieldInfo clientChannelFO = typeof(HttpChannel).GetField("_clientChannel", BindingFlags.Instance|BindingFlags.NonPublic);
//Získání instance HttpClientChannel
HttpClientChannel clientChannel = clientChannelFO.GetValue(channel);
//Získání deskriptoru proxy objektu
FieldInfo proxyObject = typeof(HttpClientChannel).GetField("_proxyObject", BindingFlags.Instance | BindingFlags.NonPublic);
//Vytvoření nového proxy objektu
IWebProxy authProxy = new WebProxy("proxy", 80);
//Pro autentizaci k proxy použijeme údaje přihlášeného uživatele
authProxy.UseDefaultCredentials = true;
//Nastavení nové proxy v objektu HttpClientChannel
proxyObject.SetValue( clientChannel, proxy );
Starší FAQ k .NET Remotingu na tomto blogu
Dva nejčastější dotazy k .NET Remotingu
Dotazy a odpovědi k .NET Remotingu
Monday, 27 March 2006 23:47:11 (Central Europe Standard Time, UTC+01:00)
.NET Framework | .Net Remoting
Sunday, 21 August 2005
Bázová třída pro business objekty - návrhový vzor Layer Supertype
Na konci spotu, v němž byla vytvořena třída DataCacheHelper, jsem slíbil, že ukážu, jak si vynutit použití objektu DataCacheHelper všemi třídami v business vrstvě. K tomu nám poslouží vzor Layer SuperType, jehož definice se dá parafrázovat takto: zaveďme společného předka pro všechny třídy jedné vrstvy aplikace, v němž zapoudříme společný kód a sdílené chování.
Nyní si popíšeme rozhraní a implementaci vzorového předka pro business objekty. Zobrazit kód. (C#)
Abstraktní třída BusinessObjectBase obsahuje dva konstruktory. Bezparameterický konstruktor je potomky vyvoláván při zakládání nové instance bez obrazu v databázi ("bez platného - perzistentního id). V tomto konstruktoru nastavíme, že objekt není změněn vůči datům v databázi (m_isDirty = false), přidělíme mu dočasné id uložené v konstantě NO_ID a voláním privátních metod setNew a setLoaded označíme objekt jako "nový - bez obrazu v databázi" a současně "není třeba nahrávat data z databáze". U každého nového objektu se také předpokládá, že jde o aktivní objekt - tedy o objekt, který nemá příznak smazán (Discarded) ani neaktivní (InActive) - viz enumerace ObjectState na konci výpisu.
Druhý konstruktor, který přijímá argument id, používají odvozené objekty, jestliže již byly dříve uloženy do databáze. Opět nastavíme příznak, že objekt není změněn vůči datům v databázi (m_isDirty), dále nastavíme proměnnou m_isLoaded na false, což je příznak, který signalizuje, že data objektu ještě nebyl nahrána z databáze, a uložíme si id objektu.
Veřejná vlastnost IsDirty vrátí true, jestliže byl objekt změněn vůči datům v databázi, vlastnost IsNew vrátí true, jestliže se jedná o nový objekt bez perzistence - čeká na "Insert".
Vlastnost IsDeleted má hodnotu true, když již byla data objektu v databázi smazána příkazem Delete (objekt jen "dožívá" v business vrstvě) nebo když u objektů, jejichž data se například kvůli zachování referenční integrity fyzicky nemažou, byl do databáze zapsán stav Discarded.
Vlastnost Id vrací unikátní identifikátor objektu, jestliže byl objekt uložen do databáze, jinak vrací konstantu NO_VALUE.
Vlastnost ObjectState vrací stav objektu s využitím hodnot enumerace ObjectState (aktivní objekt, dočasně neaktivní objekt, smazaný objekt).
Metoda Load je pro nás zajímavá hlavně tím, že si vynutíme použití třídy DataCacheHelper. Jedná se metodu se šablonou (vzor Template method), která nejprve zkontroluje, jestli jde o objekt, který již byl nahrán a/nebo který byl již smazán - pokud je jedna z těchto podmínek pravdivá, metoda Load ihned ukončí svoji činnost. Když jsou všechny předběžné podmínky pro vyvolání metody Load splněny, metoda se pokusí vyzvednout dříve uložený řádek s daty pro svou instanci z třídy DataCacheHelper. Jestliže řádek existuje, pak je vyvolána varianta metody DoInternaLoad přijímající odkaz na řádek, jinak je vyvolána metoda DoInternalLoad, která musí řádek z databáze teprve získat. Metody DoInternalLoad jsou chráněné a virtuální a za jejich implementaci odpovídají odvozené třídy - třída BusinessObjectBase dbá na dodržení základní kostry algoritmu, ale implementaci jednotlivých kroků aloritmu ponechává (musí ponechat) na podtřídách, což je hlavní idea vzoru Template Method. Po úspěšném nahrání objektu je volána metoda setLoaded, která označí objekt jako nahraný (IsLoaded bude nyní vracet true).
Poznámka: V metodě Load se zatím předpokládá, že data objektu potřebujeme nahrát pouze jednou a metoda tedy neumožňuje aktualizaci dat objektu z databáze po prvním nahrání. Jak aktualizaci dat z databáze odvozeným třídám umožníme, si ukážeme v dalším pokračování.
Metoda Save neumožní uložení objektu, jestliže jde o nenahraný objekt ("není co ukládat, data nemohla být změněna"), jestliže jde o vymazaný objekt, nebo pokud objekt nebyl změněn a současně nejde o nový objekt. Metoda tedy již na úrovni předka ošetří, že nebudou zbytečně ukládány objekty, jejichž data byla nahrána, ale uživatel je nijak nezměnil. Metoda pouze volá chráněnou virtuální metodu DoInternalSave a jak asi ti chytřejší z vás tuší, jedná se o další příklad vzoru Template Method. Voláním privátní metody setClear po úspěšném uložení objektu nastavíme na false příznak "změněn" (IsDirty) a "nový objekt" (IsNew). Jestliže byl ukládán objekt, u nějž bylo vyžádáno smazání (ObjectState=ObjectState.Discarded), je nastaven příznak IsDeleted voláním chráněné metody SetDeleted na true.
Metoda Discard slouží ke zrušení objektu, přesněji řečeno k převedení objektu do stavu ObjectState.Discarded - pokud je objekt v tomto stavu a dojde k uložení objektu, metoda DoInternalSave buď záznam v databázi fyzicky vymaže (DELETE) nebo do databáze uloží stav ObjectState.Discarded (UPDATE). Metoda Discard pouze zkontroluje, zda nepracujeme nad smazaným objektem (IsDeleted=true), u nějž opakované rušení nemá smysl, a jestliže tomu tak není, nastaví objektu stav ObjectState.Discarded a označí objekt voláním metody SetDirty jako změněný.
Chráněnou statickou metodu SaveCollection používají odvozené třídy pro ukládání objektů ve svých kolekcích. (vztah agregace nebo kompozice). Výhodou je, že v odvozených třídách nemusí být stále stejný a otravný cyklus foreach, který se liší jen typem iterovaného objektu.
Metody DoInternalLoad a DoInternalSave jsme již zmiňovaly - jsou to "sloty" používané odvozenými třídami k zavedení vlastní aplikační logiky do schématu vysokoúrovňového scénáře. Bázovou implementaci metody DoInternalLoad přijímající řádek s daty objektu mohou zavolat odvozené třídy, jestliže chtějí nahrát stav objektu (ObjectState) bez zbytečné duplikace kódu - obnovení a ukládání hodnot vlastních specifických vlastností je samozřejmě plně v režii odvozených tříd. Metoda DoInternalLoad bez argumentů ani metoda DoInternalSave nejsou abstraktní, i když by abstraktní být mohly - v business vrstvě ale občas máme třídy, u nichž nemusíme (nechceme) psát perzistenci a proto mají metody v bázové třídě prázdné tělo, abychom nebyli nuceni zcela zbytečně ve více odvozených třídách poskytovat symbolické "prázdné implementace".
Metodu Init mohou odvozené třídy použít pro vlastní inicializační kód (vytváření kolekcí a přihlašování jejich událostí atd.) - příklad uvidíte v pokračování spotu.
Metoda SetDirty nastavuje příznak "změněn oproti datům v databázi" - příznak nemá význam pro objekty pro objekty bez perzistence a pro smazané objekty.
Metoda CheckEquals porovná předané objekty - jestliže se objekty nerovnají, je objekt označen jako změněný. Jak asi tušíte, metodu volají odvozené třídy v set sekci svých vlastností (CheckEquals(oldValue, newValue)) a jsou tak zbaveni nutnosti reagovat na výsledek porovnání a ukládat si vždy každá sama za sebe příznak "jsem změněn". Když uživatel zmáčkne tlačítko uložit na formuláři a my data z formuláře přeneseme do objektu, metoda CheckEquals zajistí, že objekt bude označen jako změněný jen tehdy, pokud uživatel opravdu nějaké změny na formuláři provedl (třeba vybral jinou hodnotu v listboxu). Výhody jsou zřejmé, kód na formuláři je stále týž - pouze "tupě" přenáší data ze vstupních prvků formuláře do vlastností business objektu a ten si již potutelně a sám na úrovni předka ošetří, zda nově předané hodnoty vyžadují uložení, nebo ne, což může nastat, když uživatel z pouhého rozmaru klikne na tlačítko uložit a přitom si detail objektu celou dobu jen prohlíží.
Metoda TriggerLoad iniciuje nahrání objektu, jestliže objekt ještě nebyl nahrán, nejde o nový objekt a také nesmí jít o smazaný objekt. Metodu volají odvozené třídy při přístupu k jakékoli vlastnosti nebo metodě objektu (vzory Lazy Load, Ghost).
Význam dalších chráněných a privátních metod byl již myslím dostatečně osvětlen v předchozím textu při výkladu veřejného rozhrani, takže se nebudu opakovat.
Rozhraní IPersistable vyjadřuje minimální nárok, který musejí splňovat všechny objekty ukládající a obnovující svá data z/do perzistentního úložiště - výhodnou výchozí a závaznou osnovu realizace rozhraní poskytuje třída BusinessObjectBase.
Z enumarace ObjectState pro vyjádření základních stavů objektu prozatím nepoužívame stav InActive - význam ostatních stavů by již vám měl být po přečtení spotu dostatečně zřejmý.
Příště si ukážeme vzorovou implementaci třídy, která dědí z BusinessObjectBase.
Sunday, 21 August 2005 21:29:28 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Sunday, 07 August 2005
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Při psaní business vrstvy libovolné aplikace jste jistě narazili na následující problém, který můžeme demonstrovat na profláknutém příkladu s objednávkami a jejich položkami. Pod položkou rozumíme instanci třídy, ve které jsou uloženy informace o vybraném produktu, počtu objednaných kusů produktu a celkové ceně. Objednávka a položka objednávky jsou třídy, které jste už určitě psali tolikrát, že nemáte rádi ani jejich názvy ;)
Každá objednávka má 0..n položek a vyžádáme-li si konkrétní položku z kolekce všech položek (Items) u objednávky, která již byla uložena do databáze, musí dojít nejprve k nahrání kolekce. Je jedno, zda položky nahráváte ihned privátní metodou nazvanou třeba loadItems volanou z konstruktoru třídy Objednávka, nebo zda (a lépe) používáte zpožděné nahrávání, kdy kolekce Items je naplněna stejnou metodou teprve po prvním přístupu a konstruktory všech business objektů pouze uloží předané unikátní identifikátory (Id) a naplní své vlastnosti uloženými daty (kromě kolekcí) až po prvním přístupu k nějaké vlastnosti (kombinace vzorů Lazy Load a Ghost). Na přístupu k plnění dat objektů nezáleží, jen vždy musíte garantovat, že uživatel objektu musí přistupovat k datům objektu, aniž by si byl vědom provedených výkonnostních optimalizací - interní impementace je i zde nedotknutelné privatissimum objektu.
Jak zjistíte v metodě loadItems, jaké položky objednávka (třída Order obsahuje? Přes datovou vrstvu spustíte dotaz, která vrátí všechny záznamy s daty pro všechny položky objednávky - parametrem dotazu je samozřejmě Id objednávky. Kruciálním problémem je ale vytvoření položek objednávky (třída OrderItem) z vrácených záznamů. Jaké máme možnosti?
- Vytvoříme novou instanci OrderItem a předáme jí do konstruktoru její Id. Výhodou je, že třída Order není s třídou OrderItem nijak svázána a pouze jí předává její identifikátor. Velkou nevýhodou ale je to, že object OrderItem při obnovování svých dat z databáze bude znovu posílat dotaz do databáze "dej mi data pro mé Id", což nebude nijak efektivní přístup zvláště, když v objednávce budou desítky nebo stovky položek a každá z nich s opulentní rozhazovačností pošle svůj dotaz do databáze. Navíc je to zcela zbytečné, protože data již byla vyzvednuta v metodě loadItems, ale "jen" nebyly objektu OrderItem předána.
- V metodě loadItems přečteme záznamy položek objednávky, vyzvedneme data ze všech sloupců a pro každou položku vyvoláme konstruktor, který akceptuje všechny vlastnosti položky. Tedy kromě Id předáme i celkovou cenu, počet kusů atd. Sice jsme se již zbavili opakovaného dotazování do databáze položkami objednávky, ale objevily se další zásadní nevýhody. Třída objednávka je zatížena zbytečnou znalostí rozhraní (konstruktoru) třídy OrderItem a při změně třídy OrderItem, jakými jsou přidání nebo odebrání vlastnosti, budeme muset provést i změny ve třídě Order. Při tomto řešení jsme se právě vydali na strastiplnou a rozháranou životní cestu, nazývanou moderními mudrci ze softwarových pousteven ve svých písemných testamentech "Maintenance Nightmare". Stejný problém budeme mít při plnění vlastností OrderItem, navíc bychom u každé vlastnosti museli mít i set přístupovou metodu, z čehož by se měl všem autokritickým vývojářům obracet žaludek.
- Třídě OrderItem přidáme konstruktor, který přijímá záznam z databáze - v případě .Net Frameworku tedy objekt DataRow. Tohle řešení sejme povinnost nést břemeno znalosti rozhraní třídy OrderItem z třídy Order, ale zanechá nás s třídami, jejichž veřejné (nebo minimálně "internal") rozhraní je zapráskáno objekty z nižších vrstev. Proč by třída měla ukazovat, že je závislá na objektech DataRow, jejichž hlavní doménou je databázová vrstva? Perzistence je u business objektů nutné zlo, ne alfou a omegou a hlavním důvodem jejich existence. Bohužel, i v mnoha složitých projektech jsou stále k vidění jen truchlivé krabičky s daty z databáze a metodami Load a Save doplněné o honosnou etiketu "business vrstva" od nějakého rekvalifikovaného outsidera s kriplovsky laděným elánem Horsta Fuchse, co se v pomatení smyslů rozhodl pomoci saturovat deficit pracovních sil na trhu s vývojáři.
Než přejdu ke cachování dat, jen drobná poznámka, abych předešel dotazům. Metoda loadItems může poslat dotaz, který vrátí jen id položek objednávky, takže se data na klienta netahají dvakrát a můj problém pak působí jen jako vykonstruovaná obsese. I v tomte případě se ale místo jednoho dotazu budete potýkat s desítkami dotazů, které vyzvedávají - většinou zcela zbytečně - data jen pro jednu instanci OrderItem. Jestliže mám k tomu příležitost, je lepší vyzvednou všechna data pro všechny objekty najednou jedním "úsporným" dotazem a přitom dovolit v případě potřeby objektu přímé obnovení dat z databáze svým separátním dotazem.
Když tedy není pro nás přijatelný konstruktor s objektem DataRow, musíme při vytváření objektu "OrderItem" sdělit, odkud může načíst svá data. Jinými slovy, objekt OrderItem musí vědět, kam mu třída Order "schovala" objekt DataRow. Proč bychom ale neefektivně každému objektu extra sdělovali, odkud může načíst svá data? Zavedeme raději jeden centrální objekt - cache na příchozí data z databáze. Pak stačí objektu OrderItem předávat jen Id jako v první variantě a přitom využívat všech výhod cachování dat.
Co od takové datové cache požadovat?
- Třída musí být globálně viditelná pro všechny objekty. Půjde tedy o Singleton, respektive pro webové aplikace bude vhodné použít PseudoSingleton (Thread Specific Storage).
- Rozhraní, které se nemění s přidáváním dalších business tříd a které mohou používat jednotným způsobem všechny objekty. Takže chceme univerzální metody pro ukládání i vyzvedávání dat jakéhokoli objektu, ne nějaké stále bobtnající rozhraní s metodami StoreOrdeItem, StoreOrder, StoreXX, StoreXY, ... .
Zde je jednoduchá implementace třídy, která zatím splňuje naše nároky. Zobrazit kód
Myslím, že kód je sám o sobě dostatečně vypovídající, takže jen stručný komentář. Statický atribut Instance vrátí pro každý thread specifickou instanci třídy, která se pro jeden thread chová jako běžný Singleton. Metoda CloseInstance odstraní instanci třídy pro daný thread a tuto metodu je vhodné volat ve webové aplikaci na konci každého požadavku v obsluze události EndRequest v souboru global.asax. Metoda StoreData uschová předaný datový řádek (argument row) pro objekt daného typu (argument type) - metoda předpokládá, že řádek obsahuje sloupec Id, který je jednoznačným identifikátorem každého business objektu. Metoda GetData vrátí dříve uložený řádek pro typ v argumentu type a pro objekt, jehož id bylo předáno v argumentu businessObjectId. Po vrácení dat je uložený řádek odstraněn, aby nebyly vlastnosti business objektu permanentně obnovovány z dříve uložených a již neaktuálních dat.
Přístě si ukážeme, jak si u business objektů vynutit používání třídy DataCacheHelper bez duplikace kódu a rizika, že zapomenete v nově přidané třídě řádky z instance DataCacheHelper vyzvedávat, a také postupně z DataCacheHelperu vydělíme různé aspekty jeho chování, abychom umožnili rekonfiguraci DataCacheHelperu za běhu aplikace a adekvátně vyladili jeho činnost pro různé typy aplikací.
Sunday, 07 August 2005 19:26:34 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Sunday, 26 September 2004
Logování událostí v mediátoru
V mediátoru, kterého jsem popisoval v jednom dřívějším spotu, potřebujeme při testování logovat všechny příchozí události, které jsou předdefinovaným a bezpečným způsobem distribuovány dalším objektům. Nejméně náročnou cestou na napsání, volenou většinou vývojáři, kteří se pravděpodobně do IT rekrutují z rekvalifikovaných potomků nejzarytějších stachanovců;), je logování každé události zvláštní metodou. Po registaci nových událostí do mediátora je vždy i změněna komponenta pro logování, takže o údržbě takového kódu si můžete nechat zdát jen mrazivé sny o opětovném plíživém nahrazování kvality kvantitou.*
Na údržbu nenáročné a univerzální řešení spočívá ve vytvoření preprocesoru (viz spot o mediátoru), který je vyvolán před distribucí událostí ostatním objektům a který parametry události vypíše přes objekt Trace z jmenného prostoru System.Diagnostics.
Základní myšlenky v preprocesoru.
1) Většina (ne li každý) objekt v business vrstvě má unikátní identifikátor s názvem Id. Preprocesor události vypíše typ objektu, který událost vyvolal a jeho unikátní Id, takže je snadné dohledat zdroj problematických událostí.
2) Objekty nesoucí parametry událostí dědí vždy z třídy EventArgs. Přes reflection vypíšeme rekurzívně hodnoty vlastností do úrovně nastavené ve vlastnosti EndDumpLevel.
using System;
using System.Diagnostics;
using System.Reflection;
namespace RStein.Utilities
{
/// <summary>
/// Třída EventsLog pro logování událostí
/// </summary>
public class EventsLog
{
#region private constants
private const int BEGIN_DUMP_LEVEL = 1;
private const int DEFAULT_END_DUMP_LEVEL = 2;
#endregion private constants
#region private variables
private int m_endLevel;
#endregion private variables
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
public EventsLog() : base()
{
m_endLevel = DEFAULT_END_DUMP_LEVEL;
}
#endregion Constructors
#region Public properties
/// <summary>
/// Koncová úroveň rekurzivního výpisu vlastností objeudálosti
/// </summary>
public int EndDumpLevel
{
get
{
return m_endLevel;
}
set
{
if (value < 1)
throw new ArgumentOutOfRangeException("Value must be greater than 1");
m_endLevel = value;
}
}
#endregion Public properties
#region Public methods
/// <summary>
/// Metoda pro výpis informace o událostech
/// </summary>
/// <param name="sender">Zdroj události</param>
/// <param name="e">Parametry události</param>
public void ProcessEvent(object sender, EventArgs e)
{
Debug.Assert((sender != null) && (e != null), "EventsLog - Invalid event data");
if ((sender == null)||
(e == null))
{
return;
}
Trace.WriteLine(new string('-',25));
dumpId(sender);
dumpEventProperties(e);
}
#endregion Public methods
#region Private methods
//Zapsání Id a názvu objektu
private void dumpId (object sender)
{
Type senderType = sender.GetType();
PropertyInfo idInfo = senderType.GetProperty("Id");
string id = String.Empty;
if (idInfo != null)
id = idInfo.GetValue(sender, null).ToString();
Trace.WriteLine(String.Format("Přijata událost od objektu {0}, Id = {1}", senderType.ToString(), id));
}
//Vypsani vlastností objektu s parametry události
private void dumpEventProperties(EventArgs e)
{
Type eventType = e.GetType();
Trace.WriteLine(String.Format("Objekt události - {0}", eventType.ToString()));
PropertyInfo[] eventInfo = eventType.GetProperties();
Trace.WriteLine("Výpis hodnot událostí");
writeEventValues(eventInfo, BEGIN_DUMP_LEVEL, e);
}
//Metoda pro rekurzivní výpis hodnot vlatností
private void writeEventValues(PropertyInfo[] eventInfo, int level, object o)
{
foreach (PropertyInfo info in eventInfo)
{
//Nezpracovávat indexované vlastnosti
if (info.GetIndexParameters().Length != 0)
continue;
object propValue = info.GetValue(o, null);
if (propValue == null)
continue;
Trace.Write(new String('\t', level));
Trace.WriteLine(String.Format("Jméno - {0}, Hodnota = {1}", info.Name, propValue.ToString()));
if (level < EndDumpLevel)
writeEventValues(propValue.GetType().GetProperties(), level + 1, propValue);
}
}
#endregion Private methods
}
}
*Pozorný čtenář z tohoto spotu dokáže vyčíst i mou politickou orientaci a nechuť k určitému druhu myšlení;)
Sunday, 26 September 2004 17:02:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Monday, 20 September 2004
Česká .Net Developer Group nabírá dech
Jak už psal Michal, dneska se uskutečnilo první setkání české .Net Developer Group. K jeho postřehům bych jen dodal, že mě jakožto skeptika, co se týče úspěšnosti pořádání kolektivních akcí pod egidou velkých společností jako je Microsoft, příjemně překvapil počet účastníků, a hlavně to, že většina z nich nebyla všemu pasivně přitakávající zasmušilou většinou, která vévodí většině oficiálních seminářů a přednášek, ale že se se (skoro) všichni podíleli na vytváření programu dalších setkání.
Jestliže tedy nechcete potkávat své kolegy jen ve virtuálním prostoru internetu a nejste naprostí asociálové opevňující se s hysterickými vzlyky ve svém domě i při zmínce o návštěvě blízkého příbuzného, tak přijďte na další setkání, které se bude konat přibližně za měsíc a jehož tématem je bezpečnost v .Net Frameworku. O bezpečnosti bude přednášet Honza Šeda.
Honzovi Šedovi také patří pořádný dík za to, že si .Net Developer Group vymyslel, prosadil a celé první setkání v Microsoftu zorganizoval.
Monday, 20 September 2004 21:52:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Ostatní
Wednesday, 15 September 2004
Živě učí staré psy staré kousky
Živě zahájilo s obrozeneckým nadšením seriál o VB.NET. Nadšení serveru pro nové jazyky a polopatické vzdělávání svých čtenářů se ale ve své aktuální podobě stávají pro seriál sudičkami se smrtícími kletbami ve své bezzubé hubě.
Autor seriálu Milan Petřík se rozhodl, že postaví na hlavu všechna didaktická pravidla, která říkají, že vysvětlování nového tématu s odlišným viděním problému by nemělo být budováno na předchozích zastaralých znalostech, a raději nám předkládá svoji vizi "jednoduchého" VB jako chameleona proplouvajícího napříč technologiemi již léta skoro beze změny. To, že VB.NET je jen marketingový název Microsoftu pro jazyk budovaný na zcela jiných principech nez VB6 a nižší verze, který se svými zastaralými předchůdci sdílí některé méně významné syntaktické konstrukce, a že jen pro nepoučitelné nostalgiky je vytvořen jmenný prostor Microsoft.VisualBasic, s jehož pomocí je způsob kódování ve VB.NET degradován na hybridní VB6/.NET styl, nemusím návštěvníkům mého blogu asi připomínat.
Autor veškeré připomínky ke svému seriálu odmítá alibistickým poukazem na to, že prvních 10 dílů seriálu je určeno úplným začátečníkům, a proto nevysvětluje hned některé pokročilé, ale zásadní koncepty (třeba třídy, jmenné prostory), které přesahují ohraničený svět VB.NET. Alibistické je to proto, že se sice dle svých slov vyhýbá teoretickému vysvětlování některých pojmů, ale to mu nebrání, aby nás hned ve druhém dílu seriálu nepoučil o o logickém členění programu do modulů (Proč Microsoft před programátory skrývá své sladké tajemství, že modul ve VB.NET je jen třída se statickými metodami a syntaxe modulu je rozvinuta do třídy teprve kompilátorem?) a procedur a v zatím posledním díle se zase rozhodne, že jeho čtenáři jsou již dostatečně vyspělí, aby pochopili konstrukci Try - Catch (a to vše ještě před řídícími konstrukcemi jazyka!), ale aby jejich chápavé obvody v mozku nepřetížil, tak jim taktně zamlčí existenci klíčového slova Finally.
Dalšími lapsy jsou neustálé vypisování celé hierarchie jmenných prostorů nebo utvrzování začátečníka, že používání "magických" čísel místo konstant je v pořádku. Seriál je rozvleklý, těžkopádný a skáče bez jakékoli logické souvislosti z tématu na téma.
Shrnu-li to. Za seriálem není znát žádná myšlenková osnova a působí na mě dojmem, že si někdo v Živě řekl "No ten .Net už je docela rozšířenej, jeden děsnej seriál pro začátečníky o PHP už máme, a protože jsme server pro masového čtenáře, tak vytvoříme i podobný seriál ve VB.NET (jako bumerang se zde Microsoftu vrací, že VB má pověst lidového jazyka :), pozn. aut. ) a seženeme někoho, kdo dokáže žoviálně podat, jakej je ten VB.NET pohodovej jazyk fakt pro každou lamu".
Komu je seriál určen, opravdu netuším. Napadli mě jen ortodoxní příznivci programování v Basicu na nezastarávajícím stroji Didaktik Gamma, kteří se pod tlakem doby rozhodli migrovat na na PC a .NET. Ale že by tvořili tak početnou skupinu, aby se pro ně vyplatilo vytvářet seriál? :)
Wednesday, 15 September 2004 07:55:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Ostatní
Sunday, 15 August 2004
Windows služba pro pravidelné stahování souborů z Visual Source Safe databáze
I když je Visual Source Safe na mém soukromém žebříčku mizerných programů zcela zaslouženě na nelichotivém prvním místě a jeho programátory podezřívám, že byli do Microsoftu infiltrováni konkurenčními firmami a plní zde (nutno přiznat, že ale velmi úspěšně :) ) roli Trojského koně, musel jsem se smířit s tim, že ve firmě VSS stále používáme, byť všichni s kletbami na rtech. Protože ve VSS nemusí být ukládány jen zdrojové kódy, ale i dokumentace a analytické artefakty, o něž mají zájem i lidé*, kteří s VSS jinak nepracují a které nemá smysl nutit, aby instalovali a používali VSS klienta, rozhodl jsem se, že pro DIGI TRADE napíšu Windows službu, která vybrané projekty z VSS databáze v pravidelných intervalech uloží do adresáře v souborovém systému.
A protože se služba bude asi hodit více lidem, betu Windows služby si můžete zdarma stáhnout a otestovat.
Po rozbalení archivu musíte službu nainstalovat nástrojem InstallUtil.
installutil.exe DigiTrade.VSS.VSSService.exe
V konfiguračním souboru DigiTrade.VSS.VSSService.exe.config změňte cestu, kam má být ukládán "trace" log (hodnota atributu initializeData). Jestliže budete reportovat chyby, přiložte vždy prosím i "trace" log.
Účet, pod kterým běží služba, musí mít právo zapisovat do souboru s " trace" logem!
<add name="VSSListener"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="c:\LOGS\VSSService.log"/>
Když chcete používat jiný soubor s VSS konfigurací (viz dále), změňte v souboru hodnotu klíče IniFile.
<add key="IniFile" value="VSSManagerConfig.xml"/>
Soubor s konfigurací VSS manažera (VSSManagerConfig.xml) má následující strukturu:
Element vssManager - kořenový element.
Povolený obsah - Elementy generalSettings, commonProjects a exactProjects
Element generalSettings - základní nastavení VSS služby.
Povolený obsah - žádný
Atributy
- vssIniFile - Cesta k ini souboru VSS databáze, se kterou má služba pracovat.
- userName - Jméno uživatele ve VSS, pod kterým se bude služba přihlašovat.
- password - Heslo uživatele ve VSS (v Betě není kryptováno!).
- saveAllItems - Pokud má atribut hodnotu true, tak se ignorují sekce commonProjects a exactProjects a dojde ke stažení všech projektů z VSS databáze.
- saveDirectory - Adresář, do kterého budou uloženy soubory z VSS.
- checkerInterval - Interval v minutách, po jehož uplynutí jsou stahovány soubory z VSS databáze.
Element commonProjects - projekty, které budou staženy vždy bez ohledu na jejich místo v hierarchii projektů ve VSS. Tuto sekci využijete například tehdy, když ve většině projektů máte podprojekty se stejným názvem.(například adresář DOC s dokumentací projektu).
Povolený obsah - element project
Element exactProjects - Projekty určené absolutní cestou (začíná znakem $), které mají být staženy z VSS.
Povolený obsah - element project
Element project - element reprezentující projekt ve VSS.
Povolený obsah - žádný
Atributy
Služba využívá přes interop COM knihovnu SSAPI.dll dodávanou s VSS.
Po instalaci doporučuji vytvořit pro službu nový Windows účet, pod kterým poběží a který bude mít přidělen jen nejnutnější práva - přístup k souboru s "trace" logem, přístup k VSS databázi a právo zapisovat do adresáře určeného atributem saveData. Vhodné je také vytvoření nového uživatelského účtu ve VSS.
Pro úplnost jen podotýkám, že vím o existenci konzolového nástroje ss.exe s desítkamí neintuitivních přepínačů, který je součástí VSS, ale správa desítek nebo stovek řádků v BAT souboru mě nelákala, a proto vznikla tato služba.
*Pro účely tohoto spotu jsem se rozhodl zjednodušeně považovat projektové manažery a obchodníky za lidi, i když u některých členů těchto kast mám o jejich pravé podstatě kvůli jimi bezskrupulózně projevované deficitní intelektuální potenci důvodné pochybnosti:)
Sunday, 15 August 2004 12:07:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | VSS Windows Service
Monday, 31 May 2004
"Některé věci mají k sobě nepopsatelně blízko" - příkaz switch v jazyce C# a metoda String.IsInterned
Jazyk C# povoluje konstrukci switch, v níž se podmíněná sekce kódu zvolí podle shody proměnné typu string s konstantním řetězcem v sekci case.
switch (myVariable1 + myVariable2)
{
case “sekce1”:
{
//Zpracuj
break;
}
case “sekce 2”:
{
//Zpracuj
break;
}
default:
{
//Nedelej nic
break;
}
}
I když napsání swich výrazu s řetězcem je pro vývojáře jednoduchou záležitostí, zpracování switch výrazu kompilátorem již tak triviální není. Představte si, že porovnání shody řetězců by bylo prováděno znak po znaku. S prodloužením řetězců by docházelo k lineárnímu nárůstu doby potřebné pro porovnání řetězců. Jak asi ze své zkušenosti víte, příkaz case je vždy stejně rychlý bez ohledu na délku řetězců a k žádnému neohrabanému porovnávání řetězců znak po znaku v žádném případě nedochází. Jak je tedy příkaz case kompilátorem zpracován a rozvinut?
Všechny literály (string someString = “Ja jsem literal“) jsou při JIT kompilaci aplikace přidány do speciální hashovací tabulky vytvářené a spravované běhovým prostředím, v níž klíčem je řetězec a hodnotou odkaz (reference, adresa v paměti) na tento řetězec. Podstatné je, že řetězec se stejnou sekvencí znaků se v tabulce vyskytuje nanejvýš jednou. Takže pokud nadeklaruji kromě výše uvedené proměnné someString proměnnou someString2 a inicializuji ji stejným řetězcem jako proměnnou someString ( string someString2 = “Ja jsem literal“)), tak běhové prostředí při JIT kompilaci zjistí, že v hashovací tabulce klíč Ja jsem literal již existuje a proto do proměnné someString2 přiřadí odkaz na objekt umístěný pod tímto klíčem. Proměnné someString a someString2 odkazují poté na stejné místo v paměti, což si lze snadno ověřit voláním metody ReferenceEquals.
Z umístění do hashovací tabulky jsou při JIT kompilaci vyloučeny řetězce, jež jsou konstruovány dynamicky za běhu programu. Takže řetězec v proměnné string someString3 = “Ja jsem literal“ + someString2 přidán nebude. Třída String má ale statickou metodu Intern, jejím účelem je dodatečné vložení řetězce a odkazu na něj do hashovací tabulky. Příkazem String.Intern(someString3) je do hashovací tabulky přidán obsah proměnné someString3.
Když nechceme řetězec do hashovací tabulky přidat, ale chceme se jen dotázat, zda je v hashovací tabulce již obsažen, použijeme další statickou metody String.IsInterned. Metoda IsInterned při nalezení řetězce v hashovací tabulce vrací odkaz na tento řetězec. Když řetězec v hashovací tabulce není, metoda IsInterned vrátí null.
Nyní si už můžete sami odvodit, jak kompilátor efektivně zpracuje příkaz switch s řetězci. Po přechodu na příkaz switch je zjištěn proměnný rozhodovací řetězec, podle jehož hodnoty mají být zvoleny sekce case (switch(myVariable1 +myVariable2) ). Poté je volána metoda String. IsInterned, jíž je předán jako argument rozhodovací řetězec. Když metoda vrátí null, je jisté, že žádná sekce case nevyhovuje rozhodovacímu řetězci, protože všechny řetězce u case sekcí byly přidány do hashovací tabulky při JIT kompilaci. Jestliže metoda vrátí odkaz na řetězec, je testována shoda vráceného odkazu s odkazy řetězců u jednotlivých sekcí case. Když se reference řetězců shodují, je zvolena daná sekce case. Porovnávány jsou jen adresy řetězců v paměti (odkazy, reference), nikdy ne jednotlivé znaky, a proto rychlost provedení příkazu switch není nijak determinována délkou řetězců.
Monday, 31 May 2004 20:39:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework
Friday, 21 May 2004
Bezpečná změna verze assembly zveřejněné pro COM
Do konference Emwac VS.NET poslal dotaz pan V. Gazda, jenž se chtěl vyhnout opakované registraci .NET assembly pro COM klienty po změně verze assembly.
Zde je stručný postup vedoucí k eliminaci opakované registrace:
1) Napsal jsem jednoduchou knihovnu s názvem TestLibrary v C#. Rozhraní IType je označeno atributem InterfaceType, kterému je do konstruktoru předána hodnota ComInterfaceType.InterfaceIsDual - rozhraní podporuje brzkou (early) i pozdní (late) vazbu. Rozhraní ITest implementuje třída Test. Z třídy Test je pro Com klienty dostupná pouza metoda Test, protože jsem aplikoval atribut ClassInterface(ClassInterfaceType.None). Možnost None z enumerace ClassInterfaceType registračním nástrojům signalizuje, že z třídy mají být publikovány pouze metody z implementovaných rozhraní (tedy určitě ne zděděné metody).
using
System;
using
System.Runtime.InteropServices;
using
System.Reflection;
namespace
RStein
{
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public
interface ITest
{
void TestMethod();
}
[ClassInterface(ClassInterfaceType.None)]
public
class Test : ITest
{
public Test()
{
}
public
void TestMethod()
{
Console.WriteLine("Hello from COM " + Assembly.GetExecutingAssembly());
}
}
}
2) Vygeneroval jsem silné jméno (strong name), podepsal a zkompiloval verzi 1.0.0.0 assembly.
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile(@"..\TestLibrary.snk")]
3) Assembly jsem zaregistroval pro COM spuštěním regasm.exe.
RegAsm.exe TestLibrary.dll /tlb:TestLibrary.tlb
4) Napsal jsem primitivního klienta v C++, který přes COM volá metodu TestMethod z rozhraní ITest. Dle očekávání žádný problém.
#include
"stdafx.h"
#include
<wtypes.h>
#import
"mscorlib.tlb"
#import
"TestLibrary.tlb" no_namespace named_guids
int
_tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
ITest * pTest = NULL;
HRESULT hr = CoCreateInstance(CLSID_Test,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITest,
reinterpret_cast<void**>(&pTest)
);
pTest->TestMethod();
pTest->Release();
CoUninitialize();
}
5) Změnil jsem verzi assembly TestLib.dll na 2.0.0.0, opět jsem kód zkompiloval a umístil do GAC. Verzi 1.0.0.0 jsem z GAC smazal. Po spuštění C++ klienta jsem dostal z metody CoCreateIntance HResult, že nelze nalézt požadovaný soubor.
[assembly: AssemblyVersion("2.0.0.0")]
6) V machine.config jsem instruoval runtime, že nyní mají všichni klienti používat verzi 2.0.0.0 assembly. C++ klient opět typy z assembly TestLib.dll nalezne a žádná opakovaná registrace přes regasm.exe není třeba.
<dependentAssembly>
<assemblyIdentity name="TestLibrary" publicKeyToken="12ba27c531641c01" />
<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" />
</dependentAssembly>
Friday, 21 May 2004 22:14:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework
Friday, 14 May 2004
Kniha Network programming for the Microsoft .Net Framework
Kniha Network programming for the Microsoft .Net Framework je úvodem do síťového programování. Slovo úvodem bych zdůraznil, protože žádné rozsáhlé případové studie v ní nenaleznete.
Po obligátním a povrchním úvodu do .Net Frameworku, nás autoři seznámí se “streamy” a pochopitelně se soustředí na třídu NetworkStream. Následuje popis práce s thready a ukázky asynchronního návrhového vzoru (metody Begin*, End*)- i když je problematice věnováno plných 19 stran, nečekejte žádné "best practices", ale spíše upovídaný přepis toho, co je v MSDN. Ukázek asynchronního návrhového vzoru je v celé knize hříšně opulentní množství , takže tato kapitola je jen "lákavou" návnadou.
Dále je v knize vysvětlen význam serializace a probrány jsou XML, binární a SOAP serializace. Když už je serializace probírána do takových detailů, jakými jsou XML atributy, mohla být zmíněna i dynamická redefinice atributů za asistence třídy XmlAttributeOverrides.
Lehký úvod do jmenného prostoru System.Net, jehož smysl mi v kompozici knihy uniká, je vystřídán popisem protokolů IPV4 a IPV6 a metod třídy DNS. Tato kapitola mi připadá z celé knihy nejpovedenější, protože z ní čiší, že její autor své téma podrobně zná, ale přitom dokáže na vyhrazeném prostoru vybrat to podstatné a nezabíhat do zdržujících detailů.
Následující dvě kapitoly tvoří jádro knihy, protože se zabývají klientskými a serverovými sokety a také jejich nadstavbami s jednodušším API (TcpClient, TcpListener). Bohužel tyto kapitoly trpí také největší nevyvážeností ve zpracování tématu. Přístup autorů kolísá od pasivního přejímání informací z MSDN k neustálému zdůrazňování, že pro většinu synchronních metod existují jejich asynchronní doplňky. Když jsem se tuto zajímavou informaci dověděl potřetí a prohlédl si další úmorný Step By Step příklad určený pravděpodobně programátorům po akutní lobotomii provedené brokovnicí, měl jsem chuť připojit se dobře cíleným výstřelem k zástupu šťastných idiotů, abych dokázal kapitoly dočíst. Tam, kde by měla být tématika zpracována velmi prodrobně i s ukázkami, se autoři omezí na shrnující tabulky, jejichž pravděpodobně jediným smyslem je rychlé navýšení počtu popsaných normostran. Moje výtka směřuje hlavně k zcela nedostatečné dokumentaci enumerace SocketOption.
Kromě TCP/IP protokolu jsou zde i ukázky UDP protokolu a implementace takzvaných "Raw" soketů. "Raw" sokety jsou vysvětleny na příkladu ICMP protokolu (což není nic jiného než všem důvěrně známý PING). Slušelo by se zmínit, že na Windows 2000 a XP mohou "Raw" sokety otevřít pouze administrátoři. (ping.exe může spustit každý uživatel, jde asi o jedinou výjimku).
Důležitost tříd WebClient, WebResponse a WebRequest je vyzdvižena v kapitole nazvané "Using the Network". Tato kapitola je povedená, i když zde asi nenaleznete nic překvapivého. Zmíněny jsou cookies, různé způsoby autentizace, certifikáty, SSL.
Z dalších kapitol rychle získáte pocit, že se autoři zavázali dodat dílko o vyšším počtu stran, a proto zařadili témata jako jsou Web Services a .Net Remoting. Sice jsou vždy naťuknuty pokročilé vlastnosti (SOAP extenze, Channel sinks), ale z jejich odfláknutého vysvětlení si začátečník bude brzy zoufat a slabší nátura se k těmto konceptům kvůli neumětelství autorů již nikdy nevrátí. Jako leitmotiv knihy:) se zde objevuje podrobné vysvětlení asynchronního volání WWW služeb.
Autoři se také s těžko pochopitelnou vervou pustí i do vysvětlování Code Acces Security. Jejich snaha se ale omezí na výčet oprávnění a hrubý přehled významu konstitutivních složek CAS, takže kdybych se s CAS setkal poprvé, získám mylný dojem, že CAS je nesmysl bez jakékoli užitné hodnoty. Jsme také poučeni, že bychom měli komunikaci mezi sokety kryptovat, protože svět plný hackerů čeká na naše pakety. Děkuji za osvětu, hned se cítím jako up-to-date vývojář, teď si najdu jen ve slovníku cizích slov význam termínu kryptovat, abych ty hackery svými programy sejmul. :)
Doporučení pro psaní výkonných aplikací jsou shrnuta v mozaice nazvané "Network performance and scalability. Zajímavá je hlavně část týkající se doporučení pro používání "Nagle" algoritmu, ale ani ostatní rady vašim aplikacím neublíží.
Zcela do počtu je kratičká poslední kapitola Advancements in .Net Framework programming, jejíž originální teze by se dala shrnout "Nebojte se, v příští verzi bude zase vše lepší, výkonnější a bezpečnější".
Co říci závěrem? Jestliže se sokety začínáte a jste úplní zelenáči, kniha se Vam bude hodit, i když si z ní odnesete hlavně poznatek, že sokety jsou tady proto, aby bylo na čem demonstrovat dokonalý asynchronní návrhový vzor. :) Jste-li pokročilý vývojář a v knize hledáte neotřelé rady starých zkušených harcovníků, schovejte peněženku, zamáčkněte slzu kanoucí za odpíraným poznáním a vyberte si jiný titul.
Anotace
Název: Network Programming For the Microsoft .Net Framework
Autoři: Jones, A; Ohlund, J; Olson, L;
Nakladatelství: Microsoft Press 2004
Friday, 14 May 2004 20:05:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | .Net Remoting | ASP.NET | Web Services
Tuesday, 11 May 2004
Zkrocení zlé ženy - "Merge" replikace na MSSQL aneb co v MSDN nenaleznete
"Merge" replikace v MSSQL se používá hlavně tehdy, když má více uživatelů paralelně pořizovat data v odpojeném (offline) režimu. Bonbónkem je u "merge" replikace podpora mobilních klientů (PDA). Uživatelé se připojí, pořízená data odreplikují na server a server jim na oplátku doručí změny v datech provedené ostatními uživateli. Typickými kandidáty na integraci "merge" replikace do designu aplikace jsou systémy pro podporu obchodních zástupců v terénu. Proč se psát s vlastní komponentou pro replikaci, když je zde komponenta již dostatečně otestovaná Microsoftem na nezanedbatelném množství pokusných králíků.:) . Provoz systému s replikacemi ale přesto není triviální, protože se po čase začnou objevovat problémy, jejichž řešení po mobilním telefonu s obchodním zástupcem vyžaduje velkou dávku sebezapření a neustálého sebeujišťování, že si právě teď zasluhujete plat, takže není žádoucí mezi zuby procedit jadrné hodnocení ohledně intelektální potence protějšku, s nímž právě hovoříte.
Proto jsem pro replikace napsal manažera replikaci, jenž je natolik inteligentní, že uživatele většinou ignoruje a jen občas mu dovolí kliknout na nějaké tlačítko :) Na co byste tedy při návrhu vlastního manažera replikací neměli zapomenout?
Ještě upozorňuji, že tato doporučení se týkají hlavně mně důvěrně známé konfigurace, kdy uživatelé přistupují přes VPN k MSSQL serveru s publikací a na svém počítači jsou přihlášení pod lokálním (ne doménovým) účtem. Uživatelé mají na svém počítači databázi MSDE 2000.
1) Ověření dostupnosti lokálního databázového serveru.
2) Kontrola existence lokální databáze, do níž jsou replikována data.
3) Manažer má v rozhraní metody pro založení a zrušení lokální databáze. V konfiguračním souboru je cesta k souborům se skripty pro zrušení a založení databáze.
4) Ověření dostupnosti serveru (počítače) s replikacemi - "Ping".
5) Zjištění, že na serveru běží MSSQL. Prakticky se jedná o ověření, že se lze připojit na port, na němž "poslouchá" MSSQL (standardně se jedná o port 1433).
6) Manažer musí umět založit novou "subscription". "Subscription" je aktivní relace mezi jedním klientem replikace a MSSQL. Na "subscription" je navázána kompletní historie provedených replikací a prohlédnout si ji můžete v Enterprise Manageru. Po uplynutí nastaveného intervalu neaktivity replikace jsou "subscription" na serveru vymazány. Když obchodní zástupce odjede na čtrnáctidenní dovolenou, tak za existenci této metody si můžete dát pořádného panáka Four Roses. Lépe dva:)
7) Komplementární k metodě pro založení "subscription" je pochopitelně metoda pro zrušení staré "subscription".
8) Reinicializace "subscription" bez ztráty existujících lokálních dat. Například při podezření na neúplné schéma databáze u klienta je nutné znovu doručit inicializační snapshot se schématem. Je zbytečné zakládat novou "subscription", postačí reinicializace stávající.
9) Ověření dostupnosti složek, ve kterých je uložen snapshot. Inicializační snapshot musí být před zahájením replikace dostupný. Ověření dostupnosti složek probíhá až po impersonaci uživatele (viz bod 11).
10) Ověření dostupnosti pracovní složky na lokálním počítači, ve které je rozbalován snapshot. Ověření dostupnosti složky probíhá opět až po impersonaci uživatele (viz bod 11).
11) Impersonace speciálního uživatele, který má doménový účet v síti s MSSQL serverem, na němž je vytvořena publikace. Účet má přístup jen k složkám se snapshoty a k lokálnímu pracovnímu adresáři replikace. Celá replikace běží pod tímto speciálním účtem. Impersonaci nemůžete provádět na počítačích, na kterých jsou nainstalovány operační systémy Windows 98 nebo ME.
12) A samozřejmě musíte mít metodu pro spuštění replikace.
A na konec. Měli byste zájem o nějaký podrobný tutorial o vytváření replikací?
Tuesday, 11 May 2004 21:41:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | (MS)SQL tipy a triky
Monday, 03 May 2004
Znáte třídy ServicePoint a ServicePointManager? Měli byste...
Při používání více threadů například pro získání obsahu WWW stránek ze stejného URI nebo při asynchronním a současně prováděném volání více metod jedné WWW služby se můžete setkat s podivnými výjimkami nebo ještě podivnějšími prodlevami mezi jednotlivými voláními.
Za rovnoměrné využití síťových zdrojů je odpovědná třída ServicePointManager. Ta obhospodařuje kolekci objektů ServicePoint, které jsou vytvářeny při prvním požadavku na dané URI. Třída ServicePointManager má několik zajímavých vlastností a metod.
Vlastnost DefaultConnectionLimit - hodnota vlastnosti určuje, kolik konkurenčních připojení může ve stejném okamžiku přistupovat ke stejnému URI. V .Net Frameworku 1.1 jsou ve výchozím nastavení povolena dvě konkurenční připojení na stejné URI. Jestliže například z nějakého důvodu potřebujete asynchronně a "najednou" volat více metod WWW služby, měli byste ConnectionLimit zvýšit.
Vlastnost MaxServicePointIdleTime - tato vlastnost říká, jak dlouho musí být dané URI neaktivní, aby se stalo kořistí Garbage Collectoru. Neaktivní znamená, že s daným URI nebylo po zadanou dobu vytvořeno žádné spojení. Výchozí hodnotou je 900.000 milisekund (15 minut)
Vlastnost MaxServicePoints - tato vlastnost říká, kolik instancí ServicePoint může být vytvořeno. Jinými slovy, tato vlastnost omezuje počet URI, na které se lze v jednom okamžiku připojit. Ve výchozím nastavení žádný limit neexistuje (vlastnost obsahuje hodnotu 0).
Metoda FindServicePoint - přetížená metoda FindServicePoint vrátí objekt ServicePoint pro zadané URI. Když ServicePoint pro URI ještě neexistuje, tak metoda vytvoří a vrátí nový objekt ServicePoint, jinak je vrácen existující ServicePoint.
U třídy ServicePoint můžeme upravit vlastnosti, jež jsou po vytvoření objektu inicializovány výchozími hodnotami převzatými z třídy ServicePointManager.
ConnectionLimit - má stejný význam jako vlastnost DefaultConnectionLimit u ServicePointManageru.
MaxIdleTime = má stejný význam jako vlastnost DefaultConnectionLimit u ServicePointManageru.
Tak a teď jednoduchý demonstrační kód konzolové aplikace. Nejdříve kód, který změní DefaultConnectionLimit tak, aby odpovídal počtu požadavků (WebRequest). Vše proběhne relativně rychle a dle očekávání.
using System;
using System.Net;
using System.Threading;
namespace RStein.ServicePointManagerTester
{
class ServicePointManagerTest
{
static void Main(string[] args)
{
Console.WriteLine(ServicePointManager.DefaultConnectionLimit);
ServicePointManager.DefaultConnectionLimit = 4;
WebRequest reg = WebRequest.Create("http://wwww.atlas.cz");
WebRequest reg2 = WebRequest.Create("http://wwww.atlas.cz");
WebRequest reg3 = WebRequest.Create("http://wwww.atlas.cz");
WebRequest reg4 = WebRequest.Create("http://wwww.atlas.cz");
WaitHandle handle0 = reg.BeginGetResponse(new AsyncCallback(test), reg).AsyncWaitHandle;
WaitHandle handle1 = reg2.BeginGetResponse(new AsyncCallback(test), reg2).AsyncWaitHandle;
WaitHandle handle2 =reg3.BeginGetResponse(new AsyncCallback(test), reg3).AsyncWaitHandle;
WaitHandle handle3 =reg4.BeginGetResponse(new AsyncCallback(test), reg4).AsyncWaitHandle;
WaitHandle.WaitAll(new WaitHandle[] {handle0, handle1, handle2, handle3});
Console.WriteLine("vse");
Console.Read();
}
private static void test(IAsyncResult ar)
{
Console.WriteLine("Konec requestu");
try
{
WebRequest reg = (WebRequest)ar.AsyncState;
WebResponse response = reg.EndGetResponse(ar);
response.GetResponseStream().Close();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
Nyní změňte DefaultConnectionLimit na 1. Jak můžete pozorovat, vyřízení všech požadavků je citelně pomalejší (alespoň u mě na GPRS), protože jsou vyřizovány jeden po druhém. K URI "www.atlas.cz" může vždy přistoupit pouze jedno připojení. Mimochodem, víte proč se v MSDN neustále zdůrazňuje, že je třeba co nejdříve uzavřít všechny použité streamy? Jestliže ne, upravte řádku response.GetResponseStream() Close() ; na response.GetResponseStream(); a spusťte znovu aplikaci.
Monday, 03 May 2004 21:21:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework
Thursday, 29 April 2004
Pár poznámek k implementaci metody Equals
V poslední době mě docela překvapilo, kolik špatných implementací metody Equals jsem nalezl na webu. Jak jistě víte, metodu Equals mají ve svém rozhraní všechny třídy v .NET Frameworku, protože je deklarována v základní a pro všechny třídy povinné bázové třídě Object. Metoda Equals zjišťuje, zda jsou dvě instance shodné. Ve třídě Object naleznete dvě metody Equals. Instanční virtuální metoda Equals přijímá jeden argument typu Object a její výchozí implementace porovnává referenční identitu dvou instancí. To znamená, že metoda vrátí true pouze tehdy, když porovnává proměnné odkazující na stejné místo v paměti. U hodnotových typů je metoda Equals přepsána tak, že porovnává pomocí reflexe všechny členy instance.
public virtual bool Equals(Object obj);
Pomocné statické metodě Equals se předávají dva argumenty typu object. Metoda nejdříve zkontroluje, jestli je nějaký argument null. Pokud ano, tak metoda vrátí false, jinak zavolá instanční metodu Equals na objektu objA a předá ji objekt v argumentu objB.
public static bool Equals( object objA, object objB);
Ověření, že dvě proměnné ukazují na stejnou instanci, není ale vždy dostačující. V aplikaci potřebujeme často zjistit, zda dvě proměnné ukazující na jinou instanci stejného typu nejsou logicky shodné. Konkrétně, objednávku s číslem 3 můžeme v systému vytvořit ve dvou nezávislých kopiích, ale když porovnáváme proměnné odkazující na tyto nezávislé instance, tak chceme, aby byly instance považovány za shodné. Právě proto musíme správně přepsat metodu Equals a doplnit vlastní porovnávací logiku.
Jak postupovat? Představme si, že chceme přepsat metodu Equals v objektu Order.
public override bool Equals(object obj)
{
}
V těle metody zjistíme, jestli argument obj není null a jestli neni jiného typu, než je typ objektu, na kterém je metoda Equals volána. Jestliže je argument null nebo má odlišný typ, tak ihned vrátíme false, protože nemůžeme porovnávat zcela odlišné nebo neinicializované objekty. Po těchto nutných kontrolách můžeme argument obj bezpečně přetypovat na typ Order a výsledek uložit do proměnné order2.
if (obj == null)
return false;
if (obj.GetType != this.GetType())
return false;
Order order2 = obj as Order;
Když dědíme z jiné třídy než Object, měli bychom zavolat přepsanou metodu Equals bázové třídy. Jestliže metoda Equals bázové třídy vrátí false, musíme z potomka vrátit také false.
Nyní musíme porovnat všechny relevantní soukromé i veřejné členy třídy. Bez ohledu na to, zda porovnáváme hodnotový nebo referenční typ, delegujeme odpovědnost za porovnání na pomocnou statickou metodu Equals třídy Object. Když zjistíme, že někteří členové nejsou shodní, musíme vrátit false. Jestliže se všichni členové shodují, můžeme z metody Equals vrátit true, protože jsme právě ověřili, že obě instance jsou identické.
if (!Object.Equals(this.ReferenceProperty1, order2.ReferenceProperty1))
return false;
if (!Object.Equals(this.ValueProperty2, order2.ValueProperty2))
return false;
return true;
V objektech, které mají jednoznačný identifikátor (Id), metoda Equals často porovná jen identifikátor Id. Porovnání není zcela přesné, protože objekty mohou mít různé hodnoty atributů, ale pro business aplikace je toto porovnání většinou dostačující. U hodnotových typů (struktur) by měla být vytvořena metoda Equals, která příjimá v argumentu přímo hodnotový typ, aby se předešlo boxingu při volání zděděné metody Equals, jež, jak jsme řekli, akceptuje Object. Správně napsaná metoda Equals musí dodržovat následující čtyři pravidla.
1) Musí být reflexivní - porovnám-li instanci A s instancí A, metoda musí vždy vrátit true.
2) Musí být symetrická - jestliže volání metody A.Equals(B) vrátí true, tak B.Equals(A) musí také vrátit true.
3) Musí být tranzitivní - jestliže volání metody A.Equals(B) vrátí true a B.Equals(C) také vrátí true, musí A.Equals(C) vrátit true.
4) Musí být konzistentní - když volání metody A.Equals(B) vrátí true a nedojde ke změně objektu A ani B, tak nové volání A.Equals(B) musí opět vrátit true.
Když máme přepsanou metodu Equals, je výhodné přetížit operátoy rovná se (==) a nerovná se (!=). Implementace je opravdu jednoduchá. Využijeme statickou metodu Object.Equals, která po kontrolách popsaných výše zavolá námi přepsanou instanční metodu Equals, v níž je již obsažena všechna potřebná logika. V operátoru "nerovná se" ještě pochopitelně výsledek metody Equals negujeme.
public static bool operator != (Order order1, Order order2)
{
return !object.Equals(order1, order2);
}
public static bool operator == (Order order1, Order order2)
{
return object.Equals(order1, order2);
}
Thursday, 29 April 2004 19:58:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework
Sunday, 25 April 2004
Výběr ze seznamu Id serverových ovládacích prvků na stránce
V konferenci EMWAC se objevil příspěvek http://konference.vyvojar.cz/post.aspx?id=53046, jehož autor chce do vlastnosti ve svém serverovém ovládacím prvku uložit Id jiného ovládacího prvku na stránce. Vývojář by neměl zapisovat Id přímo, ale vybírat ze seznamu na stránce vlastností. První myšlenkou autora příspěvku bylo nadeklarovat vlastnost jako WebControl - to je samozřejmě nesmysl. Když zkusíme perzistovat vlastnost na stránce (atribut PersistenceMode), tak dosáhneme pouze toho, že validátor ASPX stránky nás bude upozorňovat, že máme na stránce 2 prvky se stejným Id.
To, o co se autor snaží, umí všichni validátoři. Jejich vlastnost ControlToValidate nabídne Id všech prvků na stránce, kteří obsahují vlastnost dekorovanou atributem ValidationProperty. Vlastnost ControlToValidate je samozřejmě typu string. Seznam Id naplní speciální TypeConverter s názvem ValidatedControlConverter.
ValidatedControlConverter nelze použít pro načtení seznamu Id všech prvků na stránce, protože, jak jsem již napsal, ta bere v úvahu jen prvky s vlastností označenou atributem ValidationProperty. Proto jsem napsal ControlConverter, který nalezne Id všech prvků na stránce.
public
class
ControlConverter : StringConverter
{
public
ControlConverter() :
base
()
{
}
public
override
StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
if
((context ==
null
) || (context.Container ==
null
))
return
null
;
object
[] foundControls = enumerateControls(context.Container);
if
(foundControls ==
null
)
return
null
;
return
new
StandardValuesCollection(foundControls);
}
public
override
bool
GetStandardValuesSupported(ITypeDescriptorContext context)
{
return
true
;
}
public
override
bool
GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return
false
;
}
private
object
[] enumerateControls(IContainer container)
{
ArrayList retList = new ArrayList();
foreach
(IComponent component
in
container.Components)
{
Control foundControl = component as Control;
if
(foundControl ==
null
)
continue
;
if
((foundControl.ID ==
null
) ||
(foundControl.ID == String.Empty))
continue
;
retList.Add(foundControl.ID);
}
return
retList.ToArray();
}
}
Converter se u vlastnosti použije takto:
[Browsable(
true
)]
[TypeConverter(
typeof
(ControlConverter))]
public
string
MyControl
{
get
{
string
myControl = (
string
) ViewState["MyControl"];
return
(myControl ==
null
? String.Empty : myControl);
}
set
{
ViewState["myControl"] =
value
;
}
}
Sunday, 25 April 2004 18:44:00 (Central Europe Standard Time, UTC+01:00)
ASP.NET | .NET Framework