\


 Tuesday, 03 March 2009
LINQ a logování na příkladu logování kroků Dijsktrova algoritmu

Na LINQu je pěkné, jak jednoduše můžeme LINQ výraz upravit nebo jej bezbolestně rozšířit o další části. Nedávno jsem publikoval článek Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů a nyní si ukážeme drobnou úpravu v kódu, která způsobí, že se před každým rekurzivním voláním vždy vypíšou i prozatímní výsledky hledání nejkratší cesty.

 

Abychom mohli zalogovat výsledek, vytvoříme si vlastní extenzní metody pro výpis informací z předaného libovolného generického IEnumerable<T> do konzole.

static class MiscExtensions
    {
        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector, string beginString, string endString)
        {
            if (source == null)
            {
                throw  new ArgumentNullException("source");
            }

            if(logDataSelector == null)
            {
                throw new ArgumentNullException("logDataSelector");
            }

            return innerLogToConsole(source, logDataSelector, beginString, endString);
        }

        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector)
        {
            return LogToConsole(source, logDataSelector, null, null);
        }

        public static IEnumerable<T>LogToConsole<T>(this IEnumerable<T> source)
        {
            return LogToConsole(source, (obj => obj.ToString()), null, null);
        }

        public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, string beginString, string endString)
        {
            return LogToConsole(source, (obj => obj.ToString()), beginString, endString);
        }

        private static IEnumerable<T> innerLogToConsole<T>(IEnumerable<T> source, Func<T, String> selector, string beginString, string endString)
        {
            if (beginString != null)
            {
                Console.WriteLine(beginString);
            }

            foreach (var obj in source)
            {
                String val = selector(obj);
                Console.WriteLine(val);
                yield return obj;
            }

            if (endString != null)
            {
                Console.WriteLine(endString);
            }
        }

    }

Metod pro logování máme více, abychom nemuseli pokaždé předat všechny argumenty. Prvním argumentem je vždy zdrojová sekvence, o jejíchž prvcích budou logovány informace. Argument logDataSelector nese odkaz na funkci, která umí z objektu ve zdrojové sekvenci získat jeho textovou reprezentaci. Jestliže delegát logDataSelector není předán, je k získání textové reprezentace objektu použita metoda ToString() zdrojového objektu. Další nepovinné argumenty beginString a endString jsou řetězce, které má extenzní funkce zapsat do konzole předtím, než jsou vypsána data o prvním objektu v zdrojové sekvenci (beginString), a po zalogování všech objektů v sekvenci (endString). V našem případě argumenty beginString a endString  použijeme k vypsání řetězců, které ohraničí jednolivé kroky algoritmu. Naše extenzní funkce je “neinvazivní”, což znamená, že nefiltruje ani nekonvertuje objekty ve zdrojové sekvenci, ale po vypsání informace o zdrojovém objektu je nezměněný objekt příkazem yield return předán k dalšímu zpracování. Předchozí věta obsahuje varování, že nechcete-li se dočkat nepříjemných překvapení, delegát předaný v argumentu logDataSelector by neměl žádným způsobem měnit data zdrojového objektu, ale pouze je pasivně číst.

Celý algoritmus i s podrobným popisem už zde nebudu opakovat, vložím sem jen pro nás zajímavou rekurzivní metodu getShortestPathInner. Podpora logování je jednoduchou úpravou, protože pouze na námi vybraném neuralgickém místě v LINQ výrazu, které chceme špehovat, zavoláme naši extenzní funkci LogToConsole. Pro lepší orientaci je přidaný kód v následujícím výpisu zvýrazněn tučným červeným písmem.

private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
                                                        where A1 : IGraphEdge<A0>
        {
            var candidates = (from node in edges
                              where !processed.Contains(node.From)
                              select node.From).Distinct();

            if (candidates.Count() == 0)
            {
                return initialGraphPath;
            }

            var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);

            var minimumGPath = (from gPath in initialGraphPath
                                where candidates.Contains(gPath.Current) &&
                                      gPath.TotalDistance == minimum
                                select gPath).First();



            var newGraphPath = from cNode in edges
                               where cNode.From.Equals(minimumGPath.Current)
                               select new GraphPath<A0>
                                       {

                                           Current = cNode.To,
                                           Previous = minimumGPath.Current,
                                           TotalDistance = cNode.Distance + minimumGPath.TotalDistance

                                       };



            var newGraphResult =
                                   (initialGraphPath.Concat(newGraphPath).Where(obj =>
                                                            !initialGraphPath.Any(
                                                                                   obj2 => obj2.Current.Equals(obj.Current) &&
                                                                                   (obj2.TotalDistance < obj.TotalDistance))))
                                                                                   .LogToConsole(obj => String.Format("{0} - {1} - {2}", 
                                                                                                                    obj.Previous, obj.Current, obj.TotalDistance),"--Další kolo algoritmu--", "--Konec kola--")
                                                                                   .ToArray();
            






            var newProcessed = processed.Union(new[] { minimumGPath.Current });

            return getShortestPathInner(newGraphResult, newProcessed, edges);

        }
    }

A zde je ukázka, jak vypadá výstup.

image

Logovat nemusíte jen do konzole, ale můžete si přidat další extenzní metody, které zohlední vaše speciální nároky, kam a jak se mají informace o objektech v sekvenci logovat. Cílem článku bylo jen ukázat, jak bezbolestné a hlavně elegantní :-) je přidání logování do stávajících LINQ výrazů.



Tuesday, 03 March 2009 16:34:35 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | Compact .Net Framework | LINQ


 Monday, 02 March 2009
Náhrada ParametrizedThreadStart delegáta v Compact .Net Frameworku

Na fórech o Compact .Net Frameworku (CNF) se často objevují stesky,  že v CNF třída Thread nemá konstruktor, který by přijímal delegáta ParametrizedThreadStart. Metodě, na kterou ukazuje delegát ParametrizedThreadStart a která bude spuštěna v novém threadu, můžeme předat jeden argument typu object .

public delegate void ParametrizedThreadStartDelegate(Object obj);

 

Ty nářky jsou liché, protože můžeme předat do konstruktoru odkaz na instanční metodu bez argumentů ve vlastním objektu, který má ve svých proměnných nebo vlastnostech stavové informace, které použije instanční metoda poté, co je zavolána z metody Start threadu.

 

Pomocí anonymních metod či lambda výrazů se ale zbavíme nutnosti deklarovat vlastní třídu. Lambda výraz funguje jako adaptér, který převede metodu s jedním argumentem na metodu bez argumentů, kterou očekává konstruktor třídy Thread.

public partial class Form1 : Form   
    {   
        public Form1()   
        {   
            InitializeComponent();   
        }   
  
        private void Form1_Load(object sender, EventArgs e)   
        {   
            int myArg = 10;   
            Thread myThread = new Thread(() => MyThreadMethodWithArgument(myArg));   
            myThread.Start();   
        }   
  
  
        void MyThreadMethodWithArgument(Object obj)   
        {   
            Console.WriteLine(obj.ToString());   
        }   
  
           
    }  

 

Jestliže chcete použít syntaxi velmi podobnou použití delegáta ParametrizedThreadStartDelegate ve “velkém” .Net Frameworku, můžete si napsat vlastní třídu ParametrizedThreadStart, která umožňuje konverzi na delegáta ThreadStartDelegate a tedy opět funguje jako adaptér, který můžeme bez problémů předat do konstruktoru třídy Thread.

using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ParametrizedThread
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            int myArg = 10;
            Thread myThread = new Thread(new ParameterizedThreadStart(MyThreadMethodWithArgument, myArg));
            myThread.Start();
        }


        void MyThreadMethodWithArgument(Object obj)
        {
            Console.WriteLine(obj.ToString());
        }

        
    }

    public delegate void ParametrizedThreadStartDelegate(Object obj);
    public class ParameterizedThreadStart
    {
        private ParametrizedThreadStartDelegate InnerDelegate { get; set; }
        private object Param { get; set; }

        public ParameterizedThreadStart (ParametrizedThreadStartDelegate del, object param)
        {
            InnerDelegate = del;
            Param = param;
        }

        public static implicit operator ThreadStart(ParameterizedThreadStart instance)
        {
            return (() => instance.InnerDelegate(instance.Param));
                     
            
        }
        

    }


}

 

Třída ParameterizedThreadStart vyžaduje, abyste do konstruktoru předali argument pro delegáta. Jestliže do konstruktoru argument ihned předat nechcete, ale chcete ve třídě Thread předat argument pro delegáta přetížené metodě Start, tak jako je tomu opět v NF, nezbývá než se na CNF uchýlit k extenzním metodám.

using System;
using System.Collections;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ParametrizedThread
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            
            Thread myThread = new Thread(new ParameterizedThreadStart(MyThreadMethodWithArgument));
            myThread.Start();
        }


        void MyThreadMethodWithArgument(Object obj)
        {
            Console.WriteLine(obj.ToString());
        }

        
    }

    public delegate void ParametrizedThreadStartDelegate(Object obj);
    public class ParameterizedThreadStart
    {
        public EventHandler<EventArgs> MyEvent;
        private ParametrizedThreadStartDelegate InnerDelegate { get; set; }
        private object Param { get; set; }
        private bool IsParamSetInConstructor { get; set; }

        public ParameterizedThreadStart(ParametrizedThreadStartDelegate del, object param)
        {
            InnerDelegate = del;
            Param = param;
            IsParamSetInConstructor = true;
        }

        public ParameterizedThreadStart(ParametrizedThreadStartDelegate del)
            : this(del, null)
        {
            IsParamSetInConstructor = false;
        }

        public static implicit operator ThreadStart(ParameterizedThreadStart instance)
        {

            return (() =>
                        {
                            
                            ThreadExtensions.SetThreadData();                                                        
                            var delArg = instance.IsParamSetInConstructor
                                             ? instance.Param
                                             : Thread.GetData(Thread.GetNamedDataSlot(ThreadExtensions.THREAD_DATA));




                            instance.InnerDelegate(delArg);
                        });
        }
    }
    

                                 


    public static class ThreadExtensions
    {
        public const string THREAD_DATA = "MethodData";
        private static Hashtable _threadDatahashTable = Hashtable.Synchronized(new Hashtable());
        
        public static void  Start (this Thread thread, object val)        
        {
            if (thread == null)
            {
                throw  new ArgumentNullException("thread");
            }
            
            _threadDatahashTable[thread.ManagedThreadId] = val;                                    
            thread.Start();            
        }

        internal static void SetThreadData()
        {
            object val = null;
            val = _threadDatahashTable[Thread.CurrentThread.ManagedThreadId];
            _threadDatahashTable.Remove(Thread.CurrentThread.ManagedThreadId);
            Thread.SetData(Thread.GetNamedDataSlot(THREAD_DATA), val);            
        }
    }
}

Operátor ThreadStart ve třídě ParametrizedThreadStartDelegate vrací složitější lambda výraz, ve kterém dojde k rozhodnutí, zda bude metodě, na kterou ukazuje InnerDelegate předán argument z konstruktoru, nebo argument, který byl předán extenzní metodě Start. Data specifická pro thread jsou v metodě SetThreadData vyzvednuta z objektu Hashtable a uložena v pojmenovaných datových slotech threadu. Extenzní metoda Start používá pro účely tohoto příkladu objekt Hashtable, protože pro Hashtable je  narozdíl od generické třídy Dictionary možné rychle získat její threadově bezpečnou (tedy z větší části threadově bezpečnou :-) ) verzi - Hashtable.Synchronized(new Hashtable()); a náš delegát ParametrizedThreadStartDelegate, přijímající typ object, si stejně na typovou bezpečnost moc nepotrpí. Tyto nevýhody by vás měly přesvědčit, že nejlepší, přímočaré a hacků prosté řešení jsem zmínil na začátku – vytvořte svoji vlastní třídu s instanční metodou a typovými vlastnostmi, které ponesou stavové informace. Další možností může být vytvoření vlastního wrapperu nad nativními API CreateThread a CreateFiber. :-) Chcete-li ale v CNF použít  ve třídě Thread delegáta ParametrizedThreadStartDelegate ve stylu NF, znáte nyní více způsobů, jak to provést.



Monday, 02 March 2009 13:48:05 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Compact .Net Framework


 Thursday, 26 February 2009
Dlouhodobější zkušenosti s Českou spořitelnou a mBank aneb můj život s českými bankami

Po delší době tímto spotem odbočuji od IT témat na tomto blogu, což snad i stálí laskaví čtenáři snesou bez újmy na svém duševním zdraví. Přesný význam předchozí věty je – tohle je můj blog a nechci v komentářích slyšet, že "tyhle" spoty  sem nepatří a že bych klidně mohl dodat další kód v C#, jak se kdysi stalo. Úvodní opakovací lekci o právech autora blogu a jeho čtenářů máme za sebou. :)

Zkušenosti s mBank po devíti měsících jsem se rozhodl sepsat, protože články o mBank jsou většinou nezajímavé pochvalné ódy, které v podtextu čtenáři sdělují, že je duševně méněcenný či alespoň negramotný v oblasti financí, jestliže své peníze doposud nezakotvil v té úžasné mBank, kde dokonce – velký nádech, přichází pointa – je vše, realističtější autoři napíšou alespoň skoro vše, zdarma. Nevím, jestli bych chtěl žít ve světě těchto rozšafných strýčků Skrblíků, pro které je jediným měřítkem kvality všech věcí cena.

