Lehká imitace některých rysů windows forms aplikací v non-windows forms aplikacích
Omluvte prosím trochu kryptický název, ale lepší a hlavně výstižnější pojmenování článku mě nenapadlo. Název je stejně jen vábnička na čtenáře, proto se podívejme, co je jím míněno.
Již několikrát mně různí vývojáři tvrdili, jak nepříjemná je pro ně práce s konzolí (windows službou, dosaďte další typy aplikací dle libosti...), protože musí řešit, aby aplikace po svém spuštění ihned neskončila, a také je pro ně problematické zajistit, aby byly některé události zpracovány vždy ve stejném threadu.
Převedeme-li emocionální stížnost do věcného jazyka, zjistíme, že to, co v těchto typech aplikací chybí, jsou následující rysy běžné windows forms aplikace:
- Windows aplikace spustí smyčku Windows zpráv (message loop) a vývojář pouze obsluhuje události formuláře. V (Compact) .Net Frameworku nám stačí zavolat Application.Run(new Form1()) a aplikace neukončí svůj běh, dokud není uzavřen poslední formulář nebo dokud ti drsnější z nás nezavolají Application.Exit. O životní cyklus aplikace, její spuštění a ukončení, se většinou nemusíme nijak starat.
- Při obsluze formuláře máme po volání metody Invoke garantováno, že předaný delegát bude vykonán v takzvaném UI threadu. Hlavním účelem metody Invoke (a sesterských metod BeginInvoke a EndInvoke) je threadově bezpečná komunikace s ovládacími prvky. Ovládací prvky ve stylu windows prvků v konzolových aplikacích (windows službách) nenajdeme, ale přesto bychom i v těchto typech aplikací občas chtěli mít nástroj, který garantuje, že všechny nebo vybrané události budou zpracovány v jednom výkonném threadu.
V tomto článku se objeví návrh, který pro non-windows forms aplikace přinese výše zmíněné rysy a přidá pár věcí navíc.
Pár vysvětlujících poznámek na úvod . Kód (přesněji řečeno draft k dalšímu rozpracování), který za chvíli uvidíte, má běžet na .Net frameworku a na Compact .Net Frameworku. Vím, že existují synchronizační kontexty pro thready, ale metodu Invoke jsem zmiňoval proto, že představuje společný jmenovatel pro obě prostředí, protože Compact .Net Framework je stále tím strýčkem - beznadějným sociálním případem, co nám nikdy nepřiveze žádné úhledně zabalené dárky, v nichž se skrývá třeba nádherná vlastnost SynchronizationContext.Current. Se znalostí tohoto omezení je také jasné, proč jsem nepoužil i další metody/vlastnosti dostupné jen ve "velkém" .Net Frameworku.
Dále v kódu jsou třídy obsahující ve svém názvu slovo *Console*. Nenechte se zmást, že mluvím dále jen o konzolových aplikacích, stejné třídy lze použít ve windows službě a dalších typech aplikací.
Zaveďme si nejdříve abstraktní třídu ConsoleTask, která je předkem všech zpracovávaných úloh v aplikaci. Zjednodušeně si můžeme třídu ConsoleTask a její potomky představit jako výchozí stavební prvky zapouzdřující chování analogické k vybraným a pro nás zajímavým rysům windows formulářů.
/// <summary>
/// Základní rozhraní pro položky zpracovávané v jedné frontě
/// </summary>
internal interface IExecuteWorkItem
{
/// <summary>
/// Implementace metody spustí úlohu
/// </summary>
void Execute();
}
/// <summary>
/// Bázová třída pro všechny úlohy
/// </summary>
abstract class ConsoleTask : IDisposable
{
#region Inner classes
/// <summary>
/// Výchozí implementace rozhraní <see cref="IExecuteWorkItem"/>
/// </summary>
private class WorkThreadItem : IExecuteWorkItem
{
#region Private variables
private Delegate m_del;
private object[] m_vals;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát, který má být spuštěn ve frontě nadřazeného objektu <see cref="ConsoleTask"/></param>
/// <param name="vals">Argumenty delegáta</param>
public WorkThreadItem(Delegate del, params object[] vals)
{
if (del == null)
{
throw new ArgumentNullException("del");
}
m_del = del;
m_vals = vals;
}
/// <summary>
/// Metoda iniciuje vykonání předaného delegáta
/// </summary>
public virtual void Execute()
{
m_del.Method.Invoke(m_del.Target, m_vals);
}
}
#endregion Inner classes
#region private variables
private ManualResetEvent m_event;
private Thread m_innerWorkingThread;
private Queue<IExecuteWorkItem> m_workQueue;
private AutoResetEvent m_workingThreadEvent;
private object m_lockQueueRoot;
private bool m_continue;
private bool m_disposed;
#endregion private variables
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
protected ConsoleTask()
{
m_lockQueueRoot = new object();
m_workQueue = new Queue<IExecuteWorkItem>();
m_event = new ManualResetEvent(false);
m_workingThreadEvent = new AutoResetEvent(false);
m_innerWorkingThread = new Thread(processWorkerThread);
m_continue = true;
m_disposed = false;
}
#endregion constructors
#region Properties
/// <summary>
///<see cref="WaitHandle"/> běžící úlohy
/// </summary>
public WaitHandle TaskWaitHandle
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
return m_event;
}
}
/// <summary>
/// Metoda vrátí true, jestliže volající thread je odlišný od threadu, který vyřizuje položky zpracovávané v jedné frontě
/// </summary>
public virtual bool InvokeRequired
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
return SlaveWorkingTask.InvokeRequired;
}
return (Thread.CurrentThread.ManagedThreadId != m_innerWorkingThread.ManagedThreadId);
}
}
/// <summary>
/// Volitelná instance <see cref="ConsoleTask"/>, která převezme odpovědnost za vyřizování položek zpracovávaných v jedné frontě
/// </summary>
public ConsoleTask SlaveWorkingTask
{
get;
set;
}
#endregion Properties
#region Methods
/// <summary>
/// Metoda garantuje, že dojde k vykonání předaného delegáta v threadu, ktrerý vyřizuje položky zpracovávané v jedné frontě
/// </summary>
/// <remarks>Metoda pouze zařadí položky ke zpracování a nečeká na výsledek volání delegáta. </remarks>
public virtual void Invoke(Delegate del, params object[] vals)
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
SlaveWorkingTask.Invoke(del, vals);
return;
}
lock (m_lockQueueRoot)
{
m_workQueue.Enqueue(new WorkThreadItem(del, vals));
m_workingThreadEvent.Set();
}
}
/// <summary>
/// Metoda spustí úlohu
/// </summary>
/// <remarks>Spuštěním úlohy se rozumí spuštění kódu v přepsané metodě <see cref="DoInternalRun"/> v samostatném threadu. Metoda Run nevrátí řízení, dokud není úloha dokončena.</remarks>
public void Run()
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
m_innerWorkingThread.Start();
ThreadPool.QueueUserWorkItem(obj => DoInternalRun());
m_event.WaitOne();
}
/// <summary>
/// Metoda ukončí úlohu
/// </summary>
public virtual void CloseTask()
{
m_continue = false;
m_workingThreadEvent.Set();
m_event.Set();
}
/// <summary>
/// Metoda pro explicitní uvolnění veškerých nepoužívaných zdrojů - součást implementace "Disposable" vzoru
/// </summary>
public void Dispose()
{
if (m_disposed)
{
return;
}
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// "Destruktor" - součást implementace "Disposable" vzoru
/// </summary>
~ConsoleTask()
{
Dispose(false);
}
/// <summary>
/// Interní implementace "Disposable" vzoru
/// </summary>
/// <param name="disposing">true - jestliže je metoda volána z metody Dispose, false, pokud je volána z destruktoru - metody Finalize</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
((IDisposable)m_workingThreadEvent).Dispose();
((IDisposable)m_event).Dispose();
m_disposed = true;
}
catch (Exception e)
{
Trace.WriteLine(e);
}
}
}
/// <summary>
/// Metoda, která musí být přepsána v odvozených třídách a která obsahuje logiku specifickou pro každou úlohu
/// </summary>
protected abstract void DoInternalRun();
/// <summary>
/// Obsluha fronty položek, které mají být zpracovány ve stejném threadu
/// </summary>
private void processWorkerThread()
{
const int EXPECTED_MINIMUM_ITEMS = 1;
while (m_continue)
{
m_workingThreadEvent.WaitOne();
if (!m_continue)
{
continue;
}
int m_count = EXPECTED_MINIMUM_ITEMS;
IExecuteWorkItem nextItem = null;
while (m_count > 0)
{
lock (m_lockQueueRoot)
{
m_count = m_workQueue.Count();
if (m_count != 0)
{
nextItem = m_workQueue.Dequeue();
}
}
try
{
if (nextItem != null)
{
nextItem.Execute();
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
m_count--;
}
}
}
#endregion Methods
}
Abstraktní třída ConsoleTask obsahuje ve svém veřejném rozhraní metodu Run, kterou spustíme úlohu. Metoda Run je šablonovou metodou (Template method), protože obsahuje závazný scénář pro veškeré odvozené úlohy. Potomci třídy ConsoleTask do scénáře vstupují na přesně vymezeném místě - v metodě DoInternalRun, která je deklarována jako abstraktní a všechny konkrétní odvozené třídy ji musí přepsat a doplnit vlastní logiku. Třída ConsoleTask tedy garantuje, že je vždy nejprve spuštěn thread vyřizující požadavky, které mají být vykonány ve stejném threadu (podrobný popis viz níže), poté je třída ThreadPool použita ke spuštění kódu v metodě DoInternalRun v jiném threadu a nakonec aktuální thread pozastavíme čekáním na signalizaci instance synchronizačního objektu ManualResetEvent (proměnná m_event). Ve vlastnosti TaskWaitHandle vydáváme stejný objekt ManualResetEvent, který může jiný thread využít k synchronizaci svého běhu s instancí třídy odvozené od třídy ConsoleTask. Tím simulujeme pro uživatele objektů odvozených z třídy ConsoleTask spuštění smyčky zpráv, protože aplikace není ukončena po zavolání metody Run. Za ukončení běhu úlohy zodpovídá metoda CloseTask - metoda uvolní pracovní thread vyřizující frontu požadavků nastavením proměnné m_continue na false a signalizací synchronizační primitivy workingThreadEvent. Dále metoda CloseTask přes signalizaci synchronizačního objektu v proměnné m_event informuje o dokončení celé úlohy - thread pozastavený v metodě Run bude uvolněn.
Třída ConsoleTask dále obsahuje definici privátní třídy WorkThreadItem, která implementuje rozhraní IExecuteWorkItem a má roli adaptéru. Instance třídy WorkThreadItem jsou jednotlivé položky, které mají být vykonány v jednom pracovním threadu. Adaptérem je třída WorkThreadItem proto, že převádí rozhraní jakéhokoli předaného delegáta na rozhraní IExecuteWorkItem. Po volání metody Execute objektu WorkThreadItem je vykonána metoda, na kterou ukazuje delegát.
Jméno vlastnosti InvokeRequired by mělo znít povědomě - metoda vrátí true, jestliže thread, který zjišťuje hodnotu vlastnosti , je odlišný od threadu, který zpracovává položky typu IExecuteWorkItem. Thread poté může použít metodu Invoke, která zajistí, že předaný delegát bude vykonán v pracovním threadu. Je to zmíněno i v dokumentaci metody Invoke, ale zde zdůrazním, že metoda Invoke zařadí pouze novou položku do fronty ke zpracování a dá signál pracovnímu threadu, že je dostupná další položka voláním metody Set na proměnné m_workingThreadEvent, což je instance synchronizační primitivy AutoResetEvent. Metoda Invoke nečeká na výsledek volání delegáta a ani není zaručeno, že po návratu z metody Invoke byl již předaný delegát vykonán. Samotná obsluha fronty položek, které mají být vykonány v jednom threadu, je soustředěna do metody processWorkerThread.
U metody Invoke a vlastnosti InvokeRequired si můžete všimnout podmíněné delegace volání na instanci ve vlastnosti SlaveWorkingTask. Jestliže vlastnost SlaveWorkingTask není null, je odpovědnost za zpracování položek přenesena na jinou instanci třídy ConsoleTask. Jednotlivé tasky mohou tvořit zárodečný řetězec odpovědnosti (Chain of responsibility) a za chvíli uvidíme, k čemu můžeme toto předávání odpovědnosti na jiné instance ConsoleTask využít.
Třída ConsoleTask také implementuje běžný .Net "Disposable" vzor pro uvolňování prostředků (rozhraní IDisposable, chráněná metoda Dispose a destruktor - metoda Finalize).
Mimikry konzolové aplikace, která se v rámci námi vykolíkovaného seznamu požadavků snaží vydávat za windows forms aplikaci, vylepšíme zavedením jednoduché fasády (vzor facade), která bude simulovat metodu Application.Run.
/// <summary>
/// Facade s rozhraním pro spuštění úkolu
/// </summary>
class ConsoleApplication
{
/// <summary>
/// Metoda spustí předaný úkol (Fasáda ke spuštění úloh napodobující známou metodu Application.Run z Windows Forms aplikací)
/// </summary>
/// <param name="task">Úkol, který má být spuštěn</param>
public static void Run(ConsoleTask task)
{
if (task == null)
{
throw new ArgumentNullException("task");
}
task.Run();
}
}
Jak vidno, metoda Run zcela deleguje vykonání na předaný ConsoleTask.
Jak se prozatím s naším modelem pracuje? Nejlepší bude napsat si potomka třídy ConsoleTask a zjistit to. Zkusme vytvořit úlohu, která na Compact .Net Frameworku zpracuje příchozí SMS.
/// <summary>
/// Třída pro zpracování přijatých SMS
/// </summary>
class SMSTask : ConsoleTask
{
#region private variables
private MessageInterceptor m_interceptor;
#endregion private variables
#region Methods
/// <summary>
/// Metoda začne sledovat SMS
/// </summary>
protected override void DoInternalRun()
{
m_interceptor = new MessageInterceptor(InterceptionAction.Notify, false);
ThreadPool.QueueUserWorkItem(
(obj) =>
m_interceptor.MessageReceived += m_interceptor_MessageReceived);
}
/// <summary>
/// Obslužná metoda uálosti <see cref="MessageInterceptor.MessageReceived"/>
/// </summary>
/// <param name="sender">Odesílatel události</param>
/// <param name="e">Argument události</param>
private void m_interceptor_MessageReceived(object sender, MessageInterceptorEventArgs e)
{
if (InvokeRequired)
{
Invoke((Action<SmsMessage>)(handleMessage), e.Message as SmsMessage);
}
else
{
handleMessage(e.Message
as SmsMessage); }
}
public override void CloseTask()
{
m_interceptor.Dispose();
base.CloseTask();
}
/// <summary>
/// Zpracování SMS
/// </summary>
/// <param name="message">SMS zpráva ke zpracování</param>
private void handleMessage(SmsMessage message)
{
Console.WriteLine(message.Body);
CloseTask();
}
#endregion methods
}
Autor tříd odvozených z bázové třídy ConsoleTask má lehkou práci, protože se soustředí jen na úkol (příjem SMS) a ne na to, že jeho kód bude vykonán v konzolové aplikaci. V přepsané metodě si přihlásíme odběr události MessageReceived - zde je událost přihlášena přes ThreadPool, ale není to nutné. Obslužná metoda události MessageReceived (m_interceptor_MessageReceived) po příjmu SMS zaručí, že SMS budou vždy zpracovány ve stejném pracovním vlákně použitím vlastnosti Invoke Required a Invoke. Jestliže je událost vyvolána v jiném než pracovním threadu obsluhujícím frontu položek ke zpracování, zavoláme metodu Invoke, které předáme delegáta ukazujícího na metodu handleMessage. K vytvoření delegáta jsme použili standardního generického delegáta Action<T>, kde jsme za generický parametr T dosadili třídu SmsMessage, jejíž instanci přijímá jako argument metoda handleMessage. Přepsali jsme i metodu CloseTask, která uvolní interceptora pro příjem zpráv a poté vyvolá implementaci metody CloseTask z bázové třídy. Zde je úloha ukončena po příjmu první zprávy voláním CloseTask z metody handleMessage, ale způsob ukončení úlohy je zcela v rukou vývojáře konkrétní úlohy.
Poznámka na okraj: U naší třídy SMSTask by bylo vhodné, když chceme přijmout jen jednu SMS, ihned si odhlásit odběr dalších zpráv, nebo si v interní proměnné nastavit, že již zpráva byla přijata a další zprávy nepředávat ke zpracování.
Novou úlohu spustíme tímto nezáludným a pro vývojáře windows forms aplikací povědomým kódem:
class Program
{
static void Main(string[] args)
{
SMSTask smsTask = new SMSTask();
ConsoleApplication.Run(smsTask);
}
}
Na vývojářské práci je nejlepší, že poté, co máte nějaký nosný nápad, můžete jej rozvíjet ad libitum. Co když chceme ve stejné aplikaci nejen přijímat SMS, ale také reagovat na události v objektu, který nás informuje o spuštěných aplikacích uživatele. Nebo chceme sledovat přes třídu SystemState informace o příchozích hovorech? Napráskat vše do jedné instance potomka třídy ConsoleTask "ResimVzdyckyVsechnoNaJednomMisteAJsemTotalneVPohodeVoe" je sice řešením, ale i jen laxním zastáncům vágně formulovaného principu jedné odpovědnosti třídy (zdravím Aleši :) ) se právě teď nasucho aktivoval podmíněný reflex, protože vědí, že při správě takové aplikace po kolegovi-pohodářovi je vztekem podmíněné zoufalecké uslintávání a hlasité nadávání to nejmenší.
Chceme určitě zachovat stávající strukturu aplikace, chceme spouštět libovolné množství různorodých úloh a navíc chceme mít možnost zpracovávat položky napříč jednotlivými úlohami ve stejném threadu - pracovní frontě. Úkol jako stvořený pro jednu z možných nenásilných inkarnací návrhového vzoru Composite v aplikaci.
/// <summary>
/// Třída reprezentující kompozitní úlohu - viz návrhový vzor Composite
/// </summary>
class CompositeTask : ConsoleTask
{
#region private variables
private ICollection<ConsoleTask> m_tasks;
#endregion private variables
#region Constructors
public CompositeTask(ICollection<ConsoleTask> tasks)
{
if(tasks == null)
{
throw new ArgumentNullException("tasks");
}
if (tasks.Count == 0)
{
throw new ArgumentException("One or more tasks are required");
}
m_tasks = tasks;
}
#endregion Constructors
#region Methods
/// <summary>
/// Spuštění všech úkolů
/// </summary>
protected override void DoInternalRun()
{
foreach (var task in m_tasks)
{
ConsoleTask task1 = task;
task1.SlaveWorkingTask = this;
ThreadPool.QueueUserWorkItem((obj) => task1.Run());
}
}
/// <summary>
/// Metoda ukončí všechny úkoly
/// </summary>
/// <remarks>Metoda pouze zavolá metodu CloseTask na všech předaných objektech <see cref="ConsoleTask"/>, ale nestará se o výsledek volání</remarks>
public override void CloseTask()
{
foreach (var task in m_tasks)
{
task.CloseTask();
}
base.CloseTask();
}
#endregion Methods
}
Metoda CompositeTask je také potomkem třídy ConsoleTask, a proto můžeme ve zbytku aplikace pracovat se stejným rozhraním, na které jsme zvyklí. Jednoduchá i složená úloha mají stejné rozhraní, takže si klient tříd nemusí být skládání úloh vědom, což je mimochodem jedna z hlavních motivací pro zavedení návrhového vzoru Composite. V konstruktoru očekáváme odkaz na kolekci dceřiných úkolů. V metodě DoInternalRun zavoláme v cyklu metodu Run všech předaných úkolů. Ještě před voláním metody Run ale nastavíme u každé úlohy vlastnost SlaveWorkingTask na aktuální objekt CompositeTask, což nám zaručí, že veškeré položky ze všech jednotlivých úloh vložené do pracovní fronty voláním metody Invoke budou zpracovány v jediném pracovním vlákně CompositeTask. Zde vidíme jeden z důvodů, proč máme vlastnost SlaveWorkingTask a proč třída ConsoleTask ve členech Invoke a InvokeRequired nejprve kontroluje, jestli má zpracovat požadavek ve své pracovní frontě, anebo existuje jiný vhodný objekt - "otrok" (SlaveWorkingTask), který se o položky postará sám. Metoda CloseTask opět nejprve zavolá metodu CloseTask na všech objektech ConsoleTask, ze kterých je aktuální instance třídy CompositeTask složena.
Opět poznámka: Snad si rozumíme v tom, že navržená třída CompositeTask není jediná možná. Jiná třída CompositeTask2 nemusí přesměrovávat pracovní frontu na sebe, další po uzavření úloh nejprve vyčká na ukončení všech dceřiných úloh. Další scénáře jistě nalezne laskavý čtenář sám.
Než třídu CompositeTask vyzkoušíme, vytvoříme si dalšího potomka Consoletask, který bude zpracovávat pravidelně vyvolávanou událost z našeho objektu.
Zde je jednoduchá "demo" třída, jejíž srdce tiká v rytmu události AliveEvent.
class MyEventClass
{
public event EventHandler<EventArgs> AliveEvent;
private bool m_continue;
private const int INTERVAL = 1000;
private object m_lockObj;
public MyEventClass()
{
m_continue = true;
m_lockObj = new object();
}
public void Start()
{
ThreadPool.QueueUserWorkItem((state) =>
{
while (m_continue)
{
Thread.Sleep(INTERVAL);
AliveEvent(this, new EventArgs());
}
});
}
public void Stop()
{
lock (m_lockObj)
{
m_continue = false;
}
}
protected void OnAliveEevent(EventArgs e)
{
if (AliveEvent != null)
{
AliveEvent(this, e);
}
}
}
Naše nová konkrétní úloha zpracovává události instance MyEventClass
class ConcreteTask : ConsoleTask
{
private const int HEART_BEAT_LIMIT = 10;
private MyEventClass m_evClass;
private int m_heartBeatcount;
private bool m_processEvent;
protected override void DoInternalRun()
{
m_evClass = new MyEventClass();
m_heartBeatcount = 0;
m_processEvent = true;
m_evClass.Start();
m_evClass.AliveEvent += evClass_AliveEvent;
}
private void evClass_AliveEvent(object sender, EventArgs e)
{
Action myAction = (Action) (
() =>
{
if (!m_processEvent)
{
return;
}
Console.WriteLine("Event fired");
m_heartBeatcount++;
if (m_heartBeatcount >= HEART_BEAT_LIMIT)
{
m_evClass.Stop();
m_processEvent = false;
CloseTask();
}
}
);
if (InvokeRequired)
{
Invoke(myAction);
}
else
{
myAction();
}
}
}
Jenom pro zajímavost je ukázáno, že metodě Invoke můžeme předat složenou (statement) lambdu (nebo anonymní metodu).
Spuštění více úloh pomocí třídy CompositeTask není odlišné od spuštění jedné úlohy.
class Program
{
static void Main(string[] args)
{
var compTask = new CompositeTask(new List<ConsoleTask>
{
new SMSTask(),
new ConcreteTask()
});
ConsoleApplication.Run(compTask);
}
}
Znovu opakuji, že článek měl za cíl ukázat, jak transponovat do jiného aplikačního rámce postupy, které Windows Forms vývojáři dobře ovládají a o kterých mi tvrdili, že jsou "přirozené". Další rozpracování těchto draftů napsaných v půlnoční chvilce nespavosti je už jen variací předvedených postupů.
Monday, 02 February 2009 14:51:15 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms