Spesso, quando si lavora con applicazioni multithread (soprattutto in applicazioni Windows Form), siamo abituati a ricorrere alla proprietà InvokeRequierd dei controlli per verificare se il thread chiamante è proprietario del controllo o se lo è un altro, e nel caso, utilizzare un delegate per utilizzare la proprietà del controllo a cui siamo interessati. Per un elemento WPF dobbiamo utilizzare un’istanza di della classe System.Windows.Threading.Dispatcher, presente tra le proprietà che un elemento WPF espone. Ad esempio, supponiamo di utilizzare un’istanza della classe System.Timers.Timer per visualizzare l’ora corrente in un elemento Label:
1: _timer = new System.Timers.Timer();
2: _timer.Interval = 1000;
3: _timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
4: _timer.Start();
La variabile _timer è dichiarata a livello di modulo nel code-behind di una Window WPF. Se nel gestore dell’evento Elapsed del Timer scrivessimo del codice simile al seguente:
1: void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
2: {
3: label1.Content = DateTime.Now.ToLongTimeString();
4: }
Verrebbe sollevata un’eccezione del tipo InvalidOperationException riportante il seguente messaggio: “The calling thread cannot access this object because a different thread owns it.”
Proviamo a scrivere un po’ di codice per utilizzare il Dispatcher:
1: public delegate void onTickTimerDelegate();
2:
3: private void UpdateTimeLabel()
4: {
5: if (label1.Dispatcher.Thread == Thread.CurrentThread)
6: {
7:
8: label1.Content = DateTime.Now.ToLongTimeString();
9: }
10: else
11: {
12: onTickTimerDelegate tickTimerDelegate = new onTickTimerDelegate(UpdateTimeLabel);
13: label1.Dispatcher.BeginInvoke(tickTimerDelegate,
14: System.Windows.Threading.DispatcherPriority.Normal,
15: null);
16: }
17: }
Se scriviamo il gestore di Elapsed in questo modo:
1: void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
2: {
3: UpdateTimeLabel();
4: }
Tutto funziona correttamente. Facciamo un ulteriore passo e modifichiamo il codice in modo che la il nostro metodo (e associato delegate) accetti anche un parametro:
1: public delegate void onTickTimerDelegateWithParameter(object par);
2: private void UpdateTimeLabel(object par)
3: {
4: if (label1.Dispatcher.Thread == Thread.CurrentThread)
5: {
6: label1.Content = DateTime.Now.ToLongTimeString() + " PAR: " + par.ToString();
7: }
8: else
9: {
10: onTickTimerDelegateWithParameter tickTimerDelegateWithParameter = new onTickTimerDelegateWithParameter(UpdateTimeLabel);
11: label1.Dispatcher.BeginInvoke(tickTimerDelegateWithParameter,
12: System.Windows.Threading.DispatcherPriority.Normal,
13: par);
14: }
15: }
In questo caso, nella contenuto della label visualizzeremo una stringa rappresentante l’ora (ore, minuti e secondi) concatenata ad una stringa che “rappresenta” l’oggetto passato come argomento. Ad esempio, se scrivessimo:
1: void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
2: {
3: UpdateTimeLabel(new Random().Next());
4: }
Oltre l’ora, verrebbe visualizzato un numero (intero) generato dalla classe Random.
Citando MSDN, la classe Dispatcher rappresenta una coda con priorità dei work item associati ad uno specifico thread. In particolare, in WPF, un oggetto Dispatcher può essere acceduto solo dal relativo Dispatcher: ad esempio un thread di Background non può essere utilizzato per aggiornare il contenuto di un Button (o una Label come nel caso specifico del post), perché quest’ultimo è associato con il Dispatcher dell’interfaccia utente (sollevando l’eccezione precedentemente descritta). Possiamo quindi utilizzre Invoke e BeginInvoke secondo se vogliamo utilizzare rispettivamente un approccio sincrono o asincrono per l’esecuzione dell’operazione da noi richiesta. Quando aggiungiamo le operazioni ad una coda è importante specificare con quale priorità vogliamo aggiungerla rispetto alle altre operazioni presenti: per far questo utilizziamo l’enum System.Windows.Threading.DispatcherPriority. I valori dell’enum variano da 0 a 10 (in verità da –1 a 10), in ordine di priorità crescente:
- 0, l’operazione non è processata (Inactive)
- 1, l’operazione è processata quando il sistema è idle (SystemIdle)
- 2, l’operazione è processata quanto l’applicazione è idle (ApplicationIdle)
- 3, l’operazione è processata dopo che le operazioni di background sono state completate (ContextIdle)
- 4, le operazioni sono processate dopo che tutte le operazioni non-idle sono state completate (Background)
- 5, le operazioni sono processate con la stessa priorità dell’input (Input)
- 6, le operazioni sono processate quando il layout ed il render hanno terminato, ma prima delle operazioni Input
- 7, le operazioni sono processate con la stessa priorità del Render (Render)
- 8, le operazioni sono processate con la stessa priorità del Data Binding (DataBinding)
- 9, le operazioni sono processate a priorità normale (tipica dell’applicazione) (Normal)
- 10, le operazioni sono processare prima delle operazioni asincrone, rappresenta la priorità più alta (Send)