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

Я разрабатываю проект на WPF и использую паттерн Репозиторий с двумя готовыми репозиториями, не поддерживающими асинхронность. Я хочу добавить асинхронность, но меня беспокоит следующая ситуация: когда пользователь нажимает кнопку "Сохранить", запускается асинхронная операция, и в это время он может попытаться изменить данные в ячейках, где они вводились. Я подумал, что использование ConcurrentBag могло бы решить эту проблему. Я написал небольшой тест с ним:

<pre><code class="cs">Person person = new Person();
person.Name = "Denis";

var list = new ConcurrentBag&lt;Person&gt;();
list.Add(person);

var task = ChangeVariableAsync(list); // задача начинает выполняться
var el = list.Last(p =&gt; p.Name == "Denis");
el.Name = "Dmitry";
Console.WriteLine($"New name: {el.Name}");

await task; // ожидаем завершения задачи

// определение асинхронного метода
async Task ChangeVariableAsync(ConcurrentBag&lt;Person&gt; people)
{
    await Task.Delay(5000);
    try
    {
        var esl = list.Last(p =&gt; p.Name == "Denis");
        esl.Name += "s";
        Console.WriteLine($"Old name: {esl.Name}");
    }
    catch (Exception)
    {
        var esl = list.Last(p =&gt; p.Name == "Dmitry");
        Console.WriteLine($"Old name: {esl.Name}");
    }
}

class Person
{
    public string Name { get; set; }
}</code></pre>

Тем не менее, данные всё равно изменяются. Как можно предотвратить изменения данных пользователем во время выполнения асинхронного метода, не теряя при этом преимущества асинхронности?
  • 18 сентября 2024 г. 13:53
Ответы на вопрос 4
Для предотвращения изменения данных пользователем во время выполнения асинхронного метода сохранения в WPF существует несколько подходов. Приведу несколько из них:

### 1. Блокировка интерфейса
Вы можете временно заблокировать интерфейс во время выполнения асинхронной операции. Это можно сделать, например, с помощью стандартных методов WPF. Вы можете использовать `IsEnabled` свойство основного окна или определенного элемента управления, чтобы блокировать его, пока идет процесс сохранения.

```csharp
private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
    SaveButton.IsEnabled = false; // Блокируем кнопку
    // Другие элементы управления, которые следует заблокировать,
    // также можно отключить здесь.

    await SaveDataAsync();

    SaveButton.IsEnabled = true; // Разблокируем кнопку
}
```

### 2. Использование `Loading` индикатора
Кроме блокировки элементов управления, вы можете показать индикатор загрузки, чтобы дать понять пользователю, что операция выполняется. Это можно сделать с помощью `ProgressBar`, который будет активен во время сохранения.

```xml
<ProgressBar x:Name="LoadingIndicator" IsIndeterminate="True" Visibility="Collapsed" />
```

В коде:

```csharp
private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
    LoadingIndicator.Visibility = Visibility.Visible; // Показываем индикатор
    SaveButton.IsEnabled = false; // Блокируем кнопку

    await SaveDataAsync();

    LoadingIndicator.Visibility = Visibility.Collapsed; // Скрываем индикатор
    SaveButton.IsEnabled = true; // Разблокируем кнопку
}
```

### 3. Использование Флагов
Вы можете также использовать простую блокировку с помощью булевого флага, чтобы отслеживать, выполняется ли уже операция:

```csharp
private bool isSaving = false;

private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
    if (isSaving) return; // Если уже сохраняем, ничего не делаем

    isSaving = true; // Устанавливаем флаг, что сохраняем
    await SaveDataAsync();
    isSaving = false; // Сбрасываем флаг
}
```

### 4. Настройка валидации данных
Даже если возможны изменения в интерфейсе, вы можете установить валидацию данных (например, при нажатии на кнопку "Сохранить"), чтобы проверить, нет ли изменений в данных. Хотя это не предотвращает изменения, это позволяет обработать возможные конфликты.

### Заключение
Выбор подхода зависит от вашего конкретного случая. В большинстве случаев блокировка интерфейса и отображение индикатора загрузки являются лучшими практиками, так как они обеспечивают пользователю хороший опыт работы с приложением и предотвращают возможные ошибки, связанные с изменением данных во время выполнения асинхронных операций.
Простейший вариант: копируйте (синхронно) в обработчике нажатия кнопки данные, которые вы сохраняете, в другое место, а там пользователь пусть хоть обизменяется: на сохраняемые данные это уже не повлияет. Если данных вдруг много, то тогда уже придется синхронизировать доступ к ним (для асинхронной синхронизации подходит SemaphoreSlim.WaitAsync/Release, а если вы можете позволить себе блокировку потока UI, то и lock сойдет). Есть и более сложные схемы - копировать только те данные, которые пользователь собрался изменить, и сохранять в таком случае именно их, а не данные из ViewModel ("копирование при записи"). Короче, дерзайте: вариантов много. 
Ну, а вариант с ползунком сам по себе ненадежен: пользователю доверять нельзя. Но как подспорье, вместе с блокировкой изменения - годится.
Данные погут меняться только от действий пользователя. Так что включите режим read only пока идет сохранение и никто ничего поменять не сможет
Всё зависит от конкретной ситуации... 
На мой взгляд, наиболее простым и действенным решением будет временное отключение элементов управления, которые позволяют пользователю менять данные. Отключаете элементы интерфейса пользователю, пока выполняется асинхронный процесс сохранения, и будет вам счастье.

В XAML вы можете сделать что-то вроде этого:
<Button Content="Сохранить" Command="{Binding SaveCommand}" IsEnabled="{Binding IsSaving}" />
<TextBox Text="{Binding Person.Name}" IsEnabled="{Binding IsSaving, Converter={StaticResource InverseBoolConverter}}" />


В ViewModel не забудьте добавить свойство IsSaving, которое будет управлять состоянием элементов:
public bool IsSaving { get; private set; }

private async Task SaveDataAsync()
{
    IsSaving = false;
    RaisePropertyChanged(nameof(IsSaving));

    await repository.SaveAsync(person);

    IsSaving = true;
    RaisePropertyChanged(nameof(IsSaving));
}
Похожие вопросы