Proč jsem šel zrovna do mBank já? Snad bude vhodné napsat, že jsem byl klasický konzervativní klient, kterého určitě mají naše banky rády, protože poplatky za vedení účtu moc nesleduju, nabídky konkurenčních bank také ne a zajímá mě pouze to, že moje jednorázové i trvalé příkazy odejdou, kdy mají, že mám v ruce platební kartu, kterou mohu použít i pro platby na internetu. Hotovost vybírám jen kvůli platbě v některých restauracích, obchody, kde kartu neberou, pro mě neexistují,  a pokud se v takovém obchodě ocitnu a zjistím, že nemohu zaplatit kartou, odcházím. To se ale nestává moc často, protože nakupuju jen, když je naše rodina přepnuta do nouzového režimu, což znamená, že z nějakého důvodu nemůže praktické záležitosti, včetně pro mě neskutečně otravného nakupování, obstarat Petra. :) Na pobočky chodit nechci, internetové bankovnictví je tou pupeční šňůrou, která mě spojuje se službami mateřské banky, a zálohu pupeční šňůry představuje GSM bankovnictví (Sim Toolkit). Tedy taková ideální ovce pro Českou spořitelnu (dosaďte další velké banky v ČR dle své situace), kterou si Česká spořitelna každý rok slušně ostříhá, aby mohla vykázat větší zisky svým, dnes finanční krizí zbídačelým, :) zahraničním vlastníkům. Česká spořitelna veškeré mé nároky na finanční služby splňuje a záleží jen na úhlu pohledu, jestli poplatky za poskytované služby považujete za součást oboustranně výhodné symbiozy, nebo za parazitování jedné strany. Tím chci říci, že samotná výše poplatků za účet by mě nikdy nedonutila poohlédnout se po jiné bance a je mi jedno, jestli si někdo z akcionářů České spořitelny dopřeje Bentley nebo roční luxusní dovolenou z poplatků.  Podotýkám to jen proto, že z některých obdivných článků o mBank vykukovala ta pravá česká nefalšovaná a ochrannou EU známku si bezesporu zasluhující česká závist. V článcích má závist zastřené rysy, ale v podtextu stále slyším takové to typické důchodcovské stěžování třeba na Radiožurnálu - znáte ne - “já jim platím…poplatky (zajíká se)… k***vam a voni jsou pak na Sejšelách, jako Koženej, co mě taky vokrad, já tady dřu, jak to, že mají víc peněz, než JÁ, eště že si to ten ten náš starej dobrej ranař Paroubek srovná”.

Můj život s Českou spořitelnou se dá rozdělit do tří etap. V první etapě byla Česká spořitelna příšernou institucí, ve které nikdo nikdy nic pořádně nevěděl. I při nějakém triviálním dotazu podrážděné pracovnice z pobočky na malém městě vždy sháněly své kolegyně z “centrály”, aby mohly společně vytvořit pompézní seznam dílčích, nepatřičných, věci se netýkajícících, ale od dalších dotazů klienta odrazujících odpovědí. Předchozí věta plná korektního newspeaku chce říci, že opakované plácání hovadin pracovníky banky bylo výborným způsobem, jak odradit své zákazníky od zbytečných návštěv a na pobočce v malém městě strávit zbytek odpoledne lakováním nehtů, což je rafinovaný druh autoerotické předehry k bujarému večeru, který jim dovolí odplavit ze svého mozku i zbylé vzpomínky na drakonické firemní školení, kde musely za necelý týden pochopit celé dvě stránky nabité shrnujícími informacemi o finančních produktech, které nabízí jejich chlebodárce. Bylo to v době, kdy slovo internet bylo posvátné, proto jsme jej psali ještě všichni s velkým I, a o internetovém bankovnictví v ČS se možná někde na centrále v bankovních kuloárech tiše spekulovalo, ale návštěvě pobočky plné oduševnělých pracovnic jste se určitě nevyhli. Pravěk. V té době jsem poprvé přemýšlel o změně banky, ale nakonec jsem u ČS zůstal.

Přišla totiž etapa dva, na kterou nostalgicky vzpomínám. Nevím, jestli se fáze dva kryje se vstupem zahraničního vlastníka, ale najednou bylo vše jinak. První dobrý signál – z malých měst zmizely pobočky, protože banka začala preferovat telefonické, poté internetové a sim toolkit bankovnictví. Pobočka ve větším městě fungovala k mé plné spokojenosti. Žádné velké fronty, když už jsem na pobočku musel zajít. Požádal jsem o embosovanou kartu a slečna věděla, o jaký produkt jde, nabídla mi ke kartě další služby a ihned mi dala písemné podmínky používání těchto služeb. Nesnažila se nic zatajit ani okecat. Sdělila mi, kdy karta přijde, a poprvé opravdu karta dorazila ve stanoveném termínu. Byl to snad bankovní pud sebezáchovy, který se u ČS aktivoval, protože když jsem šel žádat o embosovanou kartu, oznámil jsem doma, že jestli nastanou nějaké potíže, tak s ČS končím a jdu jinam. Nestalo se a já jsem si užíval etapy dva mého vztahu s bankou, kdy se z ČS někomu schopnému ve vedení podařilo vybudovat docela příjemnou banku. Jak vidíte, žádné velké nároky na služby banky nemám.

Nic na tomto světě netrvá věčně, dobré věci už vůbec ne, a ani Česká spořitelna nezůstala v té příjemné etapě “dva” dlouho. Nastala etapa tři, která trvá dodnes a kterou jsem si pojmenoval podle tajného prvního axiomu, který si musí jako základ firemní kultury asi osvojit všichni noví zaměstnanci. Axiom zní: “Všichni naši klienti jsou debilní, my to víme, podle toho s nimi jednáme a tak je chráníme.” Tento axiom byl podrobně rozpracován v prováděcích příručkách, které popisují hlavní zásady chování zaměstnance ČS vůči zákazníkům. Takže vybrané lapsy České spořitelny. Vždy říkám, že potřebuji kartu k platbám na internetu. Jaké bylo moje překvapení, když circa před dvěma lety zničehonic neprošla platba kartou za notebook na Alzasoftu. Poté, co jsem zavolal telefonickému bankéři, nebo jak honosně se ti operátoři dnes nazývají, jsem se nejprve dověděl, že slečna je na lince "nová". Mě, jakožto klienta, tyto hlášky ve stylu “jsem ještě dočasně neschopný” nemusí zajímat, to ať si ošetří sama banka, koho vyšle ke klientům. Bezděky mi v mozku vytanula asociace - konverzace se slečnami z poboček v etapě jedna – deja vu. Po několikaminutovém oboustranném trápení mi slečna sdělila, že kvůli mé bezpečnosti (sic) mi byly sníženy limity na internetové platby a že si je mohu zvednout na pobočce. Stejná situace se opakovala asi za dalších devět měsíců – jsme těhotní péčí o zákazníka a každých devět měsíců na něj vyvrhneme nějaké moudré ochranné opatření ze svého bezpečím oplývajícího a nápady nabitého mateřského finančního lůna by mohl znít úderný axiom číslo dva firemní powerpointové kultury. Pochopil jsem ale, proč to ČS dělá – při druhé návštěvě agilní slečna na pobočce, která mi oficiálně zvedala limity, ale neoficiálně mě spíš zvedala ze židle, protože mně svým milým dětským hláskem, který ve svých občasných nočních můrách slyším i dnes,  sdělovala, že musím platit jen tam, kde internet má “zámeček”. Když jsem se jí zeptal, jestli myslí “https”, nastalo trapné ticho a poté opět zašeptala, že tam musí být určitě zámeček. Slečnu jsem upozornil, že vím, co je to https, že běžně nenakupuju erotické služby na nějakých pochybných serverech, jak to pravděpodobně činí majorita jejich zákazníků, kvůli kterým stále ”upravují” limity, a že ještě jednou mi limity upraví a uvidí mě poté jen při zrušení účtu. Neberte to, podrobné školení o bezpečnosti na internetu od pěkné slečny zadarmo, škoda, že se toho nechytl nějaký jurodivý “hasalíkovec” (viz Google – heslo Radim Hasalík pro neznalé :-) ), který by na Lupu napsal další článek o blahodárném vlivu marketingu na chod virtuálního i reálného světa.

Další lapsy v rychlosti. Česká spořitelna mi sdělila, že při pravidelném obnovování karty budu mít novou kartu na pobočce vždy měsíc před vypršením platnosti staré karty, což byla zase jen pravda z PowerPointu, tedy hezkými obrázky a animacemi přibarvená lež.  Třikrát mě pracovníci ignorovali se žádostí o podrobnější konzultaci některých finančních služeb – průběh byl vždy stejný. Mám zájem (třeba) o BrokerJet, ano, kolegyně, která tomu rozumí, zde dnes není, příští týden vám zavolá a domluvíte si schůzku. Uběhl týden, měsíc, rok a kolegyně nezavolala. Pravděpodobně šlo o kolegyni, která nikdy nepřijde – poslední hit, udržujte své zákazníky v nejistotě, pořiďte si náš poslední model zaměstnance vyvíjený pod kódovým názvem Godot. K účtu mi automaticky při obnovení debetní karty pracovnice vydaly kreditní kartu jako dárek “zdarma” – je to moje hloupost, podmínky jsem neověřoval, kartu jsem si vzal a po roce si ČS strhla roční poplatek za vedení karty. Stačilo říct – karta je zdarma na rok a ne říkat, že jde o dárek pro klienty. Stejně bych si tu kreditku vzal. Nedávno mi volala paní, která chtěla, abych zašel na pobočku a pořídil si výhodnou chytrou kreditní kartu. Výhodná tato karta je, chybí bohužel dodatek, že hlavně pro Českou spořitelnu. Petru zaměstnanci ČS lámali, ať přejde na Osobní účet. Nabídli jí nevýhodné podmínky oproti předchozímu tarifu a přitom se dušovali, kolik ušetří. Axiom 1 potvrzen – jsme pro ně všichni nesvéprávní debilové, co neumí počítat.

Když přišla mBank, zkusil jsem si u ní založit účet. To, co mě nalákalo, byla slova, že jde o internetovou banku, tedy banku, která považuje stejně jako já internet za nejdůležitější pojítko mezi mnou a bankou, což by mělo mít pár přijemných důsledků. Nejen běžná správa peněz na účtu, ale také všechny žádosti o další služby a změny v současném nastavení služeb půjdou přes internetové bankovnictví. Když už bude nutné podepsat nějaké věci osobně, mKiosky by měly fungovat i v době, kdy mám čas na řešení techto věcí, a ne jako u velkých bank, u jejichž otevírací doby mě vždy jen napadá: “Pro koho je asi tak určena otevírací doba od 9:00 – 16:00?” Banka navíc nabídla účet zdarma, spořicí účet se slušným úrokem a dokázala kolem sebe udělat pořádný rozruch ještě před tím, než vstoupila na trh. Zdůrazním, že nulové poplatky jsem bral a stále beru jako příjemnou, ale určitě ne klíčovou vlastnost bankovního účtu.

Žádost o zřízení účtu jsem vyplnil ihned po vstupu banky na trh někdy v prosinci 2007, ale protože banka nestíhala odbavovat žádosti, nakonec jsem si účet založil až někdy v dubnu-květnu 2008, kdy jsem očekával, že hlavní procesy v bance již fungují. Nezvolil jsem založení účtu přes kurýra, protože k podepisování smluv o zřízení účtu zajišťuje pro mBanku jiná firma a neměl jsem moc důvěry k lidem, které si banka pravděpodobně sama neškolí.

Účet jsem založil ve finančním centru Vinohradská. Příjemnou změnou bylo to, že finanční centrum nemá sadu kukaní zvaných přepážky jako moje předchozí banka. Po vstupu do banky jsem řekl, co chci, že žádost o zřízení účtu jsem již vyplnil přes internet, a poté jsem chvíli čekal na přidělenou paní-slečnu, která se mnou měla podepsat smlouvu. Pobavilo mě jen, že slečna se po příchodu dotazovala kolegy - kdo že to tedy čeká na zřízení účtu - šeptem, který se pravděpodobně naučila na nějaké herecké škole, kde jí vtloukli do hlavy zásadu, že když se na jevišti šeptá, musejí šeptání slyšet i zadní lavice. Snad už dnes ji kolegové vysvětlili, že (m) Banka není tyjátr.:) Diskrétní zóny nejsou zase tak špatný vynález. Při podepisování smlouvy žádné problémy nenastaly – slečna byla zdvořilá, i když při některých dotazech trochu nejistá. Překvapilo mě, že vůbec neví, jak přesně probíhá zaúčtování platby provedené platební kartou mBank v cizí měně a jaký skutečný význam nese v mBank slovo zdarma, :-) což se tenkrát intenzivně probíralo i na mFóru. Podrobnosti o této hře mBank jsou na webu Vúčako, ke cti mBank slouží, že se tato informace objevila posléze na oficiálním webu. Účet byl zřízen ihned poté, co jsem opustil budovu mBanky, a stačilo jej aktivovat. Internetové bankovnictví mBank bylo popisováno už mnohokrát, zde jen řeknu, že mně jeho strohost a jednoduchost vyhovuje, s jeho ovládáním problémy nemám, i když je to asi hlavně tím, že jsem deformován svým povoláním. Jestliže jako vývojář prohlásím, že mi uživatelské rozhraní vyhovuje, mělo by to být pro lidi z mBank varování, že pro běžného Frantu uživatele může být ovládání složité a překombinované.:-) Určitě jsou některé volby nelogické, zakládání šablony, kdy jste do poslední chvíle nevěděli, zda jste jen založili nebo změnili šablonu platebního příkazu, nebo již byla první platba na základě šablony provedena, patří mezi jedny z vybraných lahůdek, kterým bylo na mFóru věnováno hodně vláken.

