In primo luogo - presumo che il thread che esegue il test abbia qualche meccanismo in grado di notificare in qualche modo i propri progressi (magari con event
o delegate
?)
Nello spirito di MVVM, considera un Model
che rappresenta lo stato e il progresso del test che puoi aggiornare dal thread di test.
Lo scopo principale di Model
sarebbe ricordare lo stato della barra di avanzamento dell'interfaccia utente (ad esempio, int
per una percentuale). Ad esempio:
public class TestModel : INotifyPropertyChanged
{
private int _progressPercentage;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public int ProgressPercentage
{
get { return _progressPercentage; }
set
{
OnPropertyChanged();
_progressPercentage = value;
}
}
}
Con i framework UI basati su MVVM (come Silverlight, WPF, ecc.) è necessario un modo per garantire che tutti gli aggiornamenti che interessano lo stato dell'interfaccia utente (e quindi le proprietà su Model
) vengano richiamati utilizzando Dispatcher
thread (il thread principale / dell'interfaccia utente).
(Related: link )
Potresti avere quanti thread di ThreadPool
vuoi, ma quei thread dovranno richiamare le modifiche allo stato dell'interfaccia utente sul thread Dispatcher
quando cambi le proprietà su Model
, poiché INotifyPropertyChanged
causa lo stato dell'interfaccia utente da modificare.
Inoltre, quando si ha a che fare con più componenti dello stesso tempo, una ObservableCollection<>
è una scelta tipica; dove ogni modello può legarsi a un componente Barra di avanzamento dell'interfaccia utente (ad es. <ListBox>
). Di nuovo, qualsiasi tipo di modifica come Add
o Remove
deve avvenire sul thread Dispatcher
.
Ecco un possibile esempio per un ViewModel in cui l'azione di iniziare un thread di test ha origine da un ICommand
(clic del pulsante forse?)
public class MyTestViewModel
{
public ObservableCollection<TestModel> AllRunningTests { get; set; } =
new ObservableCollection<TestModel>();
public ICommand RunTestCommand { get; set; }
public MyTestViewModel()
{
RunTestCommand = new DelegateCommand<object>(RunTest, _ => true);
}
private void RunTest(object parameter)
{
var testModel = new TestModel();
AllRunningTests.Add(testModel);
Task.Run(() =>
{
while (testModel.ProgressPercentage < 100)
{
Thread.Sleep(200); // Simulate an expensive process with a sleep
Dispatcher.Invoke(() => { testModel.ProgressPercentage += 5; });
}
});
}
}
Nota che Task.Run
esegue la sua azione su un thread del threadpool - Ulteriori informazioni sul blog di Stephen Cleary - Creazione compiti
Nessuno del codice sopra riportato trae vantaggio dalle capacità complete async
/ await
o Task
.
Tra i motivi per usare Task
invece di avviare un thread del threadpool, includi direttamente:
- Cancellazione dell'utente di un thread in esecuzione (
CancellationTokenSource
)
- Timeout (
Task.WhenAny
e Task.Delay
))
- Gestione semplificata delle eccezioni
- Restituzione di valori da un thread (
Task.TrySetResult
).
- Verifica del completamento del thread o dello stato di errore
Per queste cose potresti trovare utile il% co_de di Stephen: Programmazione asincrona: pattern per MVVM asincrono Applicazioni: comandi
Il AsyncCommand
può aiutare a semplificare il compito di "concatenare" più test dipendenti. Ad esempio, un elenco di oggetti AsyncCommand
.
Questo snippet presuppone che TestModel
si verifichi in un RunTestsAsync
, ogni AsyncCommand
viene eseguito al completamento dell'attività precedente, ma gli identificatori Task
/ async
impediscono il blocco del thread Dispatcher mentre ogni await
attende il completamento:
private async Task RunTestsAsync(object parameter)
{
var testQueue = GetSomeListOfTests();
foreach (var testModel in testQueue)
{
AllRunningTests.Add(testModel);
await Task.Run(() =>
{
while (testModel.ProgressPercentage < 100)
{
Thread.Sleep(200);
Dispatcher.Invoke(() => testModel.ProgressPercentage += 5);
}
});
}
}
Nota: i frammenti di codice sopra non vengono testati o controllati in un compilatore, ma i concetti sono più importanti del codice effettivo.
Le funzioni Task
, Task
e async
di C # possono essere un po 'pacchiane la prima volta che le vedi. Si consiglia di eseguire alcune letture aggiuntive e di provare esempi in un'applicazione di test per familiarizzare con il modo in cui funzionano nella pratica.
La cosa più importante da realizzare è che mentre% tradizionaleawait
blocca il thread dell'interfaccia utente, la parola chiave Thread.Wait
non bloccherà il thread dell'interfaccia utente ed è un modo molto elegante di gestire attività a esecuzione prolungata all'interno di un comando Clic pulsante o altro gestore di eventi GUI.