WPF: как синхронизировать asyn c загруженные списки - PullRequest
0 голосов
/ 09 апреля 2020

У меня есть вопрос о синхронизации между загрузкой ресурсов asyn c и сохранением выбранного элемента в правильном загруженном ресурсе. Чтобы быть ценой, у меня есть список с пользователями и одна панель с его профилем. Если я выберу этого пользователя, он будет загружен из веб-сервиса, и после этого его данные будут показаны на этой профильной панели. Загрузка пользователя может быть очень дорогой операцией (временем), поэтому я попытался сделать эту загрузку асинхронной c, чтобы предотвратить блокирование всего потока пользовательского интерфейса. Я написал в ItemChange-Event что-то вроде этого ->

ItemChangeEvent(){
   Task.Factory.StartNew(()=>{
      .. load profile from Server
      this.Dispatcher.Invoke(.. some UI changes);
   });
}

Теперь иногда случается, что пользователь, которого я выбрал в этом списке, не является пользователем, который отображается в профиле. Я предполагаю, что любая из задач откладывается и подталкивает свой контент после того, как «правильная» задача профиля пользователя завершена. Итак, как я могу добиться, чтобы загрузка была асинхронной c, но синхронизирована с текущим выбранным элементом?

Ответы [ 2 ]

1 голос
/ 09 апреля 2020

Вы можете добавить CancellationTokenSource во внешнюю область и сохранить CancellationToken в локальной переменной внутри обработчика событий. В идеале этот токен должен передаваться и использоваться методом, который выбирает профиль с удаленного сервера, чтобы избежать выполнения текущими задачами выборки данных, которые больше не нужны.

Также вместо использования неудобного Dispatcher.Invoke для переключения Возвращаясь к теме пользовательского интерфейса, вы можете воспользоваться современным и аккуратным подходом asyn c -await . Код после await автоматически продолжается в потоке пользовательского интерфейса без необходимости делать что-то особенное, кроме добавления ключевого слова async в обработчик событий:

private CancellationTokenSource _itemChangeTokenSource;

private async void ListView1_ItemChange(object sender, EventArgs e)
{
    _itemChangeTokenSource?.Cancel();
    _itemChangeTokenSource = new CancellationTokenSource();
    CancellationToken token = _itemChangeTokenSource.Token;
    var id = GetSelectedId(ListView1);
    Profile profile;
    try
    {
        profile = await Task.Run(() =>
        {
            return GetProfile(id, token); // Expensive operation
        }, token);
        token.ThrowIfCancellationRequested();
    }
    catch (OperationCanceledException)
    {
        return; // Nothing to do, this event was canceled
    }
    UpdatePanel(profile); 
}

Было бы еще лучше, если бы дорогая операция может стать асинхронным. Таким образом, вы бы не блокировали поток ThreadPool каждый раз, когда пользователь нажимал на элемент управления ListView.

profile = await Task.Run(async () =>
{
    return await GetProfileAsync(id, token); // Expensive asynchronous operation
}, token);

Обновление: I сделал попытку инкапсулировать логику, связанную с отменой c, внутри класса, чтобы такая же функциональность могла быть достигнута с меньшим количеством строк кода. Может быть заманчиво уменьшить этот код в случае, если он повторяется несколько раз в одном и том же окне или несколько windows. Класс называется CancelableExecution и имеет единственный метод Run, который принимает отменяемую операцию в виде параметра Func<CancellationToken, T>. Вот пример использования этого класса:

private CancelableExecution _updatePanelCancelableExecution = new CancelableExecution();

private async void ListView1_ItemChange(object sender, EventArgs e)
{
    var id = GetSelectedId(ListView1);
    if (await _updatePanelCancelableExecution.Run(cancellationToken =>
    {
        return GetProfile(id, cancellationToken); // Expensive operation
    }, out var profile))
    {
        UpdatePanel(await profile);
    }
}

Метод Run возвращает значение Task<bool>, которое имеет значение true, если операция была успешно завершена (не отменена). Результат успешной операции доступен через параметр out Task<T>. Этот API делает меньше кода, но также и менее читаемый код, поэтому используйте этот класс с осторожностью!

public class CancelableExecution
{
    private CancellationTokenSource _activeTokenSource;

    public Task<bool> RunAsync<T>(Func<CancellationToken, Task<T>> function,
        out Task<T> result)
    {
        var tokenSource = new CancellationTokenSource();
        var token = tokenSource.Token;
        var resultTcs = new TaskCompletionSource<T>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        result = resultTcs.Task;
        return ((Func<Task<bool>>)(async () =>
        {
            try
            {
                var oldTokenSource = Interlocked.Exchange(ref _activeTokenSource,
                    tokenSource);
                if (oldTokenSource != null)
                {
                    await Task.Run(() =>
                    {
                        oldTokenSource.Cancel(); // Potentially expensive
                    }).ConfigureAwait(false);
                    token.ThrowIfCancellationRequested();
                }
                var task = function(token);
                var result = await task.ConfigureAwait(false);
                token.ThrowIfCancellationRequested();
                resultTcs.SetResult(result);
                return true;
            }
            catch (OperationCanceledException ex) when (ex.CancellationToken == token)
            {
                resultTcs.SetCanceled();
                return false;
            }
            catch (Exception ex)
            {
                resultTcs.SetException(ex);
                throw;
            }
            finally
            {
                if (Interlocked.CompareExchange(
                    ref _activeTokenSource, null, tokenSource) == tokenSource)
                {
                    tokenSource.Dispose();
                }
            }
        }))();
    }
    public Task<bool> RunAsync<T>(Func<Task<T>> function, out Task<T> result)
    {
        return RunAsync(ct => function(), out result);
    }
    public Task<bool> Run<T>(Func<CancellationToken, T> function, out Task<T> result)
    {
        return RunAsync(ct => Task.Run(() => function(ct), ct), out result);
    }
    public Task<bool> Run<T>(Func<T> function, out Task<T> result)
    {
        return RunAsync(ct => Task.Run(() => function(), ct), out result);
    }
}
0 голосов
/ 09 апреля 2020

Я бы посоветовал вам использовать CancellationToken для отмены предыдущей задачи загрузки после выбора другого пользователя. Это может быть достигнуто в несколько шагов:

  1. Создать поле экземпляра CancellationTokenSource _tokenSource
  2. изменить ваш обработчик:
ItemChangeEvent(){
   // first, try to cancel previous event
   _tokenSource?.Cancel();
   // then, update token source; previous object will be collected eventually
   _tokenSource = new CancellationTokenSource();
   // finally, add cancellation token from token source to task creation
   Task.Factory.StartNew(()=>{
      .. load profile from Server
      this.Dispatcher.Invoke(.. some UI changes);
   }, _tokenSource.Token);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...