2013-08-06 14 views
5

Nella mia applicazione MVVM mio avviso modello prevede 3 diversi metodi di servizio, converte i dati da ciascuno in un formato comune e quindi aggiorna l'interfaccia utente mediante notifica proprietà/collezioni osservabili eccCome continuare dopo più attività senza bloccare il thread dell'interfaccia utente?

Ogni metodo nel livello di servizio inizia una nuova Task e restituisce Task al modello di visualizzazione. Ecco un esempio di uno dei miei metodi di servizio.

public class ResourceService 
{ 
internal static Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback) 
{ 
    var t = Task.Factory.StartNew(() => 
    { 
     //... get resources from somewhere 
     return resources; 
    }); 

    t.ContinueWith(task => 
    { 
     if (task.IsFaulted) 
     { 
      errorCallback(task.Exception); 
      return; 
     } 
     completedCallback(task.Result); 
    }, TaskScheduler.FromCurrentSynchronizationContext()); 

    return t; 
} 
} 

Ecco il codice chiamante e in altre parti rilevanti del modello di vista ...

private ObservableCollection<DataItem> Data = new ObservableCollection<DataItem>(); 

public ICollectionView DataView 
{ 
    get { return _dataView; } 
    set 
    { 
     if (_dataView != value) 
     { 
      _dataView = value; 
      RaisePropertyChange(() => DataView); 
     } 
    } 
} 

private void LoadData() 
{ 
    SetBusy("Loading..."); 

    Data.Clear(); 

    Task[] tasks = new Task[3] 
    { 
     LoadTools(), 
     LoadResources(), 
     LoadPersonel() 
    }; 

    Task.WaitAll(tasks); 

    DataView = CollectionViewSource.GetDefaultView(Data); 
    DataView.Filter = FilterTimelineData; 

    IsBusy = false; 
} 

private Task LoadResources() 
{ 
    return ResourceService.LoadResources(resources => 
    { 
     foreach(var r in resources) 
     { 
      var d = convertResource(r); 
      Data.Add(d); 
     } 
    }, 
    error => 
    { 
     // do some error handling 
    }); 
} 

Questo quasi opere, ma ci sono un paio di piccoli problemi.

Numero 1: nella chiamata a SetBusy all'inizio, prima di iniziare qualsiasi attività e prima chiamo WaitAll, ho impostato la proprietà su true. Questo dovrebbe aggiornare l'interfaccia utente e mostrare il controllo BusyIndicator ma non funziona. Ho anche provato ad aggiungere semplici proprietà stringa e legare quelle e non vengono aggiornate neanche. La funzionalità IsBusy fa parte di una classe base e funziona in altri modelli di viste in cui non ho più di una attività in esecuzione, quindi non credo che ci sia un problema con la notifica di proprietà o l'associazione dati in XAML.

Tutte le associazioni di dati sembrano essere aggiornate dopo che l'intero metodo è stato completato. Non vedo alcuna "prima volta eccezioni" o errori di binding nella finestra di output che mi porta a credere che il thread dell'interfaccia utente sia in qualche modo bloccato prima della chiamata a WaitAll.

Numero 2: Mi sembra di restituire le attività sbagliate dai metodi di servizio. Voglio che dopo il WaitAll venga eseguito dopo che il modello di visualizzazione ha convertito tutti i risultati di tutti i metodi di servizio nei callback. Tuttavia, se restituisco l'attività di continuazione dal metodo di servizio, la continuazione non viene mai chiamata e WaitAll attende per sempre. La cosa strana è che il controllo dell'interfaccia utente associato a ICollectionView visualizza effettivamente tutto correttamente, presumo che questo sia dovuto al fatto che Data è una raccolta osservabile e che CollectionViewSource è a conoscenza degli eventi modificati dalla raccolta.

risposta

9

È possibile utilizzare TaskFactory.ContinueWhenAll per creare una continuazione che viene eseguita quando tutte le attività di input sono state completate.

Task[] tasks = new Task[3] 
{ 
    LoadTools(), 
    LoadResources(), 
    LoadPersonel() 
}; 

Task.Factory.ContinueWhenAll(tasks, t => 
{ 
    DataView = CollectionViewSource.GetDefaultView(Data); 
    DataView.Filter = FilterTimelineData; 

    IsBusy = false; 
}, CancellationToken.None, TaskContinuationOptions.None, 
    TaskScheduler.FromCurrentSynchronizationContext()); 

Si noti che questo diventa più semplice se si utilizza C# 5 di await/async sintassi:

private async void LoadData() 
{ 
    SetBusy("Loading..."); 

    Data.Clear(); 

    Task[] tasks = new Task[3] 
    { 
     LoadTools(), 
     LoadResources(), 
     LoadPersonel() 
    }; 

    await Task.WhenAll(tasks); 

    DataView = CollectionViewSource.GetDefaultView(Data); 
    DataView.Filter = FilterTimelineData; 

    IsBusy = false; 
} 

Tuttavia, se torno il compito prosecuzione dal metodo del servizio la continuazione non viene chiamato e WaitAll attende per sempre

Il problema è che l'attività di continuazione richiede il thread dell'interfaccia utente e stai bloccando il thread dell'interfaccia utente nella chiamata WaitAll. Questo crea un deadlock che non si risolverà.

La correzione di quanto sopra deve risolvere il problema: è necessario restituire Continuazione come attività, poiché è necessario attendere il completamento, ma utilizzando TaskFactory.ContinueWhenAll si libera il thread dell'interfaccia utente in modo che possa elaborare tali continuazioni.

Si noti che questa è un'altra cosa che viene semplificato con C# 5. È possibile scrivere i vostri altri metodi come:

internal static async Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback) 
{ 
    try 
    { 
    await Task.Run(() => 
    { 
     //... get resources from somewhere 
     return resources; 
    }); 
    } 
    catch (Exception e) 
    { 
    errorCallback(task.Exception); 
    } 

    completedCallback(task.Result); 
} 

Detto questo, è in genere meglio scrivere i metodi per restituire un Task<T> invece di fornire callback, in quanto semplifica entrambe le estremità dell'utilizzo.

+0

Sto rimandando usando async/await perché non sono sicuro che funzioni "solo con i comandi WPF e Prism DelegateCommand. Task.ContinueWhenAll non sembra esistere per me, devo fare riferimento ad alcune librerie di estensioni TPL? – BenCr

+0

@BenCr Sorry -it TaskFactory.ContinueWhenAll. In generale, le cose attese/asincrone funzionano * meglio * rispetto alle continue attività con WPF. La differenza principale è che non devi saltare attraverso i circuiti impazziti per gestire le eccezioni. –

+0

Grazie Reed, sembra che funzioni molto meglio. Spero che qualcuno possa essere in grado di fornire una spiegazione per il blocco prima che fosse chiamato WaitAll, ma questo ha certamente risolto i problemi 1 e 2. Darò le cose asincrone/attese alla prossima iterazione e vedrò come vado avanti. – BenCr

Problemi correlati