Я разрабатываю проект на WPF и использую паттерн Репозиторий с двумя готовыми репозиториями, не поддерживающими асинхронность. Я хочу добавить асинхронность, но меня беспокоит следующая ситуация: когда пользователь нажимает кнопку "Сохранить", запускается асинхронная операция, и в это время он может попытаться изменить данные в ячейках, где они вводились. Я подумал, что использование ConcurrentBag могло бы решить эту проблему. Я написал небольшой тест с ним: <pre><code class="cs">Person person = new Person(); person.Name = "Denis"; var list = new ConcurrentBag<Person>(); list.Add(person); var task = ChangeVariableAsync(list); // задача начинает выполняться var el = list.Last(p => p.Name == "Denis"); el.Name = "Dmitry"; Console.WriteLine($"New name: {el.Name}"); await task; // ожидаем завершения задачи // определение асинхронного метода async Task ChangeVariableAsync(ConcurrentBag<Person> people) { await Task.Delay(5000); try { var esl = list.Last(p => p.Name == "Denis"); esl.Name += "s"; Console.WriteLine($"Old name: {esl.Name}"); } catch (Exception) { var esl = list.Last(p => p.Name == "Dmitry"); Console.WriteLine($"Old name: {esl.Name}"); } } class Person { public string Name { get; set; } }</code></pre> Тем не менее, данные всё равно изменяются. Как можно предотвратить изменения данных пользователем во время выполнения асинхронного метода, не теряя при этом преимущества асинхронности?
Для предотвращения изменения данных пользователем во время выполнения асинхронного метода сохранения в 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)); }