Immaginiamo di dover creare un processo che viene eseguito in un thread separato il quale genera eventi di notifica. Questi eventi devono essere intercettati e gestiti dall’interfaccia utente, per notificare informazioni sull’avanzamento del processo all’utente, oppure per iteragire con il proccesso stesso.
L’interfaccia è implementata da un applicazione Windows Form NET 3.5, mentre l’engine è in una class library separata.
Immaginiamo di avere una classe Engine, con un metodo Start e tre eventi che notificano l’avvio del processo, l’esecuzione del processo e il termine. Utilizzando come entry point del thread il delegato ParameterizedThreadStart, una prima implementazione potrebbe essere la seguente.
public class Engine
{
public void Start()
{
Thread thread = new Thread(EngineFunc);
thread.Start(this);
}
public event EventHandler OnStart;
public event EventHandler OnProcess;
public event EventHandler OnEnd;
static void EngineFunc(object Engine)
{
Engine engine = (Engine) Engine;
//Avvio
engine.RaiseEngineStart();
//Processo
engine.RaiseEngineProcess();
//Termine
engine.RaiseEngineEnd();
}
protected virtual void RaiseEngineStart()
{
if (OnStart != null)
{
OnStart(this, new EventArgs());
}
}
...
}
Questa implementazione apparentemente corretta funziona solamente se il codice eseguito dai gestori degli eventi non accede a risorse condivise che non implementino una gestione della concorrenza, oppure non interagisce con l’interfaccia utente. In quest’ultimo caso, se si tratta di Windows Forms, immaginando di voler segnalare l’avanzamento del processo all’utente attraverso una apposito controllo, riceveremo un eccezione System.InvalidOperationException che ci avverte : “Cross-thread operation not valid: Control 'txtLog' accessed from a thread other than the thread it was created on.”. Windows Forms controlla quindi che l’accesso alla risorse grafiche non avvenga da un thread secondario.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
engine.OnStart += new EventHandler(engine_OnStart);
...
}
private void Form1_Load(object sender, EventArgs e)
{
engine.Start();
}
void engine_OnStart(object sender, EventArgs e)
{
LogText("Processo avviato."); //Eccezione:System.InvalidOperationException
}
...
readonly Engine engine = new Engine();
}
L’eccezione viene generate, perchè come spiegato dal messaggio della stessa, viene controllato che l’accesso ai controlli di interfaccia venga effettuato dallo stesso thread di creazione del controllo. La soluzione immediata è quella di eseguire il delegato associato all’evento dal thread di esecuzione dell’interfaccia utente, per fare questo è sufficiente chiamare i metodi Invoke (sincrono) o BeginInvoke (asincrono) sulla Form, in modo da accodare l’esecuzione del codice al thread dell’interfaccia. Per implementare una soluzione immediata si possono utilizzare alternativamente gli anonymous delegate piuttosto che le lambda expression.
void engine_OnStart(object sender, EventArgs e)
{
//Anonymous method
this.BeginInvoke((MethodInvoker) (delegate
{
LogText("Processo avviato.");
}));
}
void engine_OnStart(object sender, EventArgs e)
{
//Lamda expression
this.BeginInvoke((MethodInvoker) (() => LogText("Processo avviato.")));
}
Entrambi le soluzioni funzionano correttamente. Pur essendo funzionale, l’architettura di questa soluzione non mi soddisfa: non è pratica se gli eventi sono molti e/o i gestori hanno una certa complessità. Inoltre volevo trovare una soluzione che astraesse questo problema rispetto alla specifica tecnologia di interfaccia utente.
Ho pensato quindi di rappresentare l’invocazione degli eventi, dal punto di visto del chiamante (engine) come un servizio, ed ho quindi definito il contratto astratto con un interfaccia.
public interface IInvoker
{
/// <summary>
/// Invocazione sincrona.
/// </summary>
/// <param name="method">Delegato</param>
/// <param name="args">Parametri</param>
void InvokeSyncEvent(Delegate method, params object[] args);
/// <summary>
/// Invocazione asincrona.
/// </summary>
/// <param name="method">Delegato</param>
/// <param name="args">Parametri</param>
void InvokeAsyncEvent(Delegate method, params object[] args);
}
Ora si modifica l'engine in modo da poter essere inizializzato, opzionalmente, con una referenza al servizio IInvoker appena creato. In alternativa la referenza al servizio può essere iniettata con un approccio IoC. Si crea un nuovo metodo che implementa l'invocazione sincrona di un evento standard (EventHandler) questo verificherà se è disponibile la referenza al servizio di invocazione, in questo caso utilizzerà quest'ultimo, altrimenti utilizzerà l'invocazione standard.
public class Engine
{
protected IInvoker _Invoker;
public Engine()
{
//Invocazione eventi standard
}
/// <summary>
/// Costruzione di engine con invoker specifico per gli eventi.
/// </summary>
/// <param name="Invoker">Referenza al servizio di invocazione degli eventi</param>
public Engine(IInvoker Invoker)
{
//Invocazione eventi via IInvoker
_Invoker = Invoker;
}
/// <summary>
/// Imposta il servizio di invocazione degli eventi
/// </summary>
public IInvoker Invoker {set { _Invoker = value; } }
...
protected virtual void RaiseEngineStart()
{
InvokeEvent(OnStart);
}
...
public void InvokeEvent(EventHandler EventHandler)
{
if (EventHandler != null)
{
if (_Invoker != null)
{
_Invoker.InvokeSyncEvent(
EventHandler, new object[] {this, new EventArgs()});
}
else
{
EventHandler(this, new EventArgs());
}
}
}
}
Ora basta implementare un servizio Invoker per i controlli Windows Forms.
public class WinFormsInvoker : IInvoker
{
protected Control _Control;
public WinFormsInvoker(Control Control)
{
_Control = Control;
}
public void InvokeSyncEvent(Delegate method, params object[] args)
{
_Control.Invoke(method, args);
}
public void InvokeAsyncEvent(Delegate method, params object[] args)
{
_Control.BeginInvoke(method, args);
}
}
Rimane da modificare la costruzione dell'engine da parte del client Windows Forms nel seguente modo.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
engine = new Engine(new WinFormsInvoker(this));
engine.OnStart += new EventHandler(engine_OnStart);
...
}
...
void engine_OnStart(object sender, EventArgs e)
{
LogText("Processo avviato.");
}
...
readonly Engine engine;
}
Come si può vedere con questa soluzione si raggiungono due obiettivi:
- L'invocazione cross-thread è astratta attraverso un servizio definito da un interfaccia (IInvoker) e la cui implementazione non è necessariamente conosciuta dall'engine. Il quale potrebbe essere integrato in applicazioni con diversa tecnologia di presentazione.
- Il codice di registrazione e l'implementazione degli eventi risulta uguale a quello di un applicazione single-thread, e quindi più semplice da scrivere e da leggere.
Di seguito riporto l'immagine dell'applicazione di esempio correttamente funzionante.