Vítejte na blog.vyvojar.cz Přihlásit | Registrovat | Pomoc

Vlko napísal ...

.. mostly harmless ...
Uff, ja a MVP?

Nejak sa z toho stále neviem spamätať, ale do môjho mailboxu dnes prišiel mail so subjectom: Congratulations Microsoft MVP! a text začínal:

Dear Marian Vlcak,

Congratulations! We are pleased to present you with the 2009 Microsoft® MVP Award!

Aby som predišiel pochybnostiam, skontroloval som občiansky a Marián Vlčák je skutočne moje meno.

Čo teraz?

Vo filmoch je to vždy chvíľa, keď nasleduje dojemná scéna ďakovania. Nuž ďakujem najmä svojej manželke, bez ktorej podpory, lásky a varenia by som to určite tak ďaleko nedotiahol, ďalej mojím dvom drobcom (tato vás ľúbi) a najmä mojím čitateľom. Viem, že tu v českej komunite až taký známy nie som, ale pravdepodobne som posledný blogujúci slovák na blog.vyyojar.cz a v najbližšej dobe to určite neplánujem zmeniť, pretože si stále myslím (aj keď už to možno nie je pravda), že český a slovenský programátori majú k sebe blízko a možno aj k sebe patria.

A tak najmä ďakujem tebe ty môj slovenský čitateľ, ktorý má pravdepodobne poznáš z fóra, alebo správičiek na portáli

http://www.aspnet.sk/

kde sa snažím informovať pekne po slovensky o novinkách, tipoch, overených postupoch, radách zo sveta .net (pravda viac ma priťahuje asp.net a asp.mvc). A v poslednej dobe pribudli ako doplnok k týmto informáciam tzv twitterky na

http://twitter.com/vlkodotnet

Nesmiem zabudnúť ani na ostatných. Spigiho, ktorý sa dal ukecať na správičky a vytvoril na aspnet.sk podporu pre ich zadávanie. Ľudom z MS, že si na mňa spomenuli. A aj ďalším ľuďom z našej malej slovenskej komunity, s ktorými sa mimochodom stretneme už túto nedeľu (DP (DEVELOPER PARTY) STRETNUTIE ĽUDí z ASPNET.SK), za to, že sa neokúňajú, prispievajú správičkami a tak za tých 20 mesiacov sa môžme pochváliť momentálne 1090 kusmi správičiek.

Plány do budúcnosti

Ako je stále mozog človeka zaplavený endorfínom, človek má chuť toho nasľubovať..., ale s tým už mám zlé skúseností, tak sa radšej budem držať pri zemi. Som predsalen pracujúci freelancer a k tomu bývam na Orave zašitý pred svetom uponáhľanej civilizácie, ale minimálne o správičky a twitterky neprídete a všetko naviac bude už len príjemným plusom:)

Ešte raz vďaka všetkým a dúfam, že som si to zaslúžil, inak by ma to veru mrzelo, bo viem, že existujú stovky, ba až tisíce ľudí, ktorý by si tento titul zasúžili oveľa viac, len sa boja prejaviť, preto sa netreba hanbiť, treba zozbierať odvahu a možno to budeš práve ty, kto môže nabudúci rok patriť medzi MVP.

ExpressionEval Two - the lambda case

Zdrojové kódy: http://vlko.zilina.net/dwn/blog/ExpressionEval.zip

Intermezzo

Zaregistrovaním ExpressionEval do súťaže „UKAŽ SE A VYHRAJ (Vyvíjej open-source s Microsoftem)“ som si vyrobil rest, ktorý hádam týmto blog príspevkom napravím.

Takže čo stojí v popise projektu ExpressionEval?

Popis projektu: Cieľom projektu je serializacia a deserializacia expression stromu do/z textovej podoby. Súčasťou projektu bude aj vzorovy linq provide, ktorú umožní vzdialené vykonávanie linq výrazov (zatiaľ v zjednodušenej forme bez podpory anonymných objektov) cez http protokol. Initial verzia je popísana na http://blog.vyvojar.cz/vlko/archive/2008/12/24/expressioneval-coding-story.aspx

 Je preto čas pokročiť a ponúknuť niečo viac ako popis a initial verziu a trošku sa rozpísať, pretože odteraz sa pod ExpressionEval zastrešujú 3 podprojekty:

  1. ExpressionEval - pomocou ktorého je možno z textového zápisu expression výrazu dostať funkčný expresion výraz, jednoducho ide o spôsob ako deserializovať expression výraz
  2. Evalizer - je zasa naopak spôsob ako z expression výrazu získať jeho textovú podobu, ktorá môže byť následne pomocou ExpressionEval-u prevedená späť na expression výraz
  3. RemoteLinq - toto je hlavný dôvod, prečo vznikli oba dva predchadzájuce projekty a to preniesť zo serveru linq podporu na klienta, opäť zjednodušene povedané, na klientovy používať komfort linq-u, ktorý sa ale reálne bude vykonávať na vzdialenom servere

ExpressionEval verzia Two

Oproti prvotnej verzii sa nám podpora parsera rozšírila a tak k pôvodnýmvlastnostiam:

  • statické funkcie, properties a fieldy
  • funkcie, properties a fieldy parametra
  • string reťazce bez obmedzenia, teda viacriadkové a s úvodzovkami definovanými po C#ovsky: \"
  • numerické konštanty okrem toho že číslo s bodkou je desattinný float a bez bodky Int32 je umožnené definovať typy pomocou končiaceho textu, teda (posledné tri som si vymyslel, možno v budúcnosti pridam automatickú konverziu typov):
    • 12.1 - float
    • 12.1f - float
    • 12.1d - double
    • 12 - Int32
    • 12u - UInt32
    • 12l - Int64
    • 12ul - UInt64
    • 12s - Int16
    • 12us - UInt16
    • 12b - Byte
  • indexery
  • operátory (*,/,%,+,-,>,<.<=,>=,==,!=,|,^,&,||,&&, ...)
  • vytváranie inštancii pomocou new keywordu

pribudli

  • bugfixy chýb predchadzajúcich vlastností
  • podpora záporných čísel (zaujímavé, že si na ne človek spomenie až, keď sa s nimi reálne stretne)
  • negácia pomocou ! operátora
  • konštanty null, true, false
  • lambda výrazy
  • extensions metódy (s tým súvisi nutnosť explicitne zadeklarovať typ pre aký sa majú extensions metódy deklarovať cez ExpressionEval.AddExtension funkciu)

Lambda výrazy a extension metódy

Myslím si, že tieto dve vlastnosti stoja za osobitnú odbočku, pretože implementovať ich bol ten najtvrdší oriešok, pretože v rámci zachovania, čo najvyššej rýchlosti vytvárania Expression stromu nezisťujem cez reflexiu metódu dopredu. Pri lambda výrazoch je toto ale nutné, pretože musíme dopredu vedieť typ parametru, z ktorého následne môžeme ďalej budovať expression tree, takže treba rátať s tým, že lambda výrazy môžu negatívne ovplyvniť rýchlosť.

K tomu pridajme problém s extension metód a nutnosť preskakovať prvý parameter funkcie. Ono extension metódy sú celkom problematická vec pre reflexiu. Vo visual studiu to máme jednoduché, pridáme using namespace-u a prekompiler sa o všetko sám postará. V skutočnosti sú ale namespace len doplnková informácia, ktorú pomocou expression nie je možné jednoducho queryovať. Jedinou možnosťou je proste prebehnúť všetky načítané assembly a v nich všetky typy a až potom je možné nájsť potrebnú extension metódu.

Najlepšie je uviesť príklad, teda pre vyhľadanie všetkých IQueryable<T> rozšírení z System.Linq namespacu

Evaluator.AddExtension("System.Linq", "IQueryable`1");

je potrebné

                // search all assemblies for extension methods

                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())

                {

                    // choose only methods extensions methods and method with defined type and assembly

                    var query = from type in assembly.GetTypes()

                                // only sealed not generics and not nested types contains extensions method

                                where type.IsSealed && !type.IsGenericType && !type.IsNested

                                // so take only static methods

                                from method in type.GetMethods(BindingFlags.Static

                                    | BindingFlags.Public | BindingFlags.NonPublic)

                                // and finally check ExtensionAttribute (in C# it is this keyword in first parameter)

                                where method.IsDefined(typeof(System.Runtime.CompilerServices.ExtensionAttribute), false)

                                // now compare if type name and namespace is as specified

                                where extensionsToLoad.Any(exInfo =>

                                {

                                    Type paramType = method.GetParameters()[0].ParameterType;

                                    return (paramType.Name == "IQueryable`1")

                                        && (method.DeclaringType.Namespace == "System.Linq");

                                })

                                select method;

                }

Brrr, ide o pekne výpočtovo náročnú operáciu a preto sú všetky resolvnuté typy ukladané do statickej cache, takže na to myslíte pri svojich performance testoch a pridajte viac ako jednu iteráciu:)

Evalizer

To, že úlohou evalizeru je vytvoriť z expression výrazu textový ekvivalent je vám už jasné a osobne som to na začiatku pokladal za tú najjednoduchšiu úlohu, proste sa zavolá Expression.ToString() a je vystarané. 

Akurát, že:

  • čísla sú formatované s v aktuálnej kódovej stránke
  • extension metódy neobsahujú definíciu typu a to ide o statické metódy, ich textová reprezentácia sa ale tvári, že ide o metódy normálne, čo sa celkom dobre číta, ale bohužiaľ zle serializuje
  • typ new a teda NewExpression obsahuje iba meno, nijakú informáciu o namespace, prípadne assembly
  • converzia typov (napr. byte->int) sa proste volá Convert, nikde nie je informácia o tom na aký typ by sa mala konvertnúť
  • a najlepšie nakoniec, ak použijete vonkajšiu premennú, tak tá sa automaticky neresolvne, ale je buď odkazovaná priamo, v prípade privátnej premennej je vytvorená pomocná trieda, ktorá zapuzdruje túto hodnotu

Všetky tieto problémy ale musel Evalizer vyriešiť. V prvých troch bodoch rozpísať serializačnú funkciu pre každý mysliteľný typ Expression a triedu som bol príliš lenivý, použil som fintu a potrebné informácie odpĺňam pomocou Regex-u. Convert zasa nahrádzam patričnou Convert.To<Typ>(hodnota) funkciou, čo na základné typy bohate stačí.

A nakoniec to najzložitejšie a to náhrada zapuzdrených hodnôt. Tá je nazaujímavejšia a funguje tak, že dokiaľ nejde o ExpressionParameter, tak sa snaží compilovať expression, dokiaľ nedostane návratový typ, ktorý je zo namespace System.

A čo je dôležité, všetko krásne funguje a tak

            int x = 3;

            int y = 3;

            double z = 2.2;

            bool boolValue = false;

            string stringValue = "Test";

            Func<int> () => 5 * x // sa evalizuje na (5 * 3)

 

            Func<int> () => 5 * x / y // na ((5 * 3) / 3)

 

            Func<double> () => 5 * x / y * z // na (System.Convert.ToDouble(((5 * 3) / 3)) * 2.2)

 

            Func<bool> () => (stringValue.IndexOf("T") == 0) == boolValue // na ((0 = 0) = False)

pre zaujímavejšie konštrukcie odporúčam unit testy v projekte.

RemoteLinq

Je vyvrcholenie celého projektu a jeho cieľ, aj keď sa zdá, že ide počtom riadkov o jeho nepodstatnú časť. Trošku ma zamrzí, že Microsoft mi pomocou ADO.NET Data Services (project Astoria), vlastne vytvorili niečo veľmi podobné a najmä sofistikovanejšie, ale majú proste k dispozícii viac ľudí, takže je to pre nich o to jednoduchšie, čo už len môže zmôcť jednoduchý slovenský programátor?:) Mňa aspoň môže potešiť, že umožnujem serializovať aj tie extension metódy, pomocou ktorých môžete definovať fetching funkčnosť, prípadne iné špecifické veci, bez ktorých by ste sa neradi zaobišli.

Takže späť k projektu, pretože ten si určite každý bude chcieť vyskúšať a overiť či to o čom je celý tento príspevok nie je len prázdne mlátenie hubou. Aby ste ho teda mohli spustiť a otestovať je potrebné poznať štruktúru projektov, z ktorých sa skladá:

  • RemoteLinqBase - základná knižnica, ktorá obsahuje popis modelu a základné triedy pre server časť a client časť (celý remote linq je len taký nadstrel ako by taky remote linq mal vyzerať)
  • RemoteLinqServer - server časť, ktora pomocou REST starter kitu zprístupňuje cez http protokol dáta (zmienil som sa už že príklad používa REST?)
  • RemoteLinqClient - client časť je jednoduchá konzolová aplikácia, ktorá ukazuje ako pomocou normálnych linq queries ukazuje, že to proste funguje, kto neverí, nech localhost nahradí za fiddler a sám sa presvedčí

Ako RemoteLinq v skutočnosti funguje už normálne nemam po toľkom texte ani chuť ani silu, okrem toho ide o takú oklieštenú verziu implementácie pre LLBLGen na strane servera (LLBLGen by asi priniesol do projektu projektu závislosť na komerčnej knižnici a to ja nerád, kto ale má záujem, vždy sa môžme dohodnúť, v predprodukčnom nasadení to už funguje;).

Nuž v skratke teda:

  • ako som už písal vyššie na komunikáciu na strane servera používam REST starter kit, ktorý zasa využíva WCF
  • používam DataContract serializáciu, s priamo definovanými podporovanými typmi pre serializáciu, takže si môžem odpustiť nutnosť definovať DataContract atribúty na triedov modelu
  • a pretože model na klientovi a servere je rovnaký, nerobí to problém, vy ale na to musíte myslieť
  • z linq funkčnosti mam otestované Where, Order, Count, Skip, Take, teoreticky nie sú problém aj iné, ale to už v nejakom ďalšom release, nápady sú a tieto by mali pre väčšinu prípadov postačovať
  • na strane klienta je použitý jednoduchý linq provider na základe inšpirácie z článku LINQ: Building an IQueryable Provider - Part
  • a na strane servera je IQueryable simulované jednoduchým List<T>.ToQueryable()

Nechcem vás ale obrať o radosť povŕtať sa v zdrojových kódoch a tak sa do toho puste sami, nie je to zatial žiadna sláva, zopár review kód ešte bude musieť prekonať, dôležité ale je, že to funguje, no nie?

