\


 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


 Sunday, 21 December 2008
Extenzní metoda - binární operace And pro enumerace

V diskuzním fóru se (po dlouhé době :-)) objevil jeden zajímavější dotaz, který se netýká ani toho, jak zobrazit druhý formulář v aplikaci, ba ani autor nebojuje s mizením dat po postbacku v ASP.NET aplikaci.

Ale vážně - autor dotazu by chtěl mít lepší syntaxi pro binární operaci And v enumeracích označených metaatributem Flags. Mně stávající C syntaxe (Rights & Rights.Add == Rights.Add) zcela vyhovuje a žádný další syntaktický cukřík hltat nechci,  ale přesto mě zaujalo, jak by se dal problém, tedy spíš estetická preference náročného tazatele :-),  řešit.

Tazatel přeposlal svůj dotaz i do konference na vývojáři, kde bylo nabídnuto řešení přes dočasné přetypování na typ Object a poté z typu Object na typ int. Jak zaznělo v kritice na vývojáři - "dirty" řešení je funkční, ale opakovaný boxing a unboxing hodnot není zrovna ta pravá vývojářská slast.

Zkusil jsem vymyslet jiné řešení  - přiznám se, že IL jsem nezkoumal a žádné výkonnostní testy nedělal.

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

namespace BinaryOpGenericTest
{
    [Flags]
    enum MyFlags
    {
        A = 1,
        B = 2,
        C = 4

    }

    static class EnumExtensions
    {
        private static Dictionary<Type, Delegate> m_operations = new Dictionary<Type, Delegate>();

        public static bool Contains<T>(this T firstOperand, T secondOperand) 
                                                  where T : struct
        {

            Type enumType = typeof(T);
            
            
            if (!enumType.IsEnum)
            {
                throw new InvalidOperationException("Enum type parameter required");
            }


            Delegate funcImplementorBase = null;
            m_operations.TryGetValue(enumType, out funcImplementorBase);

            Func<T, T, bool> funcImplementor = funcImplementorBase as Func<T, T, bool>;
            
            if (funcImplementor == null)
            {
                funcImplementor = buildFuncImplementor(secondOperand);
            }



            return funcImplementor(firstOperand, secondOperand);
        }


        private static Func<T, T, bool> buildFuncImplementor<T>(T val)
                                                            where T : struct
        {
            var first = Expression.Parameter(val.GetType(), "first");
            var second = Expression.Parameter(val.GetType(), "second");
                    
            Expression convertSecondExpresion = Expression.Convert(second, typeof(int));
            var andOperator = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(
                                                                                                       Expression.And(
                                                                                                            Expression.Convert(first, typeof(int)),
                                                                                                             convertSecondExpresion),
                                                                                                       convertSecondExpresion),
                                                                                             new[] { first, second });
            Func<T, T, bool> andOperatorFunc = andOperator.Compile();
            m_operations[typeof(T)] = andOperatorFunc;
            return andOperatorFunc;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            MyFlags flag = MyFlags.A | MyFlags.B;

            Console.WriteLine(flag.Contains(MyFlags.A));            
            Console.WriteLine(EnumExtensions.Contains(flag, MyFlags.C));
            Console.ReadLine();
        }
    }
}

Pár poznámek ke kódu.

  1. Nejdůležitější v kódu je generická extenzní metoda Contains. Nelze použít negenerickou metodu rozšiřující třídu Enum, protože poté začneme mít další problémy s přetypováváním hodnot z obecné Enum na konkrétní enumeraci. Řešením není ani negenerická metoda, v níž pracujeme pouze s třídou Enum. Enumerace jsou hodnotové typy, a proto je na  generický parametr T aplikováno alespoň omezení struct. Jestliže bude zájem, mohu tato rozhodnutí, zde jen zběžně zmíněná, vysvětlit v nějakém dalším spotu.
  2. Když si nejsme jisti, že nám byla za generický typ předána enumerace, musíme provést v metodě Contains další kontrolu za běhu aplikace.
  3. Binární operace And je reprezentována delegáty. Delegáty dynamicky vytvářím za běhu aplikace pro každou enumeraci v metodě buildFuncImplementor. Delegát je vytvořen pomocí abstraktního syntaktického stromu (Expression tree) a poté je na výsledném výrazu (Expression) volána metoda Compile, která vrátí delegáta typu Func<T, T, bool>.
  4. Delegáty Func<T, T, bool> ukládáme do objektu Dictionary jako obecný typ Delegate - důvodem je to, že třída EnumExtensions není generická. Místo přetypovávání delegáta z předka Delegate na typ Func<T, T, bool> by bylo možné také použít pozdní vazbu voláním metody DynamicInvoke dostupné  přímo ve třídě Delegate.
  5. Jestliže byste chtěli operaci And rozšířit i na další typy, které lze konvertovat na typ Int, mohla by se Vám hodit moje extenzní metoda pro detekci konverze generické proměnné na typ Int za běhu aplikace.


