2013-08-06 28 views
5

W mojej aplikacji MVVM mój model widoku wywołuje 3 różne metody obsługi, konwertuje dane z każdego do wspólnego formatu, a następnie aktualizuje interfejs użytkownika za pomocą powiadomień o właściwościach/obserwowalnych kolekcji itp.Jak kontynuować po wielu zadaniach bez blokowania wątku interfejsu użytkownika?

Każda metoda w warstwie usługi rozpoczyna nową Task i zwraca Task do modelu widoku. Oto przykład jednej z moich metod obsługi.

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

Oto kod wywołujący i inne istotne elementy modelu widoku ...

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

to prawie działa, ale istnieje kilka małych problemów.

Numer 1: W wywołaniu SetBusy na samym początku, przed rozpoczęciem jakichkolwiek zadań i zanim zadzwonię pod numer WaitAll, ustawię właściwość IsBusy na true. To powinno zaktualizować interfejs użytkownika i pokazać kontrolkę BusyIndicator, ale nie działa. Próbowałem również dodawać proste właściwości łańcuchów i wiązać je, a także nie są one aktualizowane. Funkcja IsBusy jest częścią klasy bazowej i działa w innych modelach widoku, w których nie ma więcej niż jednego zadania, więc nie sądzę, aby wystąpił problem z powiadomieniem o właściwościach lub powiązaniem danych w XAML.

Wszystkie powiązania danych wydają się być aktualizowane po zakończeniu całej metody. Nie widzę żadnych "wyjątków po raz pierwszy" ani błędów wiązania w oknie wyjściowym, co prowadzi mnie do przekonania, że ​​wątek interfejsu użytkownika jest w jakiś sposób blokowany przed wywołaniem WaitAll.

Numer 2: Wydaje mi się, że zwracam niewłaściwe Zadania z metod serwisowych. Chcę, aby wszystko po WaitAll było uruchamiane po przekonwertowaniu przez model widoku wszystkich wyników ze wszystkich metod usługowych w wywołaniach zwrotnych. Jeśli jednak zwrócę zadanie kontynuacji z metody serwisowej, kontynuacja nigdy nie zostanie wywołana i WaitAll czeka na zawsze. Dziwną rzeczą jest to, że kontrolka interfejsu użytkownika powiązana z ICollectionView faktycznie wyświetla wszystko poprawnie. Zakładam, że dzieje się tak dlatego, że Data jest kolekcją obserwowalną, a CollectionViewSource jest świadomy zmiany zmienionych zdarzeń.

Odpowiedz

9

Możesz użyć TaskFactory.ContinueWhenAll, aby zbudować kontynuację, która będzie działać po zakończeniu wszystkich zadań wejściowych.

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

Zauważ, że to staje się prostsze, jeśli używasz C# 5 na await/async składnia:

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

Jednak jeśli wrócę zadanie kontynuacji od sposobu eksploatacji, kontynuacja nigdy nie jest wywoływana i WaitAll na zawsze czeka

Problem polega na tym, że zadanie kontynuacji wymaga wątku UI i blokujesz wątek interfejsu użytkownika w wywołaniu WaitAll. Tworzy to zakleszczenie, które nie zostanie rozwiązane.

Naprawienie powyższego powinno poprawić to - będziesz chciał zwrócić Kontynuację jako zadanie, ponieważ to jest to, co musisz poczekać na zakończenie - ale używając TaskFactory.ContinueWhenAll zwolnisz wątek UI, aby mógł przetworzyć te kontynuacje.

Należy pamiętać, że jest to kolejna rzecz, która zostaje uproszczona z C# 5. Możesz napisać swoje inne metody jak:

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

Powiedział, że jest to zwykle lepiej napisać metod zwrócić Task<T> zamiast zapewniać wywołania zwrotne, co upraszcza oba końce użytkowania.

+0

Odkładałem używanie async/czekam, ponieważ nie jestem pewien, czy to "działa" z Poleceniami WPF i Prism DelegateCommand. Task.ContinueWhenAll wydaje się nie istnieć dla mnie, czy muszę odwołać się do niektórych bibliotek rozszerzeń TPL? – BenCr

+0

@BenCr Przepraszamy - to jest TaskFactory.ContinueWhenAll. Ogólnie rzecz biorąc, rzeczy oczekujące/asynchroniczne działają * lepiej * niż kontynuacje zadań z WPF. Główną różnicą jest to, że nie musisz przeskakiwać przez szalone obręcze, aby poradzić sobie z wyjątkami. –

+0

Dzięki Reed, wygląda na to, że działa znacznie lepiej. Mam nadzieję, że ktoś może być w stanie podać wyjaśnienie blokady przed wywołaniem WaitAll, ale to z pewnością rozwiązało problemy 1 i 2. Dam ci asynchroniczne/oczekiwane rzeczy w następnej iteracji i zobaczę, jak sobie poradzę. – BenCr