Performance

Na tú som prihliadal pri návrhu najviac a tak na prvotny parsing je použitý regex, potom sú výsledky prehnané cez stavový automat, ktorý vytvorí prvotnú stromovú štruktúru na základe ktorej sa vytvorí samotný expression tree. Aj keď som to na začiatku nepredpokladal, nie som sám:

  • od Microsoft je tu Dynamic Query Library, ktorá je súčasťou samples vo visual studiu
  • MetaLinq
  • tie predchádzajúce sa dostali do performance testu, potom tu je napr Mono.Cecil, ale ten je už o troška niečom inom a hlavne asi vyžaduje vyššie právomoci, v prípade ak máte nejaké ďalšie zaujímavé knižnice na porovnanie, dajte vedieť do komentárov

Súčasťou projektu je aj ExpressionEvalPerformanceTest projekt, ktorý je jednoduchá konzolová aplikácia na základne porovnanie výkonnosti. V porovnaní je teda ExpressionEval, DQL a MetaLinq a pre zaujímavosť aj serializácia cez Evalizer a jeho spätný prevod cez ExpressionEval.

Ako si v porovnaní ExpessionEval stojí? Po prvom rune je beznádejne posledný, ale akonáhle je knižnica v pamäti poráža všetkých súperov o hlavu, veď sa presvedčte sami tú sú výsledky (nie su testované lamba, extensions a niektoré ďalšie črty, ktoré ostatným knižniciam proste chýbaju:():

Run number: 0
Normal lambda expression creation finished in 3 ms.
Expression eval creation finished in 150 ms.
Dynamic Library creation finished in 74 ms.
Deserialization finished in 133 ms.
Expression eval through evalizer creation finished in 56 ms.
Run number: 1
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 13 ms.
Deserialization finished in 11 ms.
Expression eval through evalizer creation finished in 14 ms.
Run number: 2
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 16 ms.
Deserialization finished in 12 ms.
Expression eval through evalizer creation finished in 12 ms.
Run number: 3
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 11 ms.
Deserialization finished in 11 ms.
Expression eval through evalizer creation finished in 11 ms.
Myslím si, že výsledky netreba nijak komentovať.

TODO

A úplne nakoniec, čo nás čaká v najbližšej budúcnosti

  • vybratie licencie (tej najmenej reštriktívnej, takže sa nemusíte báť už teraz použiť knižnicu niekde v svojich projektoch)
  • uverejniť projekt na codeplexe, aby som splnil podmienky súťaže, bohužiaľ sa mi do anglického popisu dvakrát nechce
  • podpora pre anonymous triedy
  • vytvoriť RemoteLinq pre NHibernate, tak aby mal čo najviac blížil plnohodnotnému linq provideru

Možno súťaž nevyhrám, najmä keď pozerám na konkurenciu a hlavne neviem či sa nájde niekto, kto okrem mňa tento projekt aj reálne použije, ale išlo o hádam tu najzábavnejšiu prácičku v mojom poslednom období (najmä keď na začiatku som vôbec nedúfal, že je takéto niečo možné, ešteže tu máme agile) a tak nemám srdce nechať projekt hniť na jednom mieste, keď stovky betatesterov sa nemôže dočkať až ma zaplavia kopou chýb;)

VSWindowManager2008 - urobte si v oknách konečne poriadok

Zdrojové kódy: http://vlko.zilina.net/dwn/blog/VSWM2008-src.zip

Binárky: http://vlko.zilina.net/dwn/blog/VSWM2008-bin.zip

Intermezzo

Tak ako velí tradícia, aj dnes si neodpustím posťažovať sa na dôvod, prečo tento prototyp vznikol. Pretože na svojom pracovnom notebooku, ktorý je v práci bežne pripojený k HD monitoru, mám natívne rozlíšenie len 1280x800, čo by až tak nevadilo, ale občas si po večeroch zapnem visual studio aj bez externého monitoru a to potom sa medzi tymi všetkými tool oknami, okienko s kódom stráca.

Jedno je jasne, management tool okien vo Visual Studiu by potreboval nejakého pomocníka. No a pretože vo Visual Studiu 2005 som už tento problém riešil a čoho výsledkom bol VSWindowManager pre Visual Studio 2005. Ktorý bol zase inšpirovaný projektom zo zaniklého GotDotNet a teraz už na codeplexe: VSWindowManager (mimochodom verzia pre VS 2008 sa nachadza v source code časti, preto ak vám moja verzia nevyhovuje, odporúčam siahnuť po tejto).

O VSWindowManager-i

Toľko k histórii, k teórii sa dostaneme neskôr, teraz poďme k features. Takže po inštalácii nám v Tool menu pribudne nové menu VSWindowManager:

 

 a pre užívateľský komfort aj lišta do toolbaru:

Prvé štyri sú default, teda prepínajú sa podľa toho v akom režime sa visual studio nachádza (nie je umožnené ich pomocou addinu vymazať) a máte možnosť si vytvoriť neobmedzené množstvo vlastných window konfigurácii (viac o window konfiguráciach v technickej časti nižšie).

Takže vlastnosti v bodoch:

  • položka menu View config names - zobrazi zoznam všetkých konfigurácii
  • položka menu/toolbaru Save config - vyvolá dialóg v ktorom môžete buď zadať meno novej konfigurácie, alebo z comboboxu si vybrať existujúcu a tú aktuálnou konfiguráciou okien prepísať
  • položka menu Delete config - vyvolá dialóg v ktorom si môžete vybrať užívateľskú konfiguráciu, ktorú si želáte vymazať
  • položky menu [názov_konfigurácie] - vybraným nastavením nahradí aktuálnu konfiguráciu okien (aktuálna konfigurácia sa nemení)ň
  • položka toolbaru Choose config [názov_konfigurácie] - zobrazuje aktuálnu systémovú konfiguráciu vybraným nastavením nahradí aktuálnu konfiguráciu okien (aktuálna konfigurácia sa nemení) 
  • pre každú konfiguráciu si je možné cez Tools->Options položka Enviroments->Keyboard a pre zjednodušenie zadajte filter VSWindowManager zadefinovať klávesovú skratku

Rada na záver: Konfigurácia je uložená v užívateľských nastaveniach visual studia, takže je ich možné ľubovoľne exportovať a importovať cez Tools->Import and Export Settings a v strome je to All Settings->General Settings->Window Layouts.

Inštalácia

V binárkach sa nachádza okrem inštalačky, ktorá okrem application adresára (ktorý je ale úplne zbytočný) nakopíruje addin do  %user_documents%\Visual Studio 2008\Addins\, takže ak nie ste lenivý a dokážete to urobiť sami, tak si súbory VSWindowManager2008.dll aVSWindowManager2008.AddIn môžete nakopírovať sami:)

Technológia na pozadí

Aj keď funkčnosť addinu môže vyzerať na prvý pohľad implementačne komplikovaná, opak je pravdou. Pre ukladanie pozície okien nie je potrebné žiadne prechádzanie štruktúrov zobrazených okien, ale všetko podstatné už má DTE interface v sebe (začal som takto rovno na vec, pretože predpokladám, že do tohto textu sa začítajú len tí ľudia, čo si aspoň jeden addin skúšobne vygenerovali a Connect objekt aspoň okom prelistovali), konkrétne to je property _DTE.WindowConfigurations, čo je opäť interface WindowConfigurations, ktorý zaobaľuje kolekciu jednotlivých WindowConfiguration.

Kolekcia WindowConfigurations obsahuje property ActiveConfigurationName, čo nie je nič iné ako aktuálna konfigurácia okien a indexer funkciu Item, pomocou ktorého sa viete dostať ku konkrétnej inštancii WindowConfiguration. Pretože ako vstupný objekt do funkcie Item vstupuje objekt je možné ku konkrétnej inštancii sa dostať buď pomocou jej mena, alebo indexu pozície (pozor index začína od 1, asi ide o wrapper nad nejakou visual basic implementáciou, inak si to vysvetliť neviem:).

Nuž a keď už nejakú tú window konfiguráciu máte vybratú, tak pomocou funkcii:

  • Apply - nastavíte danú konfiguráciu ako aktívnu, tu by som upozornil na to, že neodporúčam meniť aktuálnu konfiguráciu, ale urobiť nova.Apply(), aktualna.Update() a  aktualna.Apply(), pretože inak sa každa zmena na aktuálnych oknách, dokiaľ neurobíte zmenu režimu automaticky aj ukladá a tym pádom sa zmysel uloženej konfigurácie vytráca, to je aj rozdiel oproti addinu na codeplexe
  • Delete - danú konfiguráciu zmaže
  • Update - pretože ako píšem v poznámke pri Apply, každá zmena sa automaticky ukladá do aktuálnej konfigurácie, tak Update je len skopírovanie aktuálnej konfigurácie do danej inštancie

Visual Studio ma 4 default window konfigurácie, ktoré mení podľa toho v akom režime sa akurát nachádzate

  • Design - štandardny coding užívateľský mód
  • Debug - visual studio debugguje nejakú aplikáciu
  • FullScreen - špeciálny mód po stlačení klávesy F11
  • NoToolWin - nemám ani potuchy, kedy sa tento mód používa, ak viete, podeľte sa v komentároch

Oproti verzii Visual Studia 2005 je ale _DTE.WindowConfigurations vo verzii 2008 inicializovaná až po plnom startupe visual studia. Pretože ale takýto event vám addin neposkytne, budete musieť byť odkázany na určitú lazy logiku, takže určite ak sa rozhodnete pre vlastnú implementáciu, inšpirujte sa mojim kódom, inak budete pár hodin rozmýšlať, prečo dostávate com exception (ja osobne som si už myslel, že táto property vo vs2008 proste nebola implementovaná).

Záver

Dúfam, že tento malý addin sa vám stane dobrým pomocníkom, veď tým ako visual studio priberá nové a nové funkčnosti a vám sa hromadí počet vyskakovacích okien, niečo podobné sa pomaly stáva nutnosťou. Možno by ešte stálo za to zvážiť nejaké automatické prepínanie na základe typu aktuálne otvoreného okna, ale taká automatická funkčnosť býva občas viac na škodu ako na úžitok, veď nakoniec klávesová skratka a automaticke prepínanie window layoutu, ktoré už v sebe visual studio má, by malo úplne postačovať.


[ANN] Správičky po roku: 666 kusov

Čas pomaly ale isto beží a je tomu už pomaly rok, čo na aspnet.sk vznikli krátke správičky: [ANN] Správičky na aspnet.sk.

Úprimne sa hneď na začiatok priznám, že cieľ tohto príspevku nie je len informatívny, ale aj agitačný. Proste získať nejakého toho nového čitateľa, alebo v lepšom prípade nového prispievateľa.

Takže, čo sa za ten rok vlastne podarilo?

  • V čase písania tohto článku sa ukazovateľ počtu zastavil na čísle 666, čo za dané obdobie dáva v priemere 1.79 správičky na deň a to vrátane sviatkov a víkendov.
  • Informačnú hodnotu jednotlivých správičiek necham radšej na čitateľa, ale povedzme, že je snaha, aby tam nejaká bola a okrem najnovších informácii sa tu nachádzajú aj bežné témy, ktoré by nás mohli trápiť.
  • Raz za čas sa objaví správička s tagom [Polemika], čo nie je nič iné ako slovenský názov pre flame (samozrejme flame intel vs amd, opera vs ff radsej nevedieme, ale témy ako [Polemika] Čo vás na .net srdí? alebo [Polemika] O SOLID a tak podobne môžu poskytnúť zaujímavé myšlienky)
  • Štýl. K nemu môžem hovoriť len za seba, no a ja miesto striktne informačného preferujem striktne osobno-názorový, čo môže v niekom spôsobiť penenie krvi v tele:) Okrem toho je takto informácia určite lepšie stráviteľná (aspoň podľa mňa).
  • Informačný zdroj v slovenskom jazyku (bohužial z neznámeho dôvodu je nepísaným pravidlom, že česi na slovenské portály nechodia), čo určite ocenia všetci, komu slovenský jazyk je príjemnejší ako anglický.

Tak či onak, správičky sú niečo medzi mikroblogingom a inteligentnými bookmarkami a je len na Vás, či si vyberiete:

No a nakoniec zaujímavosť, prípadne námet na diskusiu:

Neviem, či ste si všimli ale v súčastnosti som na blog.vyvojar.cz posledný aktívne blogujúci slovák (aj keď to "aktívne" sa dá asi brať s rezervou:). To ma privádza k otázkam:

  • Ako ďaleko sa vzdialila CZ komunita tej SK?
  • Navštevujú vôbec česi nejaké slovenské portály?
  • Pretože u nás programy po česky ešte bežne bežia, tak koľko bude trvať až bude pre CZ komunitu slovenčina cudzí jazyk?
  • A vlastne rozumie mi po slovensky ešte niekto?
Ako narvať generické triedy do kolekcie

Intermezzo

Verím tomu, že časť kódu v tomto prispevku dostanem veľkú prednášku o typovej bezbečnosti, ale na druhu stranu za splnenia určitých podmienok je to použiteľné riešenie.

Ale pekne po poriadku. Na tom mojom počiatku pri riešení jednej zaujímavej úlohy (zatiaľ s kódovým názvom REST repository, o ktorom určite až ho dovediem z prototypu do použiteľnej podoby čoskoro napíšem (BTW serializacia lambda výrazov pomocou na vianoce publikovaného ExpressionEval je proste božia:))  vyvstala potreba vytvorenia kolekcie generických tried, teda niečo ako List<Generics<Parent>>, kde by šlo vkladať všetky generika, ktoré majú špecifického parenta.

Nech som ale premýšlal ako som chcel schodnú cestu som nenašiel, je sice možné vytvoriť interface, ktorý bude negenerický, ale to by ma nútilo siahnuť do generickej triedy a mať v nej vlastne dve funkcie. Jednu vracajúcu generický a druhý nejaky parent typ. 

Ďalším riešením je generické triedy vkladať ako objekty a pred každým použitím ich pretypovať, ale nejak rozumne by som si potom musel uchovávať typ na aký to mám pretypovať a veru nič rozumné som neobjavil.

A pritom čarovným slovičkom je variácia, presnejšie covariáncia [viď správička C# 4.0: Čo je to ten Variance a ako to je s fintou zvanou default parameter?], ale kto bude čakať na .net 4.0, preto som sa vydal cestou skúmania a zistil, som že vlastne delegáty covarianciu už dávno majú (Covariance and Contravariance in Delegates (C# Programming Guide)) a dala by sa ľahko znásilniť nasledovne:

Simulujeme Covarianciu generík

Najskôr teda majme generickú triedu

 

    public class GenericsClass<TItem> where TItem : class

    {

 

        private TItem _Item;

 

        public GenericsClass(TItem item)

        {

            _Item = item;

        }

 

        public TItem GetItem()

        {

            return _Item;

        }

    }

Podmienku, že TItem je trieda hádam netreba komentovať, o primitívne typy mi nejde.

Takže hneď z úvodu je vidieť, že bez nejakej tej barličky to nepôjde, musíme si preto najskôr vytvoriť delegáta pre funkciu GetItem()

 

    public delegate TItem GetItemDelegate<TItem>();

A o to podstatné sa postará pomocná trieda:

 

    public class GenericsWrapper<TParent> where TParent : class

    {

        public GetItemDelegate<TParent> GetItem { get; set; }

        public static GenericsWrapper<TParent> GetWrapper<TItem>(GenericsClass<TItem> source)

            where TItem : class, TParent

        {

            GenericsWrapper<TParent> wrapper = new GenericsWrapper<TParent>();

            wrapper.GetItem = source.GetItem;

            return wrapper;

        }

    }

Je dôležité si všimnúť where podmienku, ktorá nám zabezpečí typovú bezpečnosť a bezproblémovú covarianciu.

Za predpokladu, že máme na začiatku tieto triedy:

 

    public class Parent

    {

        public string Value    { get; set; }

    }

 

    public class Item1 : Parent    {}

 

    public class Item2 : Parent    {}

potom môžeme vytvoriť nasledujúci kód:

 

        static void Main(string[] args)

        {

            // create generics class for type Item1

            var item1Gen = new GenericsClass<Item1>(new Item1{Value = "Item1"});

            // create generics class for type Item2

            var item2Gen = new GenericsClass<Item2>(new Item2{Value = "Item2"});

            // create generics class for base type Parent

            var collection = new List<GenericsWrapper<Parent>>();

            // fill collection with wrappers to generics classes

            collection.Add(GenericsWrapper<Parent>.GetWrapper(item1Gen));

            collection.Add(GenericsWrapper<Parent>.GetWrapper(item2Gen));

            // and do simple test

            foreach (var wrapper in collection)

            {

                Console.WriteLine("type: {0}, Value:{1}", wrapper.GetItem().GetType(), wrapper.GetItem().Value);

            }

            Console.ReadKey();

        }

Je síce potrebné pre každú funkciu vytvoriť delegáta (prácu si môžme zjednodušiť pomocou Action a Func) a potrebný kód okolo. Každa sranda niečo stojí ale bez zásahov do kódu pôvodnej generickej triedy sme dostali celkom peknú funčnosť.

Dirty full variance

Z predchádzajúceho kódu určite každý pochopil, že takto je možné riešiť aj contravarianciu, ja ale v mojej generickej triede mám aj funkcie, ktoré generikami definovaný typ používajú ako parametre. Teda ešte raz varujem plná  variancia nie je typovo bezpečná!!!

Tak si teda poďme rozšíriť našu generickú triedu o novú funkciu AddItem, ktorá berie generikou definový typ ako parameter:

 

    public class GenericsClass<TItem> where TItem : class

    {

        private TItem _Item;

 

        public TItem AddItem(TItem item)

        {

            _Item = item;

            return _Item;

        }

 

        public TItem GetItem()

        {

            return _Item;

        }

    }

Zadefinujeme si nového delegáta

 

    public delegate TItem AddItemDelegate<TItem>(TItem item);

A rozšírime wrapper triedu

 

    public class GenericsWrapper<TParent> where TParent : class

    {

        public AddItemDelegate<TParent> AddItem { get; set; }

 

        public GetItemDelegate<TParent> GetItem { get; set; }

 

        public static GenericsWrapper<TParent> GetWrapper<TItem>(GenericsClass<TItem> source)

            where TItem : class, TParent

        {

            GenericsWrapper<TParent> wrapper = new GenericsWrapper<TParent>();

            wrapper.GetItem = source.GetItem;

            wrapper.AddItem = delegate(TParent item)

            {

                return source.AddItem((TItem)item);

            };

            return wrapper;

        }

    }

Opäť je jasné, že AddItem delegáta zo source.AddItem nijak nejde dostať, preto si pomôžeme anonymným delegátom a malým pretypovaním.

Pre test pridajme ešte dve pomocné triedy

 

    public class Item3 : Parent {}

 

    public class differentItem {}

A máme finálny kód:

 

        static void Main(string[] args)

        {

            // create generics classes

            var item1Gen = new GenericsClass<Item1>();

            var item2Gen = new GenericsClass<Item2>();

            var item3Gen = new GenericsClass<differentItem>();

            // create generics list

            var collection = new List<GenericsWrapper<Parent>>();

            collection.Add(GenericsWrapper<Parent>.GetWrapper(item1Gen));

            collection.Add(GenericsWrapper<Parent>.GetWrapper(item2Gen));

            // add generics class without reference

            collection.Add(GenericsWrapper<Parent>.GetWrapper(new GenericsClass<Item3>()));

            // try to call gc if generics class for type Item3 will persist

            GC.Collect();

            // this will fails on compile time

            //collection.Add(GenericsWrapper<Parent>.GetWrapper(item3Gen));

            collection[0].AddItem(new Item1{Value = "Item1"});

            collection[1].AddItem(new Item2{Value = "Item2"});

            collection[2].AddItem(new Item3{Value = "Item3"});

            // this will fails on runtime

            // collection[1].AddItem(new Item1{Value = "Item1"});

            //

            foreach (var wrapper in collection)

            {

                Console.WriteLine("type: {0}, Value:{1}", wrapper.GetItem().GetType(), wrapper.GetItem().Value);

            }

            Console.ReadKey();

        }

A teraz je už len na Vás aby ste ma pekne sprznili v diskusii, prípadne ponúkli svoj vlastný nápad ako by ste problém riešili.

PS: Celý kód nájdete v prílohe tohto článku.

ExpressionEval coding story [Update]

Ako obyčajne začínam hneď zdrojovými kódmi, nech záujemcov o code review nemusím zdržiavať zbytočnými rečmi:)

Zdrojové kódy: http://vlko.zilina.net/dwn/blog/ExpressionEval.zip

Update č. 1

Ospravedlňujem sa, nejak som zverejnil zlú linku, už je to opravené.

Intermezzo

Pretože tu máme vianoce, rád by som vám aj ja dal od ježiška jeden malý darček a tým je knižnica, ktorá z predaného textu a pár ďalších informácii vygeneruje patričnú lambda expression. Síce by som tieto zdrojové kódy zverejnil tak, či tak, ale takto je z toho cítiť krásnu myšlienku. Ten pragmaticky dôvod je ako pri každom oos projekte užívateľský unittesting a možný ďalší rozvoj komunitou, pretože táto malá knižnička narozdiel od ostatných proof of concept coding stories pôjde do production.

Do intermezza tradične patria nejaké tie pohnútky:

  1. V administračnej sekcii sme pre jeden typ problému potrebovali zakomponovať užívateľský filter. V podstate by stačila iba jednoduchá funkčnosť, na základe id urobiť and a or operácie plus vyhodnotenie priorít podľa zátvoriek.
  2. No keď som sa už mal do toho pustiť koncept som troška rozšíril a ďalšia funkčnosť by bol bezpečnostný filter nad DAL entitami, teda aby užívateľ videl len to čo treba.
  3. A pretože by sme radi na komunikáciu server-client použili REST, toto by sa dalo ideálne použiť na serializáciu/deserializáciu Expression, kde by sa po ceste tak nejak stratila typová závislosť, teda na rovnaký Expression pre rôzne triedy s rovnakými properties.

Čo je načrtnuté v bode 2, z bodu 3 je jasne vidieť. Expression je tá správna vec, čo potrebujem, pretože:

  • môžeme ho kompilovať a cachovať v tejto kompilovanej podobe
  • umožnuje používať premenlivé parametre
  • cez linq provider je prevediteľný do sql tvaru
  • a jednoducho sa mi zdá ako praktickejšie riešenie oproti kompilácii z C# kódu [Evaluate C# Code (Eval Function)] alebo čokoľvek iné, čo sa mi podarilo vygoogliť, tak týmto aspoň zatmelím dieru na trhu:)

Kto číta moje správičky na aspnet.sk určite vie, že ma proste takéto veci zaujímaju a keďže som to potreboval do jedného projektu a najmä dostal týždňové pracovné okno, nebolo inej voľby ako sa do toho pustiť. No uznajte pri takýchto úlohach musí človek milovať svoju prácu:)

Ako používať

Po nalinkovaní knižnice stačí vytvoriť inštanciu ExpressionEval, nastaviť jej patričný kód, zaregistrovať patričné parametre, namespaces pre vyhľadávanie typov, niečo ako using v C# a pretože to veru nie je žiaden kompilátor pre získanie typu je potrebné registrovať meno assembly. Vše ukončíte volaním funkcie Eval so špecifikovaním typu delegáta. Všetko pekne vo fluent formáte. A takto nejak to mam v unitteste:

 

        [TestMethod]

        public void SimpleLambda()

        {

            Expression<Func<int, int>> lambda = new ExpressionEval("Convert.ToInt32(String.Concat(\"3\", x.ToString().ToString()))")

                .AddParam<int>("x")

                .AddLookupAssembly("ExpressionEvalTest")

                .AddLookupNamespace("ExpressionEvalTest", "ExpressionEvalTest")

                .Eval<Func<int, int>>();

            int result = lambda.Compile().Invoke(1);

            Assert.AreEqual(31, result);

        }

Síce tie AddLookup funkcie sú tam len kvôli efektu, ale to je teraz nepodstatné, dôležité je vedieť čo podporujeme:

  • statické funkcie, properties a fieldy
  • funkcie, properties a fieldy parametra
  • string reťazce bez obmedzenia, teda viacriadkové a s úvodzovkami definovanými po C#ovsky: \"
  • numerické konštanty okrem toho že číslo s bodkou je desattinný float a bez bodky Int32 je umožnené definovať typy pomocou končiaceho textu, teda (posledné tri som si vymyslel, možno v budúcnosti pridam automatickú konverziu typov):
    • 12.1 - float
    • 12.1f - float
    • 12.1d - double
    • 12 - Int32
    • 12u - UInt32
    • 12l - Int64
    • 12ul - UInt64
    • 12s - Int16
    • 12us - UInt16
    • 12b - Byte
  • indexery
  • operátory (*,/,%,+,-,>,<.<=,>=,==,!=,|,^,&,||,&&, ...)
  • vytváranie inštancii pomocou new keywordu

Ak vás to viac zaujíma, pozrite si priložené unittesty.

Performance

Súčasťou projektu je aj program pre porovnanie výkonnosti. Tá je niekde na úrovni 60 násobku natívne zapísaného lambda výrazu. Poteší aspoň, že evaluácia je predsa len o trošku rýchlejšia ako deserializovanie pomocou MetaLinq.

A coding príbeh?

Pretože sú už dnes vianoce, veľa hodín a online budem až o pár dni, rád by som tento príspevok zverejnil už dnes. Inak by som bol nútený meniť úvod a pokaziť vám tento darček a to by ja nerád. Pre dnes aspoň načrtnem:

  • prvý na rad prichádza tokenizer, čo je jednoduchy regex výraz 

        private const string cTextRegexPart = @"""((?:(?:\\"")|[^""])*)""";

        private const string cExecutableRegexPart = @"(\w+)";

        private const string cDelimiterRegexPart = @"([\(\)\[\]\.,])";

        private const string cOperatorRegexPart = @"([^\w\(\)\[\]\.,\s]+)";

        private const string cCodeParserRegex = cTextRegexPart + "|" + cExecutableRegexPart + "|" + cDelimiterRegexPart + "|" + cOperatorRegexPart;

  • ďalší na rad prichádza stavový automat, ktorý prechádza tokenizovaný text a popritom si vytvára pomocnú tree štruktúru
  • tretím krokom je vygenerovanie expression tree z pomocnej tree štruktúry

TODO

  • ak je záujem tak podrobnejšie rozpísať jednotlivé body, tak dajte vedieť do diskusie
  • dorobiť podporu pre negáciu [!]
Zoomer coding story [update 1]
Binárka: http://vlko.zilina.net/dwn/Zoomer-bin.zip

Zdrojové kódy: http://vlko.zilina.net/dwn/Zoomer-src.zip

!POZOR aplikácia je funkčná len pod zapnutým Aerom, pretože využíva služby Desktop Window Managera (alebo pod známejším názvom Aero)!

Na zdrojové kódy, ani obrázky si neuplatňujem žiadne autorské práva.

Ak sa chcete dozvedieť čo to o vyvoji aplikácie preskočte časť Intermezzo, a pustite sa rovno do devel časti.

Update č. 1

Trošku mi to nedalo spávať a tak som sa zamyslel nad problémom toho ako vykresľujem náhľad obrázka a už je tu prvý update. Tento update prináša radikálne zlepšenie výkonu. Ako som toho dosiahol?

V prvej verzii som vždy vykresľoval a registroval pre náhľad celý desktop aj keď v lupe sa vlastne zobrazoval iba jeho malý výrez. Pomocou DWM_THUMBNAIL_PROPERTIES, ktorým updatujeme pozíciu náhľadu a jeho fieldu rcSource je však možné nastaviť len aktuálny výrez z okna, ktorý chceme zoomovať.

Toto riešenie prinieslo také zvýšenie výkonu celej aplikácie, že som pridal podporu zmeny mierky a tak počas stlačenia ľavej CTRL klávesy môžete scrolovacím kolieskom myši meniť mierku od 0.5 do 10 (áno, je možne miesto zoomovania mať opak, teda miniatúru:).

Zdrojové kódy a binárka je updatnutá a preto moje tvrdenia o výkonnosti v pôvodnom článku berte trošku s rezervou;)

 