Sunday, 21 December 2008 12:22:20 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


 Thursday, 04 December 2008
GSM Net Monitor verze 0.8.0.0. Alfa

Homepage aplikace.

Instalační cab.

Návod na rozchození lokalizace pozice pomocí BTS

Změny ve verzi  0.0.8.

  1. Odstraněna chyba při inicializaci pluginu. !Určitě aktualizujte!
  2. Lepší kompatibilita s Windows Mobile 6.1.


Důležité:

Před instalací nové verze vypněte v aplikaci sledování sítě.

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.

Update: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.



Thursday, 04 December 2008 16:19:46 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  Net Monitor


 Monday, 01 December 2008
Běh aplikace na Windows Mobile při vypnutém displeji a opětovné probuzení zařízení

htc-touch-pro Jedním z problémů, se kterými se vývojáři často potýkají, je, že PDA (přesněji 7Windows Mobile Professional a Classic zařízení  - zařízení s dotykovým displejem) na rozdíl od Smartphonu (Windows Mobile Standard) po uplynutí doby nastavené v ovládacích panelech přecházejí do stavu "Off" (přesněji do stavu "Suspended" dle oficiální terminologie, stav Off má pouze Smartphone - my ale budeme dále ve spotu používat termín "off") , v němž  je provádění kódu aplikace zcela "zmrazeno". Aplikace jsou "hibernovány" a můžeme je považovat za dočasné mrtvolky.

Občas potřebujeme, aby sice došlo k vypnutí displeje zařízení a my tak drasticky a navíc zbytečně neredukovali výdrž baterie, ale aby naše aplikace na pozadí stále běžela. To je první  požadavek. Dalším z častých požadavků je schopnost aplikace probudit celé zařízení ze stavu "off".

Jestliže nám jde pouze o probuzení aplikace po uplynutí námi nastaveného času či při nějaké události (připojení k síti, změna proxy apod.), je řešení jednoduché - v našem arzenálu bude hrát prim omnipotentní funkce CeSetUserNotificationEx.

Když chceme v naší aplikaci pracovat i při vypnutém displeji (např. odpočítáváme sekundy v aplikaci "minutka") a současně probudit zařízení poté, co uběhla nastavená doba, musíme se začít přátelit s dalšími mocnými, avšak ke škodě samotné platformy Windows Mobile mizerně dokumentovanými API.

Co musíme zajistit:

  1. Zařízení nesmí přejít do "off" módu, ale musíme jej ponechat ve speciálním stavu, kdy je vypnutý displej, ale aplikace, které potřebují běžet, nejsou "zmrazeny". Námi požadovaný speciální stav se na Windows Mobile nazývá "unattended mód". Z hlediska uživatele je při "unattended módu" zařízení vypnuto, ale naše aplikace, která si o tento mód požádala, pokračuje ve své činnosti.


    K tomu nám poslouží API PowerPolicyNotify.
    PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_ON).
    Funkci PowerPolicyNotify předáme v prvním argumentu požadavek na Unattended mód a ve druhém argumentu konstantu UNATTENDED_ON s hodnotou 1 - TRUE.

  2. V unattended módu chceme zařízení zcela probudit. Opět použijeme API PowerPolicyNotify.
    PowerPolicyNotify(PowerMode.AppButtonPressed, 0)
    Tentokrát ale předáme konstantu PowerMode.AppButtonPressed, která simuluje stisknutí tlačítka "On/Off" na zařízení. Druhý argument musí být 0, funkcí s ním  při tomto volání nepracuje.

  3. Jestliže již nepotřebujeme běžet v "unattended módu", měli bychom dovolit zařízení, aby naši aplikaci hibernovalo. Nejpozději při ukončení aplikace se tedy musíme vzdát "unattended módu". Použijeme opět API PowerPolicyNotify s prvním argumentem PowerMode.UnattendedMode a ve druhém argumentu předáme konstantu UNATTENDED_OFF s hodnotou 0 - FALSE.

    PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_OFF).
    Sice jsem si ověřil, že při ukončení procesu je "unattended mód" zrušen samotnými Windows, ale je vždy příjemné  pracovat s kódem, v němž nejsou obvyklé  "prasácké" zlozvyky v  managed kódu, a nenechat tedy v naší situaci uklízet jen Garbage Collector nebo správce procesů ve Windows.