Ihned po založení účtu jsem se rozhodl, že Petru učiním spoluvlastníkem účtu. Mimochodem, Česká spořitelna zřídí k účtu disponenta, ale když jsem chtěl, abychom byli vlastníky účtu oba, tak mi bylo řečeno, že to nejde a že na vině je česká legislativa, která to neumožňuje. MBank asi našla dle ČS skulinu v českých zákonech. :) Tady poprvé jsem si uvědomil, že u mBanky nesmíte vybočit z rámce “typického” průběhu nějakého procesu. První problémy nastaly při podávání informací – už při zakládání účtu na finančním centru mi slečna řekla, že manželka se musí osobně podepsat v bance a že mám předtím ještě zavolat na mLinku, aby pro ni rovnou připravili aktivační balíček. Na infolince mi nejprve nějaký operátor po mém dotazu položil hovor, tedy poprvé jsem si myslel, že hovor “vypadl”, ale poté jsem zjistil, že jde o takovou zábavnou hru infolinky, když operátor něco neví. Poté jsem se dostal k operátorovi, který rezolutně prohlásil, že osobní návštěva není nutná, že si vezme informace o manželce po telefonu, poté pošle na naši adresu smlouvu, kterou podepíšeme a pošleme zpět. Měl pravdu, osobní návštěva nutná nebyla. Smlouva dorazila v dalším týdnu, poté aktivační balíček a vše probíhalo hladce. Do té doby, než jsme zkusili aktivaci balíčku. Kdykoli jsme se pokusili o zprovoznění uživatelského účtu, dostávali jsme nádhernou hlášku “chybí seznam jednorázových hesel”. Uznejte sami, že ta hláška vypadá, jako když teď  momentálně nefunguje správně infrastruktura internetového bankovnictví. Chvíli jsme to nechali a poté začala anabáze s infolinkou.  První volání – operátor pokládá telefon. Druhé volání  - dnes nic nedělejte, víme o chybě v internetovém bankovnictví, zítra vše poběží. Druhý den dostáváme stejnou hlášku, další volání na mLinku, obligátní položení telefonu už bereme jako zpestření všedního dne od moderní banky s novým přístupem ke klientům, při dalším hovoru operátor tvrdí, že asi potřebujeme nový aktivační balíček, protože předchozí zasmrádl, pardon propásli jsme doporučené datum aktivace, což je evidentně nesmysl. Zkouším mFórum, mezitím Petra znovu zkusí infolinku. Při dalším zavolání mluví s člověkem, který účinnou taktiku “nevím, položím telefon” vylepšuje na “huhlám si něco směsí slovenštiny, polštiny a maďarštiny” a ani na několikeré slušné upozornění, že mu není rozumět, zásadně nereaguje.  Při dalším telefonátu Petra konečně narazí na operátora, který řekne, že pravděpodobně chybí/je špatně zadán kontaktní telefon a že to ověří. Voila – během chvíle je účet aktivní. Když znáte příčinu, řešení je triviální. Pro ty z vás, kdo uvažují o společném účtu u mBanky by mohlo být zajímavé, že každá fyzická osoba v ČR může mít pouze jedno mKonto a při společném vlastnictví jednoho účtu si vaše manželka další mKonto nezaloží. Jestliže je manželka v roli disponenta, tak si může zřídit svoje vlastní mKonto.

Předchozí odstavec obsahuje popis tradičního scénáře mBanky, když nastanou potíže. To je hlavní důvod, proč najdete na mBanku na mFóru nejen nadšené, ale dnes i velmi kritické reakce. Když vše běží podle zažitých procesů, nikdo si nestěžuje. Když nastane problém, buď narazíte na někoho schopného na infolince, který problém ihned vyřeší, anebo na neschopného ignoranta, a podle toho už mBanku vnímáte. Nepříjemné je, že tyto problémy nevyřešíte přes internet, ale musíte volat na infolinku.

Také uvádění nových služeb je tragikomedie. Minulý rok jsem uvažoval, že nahradím kreditní kartu České spořitelny kreditní kartou mBank. Uvádění produktu na mFóru jsem sledoval jen letmo, ale celkově bych to shrnul – v srpnu vzrůstá očekávání, v září mBank řeší, kdy nejlépe zveřejnit nové obchodní podmínky kvůli konkurenci, no a někdy v prosinci první odvážní začali žádat o karty se všemi peripetiemi, jakou byla speciální edice mBank kreditni karta, model chytrá horákyně, která je současně aktivovaná-neaktivaná.

Chuť dělat opakovaně testera mBance u kreditky mě přešla totiž poté, co jsem požádal o výměnu své nečipové debetní karty k mKontu za čipovou a embosovanou kartu, kterou mBank nabízí nově od ledna 2009. Prý chybou dodavatele ale došlo k tomu, že lidem, kteří si požádali o výměnu někdy na začátku ledna 2009 došla karta sice čipová, ale pro jistotu neembosovaná. Už jsem si říkal, že mBank, stejně jako ČS, ví, co je pro její klienty nejlepší, ale mBank chybu uznala a kartu vyměnila. Nakonec kartu vyměnila, ale předtím se opět ukázaly veškeré nešvary mBanky. Když lidé z mBanky na mFóru (mimochodem nepřipadá vám něco shnilého ve spojeném finančním království česko-polském, když špatné karty dostanou od dodavatele klienti a neprověří je nejprve někdo v bance, zvláště když embosované čipové karty představují nový produkt?), zjistili, že karty jsou neembosované, napsali, že VŠICHNI klienti automaticky obdrží embosované karty. Pro mě ideální řešení. Posléze ale mBanka usoudila, že vyjde vstříc těm klientům, kteří preferují neembosovanou kartu a všech klientů se bude ptát, zda opravdu chtějí embosovanou kartu, nebo zda si raději ponechají neembosovanou kartu. Dozvěděl jsem se to jaksi mimochodem někde na mfóru, když mi někdy v pátek odpoledne volalo nějaké podivné číslo z polské telefonní sítě, kterému se nedalo zavolat nazpět a mě napadlo, že to pravděpodobně byl někdo z mBanky. Poté byly slíbeny omluvné emaily - tyto emaily dorazily  klientům, kteří si dle všech indicií kartu sice objednali na začátku ledna, ale roku 2008, a ne roku 2009. Nádherný exemplář neschopnosti a ukázka nedostatečné kontroly procesů na všech úrovních. Programátor vytáhl žádosti z databáze, v SQL příkazu se sekl o rok a někdo hromadně odeslal emaily, aniž by se pravděpodobně obtěžoval spárovat pro kontrolu některé emaily s lidmi, kteří na mFóru napsali, že jim dorazila neembosovaná karta. Poté jsem volal na mLinku a dohodl jsem se s operátorem, že o výměnu karty zájem mám. Pak mi volal pan Ressler z mBanky, který si vše znovu ověřil a omluvil se. Pomyslnou třešničkou byl telefonát za tři dny nato opět z mBanky, kdy se mě nějaký brigádník ptal, zda chci kartu vyměnit. To je péče. Asi dobře věděl, proč hned na začátku hovoru upozorňuje, že hovor může být nahráván, protože i já jsem už zvýšil hlas, když se mě ptal na vydání karty, o které jsem si myslel, že je na cestě. Brigádník mi vysvětlil, že na papíře má pouze informaci, že se mi poprvé nedovolali a že moji zadanou žádost znovu prověří. Konec dobrý, embosovaná karta po týdnu dorazila, ale že by platilo i “všechno dobré”?

Při běžném používání účtu je mBank z mého pohledu bezproblémová. Jednorázové platby jsou rychlé, s nečipovou kartou mBank jsem minulý rok neměl problémy při žádné platbě ani v ČR ani v zahraničí narozdíl od některých jiných klientů, kteří si stěžovali na  mFóru. Z účtu v mBance platíme naše běžné měsíční výdaje. Příjemné jsou tři výběry v každém měsíci z libovolného bankomatu v ČR zdarma u každé platební karty k účtu. Spořicí účet eMax má nyní sazbu 2,8%. I když to není nejlepší sazba na trhu, tak možnost kdykoli vybrat peníze ze spořícího účtu se hodí.

Účty v České spořitelně jsem si ale prozatím ponechal, protože mBance v některých záležitostech moc nevěřím. Jestliže chcete mít klidné spaní, raději nečtěte mFórum. Chyby, které nahlásili klienti po nasazení některých nových verzí internetového bankovnictví, a to i když si odmyslíte příspěvky od tradičních provokatérů a rodilých idiotů, kteří se zde vyskytují v hojném počtu, působí děsivě. Nedovedu si představit, že mBanka se rozhodne udělat nějaké změny v internetovém bankovnictví a mně ve stanovenou dobu neodejdou peníze za nájem nebo finančnímu úřadu. Určitě bych si tedy u mBank nezřizoval podnikatelský mBusiness účet. Už jen proto, že na účtech mBank se špatně identifikují platby a že data z mKonta nelze rozumně vyexportovat. Stačí se podívat na to, jak zmršený je csv formát. Jestliže uvažujete o mPůjčce, je možné, že vás mBank odmítne, i když jste perspektivní klient, protože jste si nikdy nepůjčili a nemáte záznam v registru. Osobní zkušenost s hypotékou nebo mPůjčkou nemám, soudím jen podle opakovaných dotazů na mFóru.

Několikrát jsem zde zmiňoval mFórum. To představuje silnou i slabou stránku mBanky. Na první pohled je mFórum výjimečné tím, že si v něm můžete postěžovat a že vám odpoví někdo z mBanky nebo mRady. Je jasné, že klienti málokdy píší o svých dobrých zkušenostech, ale ventilují na mFóru hlavně ty špatné. Při čtení mFóra je proto stále potřeba mít na mysli, že jednotlivé příspěvky nepopisují skutečnou tvář banky, ale spíš její průšvihářskou odvrácenou stranu, kterou má každá firma, ale většina firem průšvihy raději kamufluje výroky svých PR Pýthií. Možná to může znít paradoxně, ale mFórum mBance asi také prospívá při zachování její dobré pověsti. Proč? Je to takový kanál, do kterého se svede prvotní rozladění klienta, špinavé prádlo se pere tedy doma, a ne na nějakém jiném internetovém fóru třeba u finančního serveru. A lidé z mbanky mají šanci problém nějakou nestandardní cestou vyřešit a alespoň zmírnit naštvání nabroušeného zákazníka. Ale je tohle dobrý postup? Opravdu chcete být v bance, kde vám lidé píší – máš problém s mKioskem, mLinkou, napiš Adamovi Zbiejczukovi (taková šedá eminence internetových aktivit mBanky)  :) a ten tvůj problém vyřeší, sežene vhodné lidi atd.? Kde místo absolvování nějakého standardního procesu mnohdy rychleji dosáhnete kýženého výsledku přes nějaké mFórum?

Mohl bych popisovat i další věci kolem mBanky nebo podrobně rozebrat, jak si z mého pohledu mbanka vede v dialogu se zákazníky po roce. Závěr je ale zřejmý – o mBance se mluví jako o internetové bance a kvůli tomu jsem si u ní založil účet. Internetovou bankou je pro mě banka, která koná podle pravidla – přes internet se dá vyřídit vše, pozveme si vás jen na schůzky, kde musíte třeba i ze zákona prokazovat svoji totožnost. Nové produkty objednáváte ale přes internet, změny v nastavení služeb děláte přes internet, máte-li problém, napíšete nám email nebo zadáte dotaz v internetovém bankovnictví. K tomu má mBanka opravdu daleko – mBanka je hlavně nízkonákladová banka a internet je vhodným prostředkem pro udržení nízkých nákladů. Produkty a služby ale u mBanky mnohdy nezměníte/neobjednáte jinak než přes mLinku, kdy se s vámi o náklady banka částečně podělí. Hledáte-li nízkonákladovou banku, tedy chcete platit co nejnižší poplatky, je mBanka správná volba. Hledáte-li čistě internetovou banku, možná bych se být vámi poohlédl i jinde. Občas s nadsázkou přirovnávám mBanku k Lidlu, i když je to asi nespravedlivé k oběma společnostem. Sám jsem tedy v Lidlu nebyl, protože po prvním a současně posledním nákupu Petry v Lidlu, jsme se oba shodli, že umřít se dá i jinak a lépe než z mizerného jídla. :) MBank narozdíl od Lidlu používám stále. Přesto si myslím, že buď mBanka něco udělá se svými procesy, uvede další zajímavé produkty a potom jí počet klientů stále poroste, nebo po nějakém megaprůšvihu dostane etiketu banky, kde si zřizují účet jen studenti nebo ti, co mají hluboko do kapsy a zaměstnavatel nebo sociálka  po nich požaduje účet, ale nikdo, kdo má zájem i o (klidně placené) kvalitní nadstandardní služby, váží si svého času a nechce se dohadovat ani s nějakou neschopnou pipinou na přepážce u konvenčních bank ani se zhmotněním neschopnosti v podobě operátora na mLince. Můj názor je, že k oběma alternativám má mBank dnes stejně blízko.:-))



Thursday, 26 February 2009 15:28:57 (Central Europe Standard Time, UTC+01:00)       
Comments [9]  Ostatní


 Thursday, 19 February 2009
Dvě uvolněná místa na veřejném kurzu Pokročilé objektové principy a návrhové vzory 2

Dva účastníci kurzu Pokročilé objektové principy a návrhové vzory 2 zvolili dodatečně inhouse variantu školení. I ti z Vás, kteří teď v posledním týdnu dostali informaci, že kurz je předběžne obsazen, se mohou znovu přihlásit.

Rozcestník:

Pozvánka na jarní kurzy

Podrobné informace o kurzu a možnost přihlásit se na kurz

Výběr z ohlasů na předchozí kurz



Thursday, 19 February 2009 12:45:55 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  


 Monday, 16 February 2009
GSM Net Monitor verze 0.9.0

ZobrazeniMapy

Homepage aplikace.

Instalační cab.

Návod na rozchození lokalizace pozice pomocí BTS

Paypal donate

Změny ve verzi  0.0.9.

  1. Vylepšena stabilita aplikace. Pro znalce: Přesně řečeno, při startu pluginu se žádným způsobem nepracuje s RIL knihovnou. Pokud by při inicializaci pluginu byly stále na některých zařízeních potíže, zbývá už jen načítat RIL funkce dynamicky přes LoadLibrary.
  2. V souboru se zachycenými BTS se na každém řádku (poslední údaj) vypisuje informace o síti (network id). (ukázka -26.01.2009;12:41:26;18208;43649;-1;23001;) Přidáno na žádost lovců BTS.


Důležité:

Před instalací nové verze vypněte v aplikaci sledování sítě. Nejlépe starou verzi také sami deaktivujte v nastavení Today obrazovky a odnistalujte ji přes applet Přidat-Odebrat programy.

