/
d4nil
/
Konenkov_2413_software_engineering_
Обзор
Документация
Войти
/
d4nil
/
Konenkov_2413_software_engineering_
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
Interface/ViewModels/MainWindowViewModel.cs
841 строка
28 KB
Konenkov Daniel
feat: изменить поведение окна создания и редактирования двигателя при ошибке на сервере - теперь оно не зыкрывается
23 дек 2025, 20:38
23 дек 2025, 20:38
c43ab00
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using Engines.Engines; using Interface.Models; using Interface.Services.Api; using Interface.ViewModels.EditEngine; using ReactiveUI; namespace Interface.ViewModels; /// <summary> /// ViewModel главного окна. /// </summary> public class MainWindowViewModel : ViewModelBase { /// <summary> /// Минимально допустимое значение мощности для фильтров. /// </summary> private const int PowerMinLimit = 1; /// <summary> /// Максимально допустимое значение мощности для фильтров. /// </summary> private const int PowerMaxLimit = 10_000; /// <summary> /// Шаг изменения мощности в контролах. /// </summary> private const int PowerStepValueConst = 10; /// <summary> /// Клиент для взаимодействия с сервером двигателей. /// </summary> private readonly EngineApiClient _apiClient; /// <summary> /// Текущий выбранный двигатель. /// </summary> private Engine? _selectedEngine; /// <summary> /// Полный список двигателей, полученных с сервера. /// </summary> private List<Engine> _allEngines = new(); /// <summary> /// Список двигателей после применения фильтров. /// </summary> private IReadOnlyList<Engine> _filteredEngines = Array.Empty<Engine>(); /// <summary> /// Значение фильтра по производителю. /// </summary> private string _manufacturerFilter = string.Empty; /// <summary> /// Нижняя граница фильтра по мощности. /// </summary> private int? _powerMin; /// <summary> /// Верхняя граница фильтра по мощности. /// </summary> private int? _powerMax; /// <summary> /// Токен отмены текущей операции фильтрации. /// </summary> private CancellationTokenSource? _filterCts; /// <summary> /// Текстовое описание применённых фильтров. /// </summary> private string _filterSummary = "Фильтр: не установлен"; /// <summary> /// Текущее текстовое состояние приложения. /// </summary> private string _status = "Загрузка..."; /// <summary> /// Флаг, показывающий, выполняется ли операция. /// </summary> private bool _isBusy; /// <summary> /// Признак того, что хранилище сервера повреждено и его можно очистить. /// </summary> private bool _canClearStorage; /// <summary> /// Флаг разрешения отправки невалидных запросов. /// </summary> private bool _allowInvalidRequests; /// <summary> /// Индекс выбранной фабрики. /// </summary> public int SelectedFactoryIndex { get => 0; set { } } /// <summary> /// Коллекция, отображаемая в таблице. /// </summary> public IReadOnlyList<Engine> FilteredEngines { get => _filteredEngines; private set => this.RaiseAndSetIfChanged(ref _filteredEngines, value); } /// <summary> /// Выбранный двигатель. /// </summary> public Engine? SelectedEngine { get => _selectedEngine; set { this.RaiseAndSetIfChanged(ref _selectedEngine, value); this.RaisePropertyChanged(nameof(CanEditOrDelete)); } } /// <summary> /// Флаг возможности редактирования/удаления. /// </summary> public bool CanEditOrDelete => SelectedEngine != null; /// <summary> /// Флаг выбранной фабрики. /// </summary> public bool IsFactorySelected => true; /// <summary> /// Флаг генерации тестовых записей. /// </summary> public bool IsGenerationInProgress => false; /// <summary> /// Фильтр по производителю. /// </summary> public string ManufacturerFilter { get => _manufacturerFilter; set => this.RaiseAndSetIfChanged(ref _manufacturerFilter, value); } /// <summary> /// Нижняя граница мощности. /// </summary> public int? PowerMin { get => _powerMin; set => this.RaiseAndSetIfChanged(ref _powerMin, value); } /// <summary> /// Верхняя граница мощности. /// </summary> public int? PowerMax { get => _powerMax; set => this.RaiseAndSetIfChanged(ref _powerMax, value); } /// <summary> /// Отображаемое описание текущего фильтра. /// </summary> public string CurrentFilterSummary { get => _filterSummary; private set => this.RaiseAndSetIfChanged(ref _filterSummary, value); } /// <summary> /// Минимально допустимое значение мощности. /// </summary> public int PowerMinBoundary => PowerMinLimit; /// <summary> /// Максимально допустимое значение мощности. /// </summary> public int PowerMaxBoundary => PowerMaxLimit; /// <summary> /// Шаг изменения мощности в UI. /// </summary> public int PowerStepValue => PowerStepValueConst; /// <summary> /// Взаимодействие с модальными окнами редактирования. /// </summary> public Interaction<ViewModelBase, Engine?> ShowEditEngineDialog { get; } /// <summary> /// Команда добавления двигателя внутреннего сгорания. /// </summary> public ReactiveCommand<Unit, Unit> AddInternalCombusEngineCommand { get; } /// <summary> /// Команда добавления реактивного двигателя. /// </summary> public ReactiveCommand<Unit, Unit> AddJetEngineCommand { get; } /// <summary> /// Команда редактирования выбранного двигателя. /// </summary> public ReactiveCommand<Unit, Unit> EditCommand { get; } /// <summary> /// Команда удаления выбранного двигателя. /// </summary> public ReactiveCommand<Unit, Unit> DeleteCommand { get; } /// <summary> /// Команда очистки хранилища после ошибки чтения. /// </summary> public ReactiveCommand<Unit, Unit> ClearStorageCommand { get; } /// <summary> /// Команда обновления списка двигателей с сервера. /// </summary> public ReactiveCommand<Unit, Unit> RefreshCommand { get; } /// <summary> /// Команда применения фильтров. /// </summary> public ReactiveCommand<Unit, Unit> ApplyFiltersCommand { get; } /// <summary> /// Команда сброса текущих фильтров. /// </summary> public ReactiveCommand<Unit, Unit> ResetFiltersCommand { get; } /// <summary> /// Текстовое описание текущего состояния. /// </summary> public string Status { get => _status; private set => this.RaiseAndSetIfChanged(ref _status, value); } /// <summary> /// Признак выполнения фоновой операции. /// </summary> public bool IsBusy { get => _isBusy; private set => this.RaiseAndSetIfChanged(ref _isBusy, value); } /// <summary> /// Признак того, что хранилище сервера повреждено и пользователю можно предложить очистку. /// </summary> public bool CanClearStorage { get => _canClearStorage; private set => this.RaiseAndSetIfChanged(ref _canClearStorage, value); } /// <summary> /// Разрешает ли пользователь отправку невалидных запросов. /// </summary> public bool AllowInvalidRequests { get => _allowInvalidRequests; set { this.RaiseAndSetIfChanged(ref _allowInvalidRequests, value); _apiClient.AllowInvalidRequests = value; } } /// <summary> /// Конструктор. /// </summary> public MainWindowViewModel() { ShowEditEngineDialog = new Interaction<ViewModelBase, Engine?>(); var schemaPath = Path.Combine(AppContext.BaseDirectory, "EngineOpenApi.json"); _apiClient = new EngineApiClient("http://127.0.0.1:5000", schemaPath); _apiClient.AllowInvalidRequests = AllowInvalidRequests; AddInternalCombusEngineCommand = ReactiveCommand.CreateFromTask( AddInternalCombusEngineImplementation, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); AddJetEngineCommand = ReactiveCommand.CreateFromTask( AddJetEngineImplementation, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); EditCommand = ReactiveCommand.CreateFromTask(EditEngineCommand, this.WhenAnyValue(parX => parX.CanEditOrDelete)); DeleteCommand = ReactiveCommand.CreateFromTask(DeleteEngineAsync, this.WhenAnyValue(parX => parX.CanEditOrDelete)); ClearStorageCommand = ReactiveCommand.CreateFromTask(ClearStorageAsync, this.WhenAnyValue(x => x.CanClearStorage, x => x.IsBusy, (canClear, busy) => canClear && !busy)); RefreshCommand = ReactiveCommand.CreateFromTask(RefreshAsync, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); ApplyFiltersCommand = ReactiveCommand.CreateFromTask(() => ApplyFiltersAsync(CancellationToken.None)); ResetFiltersCommand = ReactiveCommand.CreateFromTask(ResetFiltersAsync); _ = RefreshAsync(); } /// <summary> /// Настраивает общие свойства диалогов редактирования двигателя. /// </summary> private EditEngineViewModelBase<T> ConfigureDialog<T>(EditEngineViewModelBase<T> vm) where T : Engine { vm.AllowInvalidRequests = AllowInvalidRequests; vm.ReportStatus ??= message => Status = message; return vm; } /// <summary> /// Открывает окно создания бензинового двигателя и отправляет данные на сервер. /// </summary> private async Task AddInternalCombusEngineImplementation() { var vm = ConfigureDialog(new EditGasolineEngineViewModel(null, EditMode.Create) { SubmitAsync = async engine => await CreateEngineRequestAsync(engine) }); var result = await ShowEditEngineDialog.Handle(vm).FirstAsync(); if (result == null) { return; } await CreateEngineAsync(result, skipServerCall: true); } /// <summary> /// Открывает окно создания реактивного двигателя и сохраняет данные. /// </summary> private async Task AddJetEngineImplementation() { var vm = ConfigureDialog(new EditRamJetEngineViewModel(null, EditMode.Create) { SubmitAsync = async engine => await CreateEngineRequestAsync(engine) }); var result = await ShowEditEngineDialog.Handle(vm).FirstAsync(); if (result == null) { return; } await CreateEngineAsync(result, skipServerCall: true); } /// <summary> /// Запускает редактирование выбранного двигателя. /// </summary> private async Task EditEngineCommand() { ViewModelBase? editViewModel = SelectedEngine switch { DieselEngine diesel => ConfigureDialog(new EditDieselEngineViewModel(diesel, EditMode.Edit) { SubmitAsync = async engine => await _apiClient.UpdateAsync(engine, CancellationToken.None) }), GasolineEngine gasoline => ConfigureDialog(new EditGasolineEngineViewModel(gasoline, EditMode.Edit) { SubmitAsync = async engine => await _apiClient.UpdateAsync(engine, CancellationToken.None) }), RamJetEngine ram => ConfigureDialog(new EditRamJetEngineViewModel(ram, EditMode.Edit) { SubmitAsync = async engine => await _apiClient.UpdateAsync(engine, CancellationToken.None) }), TurboJetEngine turbo => ConfigureDialog(new EditTurboJetEngineViewModel(turbo, EditMode.Edit) { SubmitAsync = async engine => await _apiClient.UpdateAsync(engine, CancellationToken.None) }), _ => null }; if (editViewModel is null) { return; } var result = await ShowEditEngineDialog.Handle(editViewModel).FirstAsync(); if (result == null || SelectedEngine == null) { return; } await UpdateEngineAsync(result, SelectedEngine, skipServerCall: true); } /// <summary> /// Запускает сценарий удаления выбранного двигателя. /// </summary> private async Task DeleteEngineAsync() { if (SelectedEngine is null) { return; } ViewModelBase? deleteViewModel = SelectedEngine switch { DieselEngine diesel => ConfigureDialog(new EditDieselEngineViewModel(diesel, EditMode.Delete) { SubmitAsync = async engine => await DeleteEngineRequestAsync(engine) }), GasolineEngine gasoline => ConfigureDialog(new EditGasolineEngineViewModel(gasoline, EditMode.Delete) { SubmitAsync = async engine => await DeleteEngineRequestAsync(engine) }), RamJetEngine ram => ConfigureDialog(new EditRamJetEngineViewModel(ram, EditMode.Delete) { SubmitAsync = async engine => await DeleteEngineRequestAsync(engine) }), TurboJetEngine turbo => ConfigureDialog(new EditTurboJetEngineViewModel(turbo, EditMode.Delete) { SubmitAsync = async engine => await DeleteEngineRequestAsync(engine) }), _ => null }; if (deleteViewModel is null) { return; } var result = await ShowEditEngineDialog.Handle(deleteViewModel).FirstAsync(); if (result != null) { await DeleteEngineOnServerAsync(SelectedEngine, skipServerCall: true); } } /// <summary> /// Отправляет запрос создания двигателя и обновляет список. /// </summary> /// <param name="engine">Экземпляр двигателя для сохранения.</param> /// <param name="skipServerCall">Пропустить HTTP-запрос, если он уже выполнен в диалоге.</param> private async Task CreateEngineAsync(Engine engine, bool skipServerCall = false) { IsBusy = true; LogClientAction(skipServerCall ? $"Подтверждение создания двигателя после ответа сервера; Тип={engine.TypeName}; Производитель=\"{engine.Manufacturer}\"" : $"Отправка создания двигателя: POST /engine; Тип={engine.TypeName}; Производитель=\"{engine.Manufacturer}\""); var previousCount = _allEngines.Count; try { if (!skipServerCall) { var apiResult = await _apiClient.CreateAsync(engine, CancellationToken.None); if (!apiResult.IsSuccess || apiResult.Data is null) { Status = apiResult.Error ?? "Не удалось создать двигатель"; CanClearStorage = apiResult.CanSuggestStorageCleanup; return; } engine = apiResult.Data; CanClearStorage = false; } await RefreshAsync(); Engine? created = null; if (engine.Id > 0) { created = _allEngines.FirstOrDefault(e => e.Id == engine.Id); } if (created is null && _allEngines.Count > previousCount) { created = _allEngines.OrderByDescending(e => e.Id).FirstOrDefault(); } if (created is null) { Status = "Сервер не добавил двигатель: список не изменился"; return; } SelectedEngine = created; Status = $"Добавлен двигатель ID {created.Id}"; } finally { IsBusy = false; } } /// <summary> /// Отправляет изменения двигателя на сервер. /// </summary> /// <param name="updated">Новое состояние двигателя.</param> /// <param name="original">Исходный двигатель, выбранный в списке.</param> private async Task UpdateEngineAsync(Engine updated, Engine original, bool skipServerCall = false) { IsBusy = true; LogClientAction(skipServerCall ? $"Подтверждение обновления двигателя после ответа сервера; ID={original.Id}; Тип={updated.TypeName}; Производитель=\"{updated.Manufacturer}\"" : $"Отправка обновления двигателя: PUT /engine/{original.Id}; Тип={updated.TypeName}; Производитель=\"{updated.Manufacturer}\""); try { updated.Id = original.Id; if (!skipServerCall) { var apiResult = await _apiClient.UpdateAsync(updated, CancellationToken.None); if (!apiResult.IsSuccess || apiResult.Data is null) { Status = apiResult.Error ?? "Не удалось обновить двигатель"; CanClearStorage = apiResult.CanSuggestStorageCleanup; return; } CanClearStorage = false; } await RefreshAsync(); SelectedEngine = _allEngines.FirstOrDefault(e => e.Id == original.Id) ?? _allEngines.FirstOrDefault(); Status = $"Обновлен двигатель ID {original.Id}"; } finally { IsBusy = false; } } /// <summary> /// Удаляет двигатель через API. /// </summary> /// <param name="engine">Двигатель, который требуется удалить.</param> private async Task DeleteEngineOnServerAsync(Engine engine, bool skipServerCall = false) { IsBusy = true; LogClientAction(skipServerCall ? $"Подтверждение удаления двигателя после ответа сервера; ID={engine.Id}; Тип={engine.TypeName}; Производитель=\"{engine.Manufacturer}\"" : $"Отправка удаления двигателя: DELETE /engine/{engine.Id}; Тип={engine.TypeName}; Производитель=\"{engine.Manufacturer}\""); try { if (!skipServerCall) { var apiResult = await _apiClient.DeleteAsync(engine, CancellationToken.None); if (!apiResult.IsSuccess) { Status = apiResult.Error ?? "Не удалось удалить двигатель"; CanClearStorage = apiResult.CanSuggestStorageCleanup; return; } CanClearStorage = false; } await RefreshAsync(); SelectedEngine = _allEngines.FirstOrDefault(); Status = "Двигатель удалён"; } finally { IsBusy = false; } } /// <summary> /// Отправляет запрос создания двигателя и проверяет доступность списка. /// </summary> /// <param name="engine">Двигатель, который требуется создать.</param> /// <returns>Результат создания для диалога.</returns> private async Task<ApiResult<Engine>> CreateEngineRequestAsync(Engine engine) { var createResult = await _apiClient.CreateAsync(engine, CancellationToken.None); if (!createResult.IsSuccess || createResult.Data is null) { return createResult; } var listResult = await _apiClient.GetEnginesAsync(CancellationToken.None); if (!listResult.IsSuccess) { return ApiResult<Engine>.Fail(listResult.Error ?? "Не удалось загрузить список двигателей", listResult.CanSuggestStorageCleanup); } return createResult; } /// <summary> /// Отправляет запрос удаления двигателя и возвращает результат для диалогов. /// </summary> /// <param name="engine">Двигатель, который требуется удалить.</param> /// <returns>Результат удаления с данными для диалога.</returns> private async Task<ApiResult<Engine>> DeleteEngineRequestAsync(Engine engine) { var apiResult = await _apiClient.DeleteAsync(engine, CancellationToken.None); if (!apiResult.IsSuccess) { return ApiResult<Engine>.Fail(apiResult.Error ?? "Не удалось удалить двигатель", apiResult.CanSuggestStorageCleanup); } return ApiResult<Engine>.Success(engine); } /// <summary> /// Загружает список двигателей с сервера и применяет текущие фильтры. /// </summary> private async Task RefreshAsync() { IsBusy = true; Status = "Загрузка списка..."; CanClearStorage = false; LogClientAction("Запрос списка двигателей: GET /engine/list"); try { var response = await _apiClient.GetEnginesAsync(CancellationToken.None); if (!response.IsSuccess || response.Data is null) { Status = response.Error ?? "Не удалось загрузить данные"; CanClearStorage = response.CanSuggestStorageCleanup; return; } CanClearStorage = false; var previousSelected = SelectedEngine; _allEngines = response.Data.ToList(); await ApplyFiltersAsync(CancellationToken.None); if (previousSelected != null) { SelectedEngine = _allEngines.FirstOrDefault(e => e.Id == previousSelected.Id); } Status = $"Загружено {_allEngines.Count:N0} записей"; } finally { IsBusy = false; } } /// <summary> /// Очищает повреждённое серверное хранилище и заново загружает данные. /// </summary> private async Task ClearStorageAsync() { IsBusy = true; Status = "Очистка хранилища..."; try { var result = await _apiClient.ClearStorageAsync(CancellationToken.None); if (!result.IsSuccess) { Status = result.Error ?? "Не удалось очистить хранилище"; CanClearStorage = result.CanSuggestStorageCleanup; return; } Status = "Хранилище очищено. Перезагружаем данные..."; CanClearStorage = false; await RefreshAsync(); } finally { IsBusy = false; } } /// <summary> /// Асинхронно применяет фильтры к локальной коллекции двигателей. /// </summary> /// <param name="parToken">Токен отмены операции.</param> private async Task ApplyFiltersAsync(CancellationToken parToken) { _filterCts?.Cancel(); _filterCts = CancellationTokenSource.CreateLinkedTokenSource(parToken); var token = _filterCts.Token; var snapshot = _allEngines.ToList(); try { var filtered = await Task.Run(() => FilterEngines(snapshot, token), token); await Dispatcher.UIThread.InvokeAsync(() => { if (token.IsCancellationRequested) { return; } UpdateFilteredEngines(filtered); CurrentFilterSummary = BuildFilterSummary(_manufacturerFilter, PowerMin, PowerMax, filtered.Count, snapshot.Count); }, DispatcherPriority.Background); } catch (OperationCanceledException) { // ignore } } /// <summary> /// Обновляет отображаемую коллекцию и сохраняет выбор. /// </summary> /// <param name="parFiltered">Отфильтрованные двигатели.</param> private void UpdateFilteredEngines(List<Engine> parFiltered) { var previous = SelectedEngine; FilteredEngines = parFiltered; if (previous != null && parFiltered.Contains(previous)) { SelectedEngine = previous; } else { SelectedEngine = parFiltered.FirstOrDefault(); } this.RaisePropertyChanged(nameof(CanEditOrDelete)); } /// <summary> /// Проверяет, удовлетворяет ли двигатель фильтру по производителю. /// </summary> /// <param name="parEngine">Двигатель для проверки.</param> /// <param name="parManufacturerFilter">Строка фильтра.</param> /// <returns><c>true</c>, если фильтр пустой или производитель содержит значение.</returns> private static bool MatchesManufacturer(Engine parEngine, string parManufacturerFilter) { if (string.IsNullOrWhiteSpace(parManufacturerFilter)) { return true; } return parEngine.Manufacturer?.Contains(parManufacturerFilter, StringComparison.Ordinal) == true; } /// <summary> /// Проверяет попадание двигателя в диапазон мощностей. /// </summary> /// <param name="parEngine">Двигатель для проверки.</param> /// <param name="parMin">Минимальная мощность.</param> /// <param name="parMax">Максимальная мощность.</param> /// <returns><c>true</c>, если мощность лежит в пределах.</returns> private static bool MatchesPower(Engine parEngine, int? parMin, int? parMax) { if (parMin.HasValue && parEngine.Power < parMin.Value) { return false; } if (parMax.HasValue && parEngine.Power > parMax.Value) { return false; } return true; } /// <summary> /// Формирует коллекцию двигателей, удовлетворяющих текущим фильтрам. /// </summary> /// <param name="source">Исходный список.</param> /// <param name="token">Токен отмены.</param> /// <returns>Отфильтрованный список.</returns> private List<Engine> FilterEngines(List<Engine> source, CancellationToken token) { var min = PowerMin; var max = PowerMax; var manufacturer = _manufacturerFilter?.Trim() ?? string.Empty; var list = new List<Engine>(source.Count); foreach (var engine in source) { token.ThrowIfCancellationRequested(); if (!MatchesManufacturer(engine, manufacturer)) { continue; } if (!MatchesPower(engine, min, max)) { continue; } list.Add(engine); } return list; } /// <summary> /// Формирует человеко-читаемое описание активных фильтров. /// </summary> /// <param name="manufacturer">Фильтр производителя.</param> /// <param name="min">Минимальная мощность.</param> /// <param name="max">Максимальная мощность.</param> /// <param name="filteredCount">Количество отобранных элементов.</param> /// <param name="totalCount">Общее количество элементов.</param> /// <returns>Строка с информацией о фильтре.</returns> private static string BuildFilterSummary(string manufacturer, int? min, int? max, int filteredCount, int totalCount) { var parts = new List<string>(); if (!string.IsNullOrWhiteSpace(manufacturer)) { parts.Add($"Производитель содержит \"{manufacturer}\""); } if (min.HasValue || max.HasValue) { var minText = min?.ToString() ?? "-"; var maxText = max?.ToString() ?? "-"; parts.Add($"Мощность: {minText}..{maxText}"); } var core = parts.Count == 0 ? "не установлен" : string.Join("; ", parts); return $"Фильтр: {core}; отобрано {filteredCount:N0} из {totalCount:N0}"; } /// <summary> /// Сбрасывает фильтры и пересчитывает отображаемый список. /// </summary> private async Task ResetFiltersAsync() { ManufacturerFilter = string.Empty; PowerMin = null; PowerMax = null; await ApplyFiltersAsync(CancellationToken.None); } /// <summary> /// Пишет сообщение в консоль с отметкой времени. /// </summary> /// <param name="message">Текст сообщения.</param> private static void LogClientAction(string message) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}"); } }