Intermezzo

Čakať na odpoveď od zákazníka je niekedy ubíjajúce a tak aby som vplnil voľný čas, troška som siahol do zoznamu vecí, ktoré by som rád vytvoril. A veru na prvom mieste je už nejaký ten rok lupa. Samozrejme kopec rôznych druhov rôznych magnifierov nájdete všade možne, moje predstava je ale jednoduchá. Mať lupu, ktorá bude fungovať ako lupa, teda pri pohybe by lupa zvätšovala to čo sa nachadza aktuálne pod kurzorom. A tak ubiehali roky, až prišla Vista a s ňou Aero a ja som čakal, že sa konečne Microsoft rozhýbe a takú poriadnu lupu spraví. Snaha bola, lupa konecne zobrazuje aj obsah pod sebou, ale je tu to ale, pri jej zapnutí niečo obrazovku preblikne a zmení to fonty, okrem toho to nezoomuje dynamicky obsah napr. v  media playere.

Samozrejme, že ma napadlo riešenie ako na to, ale to až neskôr, treba troška čitateľa napínať, preto najskôr popis ako fungujú súčastne lupy:

  • Vista Magnifier - nemám šajnu ako na to ide, ale to ako funguje je to najlepšie z lúp, pretože umožňuje pri zoomovaní pracovať, okrem toho zoomuje aj text pod sebou
  • screenshot magnifier - odroda lúp, ktorá periodicky robí screenshoty v okolí kurzora a tie zvätšuje, neumožňuje ale zobrazovať obsah pod sebou a tak máte na obrazovke vždy zabratý kúsok miesta
  • replace screen magnifier - táto sorta funguje ako normálna lupa, akurát to robí fikane tak, že urobí screenshot celej obrazovky a ten potom umožnuje pomocou pohybu lupy zoomovať, tak ako to robíme napr pri čítaní novín, nevýhodou je, že po takejto lupe nemôžeme klikať a dynamický obsah je freeznutý

Takže to by sme mali konkurenciu za sebou, pred nami je ten nápad. Neviem prečo to dosiaľ nikoho nenapadlo, ale veď je to také jednoduché, stači použiť náhľady okien (anglicky thumbnail). Samozrejme niečo také už bolo v XP, ale tie v aero su dynamické. Viac o teórii nižšie, dôležitá je poznámka, že toto riešenie je Aero only (tie by mali byť zatiaľ len vo viste a vo server 2007).

Pre neveriacich aby som dokázal, že to funguje, prikladám screenshot z mediaplayera a jeden zo start menu máte vyššie:

 

Pre tých ktorí si to nevšimli je lupa ten čierny obdĺžnik a skutočne všetko funguje pekne dynamicky, akurát treba rátať s tým, že takáto lupa dáva počítaču dosť zabrať, ale čo sa dá čakať od programu vytvoreného za dva a pol dňa?

Ovládanie

Aby ste neboli po prvom stlačení stratený, nezabudnite na tieto klávesové skratky

  • pravý CTRL alebo Scroll Lock skryje alebo zobrazí lupu
  • alebo ak je lupa zobrazená, pri stlačení klávesy ESC sa skryje a jej pustením sa znova zobrazí

Okrem v toho v traybare je taká biela ikonka aplikácie, ktorá má menu, pomocou ktorého program ukončíte.

Lupa má v sebe nejakú základnu podporu, ktorá simuluje aktivitu myši pod sebou, ale tá je len v prvotnom štádiu a mne funguje iba vo firefoxe a thunderbirde (okrem web obsahu, funguje iba menu), stlačiť sa dá aj start button ale je to úbohe, ak by mal niekto nápad ako pomôcť s týmto problémom, jedna kapitola článku nižšie sa bude tomuto venovať.

Krok č. 1 Klonujeme plochu

Ako už bolo povedané v úvode, celý trik s lupou je v tom, že používame dynamické náhľady, tak ako prišli s Vistou v novom DWM zvanom Aero.

Na MSDN o náhľadoch nájdete kapitolu DWM Thumbnail Overview a my z nej použijeme úplne všetko. Aj keď som na začiatku, až som začal vytvárať prvý temp projekt, bál. Generátor náhľadov ma prekvapil. Stačilo iba dodržiať poradie (z-order) okien, tak ako sú vo windows a on sa už sám postará o priesvitnosť a prekrývanie okien a tak som mal za pár hodín kópiu celého desktopu hotovú. Použil som teda:

  • DwmRegisterThumbnail funkciu, ktorá nám zaregistruje okno ako cieľ do ktorého sa bude generovať obsah náhľadu iného okna (náhľady sa dajú generovať iba do okna, na nejaké generovanie do panelu môžte zabudnúť, aj keď možno to je iba vec GDI+, mne a ale z pohodlia C# von nechce:)
  • DwmQueryThumbnailSourceSize, ktorú použijeme na zistenie skutočnej veľkosti okna z ktorého sa generuje náhľad (samozrejme to sa dá zistiť aj pomocou GetWindowRect, tá ale nejak nefunguje správne pri desktope ak je k PC pripojených viac monitorov)
  • DWM_THUMBNAIL_PROPERTIES je štruktúra pomocou ktorej nastavíte to ako sa ma generovať náhľad, teda priesvitnosť a podobné veci, my v nej ale urobíme to, že rozmery regiónu, do ktorého sa bude náhľad generovať zdvojnasobíme (alebo aj zviacnasobíme, ja som dal napevno do zdrojáku konštantu 2, pretože čím vyššia hodnota, tým viac dá generovanie náhľadov počítaču zabrať)
  • DwmUpdateThumbnailProperties nám umožní nastaviť vlastnosti z DWM_THUMBNAIL_PROPERTIES štruktúry
  • DwmUnregisterThumbnail a nakoniec treba náhľad odregistrovať, inak sa vám bude zobrazovať defaultný obrázok pre prázdne okno, ktorý nie je nič moc:)

Zaujímavé je, ako DWM náhľady zobrazuje. Predpokladám, že to robí priamo do framebuffera na grafickej karte, inak si neviem vysvetliť to, že prekresľuje absolútne všetko čo sa na danom mieste na formulári nachádza. A to je aj dôvod, prečo má moja lupa hranatý tvar. Akýkoľvek pokus o nastavenie vlastného  regionu pre orezanie formulara je ignorovaný.

Toľko teória, pustíme sa do kódu, najskôr musíme zobrať zoznám všetkých okien (ak pod oknami rozumieme všetko čo má handle) a tie testujeme pomocou winapi funkcie IsWindowVisible na viditeľnosť pre užívateľa:

        public static List<Window> GetWindows()

        {

            List<Windowresult = new List<Window>();

            EnumWindows(delegate (IntPtr hwnd, int lParam){

                if (IsWindowVisible(hwnd))

                {

                    Window win = CreateWindow(hwnd);

                    result.Add(win);

                }

                return true;

            }, 0);

            return result;

        }

No nie je cool predať do pinvoke funkcie anonymného delegáta?:) Trieda Window obsahuje zopár pomocných premennýh ako zistenie titulu, zistenie štýlov okna, atď, a podobné pozostatky z ranných fáz vývoja, ale hlavné je, že ma Dimensions property, ktorá cez api funkciu GetWindowRect zistí v gettere rozmery okna.

Dôležitá vec, na ktorú treba myslieť, je že EnumWindows vracia okna podľa ich z-order. Čiže to najvyššie položené okno je prvé a posledná by mala byť plocha desktopu. Ak teda chceme vykresiť kópiu desktopu musíme do DWM zaregistrovať okná v opačnom poradí. Kód tu pastovať nebudem, stačí siahnuť po zdrojákoch. Dôležitá je informácia, že zoznam okien volám raz za sekundu. Pretože napr. kliknutie na menu vlastne vytvára nové okno, tooltipy sú nové okna, atď. 

Každé viditeľné okno si registrujem do triedy zdedenej z PictureBox (ThumbPicture) a pridávam medzi controls formulára, jednak takto zisťujem polohu okna pri akcii myši a jednak pomocou neho robim prepočet pre posun výrezu desktopu. A pretože plochu zoomujeme, tak jej rozmery a pozícia su prepočítane podľa hodnoty zvätšenia (zatiaľ hardcoded to hodnoty 2).

Krok č. 2 Hooking po prvé

Pre hooking siahneme po overenej triede GlobalCbtHook, teda dokiaľ si nechcete písať vlastnú v C++ knižnicu, pretože z managed kódu sa ku globálnym hookom nedostanete.

Čo nás teda v hooking zaujíma?

Krok č. 3 Sleduj kurzor

Takže keďže už máme fungujúcu kópiu plochy, stačí už len presúvať okno tak aby bol kurzor v strede okna. To znamená presunúť okno na novú pozíciu a vyrátať offset pre top a left pozíciu a na základe tejto pozície presunúť náhľady na novú pozíciu pomocou DwmUpdateThumbnailProperties.

Pôvodne som to robil v hooku myši. Ten je ale pri pohybe volaný príliš často a tak som toto presunul do timera.

Čo ma trošku sklamalo je výkon presunu náhľadov, teda pri zvätšení, nezvätšený náhľad je super rýchly, ale dvojnásobne zvätšený náhľad robí DWM trošku problémy a tak lupa vyzerá ako keby za kurzorom plávala.

Krok č. 3 Daj tomu vzhľad

Vytvorenie overlay okna je z tých najjednoduchších úloh, stačí len vytvoriť správny obrázok a povedať oknu, že určitá farba je priehľadná. Jednoduchá úloha, ale veľká služba pre užívateľa:)

Krok č. 4 Emuluj myš

Toto je tá najtažšia úloha a v súčastnosti to funguje len čiastočne, kto by vedel pomôcť, nech sa pozrie do funkcie void Mouse_MouseEvent(IntPtr WParam, IntPtr LParam) v súbore Zoom.cs.

V súčastnosti je to takto. Hookneme sa na myš, zistíme, ktoré okno je na danom miesto topmost (máme na to internú štruktúru pozícii okien). V danom okne zistíme relatívnu pozíciu, tak, že od pozície myši odrátame pozíciu okna.

S relatívnou pozíciou vyhľadáme pomocou API funkcie RealChildWindowFromPoint pozíciu child okna a tomu oknu prepošleme mouse event, akurát nesmieme zabudnúť upraviť jeho pozíciu podľa pozície daného elementu.

Stále to ale nie je ono, niekde to funguje len čiastočne (mozzilla like aplikácie, total commander, ...), niekde vôbec (Visaul Studio). Niečo asi robím zle, akurát ma nenapadá čo.

Záver

Uff, pôvodne som mal v pláne použiť veľa code samplov, ale pred ich pastnutím som si uvedomil, že aplikácia je tým ako sa chaoticky vyvýjala za tie dva dni príliš poprepájana a tak bude lepšie popísať koncept, kto má záujem môže si sám siahnuť do priložených zdrojových kódov.

Dúfam, že po zverejnení týchto kódov sa nájde niekto, kto dokáže tento koncept použiť a rozšíriť do výsledku, tak aby som už konečne mal k dispozícii skutočne realistickú lupu. Tiež by možno pomohlo vytvoriť niečo ako window vo window a to posuvať, čo by určite ušetrilo čas spôsobený prekresľovaním výrezu.

Ešte mi nedá zavŕtať do MS. Trošku ma mrzí, že koncept DWM je tak nedotiahnutý a okresaný na minimum maximum a pritom je tu toľko možností viď Combiz a MacOSX. Chápem, že je problém zachovať spätnú kompatibilitu, ale ja si zasa viem predstaviť každé okno ako textúru v kvázi 3D prostredí, to by sa potom dali robiť veci a nech mi nikto nehovorí, že v dnešnej dobe je nejaka grafika, ktorá by to nezvládla. Stačilo by už len prepísať všetky grafické operácie boli presmerované na DirectX. Možno by stačilo urobiť len DirectX verzi DWM, ktovie ale mať tak prístup k zdrojovým kódom windows ...

Lokalizácia textu v databázach

Úvod do problematiky

Lokalizácia je niečo k čomu sa pri programovaní každý skôr či neskôr dostane. Tentoraz nebudem rozoberať lokalizáciu formulárov. Na to su určené resources, resources provider, ... a vlastne sa môžete pozrieť na správičku Na tému jazyková lokalizácia v ASP.NET.

<promo>Správičky na aspnet.sk su niečo ako moja malá forma mikroblogingu, snažím sa v nich informovať o novinkách z oblastí .net, ale aj teórii riadenia projektov a vlastne všetkého okolo programovania. Za 8 mesiacov ako vznikli ich uz je kopec, teda minulý týždeň sme dosiahli číslo 400. Ak vás zaujali a máte aj vy čo ostatným povedať, pridávanie nie je nijak obmedzené a prispievateľom sa môže stať ktokoľvek.</promo>

No to bola trošku odbočka, späť k téme. Takže v tomto príspevku ideme riešiť problematiku lokalizácie databázových textov. Budem sa snažiť popísať najčastejšie používane riešenia a dúfam, že v diskusii niekto navrhne nejaké ďalšie prípadne nejaké vylepšenia a ja budem môcť napísať nejaké to pokračovanie.

Okrem toho budem hodnotiť výkonnosť každého riešenia v porovnaní s predchádzajúcim (pomocou jednoduchého selectu s like filtrom pre dve jazykové verzie) v percentuálnom vyjadrení, tak ako to zobrazí query plan (na koniec pridám aj porovnanie podľa spustenia príkazu z .net kódu, taký real life test). Samozrejme k tomu potrebujem nejake tie dáta. Na to som použil generátor dát RedGate SQL Data Generator v trial verzii a do každej tabuľky som natlačil 100.000 riadkov dát.

Ako som už písal, porovnanie výkonnosti je pomocou dvoch selectoch s like podmienkou, to preto, aby sme videli ako sa dá dostať k dátam v jednom selecte, teda bez potreby implementácie niekde v kóde, proste simple SQL.

Staticky alebo dynamicky?

To je prvotná otázka lokalizácie. Možno to nie je presne podľa poučiek o lokalizácii a vlastne boli zvolené uplne prvoplánovo, preto pripájam definície:

Staticky