Protože se jedná o AlFA preview, doporučuji před instalaci Net Monitoru mít v zařízení např. SPB Pocket Plus a v něm aktivovaný safe boot - jestliže by vám "vytuhlo" zařízení, nebudete muset dělat HR (Hard Reset), protože můžete při startu zařízení dočasně deaktivovat Today pluginy.

Program pro jistotu ani nezkoušejte na zařízení, kde je nahráno TouchFlo, nebo jiný agresivní Today plugin. Riskujete zatuhnutí zařízení a je zbytečné mi potom psát dojemné maily, pokud nejste schopni si předtím udělat zálohu PDA nebo alespoň mít funkční safe-boot.

Po upgradu budete muset pravděpodobně znovu zadat svůj registrovaný email a přístupový kód. Pokud jste jej zapomněli, jděte na stránku http://gsmadmin.renestein.net a zadejte znovu svůj email. Aplikace vám nabídne opětovné zaslání emailu s přístupovým kódem.

Jestliže máte zařízení s VGA displejem, v pluginu jsou malé ikony a plugin je vykreslován na malé ploše. Plugin může být vykreslen na větší ploše  - přes kontexové menu zobrazte Nastavení pluginu a změňte na záložce Základní nastavení preferovanou výšku na obrazovce Dnes. Ikony ale stejně zůstanou malé, proto chystám plnohotnotnou VGA verzi, do té doby lze plugin plně ovládat přes kontextové menu.

 

NetM2 ZadaniKlice Net Monitor - zakladni info



Monday, 16 February 2009 12:52:47 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  Mobilitky | Nativní kód | Net Monitor


 Wednesday, 11 February 2009
Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů

Pokusil jsem se napsat Dijsktrův algoritmus pomocí LINQ konstrukcí. Pokud někdo z vás tápe, k čemu je Dijsktrův algoritmus dobrý a k čemu slouží, odkážu jej na podrobný článek na Wikipedii. Zde jen připomenu, že Dijsktrův algoritmus slouží k nalezení nejkratších cest v grafu z jednoho zdrojového uzlu ke všem ostatním uzlům. Je tak možné najít například nejkratší cestu z jednoho města do druhého. Na internetu jsem našel jeden graf, který budeme mít stále před očima a na který budeme Dijkstrův algoritmus napsaný v LINQu aplikovat.

 

Dijkstra_01_

 

Uzly A, B atd. reprezentují města, ohodnocení hrany vyjadřuje počet kilometrů (město A je od města B vzdáleno 5 km). My budeme chtít spočítat nejkratší cestu z města H do města A a nebude nás zajímat jen počet ujetých kilometrů, ale také jak vypadá celá trasa - jinými slovy, přes jaká města naše nejkratší cesta vede.

 

Nejprve si vytvoříme potřebné třídy:

public interface IGraphEdge<T>
    {
        T From { get; }
        T To { get; }
        int Distance { get; }
    }

Rozhraní IGraphEdge reprezentuje hranu grafu - vlastnost From nám říká, odkud hrana vede (z města A), vlastnost To sděluje, kam hrana vede (město B). Vlastnost Distance nese vzdálenost mezi uzly (města A a B jsou vzdálena 5 km).

 

My pracujeme s vzdálenostmi mezi městy, a proto si vytvoříme jednoduchou třídu reprezentující město. Z města nás zajímá jen název.

 

class City : IEquatable<City>
    {
        private Guid Id;
        public City(string name)
        {
            Id = Guid.NewGuid();
            Name = name;
        }

        public string Name
        {
            get;
            set;
        }

        public override bool Equals(object obj)
        {
            City secondCity = obj as City;

            if (secondCity == null)
            {
                return false;
            }
            return Equals(secondCity);

        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }


        public bool Equals(City other)
        {
            if (other == null)
            {
                return false;
            }

            if (other.GetType() != this.GetType())
            {
                return false;
            }

            return (other.Id == Id);
        }

        public override string ToString()
        {
            return Name;
        }
    }

 

Hrany grafu představují silnice mezi městy, a proto si vytvoříme třídu CityEdge, která implemetuje rozhraní  IGraphEdge a za generický parametr T dosadí třídu City.

 

 

class CityEdge : IGraphEdge<City>
    {
        #region Implementation of IGraphNode<City>

        private City m_from;
        private City m_to;
        private int m_distance;

        public CityEdge(City from, City to, int distance)
        {
            if (from == null)
            {
                throw new ArgumentNullException("from");
            }

            if (to == null)
            {
                throw new ArgumentNullException("to");
            }

            if (distance <= 0)
            {
                throw new ArgumentException("value must be greater than zero", "distance");
            }

            m_from = from;
            m_distance = distance;
            m_to = to;
        }

        public City From
        {
            get
            {
                return m_from;
            }
        }

        public City To
        {
            get
            {
                return m_to;
            }
        }

        public int Distance
        {
            get
            {
                return m_distance;
            }
        }

        #endregion

        public override string ToString()
        {
            return String.Format("From: {0}, To {1}, Distance{2}", From, To, Distance);
        }


    }

Dále potřebujeme třídu, která ponese informaci o nalezené nejkratší cestě do daného bodu a o předchozím městu, přes které musíme cestovat. Na konkrétním příkladu - z města A (námi zvolený jediné výchozí město, z nějž se počítá nejkratší cesta ke všem ostatním městům) vede nejkratší cesta do města  C (vlastnost Current), která má délku 7 Km (vlastnost TotalDistance) a současně nám vlastnost Previous vrátí přechozí město (vlastnost Previous), přes které musíme jet (město B).

 

public class GraphPath<T> : IEquatable<GraphPath<T>>
    {
        private const int DUMMY_HASH_PLACEHOLDER = 100;
        
        public T Current
        {
            get;
            set;
        }

        public T Previous
        {
            get;
            set;
        }

        public int TotalDistance
        {
            get;
            set;
        }

        public override bool Equals(object obj)
        {
            GraphPath<T> second = obj as GraphPath<T>;

            if (second == null)
            {
                return false;
            }

            return Equals(second);

        }

        public override int GetHashCode()
        {
            var prevHash = (EqualityComparer<T>.Default.Equals(Previous, default(T)) ? DUMMY_HASH_PLACEHOLDER : Previous.GetHashCode());
            
            return (Current.GetHashCode() ^ prevHash ^ TotalDistance.GetHashCode());
        }

        #region Implementation of IEquatable<T>

        public bool Equals(GraphPath<T> other)
        {
            if (other == null)
            {
                return false;
            }

            if (other.GetType() != this.GetType())
            {
                return false;
            }

            return (EqualityComparer<T>.Default.Equals(Current, other.Current) &&
                    EqualityComparer<T>.Default.Equals(Previous, other.Previous) &&
                    TotalDistance.Equals(other.TotalDistance)
                    );
        }

        #endregion
    }

 

Přípravu máme za sebou, nyní se můžeme podívat na kostru celého algoritmu a poté rozpitvat jeho jednotlivé kroky. Pamatujme - důsledně se snažíme používat, kde to jen jde, LINQ konstrukce. ;-)

 

static void Main(string[] args)
        {
            var cities = (from i in Enumerable.Range(0, 8)
                          let key = ((char)(i + 'A')).ToString()
                          select new
                                     {
                                         Key = key,
                                         Value = new City(key)
                                     }).ToDictionary(el => el.Key, el => el.Value);




            var cityNodes = new List<CityEdge>
                                {
                                    new CityEdge(cities["A"], cities["B"], 5),
                                    new CityEdge(cities["A"], cities["F"], 3),
                                    new CityEdge(cities["B"], cities["C"], 2),
                                    new CityEdge(cities["B"], cities["G"], 3),
                                    new CityEdge(cities["C"], cities["H"], 10),
                                    new CityEdge(cities["C"], cities["D"], 6),
                                    new CityEdge(cities["D"], cities["E"], 3),
                                    new CityEdge(cities["E"], cities["H"], 5),
                                    new CityEdge(cities["E"], cities["F"], 8),
                                    new CityEdge(cities["F"], cities["G"], 7),
                                    new CityEdge(cities["G"], cities["H"], 2),
                                };


            var allCityNodes = cityNodes.Concat(from cn in cityNodes
                             select new CityEdge(cn.To, cn.From, cn.Distance)).ToArray();


            var resultPath = getShortestPath(cities["H"], allCityNodes);


            var pathFromTo = resultPath.EnumerateShortestPathTo(cities["H"], cities["A"]).Reverse();




            Array.ForEach(pathFromTo.ToArray(), myP =>
                                                Console.WriteLine("Přes město {0}, Ujetá vzdálenost: {1}", myP.Current, myP.TotalDistance));

            
            Console.ReadLine();
        }

 

Na začátku metody vygenerujeme objekt Dictionary (proměnná cities), kde klíčem je název města a hodnotou objekt City. Do proměnné cityNodes uložíme hrany grafu (existující silnice mezi městy) s jejich ohodnocenim (kilometry). Silnice ale nevede jen z města A do města B, ale také z města B do A, proto do proměnné allCityNodes uložíme i zpáteční cesty mezi městy. Poté voláme metodu getShortestPath, které předáme výchozí město (zde H), pro které chceme spočítat nejkratší cesty ke všem ostatním městům, a veškeré hrany-silnice. Metoda getShortestPat vrátí nejkratší cesty, nově vytvořené extenzní metodě EnumerateShortestPathTo řekneme, že chceme vypsat nejkratší cestu z města H (první argument) do města A (druhý argument) - metoda vrátí pouze města, přes která musíme jet z města H do města A a my metodou Reverse otočíme jejich pořadí, abychom viděli cestu od H do A a nezačínali koncovým městem A. Výsledek vypíšeme s využitím metody Array.ForEach na konzoli.

Skeleton algoritmu je hotov, čas začít nabalovat extenzní maso (doslova... :-)) a LINQ svaly.

 

Zde je metoda getShortestPath.

private static IEnumerable<GraphPath<A0>> getShortestPath<A0, A1>(A0 startPoint, IEnumerable<A1> nodes)
                                            where A1 : IGraphEdge<A0>
        {

            var initialGraphPath = (from cNode in nodes
                                    where !cNode.From.Equals(startPoint)
                                    select new GraphPath<A0>
                                    {

                                        Current = cNode.From,
                                        Previous = default(A0),
                                        TotalDistance = int.MaxValue

                                    }).Distinct();


            initialGraphPath = (initialGraphPath.Concat(from cNode in nodes
                                                       where cNode.From.Equals(startPoint)
                                                       select new GraphPath<A0>
                                                           {

                                                               Current = cNode.To,
                                                               Previous = startPoint,
                                                               TotalDistance = cNode.Distance

                                                           }));



            return getShortestPathInner(initialGraphPath, new[] { startPoint }, nodes);




        }

V metodě getShortestPath vygenerujeme objekty GraphPath, které po všech výpočtech ponesou nejkratší cestu v grafu. První dotaz vybere nejdříve ze seznamu hran pouze ty hrany, v nichž vlastnost From (Odkud) nepředstavuje počáteční město (where !cNode.From.Equals(startPoint) ). Jediné, co víme, je že vlastnost TotalDistance nového objektu GraphPath po ukončení algoritmu ponese informaci o nejkratší cestě z výchozího města do konkrétního města (vlastnost Current). U těchto uzlů tedy nejkratší vzdálenost neznáme, a proto vlastnost TotalDistance inicializujeme konstantou int.MaxValue, která říká "nejkratší cestu do města Current neznám a ještě ke všemu může být hodně dlouhá" :-). Z jednoho města může vést více silnic, což by vedlo k duplicitním objektům GraphPath, a proto pro každé město ponecháme pouze jeden objekt GraphPath voláním metody Distinct. 

K objektům  GraphPath z prvního dotazu připojíme objekty GraphPath  komplementárním dotazem, který vybere pouze ty hrany, v nichž vlastnost From (Odkud) představuje počáteční město (where !cNode.From.Equals(startPoint) ). Každý objekt GraphPath z druhého dotazu má tedy vlastnost TotalDistance rovnu kilometrům z fixně stanoveného počátečního města (vlastnost Previous - v našem příkladu město H) do města (Vlastnost Current), k němuž vede z H silnice přímo. Pro počáteční město H tedy druhý dotaz vrátí dva objekty GraphPath naplněné takto - {1 - Current=C, Previous=H, TotalDistance=10} {2-Current=E, Previous=H, TotalDistance=5}.

Poté zavoláme metodu getShortestPathInner, které předáme vygenerované objekty GraphPath, seznam objektů, pro něž jsme zjišťovali  nejkratší cestu v grafu (zatím jen počáteční město  - město H), a dříve vytvořený seznam hran.

 

private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
                                                        where A1 : IGraphEdge<A0>
        {
            var candidates = (from node in edges
                              where !processed.Contains(node.From)
                              select node.From).Distinct();

            if (candidates.Count() == 0)
            {
                return initialGraphPath;
            }

            var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);

            var minimumGPath = (from gPath in initialGraphPath
                                where candidates.Contains(gPath.Current) &&
                                      gPath.TotalDistance == minimum
                                select gPath).First();



            var newGraphPath = from cNode in edges
                               where cNode.From.Equals(minimumGPath.Current)
                               select new GraphPath<A0>
                                       {

                                           Current = cNode.To,
                                           Previous = minimumGPath.Current,
                                           TotalDistance = cNode.Distance + minimumGPath.TotalDistance

                                       };



            var newGraphResult =
                                   (initialGraphPath.Concat(newGraphPath).Where(obj =>
                                                            !initialGraphPath.Any(
                                                                                   obj2 => obj2.Current.Equals(obj.Current) &&
                                                                                   (obj2.TotalDistance < obj.TotalDistance)))).ToArray();





            var newProcessed = processed.Union(new[] { minimumGPath.Current });

            return getShortestPathInner(newGraphResult, newProcessed, edges);

        }
    }

