Как я могу правильно выполнить асинхронный метод из ViewModel? - PullRequest
1 голос
/ 13 марта 2019

Для целей этого вопроса у меня есть простой Window со следующим XAML:

<StackPanel>
    <TextBox Text="{Binding MyText}" />
    <CheckBox IsChecked="{Binding IsChecked}">Check</CheckBox>
</StackPanel>

Всякий раз, когда пользователи вводят текст в TextBox или проверяют CheckBox, я хотел бы выполнить медленную задачу (например, сохранить состояние моей модели на диск). Вот вид модели:

public class ViewModel : ViewModelBase  // using GalaSoft.MvvmLight
{
    private string _myText;
    public string MyText
    {
        get => _myText;
        set
        {
            if (Set(ref _myText, value))
                Save();
        }
    }

    private bool _isChecked;
    public bool IsChecked
    {
        get => _isChecked;
        set
        {
            if (Set(ref _isChecked, value))
                Save();
        }
    }

    private async void Save()
    {
        var id = Guid.NewGuid();
        Debug.WriteLine($"Starting save {id}");
        await Task.Delay(100);  // Simulate slow task
        Debug.WriteLine($"Finished save {id}");
    }
}

Метод Save имитирует медленную задачу, такую ​​как сохранение на диск. В целях отладки он выводит уникальный идентификатор до и после выполнения этой операции. Кроме того, метод является асинхронным, потому что я не хочу, чтобы пользовательский интерфейс зависал во время операции.

Проблема в том, что после того, как пользователь что-то наберет в TextBox, а затем проверит CheckBox, свойства обновляются очень быстро. Это приводит к следующему выводу примера отладки:

Starting save 6ea6c102-cbe7-472f-b8b8-249499ff7f64
Starting save c77b4478-14ca-4243-a45b-7b35b5663d49
Finished save 6ea6c102-cbe7-472f-b8b8-249499ff7f64
Finished save c77b4478-14ca-4243-a45b-7b35b5663d49

Как видите, первая операция сохранения (с MyText) не выполняется до начала второй операции сохранения (с IsChecked). Это немного пугает меня, потому что, я думаю, данные могут быть сохранены в неправильном порядке и повреждены.

Есть ли хорошая практика для решения такого рода проблем?

Я думал о паре возможных решений. Первый - использовать что-то вроде Delay=100 в привязке TextBox. Это приведет к вызову метода Save после того, как пользователь перестанет набирать текст в течение 100 мс. Это уродливое решение по разным причинам.

Второй - использовать SemaphoreSlim. Внутри метода Save я могу окружить код try / finally, чтобы использовать семафор, как описано здесь . Это на самом деле работает, но я не уверен, что это лучший способ справиться с этой проблемой.

Ответы [ 3 ]

2 голосов
/ 13 марта 2019

Есть ли эффективная практика для решения такого рода проблем?

Если вы хотите, чтобы оба сохранения происходили, то их сериализация с блокировкой (или SemaphoreSlim) являетсяспособ сделать.Если вы хотите предотвратить запуск второго сохранения, то обычным подходом является отключение этих элементов управления во время сохранения, например, через свойство IsBusy, которое привязано к данным вашего пользовательского интерфейса.

Предостережения:

  • Установите свойство IsBusy синхронно.Это немедленно отключает элементы управления.
  • Unset IsBusy в finally, чтобы гарантировать, что оно всегда сбрасывается, даже если возникают ошибки.
1 голос
/ 13 марта 2019

Это отличный пример для Rx. Если вы не хотите использовать ReactiveUI (который может легко жить рядом с MVVMLight), все, что вам нужно, - это сигнал об изменении свойства.

Использование RxUI:

this.WhenAnyValue(x => x.MyText, x => x.IsChecked) // this you will need to emulate if you don't want RxUI
.Throttle(TimeSpan.FromMilliseconds(150)) // wait for 150ms after last signal, if there isn't any, send your own further into pipeline
.Synchronize()
.Do(async _ => await Save()) // we have to await so that Synchronize can work
.Subscribe();

Это будет ждать 150 мс после последнего изменения MyText или IsChecked, а затем выполнить сохранение один раз.

Кроме того, RxUI имеет очень умную реализацию ICommand, которая поддерживает асинхронную работу "из коробки", включая отключение команды во время работы.

1 голос
/ 13 марта 2019

Как вы сказали, я бы также сказал, что блокировка - лучший метод.

Я рекомендую использовать прямолинейное решение Stepehen Cleary: https://github.com/StephenCleary/AsyncEx

Следовательно, ваш код будет выглядеть как

private readonly AsyncLock _lock = new AsyncLock();
private async void Save()
{
    var id = Guid.NewGuid();

    using (await _lock.LockAsync())
        {
            // It's safe to await while the lock is held
            Debug.WriteLine($"Starting save {id}");

            await Task.Delay(100);  // Simulate slow task

            Debug.WriteLine($"Finished save {id}");
        }
}

, что не сильно влияет на читабельность кода, и вы просто получаете очередь асинхронных методов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...