Statický znamená, že daný text má práve jeden záznam pre každý registrovaný jazyk. Statická lokalizácia býva vätšinou veľmi rýchla, nevýhodou je, že ak chceme pridať nový jazyk, tak sú s tým spojené zložité úkony, okrem toho pri pridaní nového jazyka je už z definície potrebné pridať úplne kompletný preklad.

Dynamicky

Dynamicky znamená, že máme zvolený určitý predvolený jazyk a v prípade ak pre žiadaný jazyk neexistuje jeho preklad použije sa tá predvolená verzia. Výhoda je, že preklad môžeme vykonávať postupne, nevýhodou je, že takéto riešenie stojí vätšinou viac času.

Zaujímavosť

Najzaujímavejšie na celom probléme lokalizácie je, že z každého statického riešenia je možné vytvoriť dynamickú verziu. A plne preložená dynamická verzia je v podstate statickým riešením.

Riešenie č. 1 Statická jednotabuľka

CREATE TABLE [dbo].[LocType1](

    [ID] [uniqueidentifier] NOT NULL,

    [Name_en] [varchar](50) NULL,

    [Name_sk] [varchar](50) NULL,

 CONSTRAINT [PK_LocType1] PRIMARY KEY CLUSTERED ([ID] ASC)

)

Ako je vidieť pre každú jazykovú verziu existuje jeden sĺpec.

Selecty potom sú:

select * from LocType1 where [Name_en] like 'L%';

select * from LocType1 where [Name_sk] like 'L%';

Toto je to najprimitívnejšie riešenie, po ktorom siahne hádam každý. Je rýchle, trošku problém vidím pri implementácii do ORM aby načítavalo jazyk podľa aktuálneho jazyka, ale nič neriešiteľné.

Nevýhodou je pridanie nového jazyka, je potrebné pridať nový stĺpec. Takto to nevyzerá moc obtiažne, ale vätšinou je jazykových stĺpcov vätší počet a potom to už je trošku problém s prehľadnosťou.

Riešenie č. 2 Statická dvojtabuľka

CREATE TABLE [dbo].[LocType2](

    [ID] [uniqueidentifier] NOT NULL,

    [Name_id] [int] NOT NULL,

 CONSTRAINT [PK_LocType2] PRIMARY KEY CLUSTERED ([ID] ASC)

)

CREATE TABLE [dbo].[LocType2Names](

    [ID] [int] NOT NULL,

    [Language] [nchar](5) NOT NULL,

    [Names] [nvarchar](50) NOT NULL,

 CONSTRAINT [PK_LocType2Names_1] PRIMARY KEY CLUSTERED ([ID] ASC, [Language] ASC)

)

V tomto riešení namiesto textu ukladáme identifikátor na tabuľku, v ktorej je pre každú jazykovú verziu uložený jeden záznam.

Selectujeme nasledovne

select * from LocType2 L

 inner join LocType2Names LN on (L.Name_id = LN.ID and LN.[Language] = 'en')

 where LN.[Names] like 'L%'

select * from LocType2 L

 inner join LocType2Names LN on (L.Name_id = LN.ID and LN.[Language] = 'sk')

 where LN.[Names] like 'L%';

a spomalenie oproti riešeniu 1 je približne štvornásobne, treba ale brať v úvahu, že v tabuľke LocType2Names je dvojnásobný počet riadkov, teda 200.000 (pre každú jazykovú verziu jeden).

Komu v schéme chýba FK na tabuľku LocType2Names, tak má dobrý postreh, akurát slabé znalosti z databáz, pretože v tabuľke LocType2Names je zložený primárny kľúč.

Optimalizácia

Stačí preto vytvoriť nad stĺpcom Name_id v tabuľke LocType2 vytvoriť index

CREATE NONCLUSTERED INDEX [IX_LocType2] ON [dbo].[LocType2]

(

    [Name_id] ASC

)

A na výsledku to je hneď poznať rozdiel sa znížil na 2.5 násobok.

Riešenie č. 3 Dynamická dvojtabuľka

CREATE TABLE [dbo].[LocType3](

    [ID] [uniqueidentifier] NOT NULL,

    [Name_id] [int] NOT NULL,

 CONSTRAINT [PK_LocType3] PRIMARY KEY CLUSTERED ([ID] ASC)

)

CREATE TABLE [dbo].[LocType3Names](

    [ID] [int] NOT NULL,

    [Language] [nchar](5) NOT NULL,

    [Names] [nvarchar](50) NOT NULL,

 CONSTRAINT [PK_LocType3Names_1] PRIMARY KEY CLUSTERED ([ID] ASC, [Language] ASC)

)

Tabuľka pri tomto riešení je rovnaká ako v dvojke. Rozdiel je v napĺňaní dátami. Dynamické riešenie predpokladá, že máme vybratý nejaký predvolený jazyk, v našom prípade to je en a pre tento jazyk existuje v tabuľke LocType3Names vždy jeden záznam. Pre každý ďalší jazyk môže, ale nemusí existovať záznam, ak  neexistuje, tak by mal byť vybratý text predvoleného jazyka.

Select predvoleného jazyka je jednoduchý:

select * from LocType3 L

inner join LocType3Names LN on (L.Name_id = LN.ID)

where LN.[Names] like 'L%'

and ([Language]='en');

S aktuálnym jazykom je to už zložitejšie, ale nie nemožné:

select * from LocType3 L

inner join LocType3Names LN on (L.Name_id = LN.ID)

where LN.[Names] like 'L%'

and

(

([Language]='sk' and EXISTS(select ID from LocType3Names where ID = L.Name_id and [Language]='sk'))

or

([Language]='en' and NOT EXISTS(select ID from LocType3Names where ID = L.Name_id and [Language]='sk'))

);

S výkonnosťou default jazyka je to podobné ako s riešením 2, select aktuálneho jazyka je 11 krát pomalší ako riešenie 1 ale len 2,5 krát pomalší ako riešenie dva (bez optimalizácii). V tabuľke LocType3Names sa nachádza 100.000 záznamov pre jazyk en a 60.000 záznamov pre jazyk sk.

Optimalizácia

Pre optimalizáciu je rovnaká ako v riešení č.2, teda vytvorenie indexu

CREATE NONCLUSTERED INDEX [IX_LocType3] ON [dbo].[LocType3]

(

    [Name_id] ASC

)

a porovnávacie výsledky oproti optimalizovanému riešeniu č. 2 je to približne 3 krát a oproti optimalizovanému riešeniu č. 1 je cca 8,5 krát pomalšie. Samozrejme 8.5 násobne pomalšie neznamená až tak pomaly, presne výsledky porovnávania nájdete v tabuľke na konci článku, okrem toho nesmiete zabudnúť, že to je porovnanie podľa query plan, skutočné výsledky sú niekde inde.

Riešenie č. 4 Dynamická tabuľka s netypovým XML stĺpcom

CREATE TABLE [dbo].[LocType4](

    [ID] [uniqueidentifier] NOT NULL,

    [Names] [xml] NOT NULL,

 CONSTRAINT [PK_LocType4] PRIMARY KEY CLUSTERED ([ID] ASC)

)

Toto riešenie používa build-in xml podporu v sql serveri (všetky testy su na verzii 2005). Jazykové verziu budeme ukladať v tvare

<en>eng verzia text</en>

<sk>sk verzia text</sk>

Pretože ide o XML treba pri ukladaní textov myslieť na normalizáciu textu, preto je lepšie použiť typový xml. Ale radšej už k selectom pre default jazyk je nasledovný:

select * from LocType4

where [Names].value(N'/en[1]', 'nvarchar(50)') like 'L%';

Pre ostatné jazyky použijeme nasledovný select:

select * from LocType4