V rekurzivní metodě getShortestPathInner je soustředěno jádro Dijkstrova algoritmu. Do proměnné candidates uložíme nejprve veškerá města, která jsme ještě nezpracovali - nezjišťovali jsme, jak daleko je to od nich k jejich přímým sousedům. Do proměnné minimumGPath uložíme objekt GraphPath, který reprezentuje prozatímní nejkratší zjištěnou cestu z výchozího města a jehož vlastnost Current nese město, pro něž jsme výpočet nejkratší cesty ještě neprovedli.

Do proměnné newGraphPath uložíme množinu objektů GraphPath, které jsou sestaveny tak, že jejich předchůdcem (vlastnost Previous) je vždy právě zpracovávané město. Vlastnost To nese město, do něhož se můžeme dostat z města Previous. Vlastnost TotalDistance je inicializována součtem  hodnoty TotalDistance z procházeného objektu minimumGPath (s prozatímní nejkratší cestou) s vzdáleností z města v Previous do města ve vlastnosti Current.

Do proměnné newGraphPathResult jsou uloženy jen prozatím nejkratší cesty - jestliže tedy v předchozím kroku vypočítáme, že z města H se do města D můžeme dostat po ujetí 16 km, ale v proměnné initialGraphPath máme spočítáno, že do města H se můžeme dostat i po ujetí 8 km, je ponechána pouze lepší, tedy pro naše účely kratší cesta.

Poté na konci metody do seznamu již zpracovaných měst přidáme právě zpracované město (minimumGPath.Current) a jdeme na další kolo. Rekurzivně zavoláme metodu getShortestPathInner s vypočítanými mezivýsledky. Rekurze skončí po zpracování všech měst - nejsou  nalezeni další kandidáti na zpracování (viz podmínka if (candidates.Count() == 0 )).

 

Ještě nám zbývá extenzní metoda, která enumeruje přes města z výchozího bodu do cílového. První argument jsou vypočítané nejkratší cesty (objekty GraphPath), argument start je zvolené počáteční město a argument end je koncové město, ke kterému chceme vypsat nejkratší cestu.

static class DijkstraExtensions
    {
        public static IEnumerable<A0> EnumerateShortestPathTo<A0, A1>(this IEnumerable<A0> paths, A1 start, A1 end)
                                   where A0 : GraphPath<A1>
        {
            var pathDictionary = paths.ToDictionary(obj => obj.Current);
            var currentNode = pathDictionary[end];


            while (currentNode != null)
            {
                yield return currentNode;
                if (currentNode.Previous.Equals(start))
                {
                    yield break;
                }
                currentNode = pathDictionary[currentNode.Previous];
            }

        }
    }

 

 

Metoda si vytvoří další objekt Dictionary a postupně vrací veškerá objekty GraphPath na ceste z jednoho města (start) do druhého (end) tak, že v cyklu while z objektu Dictionary vyzvedává město-předchůdce (vlastnost Previous), přes které musíme jet, dokud se nedostaneme do výchozího města.

 

A zde je výsledek - nejkratší cesta z H do A.

 

Dijkstra_Result

Použité algoritmy, LINQ konstrukce a  extenzní metody by šlo určitě vylepšit a vytunit. Nemyslím si a nikdy jsem si narozdíl od některých rozjuchaných "VsechnoSLinqemJeTedCoolTyRetarde" MSDN blogů nemyslel, že LINQ a extenzní metody jsou univerzální kladivo na každý problém universa, včetně precizní analýzy finální odpovědi '42' :-), ale jako příklad, co lze s těmito nástroji dělat, mi Dijkstrův algoritmus přišel zajímavý. :-)

Update 12. 2.:

Petr poptával v komentářích zdrojové kódy. Zde jsou.



Wednesday, 11 February 2009 19:13:14 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  .NET Framework | Compact .Net Framework | LINQ


 Tuesday, 10 February 2009
Dobrovolní testeři pro virtuální GPS (pozice dle cell id)

Někdy v průběhu - na konci března (možná dříve, možná později) bude možné testovat moji aplikaci Virtuální GPS. Tento program zjišťuje pozici dle GSM buňky, ke které jste přihlášení, tedy podobně jako Google Maps Mobile nebo můj GSM Net Monitor. Hlavním rozdílem je to, že virtuální GPS můžete použít (teoreticky) s jakoukoli aplikaci, protože se v systému "tváří" jako běžná GPS. Tuto aplikaci poptával uživatel Pekro už někde v původním tématu o GSM Net Monitoru na ce4you http://www.ce4you.cz/forums/view_topic.asp?t=30365. Aplikaci mám prozatím vyzkoušenou na HTC Kaiser a HTC Touch Pro - spolupracuje bez problémů s TomTom a Google Maps (režim GPS). Pokud budete mít zájem o testování, napište prosím komentář k tomuto spotu nebo mi napište mail na adresu reneZAVINACrenestein.net s předmětem Tester GPS. Pokud by se vás přihlásilo více, vyhrazuji si právo vybrat jen některé z přihlášených testerů. Odměnou je pouze práce s beta verzí (myslím si) zajímavého programu. :-) Aplikace je určena pro zařízení s OS Windows Mobile (5 - 6.x). Předem děkuji. :)

A zde je ukázka TomToma, který pracuje s virtuální GPS.

 


TT2 TT1



Tuesday, 10 February 2009 18:09:09 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  Mobilitky | Ostatní


 Wednesday, 04 February 2009
Textbox nepodporující výběr textu a další specialitky

Někdy se hodí mít textbox, u kterého je skrytý "caret" (netuší někdo, jak se termín caret překládá - pouze kurzor?) a současně nepodporuje označování textu. Také můžete chtít, aby se textbox choval podobně jako při nastavení vlastnosti ReadOnly na true, ale bez "zašedlého" zobrazení textboxu, což je  většinou nechtěný průvodní jev textových polí označených pouze pro čtení.

Kód je pro Compact .Net Framework, nic vám ale nebrání přenést jej na "velký" NF. Ke zpracování Windows zpráv je použita třída NativeWindow, která je součástí OpenNetCF frameworku. Opravdu jen tato podivná sekvence zpracování Windows zpráv byla v CNF ta pravá.

 

using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using OpenNETCF.Windows.Forms;

namespace HideCaret
{
    public class NativeTxtWrapper : NativeWindow
    {
        [DllImport("CoreDll.dll")]
        private static extern bool ShowCaret(IntPtr hWnd);

        [DllImport("CoreDll.dll")]
        private static extern bool HideCaret(IntPtr hWnd);

        private TextBox m_txtBox;
        private bool m_hasFullFocus;
        
        
        private const int WM_LBUTTONDOWN = 0x201;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_MOUSEMOVE = 0x0200;
        private const int WM_CHAR = 0x0102;
        private const int WM_COMMAND = 0x0111;
        private const int WM_LBUTTONUP = 0x0202;
        private const int WM_LBUTTONDBLCLK = 0x0203;
        private const int WM_PAINT = 0x000F;
        private const int WM_KILLFOCUS = 0x0008;
        private const int WM_SETFOCUS  = 0x0007;
        




        




        public NativeTxtWrapper(TextBox txtBox)
        {
            init(txtBox);
            m_hasFullFocus = false;
        }
        private void init(TextBox txtBox)
        {
            if (txtBox == null)
            {
                throw new ArgumentNullException("txtBox");
            }

            if (txtBox.Handle != IntPtr.Zero)
            {

                AssignHandle(txtBox.Handle);
            }

            txtBox.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
            txtBox.HandleDestroyed += ((sender, e) => ReleaseHandle());
            m_txtBox = txtBox;
        }


        protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
        {
            if (m.Msg == WM_CHAR)
            {
                m_hasFullFocus = true;
            }

            if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_LBUTTONDBLCLK)
            {
                return;
            }
            
            if (((m.Msg == WM_LBUTTONUP) ||
                (m.Msg == WM_LBUTTONDOWN)) && m_hasFullFocus)
            {
                return;
            }
            if (m.Msg == WM_SETFOCUS)
            {
                base.WndProc(ref m);
                HideCaret(m_txtBox.Handle);
                return;
                //m_hasFocus = false;    
            }
            
            if (m.Msg == WM_KILLFOCUS)
            {
                m_hasFullFocus = false;
            }

            base.WndProc(ref m);
        }

    }
}

   

Jestliže chcete textbox pouze pro čtení, stačí  nepředat ke zpracování bázové třídě (base.WndProc) zprávu WM_CHAR.

 

Objektem NativeTxtWrapper lze oddekorovat jakýkoli textbox, nebo můžete vytvořit potomka třídy Textbox, který před klienty třídy skryje použití objektu NativeTextWrapper

private void Form1_Load(object sender, EventArgs e)
        {
            m_wrapper = new NativeTxtWrapper(textBox1);
            textBox1.Focus();
            
        }

Podobnou třídu mám i pro nativní projekty psané v MFC, možná se někomu z vás bude hodit. Přepsat kód do Windows API z MFC je také trivální.

 

#pragma once

#include "afxwin.h"

 

class CTextBoxEx :

    public CEdit

{

 

private:

    bool m_hasFullFocus;

 

public:

 

    CTextBoxEx(void);

    ~CTextBoxEx(void);

    virtual LRESULT WindowProc(UINT message, WPARAM wparam, LPARAM lparam);

 

};

#include "StdAfx.h"

#include "TextBoxEx.h"

 

 

 

CTextBoxEx::CTextBoxEx(void) : m_hasFullFocus(false)

{

}

 

CTextBoxEx::~CTextBoxEx(void)

{

}

 

LRESULT CTextBoxEx::WindowProc(UINT message, WPARAM wparam, LPARAM lparam)

{

 

//Readonly textbox

            if (message == WM_CHAR)

            {

                return 1;

            }

//end Readonly textbox

 

            POINT mousePoint;

            GetCursorPos(&mousePoint);

            ScreenToClient(&mousePoint);

            RECT clientRect;

            GetClientRect(&clientRect);

            BOOL isMouseInRect = PtInRect(&clientRect, mousePoint);

 

            if (((message == WM_MOUSEMOVE) || (message == WM_LBUTTONDBLCLK)) && isMouseInRect)

            {

                return 1;

            }

 

            if (((message == WM_LBUTTONUP) ||

                (message== WM_LBUTTONDOWN)) && m_hasFullFocus && isMouseInRect)

            {

                return 1;

            }

 

            if (message == WM_SETFOCUS)

            {              

                m_hasFullFocus = true;

                HideCaret();

                return 1;

 

            }

 

            if (message == WM_KILLFOCUS)

            {

                m_hasFullFocus = false;

            }

 

            return CEdit::WindowProc(message, wparam, lparam);

 

}



Wednesday, 04 February 2009 16:52:26 (Central Europe Standard Time, UTC+01:00)       
Comments [6]  .NET Framework | Compact .Net Framework | Nativní kód | Windows Forms


 Monday, 02 February 2009
Lehká imitace některých rysů windows forms aplikací v non-windows forms aplikacích

Omluvte prosím trochu kryptický název, ale lepší a hlavně výstižnější pojmenování článku mě nenapadlo. Název je stejně jen vábnička na čtenáře, proto se podívejme, co je jím míněno.

Již několikrát mně různí vývojáři tvrdili, jak nepříjemná je pro ně práce s konzolí (windows službou, dosaďte další typy aplikací dle libosti...), protože musí řešit, aby aplikace po svém spuštění ihned neskončila, a také je pro ně problematické zajistit, aby byly některé události zpracovány vždy ve stejném threadu.

Převedeme-li emocionální stížnost do věcného jazyka, zjistíme, že to, co v těchto typech aplikací chybí, jsou následující rysy běžné windows forms aplikace:

  1. Windows aplikace spustí smyčku Windows zpráv (message loop) a vývojář pouze obsluhuje události formuláře. V (Compact) .Net Frameworku nám stačí zavolat Application.Run(new Form1()) a aplikace neukončí svůj běh, dokud není uzavřen poslední formulář nebo dokud ti drsnější z nás nezavolají Application.Exit. O životní cyklus aplikace, její spuštění a ukončení, se většinou nemusíme nijak starat.
  2. Při obsluze formuláře máme po volání metody Invoke garantováno, že předaný delegát bude vykonán v takzvaném UI threadu. Hlavním účelem metody Invoke (a sesterských metod BeginInvoke a EndInvoke) je threadově bezpečná komunikace s ovládacími prvky. Ovládací prvky ve stylu windows prvků v konzolových aplikacích (windows službách) nenajdeme, ale přesto bychom i v těchto typech aplikací občas chtěli mít nástroj, který garantuje, že všechny  nebo vybrané události budou zpracovány v jednom výkonném threadu.

 

V tomto článku se objeví návrh, který pro non-windows forms aplikace přinese výše zmíněné rysy a přidá pár věcí navíc.

Pár vysvětlujících poznámek na úvod . Kód (přesněji řečeno draft k dalšímu rozpracování), který za chvíli uvidíte, má běžet na .Net frameworku a na Compact .Net Frameworku. Vím, že existují synchronizační kontexty pro thready, ale metodu Invoke jsem zmiňoval proto, že představuje společný jmenovatel pro obě prostředí, protože Compact .Net Framework je stále tím strýčkem - beznadějným sociálním případem, co nám nikdy nepřiveze žádné úhledně zabalené dárky, v nichž se skrývá třeba nádherná vlastnost SynchronizationContext.Current. Se znalostí tohoto omezení je také jasné, proč jsem nepoužil i další metody/vlastnosti dostupné jen ve "velkém" .Net Frameworku.

Dále v kódu jsou třídy obsahující ve svém názvu slovo *Console*. Nenechte se zmást, že mluvím dále jen o konzolových aplikacích, stejné třídy lze použít ve windows službě a dalších typech aplikací.

