2013-08-06 2 views
5

Dans mon application MVVM, mon modèle de vue appelle 3 méthodes de service différentes, convertit les données de chacune dans un format commun, puis met à jour l'interface utilisateur à l'aide de notifications de propriété/collections observables. Task et renvoie le Task au modèle de vue. Voici un exemple d'une de mes méthodes de service.Comment continuer après plusieurs tâches sans bloquer le thread de l'interface utilisateur?

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; 
} 
} 

Voici le code d'appel et d'autres parties pertinentes du modèle de vue ...

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 
    }); 
} 

Ce presque fonctionne, mais il y a quelques petits problèmes.

Numéro 1: Dans l'appel à SetBusy au tout début, avant de commencer des tâches et avant d'appeler WaitAll, j'ai défini la propriété IsBusy sur true. Cela devrait mettre à jour l'interface utilisateur et afficher le contrôle BusyIndicator, mais cela ne fonctionne pas. J'ai également essayé d'ajouter des propriétés de chaîne simples et de les lier et elles ne sont pas mises à jour non plus. La fonctionnalité IsBusy fait partie d'une classe de base et fonctionne dans d'autres modèles d'affichage dans lesquels aucune tâche n'est en cours d'exécution, donc je ne crois pas qu'il y ait un problème avec la notification de propriété ou la liaison de données dans le XAML.

Toutes les liaisons de données semblent être mises à jour une fois la méthode terminée. Je ne vois aucune «première exception» ou erreur de liaison dans la fenêtre de sortie, ce qui me porte à croire que le thread d'interface utilisateur est en quelque sorte bloqué avant l'appel à WaitAll.

Numéro 2: Je semble renvoyer les mauvaises tâches à partir des méthodes de service. Je veux que tout après WaitAll s'exécute après que le modèle de vue a converti tous les résultats de toutes les méthodes de service dans les rappels. Cependant, si je retourne la tâche de continuation de la méthode de service, la continuation ne sera jamais appelée et WaitAll attendra indéfiniment. La chose étrange est le contrôle de l'interface utilisateur lié à ICollectionView affiche effectivement tout correctement, je suppose que c'est parce que les données sont une collection observable et le CollectionViewSource est conscient de la collection a changé les événements.

Répondre

9

Vous pouvez utiliser TaskFactory.ContinueWhenAll pour créer une continuation qui s'exécute lorsque les tâches d'entrée sont toutes terminées.

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()); 

Notez que cela devient plus simple si vous utilisez C# 5 de await/async syntaxe:

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; 
} 

Toutefois, si je retourne la tâche de continuation de la méthode de service la poursuite ne sera jamais appelé et attendreAll attend éternellement

Le problème est que votre tâche de continuation nécessite le thread d'interface utilisateur et que vous bloquez le thread d'interface utilisateur dans l'appel WaitAll. Cela crée un blocage qui ne résout pas. La correction de ce qui précède devrait corriger ceci - vous voudrez retourner la continuation en tant que tâche, car c'est ce que vous devez attendre pour l'achèvement - mais en utilisant TaskFactory.ContinueWhenAll vous libérez le thread d'interface utilisateur afin qu'il puisse traiter ces continuations.

Notez que ceci est une autre chose qui se simplifié avec C# 5. Vous pouvez écrire vos autres méthodes:

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); 
} 

Cela étant dit, il est généralement préférable d'écrire les méthodes pour retourner un Task<T> au lieu de fournir rappels, car cela simplifie les deux extrémités de l'utilisation.

+0

Je repousse l'utilisation de l'async/await car je ne suis pas sûr que cela fonctionne "simplement" avec les commandes WPF et le Prism DelegateCommand. Task.ContinueWhenAll ne semble pas exister pour moi, dois-je référencer certaines bibliothèques d'extensions TPL? – BenCr

+0

@BenCr Désolé, c'est TaskFactory.ContinueWhenAll. En général, l'attente/async fonctionne mieux que les suites de tâches avec WPF. La principale différence est que vous n'avez pas à sauter à travers des cerceaux fous pour gérer les exceptions. –

+0

Merci Reed, cela semble fonctionner beaucoup mieux. Espérons que quelqu'un puisse fournir une explication pour le blocage avant que WaitAll soit appelé, mais cela a certainement corrigé les problèmes 1 et 2. Je vais donner un async/wait à la prochaine itération et voir comment je m'entends. – BenCr

Questions connexes