V následujícím příkladu naleznete také tento zakomentovaný kód:

 

powerSettings = SetPowerRequirement("BKL1:", (int)DeviceState.FullOn, POWER_FORCE, IntPtr.Zero, 0);

                if (powerSettings == IntPtr.Zero)
                {
                    MessageBox.Show("Failed"); 
                }

K zapnutí displeje je použito API SetPowerRequirement. Toto API ale v "unattended" módu selhává - vždy mi na zařízeních, která mám doma, vrací false. Dále musíte znát zkratku zařízení, pro které chcete nastavit nový režim. Na většině zařízení je displej identifikován zkratkou BKL1: - abyste si ale byli jisti, musíte název najít v registrech - klíče pod HKLM\Drivers\Active\*. API PowerPolicyNotify je tedy pro naše účely mnohem vhodnější.

Pozor: Některá zařízení (pokud se dobře pamatuji, tak např. MIO) nemají od výrobce podporu "unattended" módu a aplikace je stejně vždy zmrazena.

Zde je slíbený příklad - na formuláři je pouze listbox, do kterého je každých 5 vteřin přidáno další číslo. Můžete si ověřit, že čísla přibývají i v "unattended" módu. Po uplynutích každých 75 vteřin (konstanta WAKE_UP_COUNTER  (15) * 5) je zařízení opakovaně probuzeno k "plnému vědomí", což se u něj projeví rozsvíceným displejem. :-)

V ovládacích panelech (záložka Systém, položka Napájení, ikona Upřesnit) nastavte vypnutí displeje i zařízení po uplynutí jedné minuty neaktivity, abyste si příklad užili. ;-)

 

 

using System.Runtime.InteropServices;
using System.Windows.Forms;
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;

namespace PowerPDAProject
{
    public delegate void MethodInvoker();
    public partial class Form1 : Form
    {
        
        public const int UNATTENDED_ON = 1;
        public const int POWER_FORCE = 0x1000;
        public const int UNATTENDED_OFF = 0;

        public const int  WAKE_UP_COUNTER = 15; 
        private int m_secondsCount;
        private IntPtr powerSettings;
        
        public enum PowerMode
        {
            ReevaluateStat = 0x0001,
            PowerChange = 0x0002,
            UnattendedMode = 0x0003,
            SuspendKeyOrPwrButtonPressed = 0x0004,
            SuspendKeyReleased = 0x0005,
            AppButtonPressed = 0x0006
        }

        [DllImport("CoreDll.dll")]
        public static extern int PowerPolicyNotify(PowerMode powerMode, int flags);


        public enum DeviceState : int
        {

            Unspecified = -1,
            FullOn = 0,
            LowOn,
            StandBy,
            Sleep,
            Off,
            Maximum

        }
               

            [DllImport("CoreDll.DLL")]
            public static extern IntPtr SetPowerRequirement(String pvDevice, int DeviceState, int DeviceFlags, IntPtr pvSystemState, int StateFlags);


            [DllImport("CoreDll.DLL")]
            public static extern uint ReleasePowerRequirement(IntPtr hPowerReq);

        



        public Form1()
        {
            
            InitializeComponent();
            m_secondsCount = 0;
        }

        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {

        }

        private void Form1_Load(object sender, EventArgs e)
        {
            bool result = (PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_ON) != 0);
            MessageBox.Show(result.ToString());
            timer1.Enabled = true;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            m_secondsCount++;
            listBox1.Invoke((MethodInvoker)(
                                            () => listBox1.Items.Add(m_secondsCount.ToString())));