Zaveďme si nejdříve abstraktní třídu ConsoleTask, která je předkem všech zpracovávaných úloh v aplikaci. Zjednodušeně si můžeme třídu ConsoleTask a její potomky představit jako výchozí stavební prvky zapouzdřující chování analogické k vybraným a pro nás zajímavým rysům windows formulářů.

    /// <summary>
    /// Základní rozhraní pro položky zpracovávané v jedné frontě
    /// </summary>
    internal interface IExecuteWorkItem
    {
        /// <summary>
        /// Implementace metody spustí úlohu
        /// </summary>
        void Execute();
    }

    /// <summary>
    /// Bázová třída pro všechny úlohy
    /// </summary>
    abstract class ConsoleTask : IDisposable
    {
        #region Inner classes
        /// <summary>
        /// Výchozí  implementace rozhraní <see cref="IExecuteWorkItem"/>
        /// </summary>
        private class WorkThreadItem : IExecuteWorkItem
        {
            #region Private variables
            private Delegate m_del;
            private  object[] m_vals;
            #endregion Private variables


            /// <summary>
            /// Konstruktor
            /// </summary>
            /// <param name="del">Delegát, který má být spuštěn ve frontě nadřazeného objektu <see cref="ConsoleTask"/></param>
            /// <param name="vals">Argumenty delegáta</param>
            public WorkThreadItem(Delegate del, params object[] vals)
            {
                if (del == null)
                {
                    throw new ArgumentNullException("del");
                }

                m_del = del;
                m_vals = vals;

            }

            /// <summary>
            /// Metoda iniciuje vykonání předaného delegáta
            /// </summary>
            public virtual void Execute()
            {
                m_del.Method.Invoke(m_del.Target, m_vals);
            }
        }
        #endregion Inner classes


        #region private variables
        private ManualResetEvent m_event;
        private Thread m_innerWorkingThread;
        private Queue<IExecuteWorkItem> m_workQueue;
        private AutoResetEvent m_workingThreadEvent;
        private object m_lockQueueRoot;
        private bool m_continue;
        private bool m_disposed;
        #endregion private variables

        #region constructors
        /// <summary>
        /// Konstruktor
        /// </summary>
        protected ConsoleTask()
        {
            m_lockQueueRoot = new object();
            m_workQueue = new Queue<IExecuteWorkItem>();
            m_event = new ManualResetEvent(false);
            m_workingThreadEvent = new AutoResetEvent(false);
            m_innerWorkingThread = new Thread(processWorkerThread);
            m_continue = true;
            m_disposed = false;
        }

        #endregion constructors

        #region Properties
        /// <summary>
        ///<see cref="WaitHandle"/> běžící úlohy
        /// </summary>
        public WaitHandle TaskWaitHandle
        {
            get
            {
                if (m_disposed)
                {
                    throw new ObjectDisposedException("ConsoleTask");
                }

                return m_event;
            }
        }

        

        /// <summary>
        /// Metoda vrátí true, jestliže volající thread je odlišný od threadu, který vyřizuje položky zpracovávané v jedné frontě
        /// </summary>
        public virtual bool InvokeRequired
        {
            get
            {
                if (m_disposed)
                {
                    throw new ObjectDisposedException("ConsoleTask");
                }

                if (SlaveWorkingTask != null)
                {
                    return SlaveWorkingTask.InvokeRequired;
                }
            
                return (Thread.CurrentThread.ManagedThreadId != m_innerWorkingThread.ManagedThreadId);

            }

        }

        /// <summary>
        /// Volitelná instance <see cref="ConsoleTask"/>, která převezme odpovědnost za vyřizování položek zpracovávaných v jedné frontě
        /// </summary>
        public ConsoleTask SlaveWorkingTask
        {
            get;
            set;
        }
        #endregion Properties


        #region Methods
        /// <summary>
        /// Metoda garantuje, že dojde k vykonání předaného delegáta v threadu, ktrerý vyřizuje položky zpracovávané v jedné frontě
        /// </summary>
        /// <remarks>Metoda pouze zařadí položky ke zpracování a nečeká na výsledek volání delegáta. </remarks>
        public virtual void Invoke(Delegate del, params object[] vals)
        {
            if (m_disposed)
            {
                throw new ObjectDisposedException("ConsoleTask");
            }
            
            if (SlaveWorkingTask != null)
            {
                SlaveWorkingTask.Invoke(del, vals);
                return;
            }
            
            lock (m_lockQueueRoot)
            {
                m_workQueue.Enqueue(new WorkThreadItem(del, vals));
                m_workingThreadEvent.Set();
            }
        }

        /// <summary>
        /// Metoda spustí úlohu 
        /// </summary>
        /// <remarks>Spuštěním úlohy se rozumí spuštění kódu v přepsané metodě <see cref="DoInternalRun"/> v samostatném threadu. Metoda Run nevrátí řízení, dokud není úloha dokončena.</remarks>
        public void Run()
        {
            if (m_disposed)
            {
                throw new ObjectDisposedException("ConsoleTask");
            }
            
            m_innerWorkingThread.Start();
            ThreadPool.QueueUserWorkItem(obj => DoInternalRun());
            m_event.WaitOne();
        }

        
        /// <summary>
        /// Metoda ukončí úlohu
        /// </summary>
        public virtual void CloseTask()
        {
            m_continue = false;
            m_workingThreadEvent.Set();            
            m_event.Set();
        }
        
        /// <summary>
        /// Metoda pro explicitní uvolnění veškerých nepoužívaných zdrojů  - součást implementace "Disposable" vzoru
        /// </summary>
        public void Dispose()
        {
            if (m_disposed)
            {
                return;
            }
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// "Destruktor" - součást implementace "Disposable" vzoru
        /// </summary>
        ~ConsoleTask()
        {
            Dispose(false);
        }

        /// <summary>
        /// Interní implementace "Disposable" vzoru
        /// </summary>
        /// <param name="disposing">true - jestliže je metoda volána z metody Dispose, false, pokud je volána z destruktoru - metody Finalize</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    ((IDisposable)m_workingThreadEvent).Dispose();
                    ((IDisposable)m_event).Dispose();                    
                     m_disposed = true;
                }
                catch (Exception e)
                {
                    
                    Trace.WriteLine(e);
                }
                
            }
        }

        /// <summary>
        /// Metoda, která musí být přepsána v odvozených třídách a která obsahuje logiku specifickou pro každou úlohu
        /// </summary>
        protected abstract void DoInternalRun();

        /// <summary>
        /// Obsluha fronty položek, které mají být zpracovány ve stejném threadu
        /// </summary>
        private void processWorkerThread()
        {

            const int EXPECTED_MINIMUM_ITEMS = 1;

            while (m_continue)
            {
                m_workingThreadEvent.WaitOne();
                if (!m_continue)
                {
                    continue;
                }

                int m_count = EXPECTED_MINIMUM_ITEMS;
                IExecuteWorkItem nextItem = null;

                while (m_count > 0)
                {
                    lock (m_lockQueueRoot)
                    {
                        m_count = m_workQueue.Count();

                        if (m_count != 0)
                        {
                            nextItem = m_workQueue.Dequeue();
                        }
                    }


                    try
                    {
                        if (nextItem != null)
                        {
                            nextItem.Execute();
                        }
                            
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(ex);
                    }

                    m_count--;
                }


            }
        }

        #endregion Methods
    }

 

Abstraktní třída ConsoleTask obsahuje ve svém veřejném rozhraní metodu Run, kterou spustíme úlohu. Metoda Run je šablonovou metodou (Template method), protože obsahuje závazný scénář pro veškeré odvozené úlohy. Potomci třídy ConsoleTask do scénáře vstupují na přesně vymezeném místě - v metodě DoInternalRun, která je deklarována jako abstraktní a všechny konkrétní odvozené třídy ji musí přepsat a doplnit vlastní logiku. Třída ConsoleTask tedy garantuje, že je vždy nejprve spuštěn thread vyřizující požadavky, které mají být vykonány ve stejném threadu (podrobný popis viz níže), poté je třída ThreadPool použita ke spuštění kódu v metodě DoInternalRun v jiném threadu a nakonec aktuální thread pozastavíme čekáním na signalizaci instance synchronizačního objektu ManualResetEvent (proměnná m_event). Ve vlastnosti TaskWaitHandle vydáváme stejný objekt ManualResetEvent, který může jiný thread využít k synchronizaci svého běhu s instancí třídy odvozené od třídy ConsoleTask. Tím simulujeme pro uživatele objektů odvozených z třídy ConsoleTask spuštění smyčky zpráv, protože aplikace není ukončena po zavolání metody Run. Za ukončení běhu úlohy zodpovídá metoda CloseTask - metoda uvolní pracovní thread vyřizující frontu požadavků nastavením proměnné m_continue na false a signalizací synchronizační primitivy workingThreadEvent. Dále metoda CloseTask přes signalizaci synchronizačního objektu v proměnné m_event informuje o dokončení celé úlohy - thread pozastavený v metodě Run bude uvolněn.

Třída ConsoleTask dále obsahuje definici privátní třídy WorkThreadItem, která implementuje rozhraní IExecuteWorkItem a má roli adaptéru. Instance třídy WorkThreadItem jsou jednotlivé položky, které mají být vykonány v jednom pracovním threadu. Adaptérem je třída WorkThreadItem proto, že převádí rozhraní jakéhokoli předaného delegáta na rozhraní IExecuteWorkItem. Po volání metody Execute objektu WorkThreadItem je vykonána metoda, na kterou ukazuje delegát.

Jméno vlastnosti InvokeRequired by mělo znít povědomě - metoda vrátí true, jestliže thread, který zjišťuje hodnotu vlastnosti , je odlišný od threadu, který zpracovává položky typu IExecuteWorkItem. Thread poté může použít metodu Invoke, která zajistí, že předaný delegát bude vykonán v pracovním threadu. Je to zmíněno i v dokumentaci metody Invoke, ale zde zdůrazním, že metoda Invoke zařadí pouze novou položku do fronty ke zpracování a dá signál pracovnímu threadu, že je dostupná další položka voláním metody Set na proměnné m_workingThreadEvent, což je instance synchronizační primitivy AutoResetEvent. Metoda Invoke nečeká na výsledek volání delegáta a ani není zaručeno, že po návratu z metody Invoke byl již předaný delegát vykonán. Samotná obsluha fronty položek, které mají být vykonány v jednom threadu, je soustředěna do metody processWorkerThread.

U metody  Invoke a vlastnosti InvokeRequired si můžete všimnout podmíněné delegace volání na instanci ve vlastnosti SlaveWorkingTask. Jestliže vlastnost SlaveWorkingTask není null, je odpovědnost za zpracování položek přenesena na jinou instanci třídy ConsoleTask. Jednotlivé tasky mohou tvořit zárodečný řetězec odpovědnosti (Chain of responsibility) a za chvíli uvidíme, k čemu můžeme toto předávání odpovědnosti na jiné instance ConsoleTask využít.

Třída ConsoleTask také implementuje běžný .Net "Disposable" vzor pro uvolňování prostředků (rozhraní IDisposable, chráněná metoda Dispose a destruktor - metoda Finalize).

Mimikry konzolové aplikace, která se v rámci námi vykolíkovaného seznamu požadavků snaží vydávat za windows forms aplikaci, vylepšíme zavedením jednoduché fasády (vzor facade), která bude simulovat metodu Application.Run.

 

    /// <summary>
    /// Facade s rozhraním pro spuštění úkolu
    /// </summary>
    class ConsoleApplication
    {
        /// <summary>
        /// Metoda spustí předaný úkol (Fasáda ke spuštění úloh napodobující známou metodu Application.Run z Windows Forms aplikací)
        /// </summary>
        /// <param name="task">Úkol, který má být spuštěn</param>
        public static void Run(ConsoleTask task)
        {
            if (task == null)
            {
                    throw new ArgumentNullException("task");
            }
            
            task.Run();
        }
    }

Jak vidno,  metoda Run zcela deleguje vykonání na předaný ConsoleTask.

Jak se prozatím s naším modelem pracuje? Nejlepší bude napsat si  potomka třídy ConsoleTask  a zjistit to.  Zkusme vytvořit úlohu, která na Compact .Net Frameworku zpracuje příchozí SMS.

 

    /// <summary>
    /// Třída pro zpracování přijatých SMS
    /// </summary>
    class SMSTask : ConsoleTask
    {
        #region private variables
        private MessageInterceptor m_interceptor;
        #endregion private variables

        #region Methods
        /// <summary>
        /// Metoda začne sledovat SMS
        /// </summary>
        protected override void DoInternalRun()
        {
            m_interceptor = new MessageInterceptor(InterceptionAction.Notify, false);
            ThreadPool.QueueUserWorkItem(
                (obj) =>
                        m_interceptor.MessageReceived += m_interceptor_MessageReceived);

            
            
        }

        /// <summary>
        /// Obslužná metoda uálosti <see cref="MessageInterceptor.MessageReceived"/>
        /// </summary>
        /// <param name="sender">Odesílatel události</param>
        /// <param name="e">Argument události</param>
        private void m_interceptor_MessageReceived(object sender, MessageInterceptorEventArgs e)
        {

            if (InvokeRequired)
            {
                Invoke((Action<SmsMessage>)(handleMessage), e.Message as SmsMessage);
            }

            else

            {

                  handleMessage(e.Message as SmsMessage);

            }

} public override void CloseTask() { m_interceptor.Dispose(); base.CloseTask(); } /// <summary> /// Zpracování SMS /// </summary> /// <param name="message">SMS zpráva ke zpracování</param> private void handleMessage(SmsMessage message) { Console.WriteLine(message.Body); CloseTask(); } #endregion methods }

Autor tříd odvozených z bázové třídy ConsoleTask má lehkou práci, protože se soustředí jen na úkol (příjem SMS) a ne na to, že jeho kód bude vykonán v konzolové aplikaci. V přepsané metodě si přihlásíme odběr události MessageReceived - zde je událost přihlášena přes ThreadPool, ale není to nutné. Obslužná metoda události MessageReceived (m_interceptor_MessageReceived)  po příjmu SMS zaručí, že SMS budou vždy zpracovány ve stejném pracovním vlákně použitím vlastnosti Invoke Required a Invoke. Jestliže je událost vyvolána v jiném než pracovním threadu obsluhujícím frontu položek ke zpracování, zavoláme metodu Invoke, které předáme delegáta ukazujícího na metodu handleMessage. K vytvoření delegáta jsme použili standardního generického delegáta Action<T>, kde jsme za generický parametr T dosadili třídu SmsMessage, jejíž instanci přijímá jako argument metoda handleMessage. Přepsali jsme i metodu CloseTask, která uvolní interceptora pro příjem zpráv a poté vyvolá implementaci metody CloseTask z bázové třídy. Zde je úloha ukončena po příjmu první zprávy voláním CloseTask z metody handleMessage, ale způsob ukončení úlohy je zcela v rukou vývojáře konkrétní úlohy.

Poznámka na okraj: U naší třídy SMSTask by bylo vhodné, když chceme přijmout jen jednu SMS, ihned si odhlásit odběr dalších zpráv, nebo si v interní proměnné nastavit, že již zpráva byla přijata a další zprávy nepředávat ke zpracování.

Novou úlohu spustíme tímto nezáludným a pro vývojáře windows forms aplikací povědomým kódem:

 

    class Program
    {
        static void Main(string[] args)
        {
            SMSTask smsTask = new SMSTask();
            ConsoleApplication.Run(smsTask);
        }
    }

Na vývojářské práci je nejlepší, že poté, co máte nějaký nosný nápad, můžete jej rozvíjet ad libitum. Co když chceme ve stejné aplikaci nejen přijímat SMS, ale také reagovat na události v objektu, který nás informuje o spuštěných aplikacích uživatele. Nebo chceme sledovat přes třídu SystemState informace o příchozích hovorech? Napráskat vše do jedné instance potomka třídy ConsoleTask "ResimVzdyckyVsechnoNaJednomMisteAJsemTotalneVPohodeVoe" je sice řešením, ale i jen laxním zastáncům vágně formulovaného principu jedné odpovědnosti třídy (zdravím Aleši :) ) se právě teď nasucho aktivoval podmíněný reflex, protože vědí, že při správě takové aplikace po kolegovi-pohodářovi je vztekem podmíněné zoufalecké uslintávání a hlasité nadávání to nejmenší. :-)

