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

Для целей этого вопроса у меня есть простой 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, чтобы использовать семафор, как описано здесь. Это действительно работает, но я не уверен, что это лучший способ решить эту проблему.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
107
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

Я рекомендую использовать простое решение 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}");
        }
}

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

Это работает, мне просто нужно было поместить Debug.WriteLine внутри оператора using, чтобы вывод «Starting» отображался правильно. В противном случае две «Начальные» строки печатаются одна за другой.

redcurry 13.03.2019 15:21

@redcurry Вы совершенно правы. Я собираюсь отредактировать его для будущих поисков.

Albert Alonso 13.03.2019 15:36

Is there a good practice for dealing with this kind of issue?

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

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

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

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

redcurry 13.03.2019 15:27

Это отличный образец для Rx. Если вы не хотите использовать Реактивный интерфейс (который может легко жить рядом с 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, которая из коробки поддерживает асинхронную работу, в том числе и отключение команды во время работы.

Очень интересно! Я обязательно проверю ReactiveUI. Спасибо.

redcurry 13.03.2019 17:23

Другие вопросы по теме