            if (m_secondsCount % WAKE_UP_COUNTER == 0)
            {

                PowerPolicyNotify(PowerMode.AppButtonPressed, 0);
                //powerSettings = SetPowerRequirement("BKL1:", (int)DeviceState.FullOn, POWER_FORCE, IntPtr.Zero, 0);

                //if (powerSettings == IntPtr.Zero)
                //{
                //    MessageBox.Show("Failed"); 
                //}
            }
        }

        private void Form1_Closing(object sender, CancelEventArgs e)
        {
            timer1.Enabled = false;
            timer1.Dispose();
            PowerPolicyNotify(PowerMode.UnattendedMode, UNATTENDED_OFF);
            ReleasePowerRequirement(powerSettings);
            
        }
    }
}


Monday, 01 December 2008 18:23:53 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Compact .Net Framework | Mobilitky


 Tuesday, 11 November 2008
Pozvánka na kurzy - nový kurz Pokročilé návrhové vzory a objektové principy 2

Rád bych Vás pozval na další běh kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a hlavně Vás chci seznámit se zcela novým kurzem Pokročilé návrhové vzory a objektové principy 2.

Kurz Pokročilé návrhové vzory a objektové principy 2 je volným pokračováním kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1. Pojetím kurzu Pokročilé návrhové vzory a objektové principy 2 jsem se snažil vyhovět účastníkům předchozího kurzu, kteří mi, volně parafrázováno, říkali: "Nejlepší jsou ty části, kde probíráme jeden příklad za druhým a kde říkáte  - takto to dělám já." Můj zákazník, můj pán (zvláště, jestliže se v záměrech zcela shodneme :-D) - nový kurz je prošpikován příklady, tipy, kódem, vzorovými aplikacemi. Budu se těšit na oponeturu mých postupů. ;-)

Datum konání kurzu:  9. 3. - 11. 3. 2009

Místo konání: Hotel VILLA Praha  Okrajní 1, 100 00, Praha 10

U hotelu VILLA je  možné parkovat, po celý den máme k dispozici wifi připojení.

Pro jistotu dodám, že na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.

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

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

Předpoklady pro absolvování kurzu:

  1. Znalost alespoň jednoho z rodiny "C" jazyků (C#, Java) - příklady na školení jsou v jazyce C#.
  2. Částečná znalost UML = neutečete zděšeni z kurzu, když zobrazím diagram tříd.
  3. Nenávist ke kariéře zručného klikače a zaškrtávače ve vizuálních návrhářích a "wizardech", co s velkou vášní vytváří jedno strhující uživatelské rozhraní pro číselníky za druhým.
  4. Vhodné, nikoliv však nutné, je i absolvovat nejdříve školení Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1.
Program kurzu
  • Layer Supertype pro další vrstvy aplikace – vrstva pro řízení procesů a business transakcí.
  • Deklarativní změny v logice procesů v nasazené aplikaci prováděné samotným uživatelem.
  • Evidence historie objektů - různé přístupy.
  • Vlastní správce historie pro .Net Framework a Javu.
  • Řešení konkurenčního přístupu k datům.
  • Optimistická konkurence - různé implementace.
  • Pesimistická konkurence - různé implementace.
  • Pesimistická konkurence - různé implementace.
  • Konkurence napříč objektovými modelem - "Coarse grained lock" - různé implementace.
  • Thread Specific Storage – vlastní řešení.
  • Modelovani uživatelem zadávaných výběrových podminek (např. uživatelem definované sestavy nad objednávkami) – můj „Conditions“ vzor.
  • Návrh a implementace netriviálního právového frameworku.
  • Různé způsoby vyhodnocování práv - změna logiky za běhu aplikace.
  • Kde všude se nám hodí myšlenky návrhového vzor Accounting - modelování business aplikací jako množiny souvisejících transakcí.
  • Návrhové vzory Query a Repository a jejich vazba na „Conditions“ vzor.
  • Různé přístupy k vytváření uživatelského rozhraní - Model-View-Controller, Model-View-Presenter, Passive View - můj vlastní Form Controller.
  • Aplikace založené na pluginech – vzorové přístupy a doporučení.
  • Správa "cizích" pluginů/služeb ve vlastních aplikacích.
  • Vzor Component Configurator - správa pluginů.
  • Vzor Interceptor - ukázky business aplikací, které jsou rozšiřovány za běhu aplikace s minimálním úsilím a bez strastí opakovaného nasazení aplikace.
  • Kdy použít vzor Special Case?
  • Remote Facade a Data Transfer Object - různé přístupy k distribuované aplikaci.
  • Vzory pro zpracování požadavků na aplikaci-službu.
  • Kódování vzoru Acceptor-Connector.
  • Asynchronous Completion Token - vlastní pomocné objekty pro zjednodušení asynchronních úloh.
  • Kódování vzoru Proactor.
  • Kódování vzoru Reactor.
  • Thread Safe Interface - co pro nás znamená v moderních prostředích (Java a .Net Framework)
  • Co jsou takzvané “Enterprise segmenty” v business aplikacích?
  • V průběhu celého kurzu - kompletní případová studie existující business aplikace, v níž jsou zakódovány postupy zmiňované na kurzu - dlouhá procházka kódem. :)

 

Kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1

Datum konání kurzu:  20. 4. - 22. 4. 2009

Místo konání: Hotel VILLA Praha  Okrajní 1, 100 00, Praha 10

U hotelu VILLA je  možné parkovat, po celý den máme k dispozici wifi připojení.

Pro jistotu dodám, že na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.

Organizační informace ke kurzu

Program kurzu

Výběr z ohlasů na kurz



Tuesday, 11 November 2008 16:38:10 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML


 Friday, 07 November 2008
Změna nastavení aplikace pro stahování TV programu

Po  opakovaných dotazech v mailu, proč se posledních 14 dní nestahuje TV program automaticky do MDA, dávám odpověď sem, abych nemusel všem odpovídat individuálně.

Pravděpodobně se opět trochu změnilo generování TV programu a je nutné upravit kofiguraci aplikace.

V konfiguračním souboru RStein.SimpleWMDownloader.exe.config se ujistěte, že máte nastaveny následující klíče takto:

<add key="StartDate" value="20081104"/>
<add key="StartDateValue" value="1225753200"/>
<add key="DayOffsetValue" value="86400"/>

 

Vzorový konfigurační soubor může vypadat takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="StartDate" value="20081104"/>
        <add key="DayQueryKey" value="dny[]"/>
        <add key="StartDateValue" value="1225753200"/>
        <add key="DayOffsetValue" value="86400"/>
        <add key="NumberOfDays" value="8"/>
      </appSettings>
            <system.diagnostics>
            <trace autoflush="true" indentsize="0">
                <listeners>
                    <add name="myListener"
                      type="System.Diagnostics.TextWriterTraceListener"
                      initializeData="c:\Rstein.SimpleWMDownloader.log" />
                </listeners>
                </trace>
     </system.diagnostics>
</configuration>

 

Podrobné informace o aplikaci a instalace

 

A na okraj dodám, že aplikaci nemusíte použivat jen pro stahování TV programu, ale lze s ní stáhnout jakoukoli stránku na webu, kterou chcete při synchronizaci přes AS uložit do (M|P)DA. Stačí do souboru DownloadConfig.txt přidat další řádky v tomto tvaru:

URL;cesta k souboru na pda

Příklad:

http://www.example.com/vzor.txt;\Storage card\example\vzor.txt
http://www.example.com/dalsi2.txt;\Storage card\example\dalsi2.txt



Friday, 07 November 2008 11:29:27 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | Compact .Net Framework | Mobilitky | Ostatní


 Tuesday, 14 October 2008
Prezentace o "netradičních" návrhových vzorech pro CZ JUG ke stažení

Pro CZJUG jsem přednášel o "netradičních" vzorech - myšleno méně známých  vzorech a možná málo známých aspektech provařených vzorů. Tímto ještě jednou děkuji Dagimu a lidem ze SUNu za pozvání  na příjemnou akci i za jejich tolerantní přístup k mému, s nadsázkou,  mluvení o provaze (=C#, Visual Studio) v domě oběšencově (respektive z pohledu konkurence určitě vhodného kandidáta na business oběšence). :-)

Prezentaci si můžete stáhnout v ppt formátu nebo v pdf přímo na stránkách CZJUG.



Tuesday, 14 October 2008 19:18:42 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  Analytické drobky | Návrhové vzory | UML


 Sunday, 05 October 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#

V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární  a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.

binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);