Chceme určitě zachovat stávající strukturu aplikace, chceme spouštět libovolné množství různorodých úloh a navíc chceme mít možnost zpracovávat položky napříč jednotlivými úlohami ve stejném threadu - pracovní frontě. Úkol jako stvořený pro jednu z možných nenásilných inkarnací návrhového vzoru Composite v aplikaci.

 

    /// <summary>
    /// Třída reprezentující kompozitní úlohu - viz návrhový vzor Composite
    /// </summary>
    class CompositeTask : ConsoleTask
    {
        #region private variables
        private ICollection<ConsoleTask> m_tasks;
        #endregion private variables

        #region Constructors
        public CompositeTask(ICollection<ConsoleTask> tasks)
        {
            if(tasks == null)
            {
                throw new ArgumentNullException("tasks");
            }
            if (tasks.Count == 0)
            {
                throw new ArgumentException("One or more tasks are required");
            }
            m_tasks = tasks;
        }
        #endregion Constructors

        #region Methods
        /// <summary>
        /// Spuštění všech úkolů
        /// </summary>
        protected override void DoInternalRun()
        {
            foreach (var task in m_tasks)
            {
                ConsoleTask task1 = task;
                task1.SlaveWorkingTask = this;
                ThreadPool.QueueUserWorkItem((obj) => task1.Run());
                
            }
        }
        /// <summary>
        /// Metoda ukončí všechny úkoly
        /// </summary>
        /// <remarks>Metoda pouze zavolá metodu CloseTask na všech předaných objektech <see cref="ConsoleTask"/>, ale nestará se o výsledek volání</remarks>
        public override void CloseTask()
        {
            foreach (var task in m_tasks)
            {
                task.CloseTask();
            }
            
            base.CloseTask();
        }
        #endregion Methods
    }

Metoda CompositeTask je také potomkem třídy ConsoleTask, a proto můžeme ve zbytku aplikace pracovat se stejným rozhraním, na které jsme zvyklí. Jednoduchá i složená úloha mají stejné rozhraní, takže si klient tříd nemusí být skládání úloh vědom, což je mimochodem jedna z hlavních motivací pro zavedení návrhového vzoru Composite. V konstruktoru očekáváme odkaz na kolekci dceřiných úkolů. V metodě DoInternalRun zavoláme v cyklu metodu Run všech předaných úkolů. Ještě před voláním metody Run  ale nastavíme u každé úlohy vlastnost SlaveWorkingTask na aktuální objekt CompositeTask, což nám zaručí, že veškeré položky ze všech jednotlivých úloh vložené do pracovní fronty voláním metody Invoke budou zpracovány v jediném pracovním vlákně CompositeTask. Zde vidíme jeden z důvodů, proč máme vlastnost SlaveWorkingTask a proč třída ConsoleTask ve členech Invoke a InvokeRequired nejprve kontroluje, jestli má zpracovat požadavek ve své pracovní frontě, anebo existuje jiný vhodný objekt  - "otrok" (SlaveWorkingTask), který se o položky postará sám. Metoda CloseTask opět nejprve zavolá metodu CloseTask na všech objektech ConsoleTask, ze kterých je aktuální instance třídy CompositeTask složena.

Opět poznámka: Snad si rozumíme v tom, že navržená třída CompositeTask není jediná možná. Jiná třída CompositeTask2 nemusí přesměrovávat pracovní frontu na sebe, další po uzavření úloh nejprve vyčká na ukončení všech dceřiných úloh. Další scénáře jistě nalezne laskavý čtenář sám. :-)

 

Než třídu CompositeTask vyzkoušíme, vytvoříme si dalšího potomka Consoletask, který bude zpracovávat pravidelně vyvolávanou událost z našeho objektu.

Zde je jednoduchá "demo" třída, jejíž srdce tiká v rytmu události AliveEvent.

 

 

class MyEventClass
    {
        public event EventHandler<EventArgs> AliveEvent;
        private bool m_continue;
        private const int INTERVAL = 1000;
        private object m_lockObj;

        public MyEventClass()
        {
            m_continue = true;
            m_lockObj = new object();
        }

        
        public void Start()
        {
            ThreadPool.QueueUserWorkItem((state) =>
                                             {
                                                 while (m_continue)
                                                 {
                                                     Thread.Sleep(INTERVAL);
                                                     AliveEvent(this, new EventArgs());
                                                 }
                                                     
                                             });
                                            
            
            
        }        

        public void Stop()
        {
            lock (m_lockObj)
            {
                m_continue = false;    
            }
            
        }
        protected void OnAliveEevent(EventArgs e)
        {
            if (AliveEvent != null)
            {
                AliveEvent(this, e);
            }
        }
    }

 

Naše nová konkrétní úloha zpracovává události instance MyEventClass

 

 

    class ConcreteTask : ConsoleTask
    {

        private const int HEART_BEAT_LIMIT = 10;
        
        private MyEventClass m_evClass;
        private int m_heartBeatcount;
        private bool m_processEvent;


        protected override void DoInternalRun()
        {
            m_evClass = new MyEventClass();
            m_heartBeatcount = 0;
            m_processEvent = true;

            m_evClass.Start();
            m_evClass.AliveEvent += evClass_AliveEvent;

             
        }

        private void evClass_AliveEvent(object sender, EventArgs e)
        {
            
            Action myAction = (Action) (

                                           () =>
                                               {
                                               
                                                        if (!m_processEvent)
                                                        {
                                                            return;
                                                        }
                                                    

                                               
                                                   Console.WriteLine("Event fired");

                                                   m_heartBeatcount++;

                                                   if (m_heartBeatcount >= HEART_BEAT_LIMIT)
                                                   {
                                                       m_evClass.Stop();
                                                       m_processEvent = false;
                                                       CloseTask();
                                                   }
                                               }
                                       );
            if (InvokeRequired)
            {
                Invoke(myAction);

            }
            else
            {

                myAction();
            }            

        }
        
    }

Jenom pro zajímavost je ukázáno, že metodě Invoke můžeme předat složenou (statement) lambdu (nebo anonymní metodu).

 

Spuštění více úloh pomocí třídy CompositeTask není odlišné  od spuštění jedné úlohy.

 

class Program
    {
        static void Main(string[] args)
        {
            var compTask = new CompositeTask(new List<ConsoleTask>
                                                 {
                                                     new SMSTask(),
                                                     new ConcreteTask()
                                                 }); 

            ConsoleApplication.Run(compTask);
        }
    }

Znovu opakuji, že článek měl za cíl ukázat, jak transponovat do jiného aplikačního rámce postupy, které Windows Forms vývojáři dobře ovládají a o kterých mi tvrdili, že jsou "přirozené". Další rozpracování těchto draftů napsaných v půlnoční chvilce nespavosti je už jen variací předvedených postupů. ;-)



Monday, 02 February 2009 14:51:15 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms


 Monday, 12 January 2009
Odhlášení uživatele z aplikace po uplynutí nastavené doby

Jedním z požadavků na Pocket PC (Windows Mobile ) aplikace je  odhlášení uživatele po uplynutí stanovené doby, kdy s aplikací nepracoval, aby se zmenšilo riziko, že uživatel někde PDA položí, někdo jiný PDA  najde a ochutná z gurmánského menu  položku "co nám dnes servírují za citlivé údaje" nebo  se vrhne na žertovné a bezrizikové mazání dat na serveru pod identitou nenáviděného kolegy. Tedy alespoň nějak takto si představuji důvody, kvůli kterým zákazníci na automatickém odhlašování tak lpějí. :)

Jestliže jste někdy zkusili automatické odhlašování napsat, víte, že kvůli podivnému chování modálních dialogů ve Windows Mobile se nejedná o příjemný úkol. Mezi podivné chování modálních dialogu počítám to, že ve správci procesů vždy vidíte všechny  formuláře, ne pouze poslední otevřený, a navíc např. třída v Symbol SDK pro práci se čtečkou čárových kódů při zobrazení více modálních formulářů tiše a nehrdinsky zhyne.

Podívejme se na jedno z možných řešení.

Všechny formuláře v aplikaci budou skryté za rozhraním IStackForm.

    public interface IStackForm
    {
        void BeginShowErrors();
        void EndShowErrors();        
        void CloseForm();
        void NotifyInactivityTimeout();
        void SetLastTitle();
        void ResetTitle();        
    }

Metoda BeginShowErrors notifikuje formulář, že je v aplikaci aktivním formulářem, který má zobrazovat případné uživatelské chyby. Metoda EndShowErrors sděluje formuláři, že nyní nemá na nastalé chyby sám reagovat. Jak uvidíme, metoda EndShowErrors se nám bude hodit proto, abychom potlačili chybová hlášení při odhlašování uživatele a místo odhlášení tak nezůstali v nějakém, nyní pro uživatele zcela nezajímavém, dialogu.

Metoda CloseForm uzavře formulář. Metoda NotifyInactivityTimeout slouží k informování formuláře, že uživatel bude odhlášen z aplikace, a formulář může uložit změny nebo uvolnit používané prostředky .

Metoda Reset Title přikazuje formuláři, aby nastavil svůj titulek (vlastnost Text) na prázný řetězec. Vlastnost SetlastTitle nutí formulář, aby obnovil svůj titulek na hodnotu, kterou měl  před voláním metody ResetTitle. Tyto na první (a možná i na druhý) pohled podivné kejkle slouží k simulaci modálního formuláře - přesněji řečeno, využívám toho, že ve správci úloh se zobrazí pouze formuláře s nastaveným titulkem. Jestliže máte v aplikaci pouze jeden formulář s neprázdným titulkem, tak jen tento formulář je zobrazen ve správci úloh a další formuláře s prázdným titulkem nejsou viditelné a ani běžným způsobem pro uživatele dostupné.

 

Takto může vypadat bázová třída FormBase (Layer Supertype pro UI) s výchozí implementací rozhraní IStackForm.

    public class FormBase : Form, IStackForm
    {
        [DllImport("coredll", EntryPoint = "SetForegroundWindow")]
        private static extern bool SetForegroundWindow(IntPtr hWnd); 

        #region protected variables
        protected bool m_InactivityTimeout;
        protected bool m_CloseRequired;
        protected string m_LastTitle;
        protected FormNativeWrapper m_NativeWrapper;
        protected bool m_InError;
        #endregion protected  variables
        #region IStackForm Members
        public  delegate void MyInvokeDelegate();
        public FormBase()
        {
            InitializeComponent();
            m_LastTitle = String.Empty;
            KeyPreview = true;
            
            m_NativeWrapper = new FormNativeWrapper((Form)this);
        }

        private static void updateLastUpdate()
        {
            FormNativeWrapper.LastFormUpdate = DateTime.Now;
        }

        public virtual void BeginShowErrors()
        {
            ErrorInfo.NewError += new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
        }

        void ErrorInfo_NewError(object sender, ErrorInfoEventArgs e)
        {
            if (InvokeRequired)
            {
                Invoke((EventHandler<ErrorInfoEventArgs>)ErrorInfo_NewError, sender, e);
                return;
            }

            m_InError = true;
            MessageBox.Show(e.Error.Description,
                             Resource1.General_AppCaption,
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Exclamation,
                             MessageBoxDefaultButton.Button1);
        }

        public virtual void ClearErrorFlag()
        {
            m_InError = false;
            ErrorInfo.ClearErrors();
        }
        public virtual void EndShowErrors()
        {
            ErrorInfo.NewError -= new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
        }

        public virtual void CloseForm()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)CloseForm);
                return;
            }

            Close();
        }

        public virtual void NotifyInactivityTimeout()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)NotifyInactivityTimeout);
                return;
            }
            
            m_InactivityTimeout = true;
            ResetTitle();

            try
            {
                if (User.CurrentUser != null)
                {

                    User.CurrentUser.Logout();
                    Session.Current.EndSession();
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
            
            //CloseForm();
        }

        public virtual void SetLastTitle()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)SetLastTitle);
                return;
            }
            
            Text = m_LastTitle;
            SetForegroundWindow(Handle);
        }

        public virtual void ResetTitle()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)ResetTitle);
                return;
            }
          m_LastTitle = Text;
          Text = String.Empty;
        }
        
        
         
        protected override void OnClosed(EventArgs e)
        {


            if (!m_InactivityTimeout)
            {
                FormStack.Instance.Pop();
            }

            base.OnClosed(e);
        }

        private void FormBase_Load(object sender, EventArgs e)
        {
            FormStack.Instance.Push(this);
            updateLastUpdate();
        }

    }