where [Names].value(N'if(/sk[1])

then /sk[1]

else /en[1]', 'nvarchar(50)') like 'L%';

Ako vidiet v selectoch sa používa xpath verzia v selecte pre iné jazyky mi troška dala zabrať, pretože xpath v sql serveri 2005 nepodporuje union (preto cesta cez (/en[1] | /sk[1])[last()] neviedla a (/en[1],/sk[1])[last()] bol podla query planu pomalý na merge dvoch subqueries, preto verzia s if.

Takéto riešenie bez optimalizácii ani nemá zmysel porovnávať, proste je to príliš pomalé, preto nasleduje optimalizácia

Optimalizácia

Optimalizácia XML je jednoduchá stačí vytvoriť xml index

CREATE PRIMARY XML INDEX [XML_IX_LocType4] ON [dbo].[LocType4] ([Names])

a výsledky sú hneď prijateľnejšie default jazyk cca 90 krát oproti riešeniu č.3 a pri inom jazyku je rozdiel cca 35 násobný. Aj keď divné je, že real life testy hovoria o niečom inom, ale o tom až v závere.

Poznámka

K riešeniu xml treba dodať, že aj keď sa zdá, že je neefektívne a pomalé, v prípade ak nepotrebujete dáta filtrovať a xml dáta viete rozparsrovať priamo na klientovi tam kde je to potrebne, teda niekde v ORM vrstve ide o veľmi rýchle a efektívne riešenie, problém je prístup k hodnote priamo v sql queries.

Riešenie č. 5 Dynamická tabuľka s typovým XML stĺpcom

CREATE TABLE [dbo].[LocType5](

    [ID] [uniqueidentifier] NOT NULL,

    [Names] [xml](CONTENT [dbo].[Languages]) NOT NULL,

 CONSTRAINT [PK_LocType5] PRIMARY KEY CLUSTERED ([ID] ASC)

)

a samozrejme nezabudnuť na xml schému´, ktorá sa vytvára ako prvá

CREATE XML SCHEMA COLLECTION [dbo].[Languages]

AS N'<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <xsd:element name="en" type="xsd:string" />

    <xsd:element name="sk" type="xsd:string" />

</xsd:schema>';

Dotazy sú samozrejme rovnaké ako v predchádzajúcom prípade

select * from LocType5

where [Names].value(N'/en[1]', 'nvarchar(50)') like 'L%';

a

select * from LocType5

where [Names].value(N'if(/sk[1])

then /sk[1]

else /en[1]', 'nvarchar(50)') like 'L%';

Optimalizácia

podobne ako v predchádzajúcom prípade:

CREATE PRIMARY XML INDEX [XML_IX_LocType5] ON [dbo].[LocType5] ([Names])

Podľa query plan je typový dataset efektívnejší ako netypový v xpath queries, a ten indexovaný dotaz pre iný jazyk je podľa neho len 8 krát pomalší ako query pre iný jazyk v riešení č. 3, čo určite nie je zlé.

ALE

Je tu však jedno ale, real .net aplication testy hovoria niečo iné a to že tabuľky s xml indexom sú pomalšie ako tie bez indexu. Je to zaujímavé preto, že query plan hovorí niečo iné. Poďme preto k Real life testom.

Real life testy

V query plane sú výsledky výkonnosti jednotlivých príkazov označené ako Query cost (relative to batch): xx %, čo v preklade znamená náklady na vykonanie príkazu relatívne k ostatným príkazom v dávke. A podľa toho som sa chcel v tomto článku orientovať naštastie som si spravil jednoduchý net programček, ktorý otvorí connection na db, vykoná príkaz na select, prejde všetky vrátene riadky a po každom riadku prirátava číslo do výsledku, tak aby som aspoň približne simuloval prácu s dátami.

Výsledky ma ale prekvapili, veď posúdte sami z výsledkov. Pre každý typ testu je vytvorený jedna funkcia vo tvare LocType[číslo riešenia][i ak nad indexovanou tabuľkou][en/sk značí či ide o default, alebo sk jazyk] a samozrejme v hlavnej tabuľke je vždy 100.000 záznamov, filter by mal vyfiltrovať pre like 'L%' približne 6300 riadkov, pretože boli generovane náhodným generátorom, je to číslo vždy niekde okolo.

Prvý výsledok je po reštarte sql servera, keď by mala byť cache čistá:

LocType1en:      6328 records    262,36 ms     1878241 ticks

LocType1sk:      6328 records    47,97 ms       343433 ticks

LocType2en:      6300 records    646,33 ms     4627106 ticks

LocType2sk:      6300 records    133,72 ms     957282 ticks

LocType2ien:    6300 records    325,30 ms     2328850 ticks

LocType2isk:    6300 records    84,80 ms       607070 ticks

LocType3en:      6300 records    385,45 ms     2759474 ticks

LocType3sk:      6300 records    123,71 ms     885648 ticks

LocType3ien:    6300 records    305,38 ms     2186215 ticks

LocType3isk:    6300 records    120,77 ms     864580 ticks

LocType4en:      6280 records    2082,24 ms     14906973 ticks

LocType4sk:      6329 records    1702,58 ms     12188952 ticks

LocType4ien:    6280 records    4199,63 ms     30065513 ticks

LocType4isk:    6329 records    3537,15 ms     25322760 ticks

LocType5en:      6280 records    916,10 ms     6558438 ticks

LocType5sk:      6329 records    1110,94 ms     7953343 ticks

LocType5ien:    6280 records    3062,26 ms     21922991 ticks

LocType5isk:    6329 records    1720,26 ms     12315531 ticks

a tento je

LocType1en:      6328 records    154,15 ms     1103566 ticks

LocType1sk:      6328 records    39,30 ms       281330 ticks

LocType2en:      6300 records    90,11 ms       645090 ticks

LocType2sk:      6300 records    87,05 ms       623228 ticks

LocType2ien:    6300 records    81,64 ms       584478 ticks

LocType2isk:    6300 records    79,59 ms       569803 ticks

LocType3en:      6300 records    79,47 ms       568954 ticks

LocType3sk:      6300 records    114,78 ms     821738 ticks

LocType3ien:    6300 records    73,36 ms       525182 ticks

LocType3isk:    6300 records    111,61 ms     799059 ticks

LocType4en:      6280 records    1205,16 ms     8627860 ticks

LocType4sk:      6329 records    1735,63 ms     12425520 ticks

LocType4ien:    6280 records    2445,16 ms     17505092 ticks

LocType4isk:    6329 records    3366,28 ms     24099517 ticks

LocType5en:      6280 records    559,32 ms     4004201 ticks

LocType5sk:      6329 records    1056,06 ms     7560451 ticks

LocType5ien:    6280 records    1069,32 ms     7655323 ticks

LocType5isk:    6329 records    1713,61 ms     12267920 ticks

Samozrejme prvý select treba brať s rezervou, pretože sa vytvára prvé pripojenie do connection poolu, ale ďalšie výsledky sú zaujímave.

Nechápem ako môže byť tabuľka s xml indexom približne 2 krát pomalšia ako tabuľka bez indexu, proste mi to nejde dohlavy, najmä keď query plán hovori niečo úplne iné.

Zhrnutie

Dať nejaké globálne odporúčanie sa určite nedá, najmä verím tomu, že mojich päť riešení nepokrýva celé spektrum a preto dúfam, že niekto v diskusii pridá nejaký ten link, prípadne nápad a ja pripravím druhú časť, prípadne tretiu, kde by som mohol pridať ako vybrané riešenia začleniť do ORM (LLBLGEN, s ktorým som núteny pracovať v práci a môjho obľúbenca nHibernate).

Takže tu sú moje subjektívne názory na jednotlivé riešenia:

  1. Riešenie č. 1 je určené pre masochistov, pridanie nového jazyku vás donúti prejsť všetky tabuľky, najmä preto, že verzia jazyka je hardcoded v selectoch ako prípona pre názvy stĺpcov. Teda priznám sa toto som používal v mojích php projektoch:).
  2. Dvojka je už na tom lepšie a ak máte nejaký tool, ktorý vám pre podporu nového jazyka nakopíruje hodnoty z iného jazyka, môže ísť o veľmi efektívne riešenie. Problém je ale, že pri editore takéhoto reťazca musíte nútiť zákazníka vyplniť všetky jazykové verzie (prípadne sa môže nakopírovať jedna hodnota do všetkých jazykov, to však urobí problem pri opätovnej editácii, čo je už zmenená hodnota a čo už nie).
  3. Trojka je velico pekné riešenie ako z pohľadu rýchlosti, tak zo stránky implementácie riešenia.
  4. Štvorka je skôr len prototyp, určite ak xml na sql serveri, tak typový dataset.
  5. Päťka je taký favorit pre priaznivcov moderných technológii. V prípade ak budete hodnotu parsrovať napr pomocou regexu na clientovi a len ak je to potrebné a nebudete potrebovať filtrovať podľa lokalizovanej hodnoty, tak ide o efektívnejšie riešenie ako trojka.

Ako vidieť moji favoriti sú 3 a 5 a pre päťku mám ešte plány do budúcna.

Čo ďalej alebo čaká nás v druhej časti

Určite by som koncept xml lokalizácie v 4. a 5. riešení chcel rozšíriť pomocou Regex v storovanej funkcii/procedúre a potom by som sa rád nechal inšpirovať nejakým nápadom z diskusie, tak smelo do toho.

BTW: Samozrejme ak by niekto neveril výsledkom, môžem nejak vymyslieť export real dát nad ktorými som testoval, ale dnes sa mi už nechce, okrem toho nejde o nič, čo by ste si podľa tohto článku nedokázali zreplikovať.

StringEnum podľa vlka

Dnes len tak pri bežnom dennom kódení vyvstala z môjho kódu interná potreba textového enumeration typu (ach tá angličtina, vôbec mi do hlavy nejde rozumný slovenský preklad).

Nuž a ako to už bežne býva, keď niečo nie je v systéme  na pomoc rýchlo prichádza google a v ňom zopár článkov (String Enumerations in C#, How do you convert a string into an enum?, Enum With String Values In C#, atď) popisujúci tieto základné spôsoby:

  1. Enum s konverziou na string - ja ale potrebujem ukladať pre jednotlive hodnoty kód, čo potom v zdrojákoch nevyzerá najlepšie.
  2. Konštanty - super použitie, avšak chýba kontrola hodnoty (vlastne pri pretypovaní ju nemá ani enum).
  3. Static readonly - tu už je možné použiť ako hodnotu triedu aj s kontrolou na hodnotu, nevýhodou ale je nemožnosť použitia v switch.
  4. Atribúty s extensions metódou - pekné riešenie, akurát reflexia je vždy brzdou aplikácie, extensions metódy mi nikdy nevoňali a pri pretypovaní by teoretický mohli vzniknúť ťažko odhaliteľné chyby.

Všetko to proste viedlo k jednému záveru. Svet potrebuje ešte jedno riešenie a to podľa vlka:). Najskôr teda požiadavky:

  • použitie v swith príkaze, teda hodnota musí byť const
  • aspoň jednoduchá kontrola na vkladanú hodnotu
  • a čo najjednoduchšie použitie

Základ bude tvoriť trieda obsahujúca hodnoty ako konštanty:

    public class StringEnum 

    {

        public const string SomeValue = "val1";

 

        public const string OtherValue = "val2";

 a umožňovať by mala nasledujúce konštrukcie:

            StringEnum value1 = StringEnum.OtherValue;

            StringEnum value2 = StringEnum.SomeValue;

            switch (value1)

            {

                case StringEnum.OtherValue:

                    Console.WriteLine(value1);

                    break;

                case StringEnum.SomeValue:

                    Console.WriteLine(value1);

                    break;

            }

            bool compare = value1 == value2;

            compare = value1 == StringEnum.OtherValue;

Aby som zabezpečil čo najväčšiu znovupoužiteľnosť, vytvoril som si bázovú triedu, ktorá bude robiť všetko to, čo netreba kopírovať medzi projektami. Teda obsahuje hodnotu inštancie enumu a v protected konštruktore ošetruje vkladanú hodnotu, pomocou výnimky StringEnumValueException a abstraktnej funkcie GetValidValues, ktorá vracia zoznam platných hodnôt

    public abstract class BaseStringEnum

    {

        /// <summary>

        /// Gets the valid values.

        /// </summary>

        /// <returns>Should return valid string values for this string enum.</returns>

        protected abstract List<string> GetValidValues();

        /// <summary>

        /// Gets or sets the value.

        /// </summary>

        /// <value>The value.</value>

        public string Value { get; private set; }

 

        /// <summary>

        /// Initializes a new instance of the <see cref="StatisticsTypeEnum"/> class.

        /// </summary>

        /// <param name="value">The value.</param>

        protected BaseStringEnum(string value)

        {

            // test if value is valid

            if (!GetValidValues().Contains(value))

            {

                throw new StringEnumValueException(this.GetType(), value);

            }

            Value = value;

        }

Potiaľ je to jednoduché nasleduje pár jednoduchých overridov, prvý na textovú reprezentáciu projektu:

        public override string ToString()

        {

            return Value;

        }

a veľmi dôležitá funkcia pre porovnávanie dvoch objektov, ktorá nám zabezpečí, aby sme, keď to je potrebné, porovnávali len textovú podobu objektu

        public override bool Equals(object obj)

        {

            // compare with string

            if (obj is string)

            {

                return string.Equals(obj, Value);

            }

            // compare with same string enum

            else if ((this.GetType() == obj.GetType()) && (obj is BaseStringEnum))

            {

                return string.Equals((obj as BaseStringEnum).Value, Value);

            }

            // call base compare

            return base.Equals(obj);

        }

a hlavne podstatná finta, ktorá nám zabezpečí implicitnú konverziu nášho string enumu na string

        public static implicit operator string(BaseStringEnum stringEnum)

        {

            if (stringEnum == null)

            {

                return null;

            }

            return stringEnum.Value;

        }

Na základnú triedu to úplne stačí, zostáva nám implementovať potomka, tu je potrebné definovať vlastné hodnoty enumu a abstraktnú funkciu GetValidValues, ktorá vracia povolené hodnoty pre náš enum

    public class StringEnum : BaseStringEnum

    {

        public const string SomeValue = "val1";

 

        public const string OtherValue = "val2";

 

        /// <summary>

        /// Gets the valid values.

        /// </summary>

        /// <returns>

        /// Should return valid string values for this string enum.

        /// </returns>

        protected override List<string> GetValidValues()

        {

            return new List<string> { SomeValue, OtherValue };

        }

privátny konštruktor, ktorý zabezpečí naplnenie hodnoty a znemožní vytvorenie inštancie z kódu

        private StringEnum(string value) : base (value)

        {

        }

a aby to všetko pekne fungovalo, tak ako má, tak tá najdôležitejší časť celého riešenia a to implicitná konverzia zo stringu na náš StringEnum typ

        public static implicit operator StringEnum(string value)

        {

            return new StringEnum(value);

        }

To je všetko, takáto trieda sa už potom normálne používa ako enum, možno sa ešte hodí označiť base triedu pomocou Serializable atribúta, ale to už nechám na každého uváženia a potrieb.

A nakoniec zostávajú už len zdrojové kódy StringEnum source a nevýhody:

  • Aj keď to simuluje enum funkčnosť enum to nikdy nebude:)
  • chýba default hodnota, tak ak má niekto nápad ako toto ošetriť, teda konverziu null hodnoty na default hodnotu

 

 

 

 

MultiScreenSwitcher coding story
Binárka: http://vlko.zilina.net/dwn/MultiScreensSwitcher-bin.zip

Zdrojové kódy: http://vlko.zilina.net/dwn/MultiScreensSwitcher-source.zip

Na zdrojové kódy, ani obrázky si neuplatňujem žiadne autorské práva, takže môžete s nimi robiť čo len chcete:)

Ak sa chcete dozvedieť čo to o vyvoji aplikácie preskočte časť Intermezzo, a pustite sa rovno do devel časti.

Intermezzo

Dnes to bude opäť o návykovej veci a to je multi monitor desktop (v preklade hádam viac zobrazovacio-jednotková plocha). Osobne sa priznám, že som pokušeniu mať k notebooku pripojený externý monitor dlho odolával až raz sa tak nejak uvoľnila jeden 20" LCD a po pár dňoch som už patril medzi tých, čo si devel život bez externého monitora nevedia ani prestaviť. Vzhľadom na spôsob vykresľovania vo WinXP je pre vývojara vlastne nevyhnutnosťou (kto nevie, tak starý zobrazovací engine si neukladal vyrenderovanú grafiku do nejakej medzipamäte, ako je v súčastnosti GPU RAM, ale každe prekrytie okna bolo indikované cez InvalidateRect novým prekreslením) a ak ste teda nemali dosť veľký monitor aby ste mali na rovnakej obrazovke vedľa seba debugovanú aplikáciu a vývojové IDE, pri trošku vätšom zatažení procesora, ste sa proste učakali. Troška z nadsázkou by sa dalo povedať, že externý monitor bol pre mňa niečo ako druhý core na procesore:)

Nuž a k takémuto multi-screen desktopu patria nejaké tie produktivitu zvyšujúce aplikácie.

  • Multi Monitor Mouse (M3) - ak stále hľadate kurzor na obrazovke je to určite to pravé pre vás, stači stlačit CTRL a okolo kurzora sa zobrazí animacia kružkov, a len slepý si ho už potom nevšimne, okrem toho je tu klávesová skratka ALT+~, čo prenesie kurzor na rovnakú pozíciu ale na nasledujúcom monitore
  • MultiArrows- (treba pohľadať na stránke odkaz na stiahnutie je pekne zašitý) ktorý zobrazil v caption bare tlačítko, po kliknutí na ktoré preniesol okno na susedný monitor, okrem toho to vie robiť aj klávesovou skratkou.

Určite existuje kopec iných komerčných aplikácii, ale tieto dve free mi úplne postačovali, pretože mám taskbar umiestnený zvisle (aké iné umiestnenie pri widescreene?), tak nejaké rozšírenie na ďalšiu obrazovku mi nič neprináša, tak isto ako okno zobrazené cez dve obrazovky alebo vlastný obrázok na každom pozadí. Sice to dobre vyzerá ale prakticke využitie žiadne. A desktop background? Ten vidím akurát tak ráno, keď zapínam počítač, inak po vätšinu času mam okná maximalizované, z toho istého dôvodu vôbec nepoužívam desktop shortcuts.

Nuž všetko pekne fungovalo, kým som nezmenil počítač a s ním aj operačný systém. Prosím nekameňovať, mne sa Aero páči, najmä pre natívny vzhľad WPF aplikácii, k tomu v dnešnej dobe aj tak programujem iba web aplikácie, takže už ma rozdiely pri designovaní winforms nezaujímajú. A odvtedy kvôli Aeru som prišiel o buttoniky v caption bare vo MultiArrows, čo ma veľmi ranilo a ako programájtor som sa rozhodol, že zvediem svoj súkromný boj a vrátim tlačítka tam, kde patria, teda do caption baru.

Skôr ako začnem s tým skutočne zaujímavým, teda samotným skutočným coding story, ešte si prihrejem polievočku tým, že zverejnený kód je tak trochu unikátny, pretože som ešte nenašiel komerčné riešenie, ktoré by zobrazovalo caption buttony aj v Aeru, samozrejme buttoniky fičia aj bez Aera. Ako na to si ukažeme pekne poporiadku.

Krok č.1 Caption button

Skôr ako začnete očákavať nejaky super extra C++ kód s kopcom hackou, tak vás sklamem. App je napísana v C#. Tak kde je tá finta?

Základom celej finty je skutočnosť, že do caption baru v Aero proste kresliť nie je možné, teda ak si celý caption bar odznova nenaprogramujete ako v tomto riešení Take Control of Your Non-Client-Area!. Je to preto, že caption bar je vykresľovaný priamo do GPU, ak teda nemáte priamy pristup do grafickej karty asi si neškrtnete.

Našťastie google je vševedúci a mne sa podarilo násť riešenie. Adding caption buttons to the Non-client area on Vista je to pravé, čo potrebujeme, stačí vytvoriť priesvitný formulár a umiestniť ho vedľa min-max-close tlačidiel a pri akomkoľvek premiestňovaní, resizovaní, minimalizovaní, maximalizovaní a restorovaní pozíciu okna prekresliť. Takýto formular (skôr sa hodí termín okno [window]) nazveme overlay.

Krok č.2 Sploď okno a nechaj si ho adoptovať

Takže spôsob ako zobraziť button v caption buttonu našeho okna máme, teraz zostáva, už len pri štarte zistiť všetky otvorené okna a naše overlay okno nad ním zobraziť.

Na zoznam otvorených okien je ako stvorená WinApi funkcia EnumDesktopWindows nuž a zostáva už len okno zobraziť. Teda nielen zobraziť, okno musí byť childom okna, ktoré prekrývame. To preto aby naše overlay okno bolo za každých okolnosti nad daným oknom a pritom neprekrývalo iné okna, teda nijaky topmost atribút nevyhovuje.

Nedajte sa zlákať SetParent funkciou, tá síce spraví z našeho okna child, ale nie okno, ale niečo ako control, a takýto control nemôže byť vykreslený mimo client areu parent okna. Na prekvapenie na to nepotrebujete žiadnu api funkciu priamo v .net je override show funkcie Form.Show(IWin32Window) a máme to. Okno je child a pritom je to stále okno. A IWin32Window sa netreba báť implementuje len Handle property

    public class WindowWrapper : System.Windows.Forms.IWin32Window

    {

        public WindowWrapper(IntPtr handle)

        {

            Handle = handle;

        }

 

        public IntPtr Handle

        {

            get;

            private set;

        }

    }

 a mi predsa parent handle poznáme, čiže už stačí len:

this.Show(new WindowWrapper(_TargetWindowHandle));

 

Dosiaľ je to stále kúsok koláčika, preto je čas na tu tažkú časť a tou je hooking. A hooking ma zabezpečiť aby sa naše overlay okienko hybalo spolu s jeho rodičom.

Krok č.3 Sleduj rodiča, nech žiaden jeho čin nie je bez odozvy

Heslo ako z Orwella, ale jediný spôsob ako zabezpečiť aby sa naše okno hýbalo spolu s jeho rodičom, je zavesiť sa na jeho WndProc a zachytiť každú message, spracovať ju a ak sa okno hýbe, hýbať sa musí aj overlay.

Prvý nápad bol zistiť adresu WndProc okna a nahradiť ju vlastnou. Čo som už kedysi dávno robil, akurát do bolo už asi dosť dávno, podľa dokumentácie SetWindowLong bolo možné zmeniť GWL_WNDPROC na cudzom okne naposledy vo verzii win 98. To si už človek uvedomí, že asi už starneme:). Čiže musíme ísť znova cestou Global hooku.

Bohužiaľ už z predchádajúceho coding story vieme, že s hookmi to v .net nie je jednoduché a svoje som si užil aj ja. Samozrejme je nutne použiť na global hook unmanaged kód v externej knižnici, teda v našom prípade C++. Pár dni som strávil rôznym skušaním a už som si myslel, že vo vistách hádam ani nie je možné urobiť global hook až som bol tak zúfaly, že som sa rozhodol posledný krát použiť google a trafil som tie správne keywordy, teda výsledkom bol Using Window Messages to Implement Global System Hooks in C# na codeproject.com.

A potom mi trklo, kde som robil chybu, použiť moju .net funkciu na globálne zachytávanie windows správ je proste nedostatočné ako z výkonnostného, tak aj z hľadiska bezpečnosti umiestnenia jej pointera v pamäti. Tuto je to ale robené šalamúnsky v dll sa message zachytí a cez SendNotifyMessage poslať do našeho okna, a tam už stačí vo WndProc zachytiť. Čo ma najviac naštvalo je, že presne takto som to robil kedysi v malej delphi aplikácii, na tvorbu keyboard shorcutov.

Koncept preposielania messages do wndproc sa mi zapáčil natoľko, že som ho použil aj priamo na preposielanie správ medzi hlavným správcovským oknom a jednotlivými overlay oknami.

Hodilo by sa pastnuť nejaký ten kód, ale ten je už popísaný v článku na codeproject, a vy si asi budete musieť zvyknúť na to, že tento článok je viac filozofický ako codologický, na to tu je priamo zdrojový kód celého projektu.

Krok č.4 Tie správne správy pre mňa

Takže máme globálny hook na všetky window message. A že ich je hŕba upravil som zdrojový kód global hook dll aby filtrovala len tie podstatné, myslíte na to ak budete túto dll niekde používať. A že ktoré správy su pre nás podstatné.

WM_GETMINMAXINFO

Správa je vyvolaná pri menení veľkosti okna a teda je nutné aj overlay premiestniť.

WM_SIZE

Táto správa je volaná pri zmene veľkosti okna, okrem toho je práve táto správa volaná aj pri minimalizácii a restorovaní okna, teda práve v čase, keď náš overlay potrebujeme skryť, alebo zobraziť. Na to slúži parameter wParam.

WM_WINDOWPOSCHANGED

Tentoraz je to správa určená na zistenie zmeny pozície okna a možno na vaše prekvapeni, pri minimalizovani v maximalizovanom stave, nie je WM_SIZE správa volaná, ale je volaná práve táto správička. Či je okno minimalizované alebo restorované zistíme z lParam, ktorá obsahuje štruktúru WINDOWPOS v premennej flag. Používam malú fintu a túto flag hodnotu zisťujem už priamo v hook dll, pretože pri preposielaní správy medzi oknami asi handle na túto štruktúru vyprší a dostával som error o prístupe do chránenej pamäte, takže lParam = WINDOWPOS.flag.

WM_DESTROY

Toto je asi jasné každému, proste je to čas zrušiť naše overlay okno. Možno za zaujímavosť stojí informácia, že pri killnutí okna, sa táto správa nezavolá, takže vám možno nejaké overlay zostanú vysieť.

WM_DISPLAYCHANGE

Správa generovaná pri zmene rozlíšenia (to sa stáva pri pridaní nového, alebo odobratí existujúceho monitora), a teda čas vypnúť na chvíľu vytváranie overlay okien, pretože je generované enormné množstvo správ a môže to zahltiť našu app a tým celý systém. Jo a samozrejme je to čas vypnúť hook.

WM_DWMCOMPOSITIONCHANGED

Reakcia aplikácie je taká istá ako pri predchádzajúcej správe, akurát táto je volaná pri zapnutí a vypnutí aera.

Krok č.5 A nesmieme zabudnúť na nové a zrušené okná

Pre novovytvorené okná je tiež potrebné vytvoriť nové overlays. Na to použijeme opäť globálny hook, tentoraz shell hook, ktorý má HSHELL_WINDOWCREATED a HSHELL_WINDOWDESTROYED správy a už sa stačí ne len zavesiť a nový overlay, alebo overlay, ktorého zrušenie sme nezachytili pomocou WM_DESTROY (tak ma teraz napadá, že to bude asi blbosť, že? asi je to teda nejaký pozostatok starého kódu:), jednoducho zrušiť.

Krok č.6 Finalizujeme

Takže už nám zostáva len pár vecí do ukončenia (samozrejme v rámci prvého release):

  • pozdržať zobrazenie overlay-u pri restorovaní okna za zapnutého aera, z dôvodu animácie
  • minimálny počet monitorov, pri ktorom zobrazovať overlay, pri jednom monitore nemá asi zmysel zobrazovať tlačítka na prenos na ďalší monitor
  • pridať kód, ktorý po stlačení prislušného tlačidla prenieslo aplikáciu na ďalší monitor
  • nejaký ten tuning pretože pozícia overlay-u je troška rozdielna pri aere a bez aera
  • nastavenia pekne do app.config
  • tray icon aplikácie s menu pre ukončenie, reinicializáciu overlay-ov a about dialogom, aby si na mňa niekto v zlom spomenul

Záver

Uff, napísať tento článok je asi príliš veľká porcia, pretože je toho tak veľa, čo by sa dalo ešte spomenúť. Za prvé som ale vyčerpal všetku chuť a za druhé na to tu máte zdrojáky, z ktorých dúfam vznikne nejaká ta verzia dva, ktorá odstráni nejaké tie bugy a vylepší výkonnosť aplikácie.

Known bugs

Táto sekcia sa určite skoro rozrastie, zatiaľ mám jeden na 100% reprodukovateľný:

  • ak je v ie zapnutá podpora pre debugovanie javascriptu a pri spustení debugu z visual studia, každý druhý krát aplikácia zatuhne, je nutné zavrieť IE okno natvrdo, potom sa všetko rozbehne ako má

 

 

 

 

 

WPF OnScreenKeybord coding story [update 1]

Dnes nás čaká dosť rozsiahly príspevok (asi som sa nechal uniesť). Preto ak štýl akým píšem, alebo rozsah textu Vás odrádza, odporúčam skok na koniec článku, kde je odkaz na zdrojové kódy ako aj binárku. Nech vašej pozornosti neujde aj varovanie, ktoré je hneď za linkami na stiahnutie, určite schladí vaše nadšenie.

[update 1] linka na zdrojové kódy bola rovnaká ako na binárku, teraz by už malo byť všetko v poriadku

Intermezzo

Občas sa človeku pritrafí, taký ten výnimočný deň, keď sa mu splnia také malé detské sny a po mesiacoch práce mu v balíčku príde vysnívaný počítač typu Tablet PC (z antireklamných dôvodov pre uvediem len link). Dôvodov, prečo som si vybral na vývoj akurát 12.1" riešenie je viacero, ale hlavným asi je to, že až si raz kliknete prstom na obrazovku a kurzor sa tam dokonca aj pohne (s kruhovou toleranicou od 1 do 10 px podľa kvality digitalizera), tak človek niekde vo vnútri cíti: "toto je budúcnosť".

A teraz sa už dostávame k praktickému používaniu. Používanie prsta na ovládanie je proste veľmi návyková záležitosť až tak, že sa časom zabúdate a ťukáte aj na externý monitor a teda handričku musíte mať poruke. Tú samozrejme musíte mať aj na tablet, aj keď momentálne zisťujem, že je lepšie je ho nechať riadne ucapať, to sa už nejaká tá mastnosť stratí, ale to je už hold daň za technológiu.

A pomaly sa dostávame k jadru problému a tým sú znaky, občas, vlastne dosť často potrebujete zadať nejakú tu sekvenciu znakov. Pretože Vista v sebe už obsahuje Tablet PC extensions, voľba operačného systému bola jednoznačná. Predstavy sa už ale od reality líšili, pôvodná prestava bola, že vista obsahuje takúto UMPC klávesnicu:

Bohužiaľ dostanete niečo takéto:

A čo je horšie táto klávesnica sa zobrazuje prejdením kurzora cez cca 5px široký panel schovaný úplne na okraji obrazovky a následným kliknutím na panel, ktorý následne vyskočí:

Pretože vätšina ľudí ma bruška prsta široké od 1 cm do 3 cm, a teda stred od 0.5 cm do 1.5 cm, k tomu treba prirátať zvýšený okraj a teda sa tento úkon dá zaradiť do kategórie nemožné. Samozrejme vätšina tablet pc umožnuje aj detailny posun kurzora pomocou nejakej pomocnej ikony, ale to už nie je single klik ale klik na max dosažiteľnú pozíciu, klik na pomocnú ikonu, presun a klik.

Zobrazená klávesnica nie je samozrejme jediným možnosťou vstupu, ďalšou je písaný text, ale na ten už musite držať v ruke pero, ale ja potrebujem pre pracu v tablet režime niečo šikovnejšie. Ale prečo chodiť okolo horúcej kaše, potrebujem niečo takéto:

Tak toto je to čo chceme dosiahnuť a asi aj to prečo ste začali čítať tento článok. Tak po suchom úvode poďme do práce.

KROK č.1 Vzhľad

Pretože mám rád nové technológie a všetko v živote treba raz skúšať, voľba padla na WPF, vektory sú pekné, dajú sa ľubovoľne resizovať a jednoducho nakresliť.

Takže stačí spustiť Inkscape urobiť mriežku, vyplniť mriežku, vymazať mriežku a výplňam nastaviť border a po cca 4 hodinách máte grafický základ vo formáte .svg. Na konverziu do xaml stačí už len použiť šikovný programček ViewerSvg.

Hmm. Hovoríte si, veď to je jednoduché. To áno, ale to nie je všetko, čaká Vás ta horšia časť a to umiestniť nad každé tlačidlo (po svg importe element Path) a tieto dva elementy groupnuť do elementu Canvas z dôvodu aby sme nad tlačidlom mohli urobiť hover efekt a bohužial Path nemôže obsahovať child elementy narozdiel od Canvasu. Rutinná práca ale výsledok stojí za to:

V tejto fáze som bol nadšený, mam panel pre pravú ruku s pekným hover efektom, je časť zavrieť Microsoft Expression Blend 2 a nastáva coding fáza.

KROK č.2 Stlačme kláves po prvé

Tak a teraz trošku teórie. V podstate sú dve možnosti ako urobiť on screen klávesnicu

  1. Pri kliku na klávesu zachytiť aktuálny focus, stlačiť klávesu vrátiť focus a poslať stisk klávesy (viď Onscreen Keyboard). Ale povedzme si úprimne: toto riešenie je hrozné, pretože ak nenastavíme carret position na koniec, tak nám zmaže input (v prípade štandardného textboxu, ten štandardne pri focuse označí celý text) a k tomu to preblikávanie focusu.
  2. Vo WndProc zachytiť správu WM_MOUSEACTIVATE a vrátiť MA_NOACTIVATE okrem toho ešte pridať v CreateParams funkcii do StyleEx WS_EX_NOACTIVATE flag.

Toľko teórie, čerpať zaujímavé informácie môžte z článku On-screen Keyboards a pre nás je dôležité, že voľba padla na možnosť č.2.

Teória robí majstra, bohužiaľ WPF okno nemá ani WndProc ani CreateParams. Niet sa čomu čudovať, je to nová technológia, ktorá sa snaží vyhýbať balastu starých API funkcií a okrem toho všetko by už malo byť prístupné cez vlastnosti objektu, teda žiadné skryté vlastnosti cez nejaký nepublikovanú flag hodnotu. Teda máme tu atribút Focusable="false" bohužiaľ to funguje len na text input elementy.

Musíme isť hlbšie a porozmýšľať. Veď nech sa na to pozriem z akejkoľvek strany, wpf okno je stale postavené na starom jadre a teda musí existovať spôsob ako získať jeho handle  a k čomu máme handle, tomu vieme zameniť WndProc aj nastaviť cez API funkcie StyleEx. Pár dobre mierených dotazov do google nám vráti WindowInteropHelper triedu. A my sa môžme pustiť konečne do kódu:

        private void Window_Loaded(object sender, RoutedEventArgs e)

        {

            WindowInteropHelper windowHandle = new WindowInteropHelper(this);

 

            // hWnd is the window you want to subclass..., create a new

            // delegate for the new wndproc

            newWndProc = new WindowAPI.Win32WndProc(OverridedWndProc);

            // subclass

            oldWndProc = WindowAPI.SetWindowLong(

                windowHandle.Handle,

                WindowAPI.GWL_WNDPROC,

                newWndProc);

 

            //get current window style of child form

            int style = WindowAPI.GetWindowLong(windowHandle.Handle, WindowAPI.GWL_STYLE);

 

            //set new style

            WindowAPI.SetWindowLong(windowHandle.Handle, WindowAPI.GWL_STYLE, (style | WindowAPI.WS_EX_NOACTIVATE));

        }

 Vo WindowAPI triede sa nachádzaju namapované funkcie (definície možné nájsť na pinvoke.net). Overridnut WndProc už potom vyzerá ibá takto:

        private int OverridedWndProc(IntPtr hWnd, int Msg, int wParam, int lParam)

        {

            switch (Msg)

            {

                case WindowAPI.WM_MOUSEACTIVATE:

                    //System.Windows.Forms.MessageBox.Show("Clicked");

                    return WindowAPI.MA_NOACTIVATE;

                default:

                    break;

            }

            return WindowAPI.CallWindowProc(oldWndProc, hWnd, Msg, wParam, lParam);

        }

Nasleduje kompilácia a samozrejme veľké sklamanie, pretože WPF window pri WS_EX_NOACTIVATE flagu urobí z okna pekný statický obrázok, ktorý nereaguje na žiadne vonkajšie podnety. Niekde na msdn som našiel príspevok, že možno niekedy v budúcnosti bude wpf také niečo podporovať. Tudy cesta nevedie. Tak to skúsme inak.

KROK č.3 Stlačme kláves po druhý krát

Pretože máme spravený vzhľad vo WPF a vieme, že onscreen klávesnicu je možné spraviť iba na štandardnom win okne, je potrebné tieto dve technolófie spojiť. Na to už človek nepotrebuje ani google, aby našiel ElementHost control. Potom už stačí iba použiť našu non activate window techniku:

        private const int WS_EX_NOACTIVATE = 0x08000000;

 

        protected override CreateParams CreateParams

        {

            get

            {

                CreateParams createParams = base.CreateParams;

                createParams.ExStyle = createParams.ExStyle | WS_EX_NOACTIVATE;

                return createParams;

            }

        }

 

        private const int WM_MOUSEACTIVATE = 0x0021;

        private const int MA_NOACTIVATE = 0x0003;

 

        protected override void WndProc(ref Message m)

        {

            //If we're being activated because the mouse clicked on us...

            if (m.Msg == WM_MOUSEACTIVATE)

            {

                //Then refuse to be activated, but allow the click event to pass through (don't use MA_NOACTIVATEEAT)

                m.Result = (IntPtr)MA_NOACTIVATE;

            }

            else

                base.WndProc(ref m);

        }

A na prekvapenie, takto vytvorené okno s WPF user controlom v sebe už pracuje ako má. Teda reaguje na všetky myšie eventy a nekradne focus aktívnej aplikácii.

Po dlhej dobe konečne dobrá správa už nám iba chýba ľavá časť klávesnice.

KROK č.4 Flip it horizontal

Dostávam sa do pracovnej horúčky otáčam wpf control pomocou transformácie <ScaleTransform ScaleX="-1" ScaleY="1"/>. Ostáva už len tak isto transformovať všetky textové elementy podobným spôsobom, aby text nebol zobrazený zrkadlovo. A už veselo kompilujeme..

Išlo to príliš rýchlo, že? Ale tak to v našom živote nechodí a ja narážam na známy WPF bug [BUG 3.5 BETA 2] BitmapEffect + RenderTransform = Incorrect clipping. Našťastie existuje workarround s nastavením Background="Transparent".

Aj toto sa Vám zdalo moc rýchle (v pozadí je cca 4 hodiny zmien kódu a hľadania správneho keywordu do googlu)? Tak nasleduje ďalší kopanec a to, že canvas elementy, v ktorých su path elementy sú štvorcové a transparent farba zachytáva mouse efekty a keďže sa rôzne prekrývaju. Výsledom je, že celá hover funkčnosť je v háji a my sa môžeme vrátiť k Inkscape otočiť obrázok a manuálne opäť urobiť opäť bod 1.

KROK č.5 Make it transparent

Tak nejak začínam šípiť, prečo sú programátori tak dobre platený, pretože nastaviť transparentnosť okna určite nestačí aby hostovaný WPF control bol priesvitný. A tak musíme použiť region aby okno aspoň z diaľky vyzeralo ako wpf okno z bodu 1.

Stačí nastaviť správnu farbu pozadia. Urobiť screenshot okna, trošku screenshot upraviť aby sa niekde vo vnútri neobjavovali rovnaké farby ako tie podľa ktorých okno budeme orezávať a správny kod na vytvorenie Custom Shape Form nájdete tu.

KROK č.6 A nezabudnúť na užívateľskú príťažlivosť

Aj keď mame virtuálnu klávesnícu peknú oblú, priesvitnú a vlastne nech by bola akokoľvek krásna, ak bude stále otravovať na obrazovke určite to užívateľov nepoteší. Musíme teda správiť aktívne zóny, pri kliknutí na ktoré sa naša virtuálna klávesnica zobrazí.

Na aktívne zóny potrebujeme mouse global hook. A samozrejme až taký v C# napíšete (tak ako ja), zistíte, že .net global hook-y v managed kóde nepodporuje. Zostáva nám teda C++ a unmanaged kód. Opäť si pomôžeme opensource zdrojmi a použijeme jeden napr. z Global System Hooks in .NET.

Aby sme opäť neboli len copy'n'paste programátormi, musíme ešte upraviť C++ kód, aby po kliknutí na aktívnu zónu nam bolo umožnené zahodiť všetky nechcené mouse eventy. Pri použití prsta na daný plochu klikneme a ak sa na danom mieste nachádza nejaká aplikácia, dochádza k nechcenému prekliku.

KROK č.7 Finalizujeme

Ostávaju už len drobnosti ako:

  • konfigurácia kláves pomocou XML súboru
  • konfigurácia aplikácie
  • troška kódu aby sa klávesnica po určitej nečinnosti sama schovala
  • štipku medzerníka
  • systray ikonku spolu s menu, aby sme aplikáciu vedeli aj zavrieť
  • zverejniť zdrojový kód

A najťažšia časť:

  • napísať tento blog príspevok aby sa už nikto nikdy nedopustil podobných omylov

Výsledok práce

Binárka: http://vlko.zilina.net/dwn/onscreenkeyboard.zip

Zdrojové kódy: http://vlko.zilina.net/dwn/onscreenkeyboard-src.zip

Na zdrojové kódy, ani obrázky si neuplatňujem žiadne autorské práva. Ak ma niekto chuť môže aplikáciu umiestniť na codeplex, alebo podobné miesto, aplikácii to určite pomôže.

VAROVANIE

Pre použitie WPF hostingu sa aplikácia stala žrútom pamäti. 85 MB mi príde na množstvo kódu a to čo má aplikácia robiť fakt prehnané. Tu je každá rada drahá, nenájde sa nejaká po ruke?

Zaujímavy TODO hack

Dnes ma pri písaní správičky C# Pre Processor direktívy napadol celkom zaujímavy TODO hack:

#warning //TODO: tato sprava sa zobrazi ako warning pri kompilacii a ako polozka v tasklistu ak je subor otvoreny

Teda takýto TODO komentár sa zobrazí v Task Liste ak máte daný súbor otvorený, alebo ako warning pri kompilácii.

Hmmm zaujímave, ale kto vie či aj prakticky využiteľné používať warningy ako todo commenty:)

[ANN] Správičky na aspnet.sk

Intro

Kedysi dávno tu na vývojari vychádzala zaujímava séria článkov Pár komentovaný odkazov od Slava Furmana. Zopár pamätníkov si na to určite so slzou v oku spomenie. Mne osobne to trošku pootvorilo dvere do sveta plného tajov zaujímavých technológii, postupov, atď.

Plný naivného nadšenia, som preto na portáli aspnet.sk minulý rok v októbri vytvoril diskusný thread, kde som začal pastovať linky nájdene pri browsovani rôznymi webmi a rss feedami,  spolu so slovenským popisom, čo by sa na nich zaujímave mohlo nachádzať (link: Zaujimave linky). A že sa mi to celkom začalo dariť, tak som inšpiroval Spigiho, čo je vlastne niečo ako veľký tato tohto portálu, aby vytvoril na správičky osobitnú sekciu.

Announce

A tak si teda dovoľujem do našeho malého česko-slovenského komunitného sveta uviesť tento nový projekt:

Správičky na aspnet.sk a k ním priradený RSS feed.

 

PS: Nakoniec by sa možno hodilo niekoľko tych lepých fráz ako "Nech vám to slúži k úžitku." alebo "Zostáva už dodať, že sa vam obsah a forma zapáči natoľko, že sa na tvorbe obsahu budete spolupodieľať." alebo naivne povzbudzovacio "Tak načo čakáte hor sa do čítania." alebo som už videl aj "Nech vás sprevádza sila.", ale ja osobne ich nemusím takže končím bez nich.

Resolve absolute path from relative

Dnes to bude rychlé, ale možno sa tento kód niekomu v buducnosti hodí.

Mam knižnicu, ktorej ak sa predá relativna cesta k súboru, tak nefunguje spravne, musel som teda vyrobiť kód, ktory ak sa mu preda relativna a aktuálna cesta, vráti absolútnu cestu k danému súboru.

Jej funkčnosť si možete skontrolovať na týchto výsledkoch:

currentPath: c:\Program Files\some application\bin\Release
ResolveAbsolutePath("test.pst", currentPath) becomes to:
c:\Program Files\some application\bin\Release\test.pst
ResolveAbsolutePath(".\test.pst", currentPath) becomes to:
c:\Program Files\some application\bin\Release\test.pst
ResolveAbsolutePath("..\..\test.pst", currentPath) becomes to:
c:\Program Files\some application\test.pst
ResolveAbsolutePath(@"D:\level1\level2\level3\test.pst", currentPath) becomes to:
D:\level1\level2\level3\test.pst
ResolveAbsolutePath("..\..\test.pst", @"\\vlko\level1\level2") becomes to:
\\vlko\test.pst

Nuž a tu nasleduje kód, len tak pre zaujímavosť máte niekto ešte nejaky iný sposob ako dosiahnuť podobného výsledku?

        /// <summary>
        /// Resolves the absolute path.
        /// </summary>
        /// <param name="relativePath">The relative path.</param>
        /// <param name="currentPath">The current path.</param>
        /// <returns></returns>
        static string ResolveAbsolutePath(string relativePath, string currentPath)
        {
            // some constants
            const string cParentDir = "..";
            const string cThisDir = ".";
            // split relative path
            string[] relPath = Regex.Split(relativePath, @"\\", RegexOptions.Singleline);
            // initialize relative level to zero
            int relLevel = 0;
 
            // if nothing to change return relative path
            if ((relPath.Length > 1)
                && (relPath[0] != cParentDir)
                && (relPath[0] != cThisDir))
            {
                return relativePath;
            }
 
            // create string builder for results
            StringBuilder result = new StringBuilder();
            // split current path
            string[] curPath = Regex.Split(currentPath, @"\\", RegexOptions.Singleline);
            // initialize current path level to last item
            int curLevel = curPath.Length - 1;
            // if last item empty (curPath ends with backslash)
            if (curPath[curLevel] == string.Empty)
            {
                --curLevel;
            }
            // loop till nothing to shift
            bool doPathLoop = true;
            while (doPathLoop)
            {
                // depends on relative path level do
                switch (relPath[relLevel])
                {
                    // if shift to parent decrease current path level and increase relative path level
                    case cParentDir:
                        --curLevel;
                        ++relLevel;
                        break;
                    // if stay on current level increase relative path level
                    case cThisDir:
                        ++relLevel;
                        break;
                    // if all others then end looping
                    default:
                        doPathLoop = false;
                        break;
                }
            }
            // correct relative path level
            if (relLevel > (relPath.Length - 1))
            {
                relLevel = relPath.Length - 1;
            }
            // correct current path level
            if (curLevel < 0)
            {
                curLevel = 0;
            }
            // add to result path all necessary levels of current path
            for (int i = 0; i <= curLevel; ++i)
            {
                if (i > 0)
                {
                    result.Append("\\");
                }
                result.Append(curPath[i]);
            }
            // add to result path all necessary levels of relative path 
            for (int i = relLevel; i < relPath.Length; ++i)
            {
                result.Append("\\" + relPath[relLevel]);
            }
            // finally return result
            return result.ToString();
        }

 

 

 

 

OT: Zobereme veľké kladivo na Mono?

O zverejnení zdrojových kodov .NET Framework 3.5 pod Microsoft Reference License (MS-RL) sa hneď objavilo mnoho  blog príspevkov oslavujúcich tento akt.

Nie je ale všetko zlato, čo sa bliští a nie je shared source licencia ako open source licencia, táto hovorí:

"Reference use" means use of the software within your company as a reference, in read only form, for the sole purposes of debugging your products, maintaining your products, or enhancing the interoperability of your products with the software, and specifically excludes the right to distribute the software outside of your company.

Teda v skrátenej forme môžete do zdrojaku nakuknúť, bohužial, ale nič z neho nemožete použiť, pretože akonáhle tak urobite, porušujete dané licenčne ujednanie.

Nuž a tu začína naše veľké kladivo dostavať reálnu podobu. Mono je open-source multiplatformná implementácia .net frameworku. A iste mi dá každý zapravdu, že ak dvaja robia tú istú implementáciu podľa špecifikácie je dosť možné, že sa im podarí napísať podobný, možno aj rovnaký kód.

Teraz keď máme zdrojové kódy pekne na očiach bude ťažké dokázať, či daný kód je, alebo nie je pastnutý a tým pádom porušuje licenciu, alebo je invenciou ducha jednotlivca.

Skôr ako ja úbohy neznalec budem túto tému rozpitvávať, pridám radšej nasledujúce odkazy, kde je táto téma spracovaná lepšie ako to kedykoľvek budem môcť ja zvládnuť:)

Čo poviete, znie to pekne strašidelne a tak sa silne pohrávam s myšlienkou, či sa bez týchto zdrojových kódov nenaučím žiť.

<joke>Čo tak spísať petíciu na uzatvorenie zdrojových kódov k ms produktom?</joke>

 

Více článků Další stránka »