V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je  funktor equal_to binárním funktorem, poznáme z jeho deklarace.

template<class Type>
   struct equal_to : public binary_function<Type, Type, bool> 
   {
      bool operator()(
         const Type& _Left, 
         const Type& _Right
      ) const;
   };

Na druhém řádku  příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type&  _Right)  s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right  (konkrétně v našem případě jde o podmínku  10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude  představovat při každém volání "adaptovaného" funktoru equal_to  adaptérem equalPredicate  hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.

Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").

    static class ComparerEx
    {
        public static bool GreaterThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) > 0;
        }

        public static bool LessThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) < 0;
        }
    }

Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func<  >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.

Nejprve se podívejme na použití adaptérů.

 

class Program
    {
        static void Main(string[] args)
        {

            
            Random rand = new Random();

            //Vygenerování náhodných čísel v rozsahu 1..100
            List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));

            //Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
            var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
            predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
            predicate = FuncExtension.Not(predicate);
            
            //LINQ dotaz  - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
            var result = myList
                                .Where(predicate)
                                .Select((elem, index) => new {elem, index});
                         

            //Výpis LINQ dotazu
            foreach (var res in result)
            {
                
                Console.WriteLine("{0}:{1}", res.index, res.elem);
                
            }

            //Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
            var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
            
            //Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
            foreach (var val in vals)
            {
                Console.WriteLine(val);
            }

            Console.ReadLine();
        }
    }

V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.

Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než  10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná  při volání funkce Bind2nd.

V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".

Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)

Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.

Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.

Jak adaptéry pracují? Podívejme se na funkci Bind2nd.

 public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument  typu  T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.

Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.

public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }

Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.

Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy". :-)

 

 

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

namespace FunctionExtensions
{
    static class FuncExtension
    {
        
        public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(bindValue, arg));
        }

        public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

        
        public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
        {
            return (arg => !originalFunc(arg));
        }

        public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
        {
            return ((arg1, arg2) => !originalFunc(arg1, arg2));
        }

        public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
        {
            return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
        }

        public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
        }

        public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1  => originalFunc(arg1) && originalFunc2(arg1));
        }

        public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2)  || originalFunc(arg1, arg2));
        }
        
        public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
        }

        public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }
    }
}


Sunday, 05 October 2008 17:34:45 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ | Návrhové vzory


 Monday, 15 September 2008
Několik poznámek k přetypovávání generických kolekcí

Při práci s generickými kolekcemi asi každy občas zatouží převést generickou kolekci s objekty typu B na generickou kolekci s objekty typu A, přičemž instinktivně očekává, že když je typ A předkem typu B, žádný problém při konverzi nenastane a navíc půjde o konverzi implicitní - automatickou. Instinkty, intuice a další feminní rysy jsou ale při programování spíš zátěží (že by jeden z hlavních důvodů, proč je stále tak málo programátorek? ) :-) )

Konkrétně - mějme tyto dvě třídy.

 

    class Test
    {

        public override string ToString()
        {
            return "Test";
        }
    }


    class SpecialTest : Test
    {
        public override string ToString()
        {
            return "SpecialTest";
        }
    }
    
Vytvoříme si kolekci (List) odvozených tříd SpecialTest.
List<SpecialTest> srcList = new List<SpecialTest> { new SpecialTest(), new SpecialTest() };

Při pokusu přetypovat List<SpecialTest> na kolekci objektů typu Test (List<Test>)  neuspějeme.

List<Test> invalidAttemptList = srcList;

Cannot implicitly convert type 'System.Collections.Generic.List<CollectionInheritance.SpecialTest>' to 'System.Collections.Generic.List<CollectionInheritance.Test>'). Důvod je zřejmý - dva generické objekty List, jeden s generickým argumentem Test a druhý s generickým argumentem SpecialTest, jsou dvě zcela rozdílné a nezávislé třídy a skutečnost, že třída SpecialTest je potomkem třídy Test, neznamená, že by stejný vztah platil mezi třídami List<SpecialTest> a List<Test>. Autoři jazyka C# (dle svých vyjádření prozatím?) zavedli toto omezení kvůli typové bezpečnosti.