Kvůli souvislostem jsem v kódu ponechal i ukázku implementace metod BeginShowErrors a EndShowErrors. Již na úrovni FormBase zajistíme, že chyby jsou uživateli zobrazovány vždy tak, aby rodičem dialogu s chybovým hlášením byl aktuální formulář. Jak jsem už zmínil, metody SetLastTitle a ResetTitle pouze pracují s titulkem formuláře. U každé metody zajistíme, že je volána v UI threadu  - proto se na počátku metod objevuje vždy kontrola vlastnosti InvokeRequired a jestliže je metoda spuštěna v jiném threadu, metodou Invoke si vynutíme opětovné a nyní již bezpečné volání stejné metody v UI threadu.

Pro podporu automatického odhlášení uživatele jsou pro nás důležité tyté části kódu:

  1. Ihned v konstruktoru je každý formulář oředáh instanci třídy FormNativeWrapper.
    m_NativeWrapper = new FormNativeWrapper((Form)this);
    Třídu NativeWrapper uvidíme za chvíli, zde jen řeknu, že FormNativeWrapper sleduje vstupy od uživatele (stisknutí tlačítka, zadání textu...) a zajišťuje, že kdykoli uživatel pracuje s nějakým prvkem na formuláři, uloží se aktuální datum a  čas do proměnné, která nese informaci, kdy naposledy uživatel pracoval s aplikací. Chceme sledovat aktivity uživatele a proto si požádáme o zasílání všech stisknutých kláves (KeyPreview = true;).
  2. Při nahrání formuláře (FormBase_Load) je každý formulář vložen do zásobníku (LIFO kontajner) používaných formulářů.
    FormStack.Instance.Push(this);
    Kód třídy FormStack projdeme za chvíli. Současně také pomocí metody updateLastUpdate(); při nahrání formuláře uložíme informaci, že uživatel je aktivní. Metoda updateLastUpdate uloží aktuální datum a čas do statické vlastnosti FormNativeWrapper.LastFormUpdate.
  3. Při zavření formuláře (metoda OnClosed) vyjmeme formulář ze zásobníku, ale jen tehdy, jestliže je formulář zavřen uživatelem - pokud je formulář zavřen aplikací, protože je právě odhlašován uživatel, již se zásobníkem formulářů nepracujeme.
  4. V metodě NotifyInactivityTimeout, která je vyvolána vždy, když má být uživatel automaticky odhlášen, na úrovni předka zajistíme odhlášení uživatele voláním metody User.CurrentUser.Logout(); . V kódu jsem ponechal také řádky, které ukazují, že zde ukončíme například "session" uživatele a  uložíme změny.

 

Následuje třída FormNativeWrapper.

public class FormNativeWrapper : NativeWindow
    {
        private const int  WM_LBUTTONDOWN = 0x201;        
        private const int WM_KEYDOWN  = 0x0100;
        private const int WM_MOUSEMOVE = 0x0200;
        private const int WM_CHAR = 0x0102;
        private const int WM_COMMAND = 0x0111;


        private static DateTime m_lastUpdate;
        private static Object _lockObj = new object();

        public static DateTime LastFormUpdate
        {
            get
            {
                lock (_lockObj)
                {
                    return m_lastUpdate;
                }
            }
            set
            {
                lock(_lockObj)
                {
                    
                    m_lastUpdate = value;
                }
            }
        }
        public FormNativeWrapper(Form form)
        {            
            init(form);
        }

        public FormNativeWrapper(IStackForm form)
        {
            init(form as Form);
        }

        private void init(Form form)
        {
            if (form == null)
            {
                throw new ArgumentNullException("form");
            }

            if (form.Handle != IntPtr.Zero)
            {
                AssignHandle(form.Handle);
            }
            
            form.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
            form.HandleDestroyed += ((sender, e) => ReleaseHandle());
        }
        
        static FormNativeWrapper()
        {
            LastFormUpdate = DateTime.Now;
        }
        
        protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
        {
            if ((m.Msg == WM_KEYDOWN) ||
                (m.Msg == WM_LBUTTONDOWN)||
                (m.Msg == WM_MOUSEMOVE)||
                 (m.Msg == WM_CHAR) ||
                 (m.Msg == WM_COMMAND)
                )
            {
                LastFormUpdate = DateTime.Now;
            }
            base.WndProc(ref m);
        }
        
    }
  1. Datum poslední zaregistrované aktivity uživatele  - vlastnost LastFormUpdate  - máme pro jednoduchost přímo ve třídě FormNativeWrapper. Přistupovat k vlastnosti může kód běžící v různých threadech, a proto jsou přístupové metody obaleny sekcí lock.
  2. Třída FormNativeWrapper dědí z NativeWindow. Tato třída je součástí OpenNetCF frameworku. Třída zpracovává Windows zprávy zaslané formuláři, který jí byl předán do konstruktoru. V přepsané metodě WndProc nastavíme datum poslední aktivity uživatele, jestliže uživatel píše na klávesnici, používá stylus, stiskl tlačítko či vyvolal položku v menu.

S datem poslední aktivity musí pracovat další třída, která bude zodpovědná za informování dalších tříd v aplikaci, že již uživatel po stanovenou dobu s aplikací nepracoval a že by tedy měl být odhlášen. Takovou třídou je třída InactivitySimpleGuard.

 

    static class InactivitySimpleGuard
    {

        public static event EventHandler<EventArgs> UserInactivity;

        private const int INACTIVITY_CHECK = 10000;
        private static Timer _timer;
        private static int _inactivityInterval = Timeout.Infinite;
        

        public static void Start(int inactivityCheck)
        {
            if (_timer != null)
            {
                _timer.Dispose();
                _timer = null;
            }


            if (inactivityCheck != Timeout.Infinite)
            {
                _inactivityInterval = inactivityCheck;
                _timer = new Timer(onTimer, null, INACTIVITY_CHECK, INACTIVITY_CHECK);
            }
                                    
        }

        public static void Stop()
        {
            if (_timer != null)
            {
                _timer.Change(Timeout.Infinite, Timeout.Infinite);
                _timer.Dispose();
                _timer = null;
            }

        }

        private static void onTimer(object state)
        {
            if (User.CurrentUser != null &&
                checkUserInactivity())
            {
                OnUserInactivity(new EventArgs());
            }
                
        }

        private static void OnUserInactivity(EventArgs e)
        {
            if (UserInactivity != null)
            {
                UserInactivity(null, e);
            }
        }

        private static bool checkUserInactivity()
        {
            TimeSpan span = (DateTime.Now - FormNativeWrapper.LastFormUpdate);

            if (span.TotalSeconds > _inactivityInterval)
            {
                return true;            
            }
            
            return false;
        }
    }
  1. Třída InactivitySimpleGuard je statická, protože jsem měl své důvody v konkrétním řešení, proč ji udělat statickou. Nikdo vám samozřejmě nebrání udělat z třídy InactivitySimpleGuard běžnou instanční třídu, která bude singletonem, nebo budete mít (testovatelné) rozhraní, za něž bude dosazena konkrétní třída přes DI za běhu aplikace. V CNF je ale občas lepší ve jménu vyššího principu microsoftího zatratit své vzletné myšlenky. :)
  2. Rozhraní třídy je jednoduché. Metodě Start předáme počet sekund, které představují časový interval, po jehož  uplynutí je uživatel odhlášen, jestliže s aplikací nepracoval. Metoda Stop zruší objekt Timer, což znamená, že uživatel nebude nikdy automaticky odlogován, protože se jeho aktivita nebere v úvahu. Událost UserInactivity je vyvolána vždy, když uplyne zadaný časový interval, v němž uživatel s aplikací nepracoval, a aplikace tak může uživatele odhlásit. Kontrola doby neaktivity uživatele je v privátní metodě checkUserInactivity, která je volána metodou onTimer. Metoda onTimer je "callback" metodou, která je vyvolána objektem _timer vždy po uplynutí zadaného časového intervalu (délku časového intervalu intervalu  představuje konstanta INACTIVITY_CHECK).

Událost UserInactivity si přihlásí zásobník všech formulářu v aplikaci (třída FromStack) a po vyvolání události zavře všechny otevřené formuláře a zobrazí přihlašovací obrazovku. Následuje kód třídy FormStack.

class FormStack
    {
        private Stack<IStackForm> m_forms;
        private IStackForm prevForm;
        private bool m_noSetErrors;
        
        protected FormStack()
        {
            m_forms = new Stack<IStackForm>();
            InactivitySimpleGuard.UserInactivity += new EventHandler<EventArgs>(InactivitySimpleGuard_UserInactivity);
            m_noSetErrors = false;
        }

        void InactivitySimpleGuard_UserInactivity(object sender, EventArgs e)
        {
            try
            {
                InactivitySimpleGuard.Stop();
                //Konvence, že poslední formulář je vždy přihlašovací formulář
                m_noSetErrors = true;
                while (FormsCount() > 1)
                {
                    IStackForm frm = Pop();
                    frm.EndShowErrors();
                    frm.NotifyInactivityTimeout();
                    frm.CloseForm();
                }
                
                Debug.Assert(FormsCount() == 1);

                IStackForm loginFrm = Peek();

                try
                {
                    loginFrm.EndShowErrors();
                    loginFrm.SetLastTitle();
                    loginFrm.NotifyInactivityTimeout();
                    
                }
                finally
                {
                    loginFrm.BeginShowErrors();
                    m_noSetErrors = false;
                    //loginFrm.BeginShowErrors();
                }

            }
            
            catch (Exception ex)
            {
                //Ignorovat?
                Debug.WriteLine(ex);
            }
        }


        public virtual void Push(IStackForm form)
        {
            if (form == null)
            {
                throw new ArgumentNullException();
            }

            

            if (FormsCount() >= 1)
            {
                prevForm = m_forms.Peek();
                prevForm.ResetTitle();
                prevForm.EndShowErrors();
            }

            form.BeginShowErrors();
            m_forms.Push(form);
        }

        public virtual IStackForm Pop()
        {
            
            IStackForm retForm = m_forms.Pop();
            retForm.EndShowErrors();

            if (FormsCount() >= 1)
            {
                
                IStackForm currentForm = m_forms.Peek();
                if (!m_noSetErrors)
                {
                    currentForm.BeginShowErrors();
                }
                
                currentForm.SetLastTitle();
            }

            
            return retForm;
        }

        public virtual IStackForm Peek()
        {
            return m_forms.Peek();
        }
        
        
        public virtual int FormsCount()
        {
            return m_forms.Count;
        }
        
        
        public static FormStack Instance
        {
            get
            {
                return FormStackInternal.Stack;
            }
        }
            
        private class FormStackInternal
        {
            public static FormStack Stack = new FormStack();
        }
    }

Ve třídě FormStack jsou veškeré formuláře uloženy v pořadí, v němž byly zobrazeny. Za přidání a odebrání formulářů ze zásobníku je odpovědná třída FormBase, která používá veřejné metody Push a Pop.
Nás nyní hlavně zajímá obsluha události UserInactivity třídy InactivitySimpleGuard.

  1. V okamžiku, kdy je událost vyvolána, požádáme třídu InactivitySimpleGuard voláním její metody Stop, aby přestala aktivitu uživatele vyhodnocovat, protože uživatel má být odhlášen a je zbytečné a nesmyslné, aby byla událost UserInactivity vyvolávána, když není uživatel přihlášen.
  2. Postupně zavřeme všechny formuláře kromě přihlašovacího formuláře. Platí konvence, že poslední formulář v zásobníku, tedy první přidaný formulář, je přihlašovací formulář. Každý uzavíraný formulář nejprve dostane příkaz, aby nezobrazoval chybová hlášení, poté formuláři dovolíme zareagovat na vypršení doby neaktivity a nakonec formulář zavřeme.
    frm.EndShowErrors();
    frm.NotifyInactivityTimeout();
    frm.CloseForm();
  3. S přihlašovací formulářem zacházíme jinak. Také nechceme,aby prozatím zobrazoval chyby, dále u něj obnovíme titulek, aby byl viditelný i ve správci úloh jako aktivní formulář, a poté i jemu dáme šanci zareagovat na automatické odhlášení uživatele. Nakonec se přihlašovací formulář stane formulářem, který při novém přihlášení uživatele opět může zobrazovat nastalé chyby. Jak si můžete všimnout, v této fázi se snažíme nereagovat na výjimky -  naším hlavním cílem je zobrazení přihlašovacího formuláře.
    try
    {
    loginFrm.EndShowErrors();
    loginFrm.SetLastTitle();
    loginFrm.NotifyInactivityTimeout();
    }
    finally
    {
    loginFrm.BeginShowErrors();
    m_noSetErrors = false; //loginFrm.BeginShowErrors();
    }

 

Chceme-li odhlásit uživatele po uplynutí stanovené doby nečinnosti, stačí po startu aplikace zavolat metodu InactivitySimpleGuard.Start.

 

int timeOut = ServerConfiguration.Instance.GetPdaTimeout();
                        InactivitySimpleGuard.Start(timeOut);
 
 
 

Kód se dá dále vylepšit. FormNativeWrapper sleduje události na formuláři, ale když máte v aplikaci panel, tak některé notifikace pro rodiče okna pohltí panel a vlastnost LastFormUpdate není aktualizována. Potom by mohlo dojít k tomu, že uživatel je odhlášen, i když s aplikací pracuje. :-) Je ale jednoduché ve tříde  FormBase vyhledat všechny panely a obalit je nativním wrapperem. Vlastnost LastFormUpdate můžete také modifikovat při načtení libovolného čárového kódu - jinými slovy, sami jste zodpovědni za to, co bude aplikace považovat za aktivitu uživatele.



Monday, 12 January 2009 13:56:25 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  Compact .Net Framework