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





Как вы сказали, я бы также сказал, что блокировка - лучший метод.
Я рекомендую использовать простое решение 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}");
}
}
что на самом деле не сильно влияет на читабельность кода, и вы просто получаете очередь асинхронных методов.
@redcurry Вы совершенно правы. Я собираюсь отредактировать его для будущих поисков.
Is there a good practice for dealing with this kind of issue?
Если вы хотите, чтобы оба сохранения произошли, то сериализация их с помощью блокировки (или SemaphoreSlim) — это способ сделать это. Если вы хотите предотвратить запуск второго сохранения, то обычным подходом является отключение этих элементов управления во время выполнения сохранения, например, с помощью свойства IsBusy, которое привязано к данным вашего пользовательского интерфейса.
Предостережения:
IsBusy синхронно. Это немедленно отключает элементы управления.IsBusy в finally, чтобы гарантировать, что он всегда будет отключен, даже если возникнут ошибки.Причина, по которой я не могу использовать подход IsBusy в этом конкретном случае, заключается в том, что текст не обновляется с помощью привязки данных до, флажок установлен, поэтому и текст, и состояние флажка обновляются с помощью взаимодействия с пользователем Один. . Другими словами, отключать флажок было бы слишком поздно.
Это отличный образец для 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. Спасибо.
Это работает, мне просто нужно было поместить
Debug.WriteLineвнутри оператораusing, чтобы вывод «Starting» отображался правильно. В противном случае две «Начальные» строки печатаются одна за другой.