Možností, jak konverzi provést, je mnoho. Vyjmenujme alespoň ty, které nově přinesl LINQ.

LINQ nám nabízí pro daný účel metodu Cast, která zkonvertuje prvky ze zdrojové kolekce (IEnumerable) na (generický) typ předaný metodě.

IEnumerable<Test> ieList = srcList.Cast<Test>();

Jestliže by nebylo možné všechny prvky v kolekci převést na 'Test', bude vyvolána výjimka. Když máme v kolekci směs objektů z různých tříd nebo podporujících různá rozhraní, můžeme přetypovat pomocí dalšího standardního LINQ operátoru OfType, který do výsledné kolekce vloží jen ty objekty, které se pomocí operátoru as podaří přetypovat na cílový typ. Objekty, které přetypovat na cílový typ (v následujícím kódu tedy na  typ Test) nelze, jsou ignorovány.

IEnumerable<Test> ieList2 = srcList.OfType<Test>();

Jestliže nechceme mít jako výslednou kolekci typ IEnumerable, můžeme použít další LINQ operátor ToList() a výsledek přetypování nám bude vrácen v instanci List<T>.

List<Test> testList = srcList.OfType<Test>().ToList();

Alternativou k předchozímu zápisu může být využití konstruktoru třídy List.

List<Test> testList2 = new List<Test>(srcList.OfType<Test>());

 

Jestliže nechcete vždy pracovat jen s kolekcí typu List a chcete přetypovávat například na typové kolekce, využijete mé extenzní metody.

 

Collection<Test> trgList = srcList.WideningConvert<SpecialTest, Test, Collection<Test>>();

Metodě WideningConvert předáte jako typové argumenty aktuální typ v generické kolekci (SpecialTest), cílový-výsledný typ, na který má být zdrojový typ převeden (Test) a typ kolekce, která má být vrácena. Kolekce musí mít bezparametrický konstruktor a také musí podporovat rozhraní ICollection<T>.

Pomocí extenzní metody lze přetypovávat i z kolekce objektů typu "předek" na kolekci "potomků".

  

Collection<SpecialTest> nextList = trgList.NarrowingConvert<Test, SpecialTest, Collection<SpecialTest>>();
 
 

Metoda WideningConvert svým názvem dává najevo, že je určena pro implicitní ("bezpečnou") konverzi, kdy převádíte kolekci potomků na kolekci předků. Obdobně, metoda NarrowingConvert přetypovává kolekci objektů typu "předek" na kolekci objektů typ "potomek (explicitni konverze). Metoda NarrowingConvert se pokusí převést každý objekt ve zdrojové kolekci na cílový typ ("Potomek") pomocí operátoru as. Jestliže přetypování selže, je zdrojový objekt ignorován, a proto může výsledná kolekce vrácená metodou NarrowingConvert obsahovat méně prvků než kolekce zdrojová.

 

Zde je kompletní výpis metod. Bylo by samozřejmě možné začít uvažovat nad zjednodušením kódu pro přetypovávání, ale to si necháme "napříště".

 

    public static class ConvertExtensions
    {
        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "potomky" na generickou kolekci s "předkem"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        public static R WideningConvert<T0, P, R>(this IEnumerable<T0> source) 
                                             where R : ICollection<P>, new()
                                             where T0: P            
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {
                retCol.Add(srcElem);
            }
            return retCol;
        }

        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "předkem" na generickou kolekci "potomků"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        ///<remarks>Počet prvků v cílové kolekcí může být menší než počet prvků v zdrojové kolekci, protože veškeré objekty, které nelze zkonvertovat na <typeparamref name="R"/>, jsou ignorovány</remarks>
        public static R NarrowingConvert<T0, P, R>(this IEnumerable<T0> source)
                                                  where R : ICollection<P>, new()
                                                  where P : class, T0
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {

                P retValue = srcElem as P;

                if (retValue != null)
                {
                    retCol.Add(retValue);
                }
            }
            return retCol;
        }
      
                                             
    }


Monday, 15 September 2008 13:29:48 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