/
dOJer113
/
Insect
Обзор
Документация
Войти
/
dOJer113
/
Insect
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
UI/ViewModels/MainWindowViewModel.cs
521 строка
16 KB
alex
Валидация
21 дек 2025, 20:06
21 дек 2025, 20:06
640f9dc
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using insects; using UI.Models; using UI.Services.Api; using UI.ViewModels.EditInsect; using ReactiveUI; namespace UI.ViewModels; /// <summary> /// Главная VM: CRUD насекомых через REST API. /// </summary> public class MainWindowViewModel : ViewModelBase { private readonly InsectApiClient _apiClient; private readonly List<Insect> _allInsects = new(); private IReadOnlyList<Insect> _filteredInsects = Array.Empty<Insect>(); private Insect? _selectedInsect; private string _nameFilter = string.Empty; private string _mouthpieceFilter = string.Empty; private int? _minId; private int? _maxId; private string _filterSummary = "Фильтр: все"; private string _status = "Загрузка..."; private bool _isBusy; private CancellationTokenSource? _filterCts; private bool _hasStorageError; private string? _storageErrorMessage; public MainWindowViewModel() { ShowEditInsectDialog = new Interaction<ViewModelBase, Insect?>(); var schemaPath = Path.Combine(AppContext.BaseDirectory, "InsectOpenApi.json"); _apiClient = new InsectApiClient("http://127.0.0.1:5000", schemaPath); AddLadybugCommand = ReactiveCommand.CreateFromTask(AddLadybugAsync, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); AddNecrophagesCommand = ReactiveCommand.CreateFromTask(AddNecrophagesAsync, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); EditCommand = ReactiveCommand.CreateFromTask(EditInsectAsync, this.WhenAnyValue(x => x.CanEditOrDelete)); DeleteCommand = ReactiveCommand.CreateFromTask(DeleteInsectAsync, this.WhenAnyValue(x => x.CanEditOrDelete)); RefreshCommand = ReactiveCommand.CreateFromTask(RefreshAsync, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); ApplyFiltersCommand = ReactiveCommand.CreateFromTask(ApplyFiltersAsync); ResetFiltersCommand = ReactiveCommand.CreateFromTask(ResetFiltersAsync); _ = RefreshAsync(); } public Interaction<ViewModelBase, Insect?> ShowEditInsectDialog { get; } public IReadOnlyList<Insect> FilteredInsects { get => _filteredInsects; private set => this.RaiseAndSetIfChanged(ref _filteredInsects, value); } public Insect? SelectedInsect { get => _selectedInsect; set { this.RaiseAndSetIfChanged(ref _selectedInsect, value); this.RaisePropertyChanged(nameof(CanEditOrDelete)); } } public string NameFilter { get => _nameFilter; set => this.RaiseAndSetIfChanged(ref _nameFilter, value); } public string MouthpieceFilter { get => _mouthpieceFilter; set => this.RaiseAndSetIfChanged(ref _mouthpieceFilter, value); } public int? MinId { get => _minId; set => this.RaiseAndSetIfChanged(ref _minId, value); } public int? MaxId { get => _maxId; set => this.RaiseAndSetIfChanged(ref _maxId, value); } public string FilterSummary { get => _filterSummary; private set => this.RaiseAndSetIfChanged(ref _filterSummary, value); } public string Status { get => _status; private set => this.RaiseAndSetIfChanged(ref _status, value); } public bool IsBusy { get => _isBusy; private set { this.RaiseAndSetIfChanged(ref _isBusy, value); this.RaisePropertyChanged(nameof(CanEditOrDelete)); } } public bool CanEditOrDelete => SelectedInsect != null && !IsBusy; public ReactiveCommand<Unit, Unit> AddLadybugCommand { get; } public ReactiveCommand<Unit, Unit> AddNecrophagesCommand { get; } public ReactiveCommand<Unit, Unit> EditCommand { get; } public ReactiveCommand<Unit, Unit> DeleteCommand { get; } public ReactiveCommand<Unit, Unit> RefreshCommand { get; } public ReactiveCommand<Unit, Unit> ApplyFiltersCommand { get; } public ReactiveCommand<Unit, Unit> ResetFiltersCommand { get; } private async Task AddLadybugAsync() { if (_hasStorageError) { Status = _storageErrorMessage ?? "Файл данных на сервере повреждён. Исправьте и обновите."; return; } var vm = new EditLadybugViewModel(null, Mode.Create); var created = await RunDialogWithServerRetryAsync(vm, insect => _apiClient.CreateAsync(insect, CancellationToken.None)); if (created != null) { await RefreshAsync(); var picked = created.Id > 0 ? _allInsects.FirstOrDefault(e => e.Id == created.Id) : _allInsects.OrderByDescending(e => e.Id).FirstOrDefault(); SelectedInsect = picked ?? _allInsects.FirstOrDefault(); Status = picked?.Id > 0 ? $"Создано насекомое ID {picked.Id}" : "Насекомое создано"; } } private async Task AddNecrophagesAsync() { if (_hasStorageError) { Status = _storageErrorMessage ?? "Файл данных на сервере повреждён. Исправьте и обновите."; return; } var vm = new EditNecrophagesViewModel(null, Mode.Create); var created = await RunDialogWithServerRetryAsync(vm, insect => _apiClient.CreateAsync(insect, CancellationToken.None)); if (created != null) { await RefreshAsync(); var picked = created.Id > 0 ? _allInsects.FirstOrDefault(e => e.Id == created.Id) : _allInsects.OrderByDescending(e => e.Id).FirstOrDefault(); SelectedInsect = picked ?? _allInsects.FirstOrDefault(); Status = picked?.Id > 0 ? $"Создано насекомое ID {picked.Id}" : "Насекомое создано"; } } private async Task EditInsectAsync() { if (SelectedInsect is null) { return; } if (_hasStorageError) { Status = _storageErrorMessage ?? "Файл данных на сервере повреждён. Исправьте и обновите."; return; } ViewModelBase? editViewModel = SelectedInsect switch { Ladybug ladybug => new EditLadybugViewModel(ladybug, Mode.Edit), Necrophages necro => new EditNecrophagesViewModel(necro, Mode.Edit), _ => null }; if (editViewModel is null) { return; } var updated = await RunDialogWithServerRetryAsync(editViewModel, insect => _apiClient.UpdateAsync(insect, CancellationToken.None)); if (updated != null && SelectedInsect != null) { await RefreshAsync(); SelectedInsect = _allInsects.FirstOrDefault(e => e.Id == updated.Id) ?? _allInsects.FirstOrDefault(); Status = $"Обновлено насекомое ID {updated.Id}"; } } private async Task DeleteInsectAsync() { if (SelectedInsect is null) { return; } if (_hasStorageError) { Status = _storageErrorMessage ?? "Файл данных на сервере повреждён. Исправьте и обновите."; return; } ViewModelBase? deleteViewModel = SelectedInsect switch { Ladybug ladybug => new EditLadybugViewModel(ladybug, Mode.Delete), Necrophages necro => new EditNecrophagesViewModel(necro, Mode.Delete), _ => null }; if (deleteViewModel is null) { return; } var confirmation = await ShowEditInsectDialog.Handle(deleteViewModel).FirstAsync(); if (confirmation != null) { await DeleteInsectOnServerAsync(SelectedInsect); } } private async Task CreateInsectAsync(Insect insect) { IsBusy = true; LogClientAction($"POST /insects ({FormatInsectDetails(insect)})"); try { var apiResult = await _apiClient.CreateAsync(insect, CancellationToken.None); if (!apiResult.IsSuccess || apiResult.Data is null) { Status = apiResult.Error ?? "Ошибка при создании насекомого"; return; } LogClientAction($"POST /insects -> создан ID {apiResult.Data.Id}"); await RefreshAsync(); var created = apiResult.Data.Id > 0 ? _allInsects.FirstOrDefault(e => e.Id == apiResult.Data.Id) : _allInsects.OrderByDescending(e => e.Id).FirstOrDefault(); SelectedInsect = created ?? _allInsects.FirstOrDefault(); Status = created?.Id > 0 ? $"Создано насекомое ID {created.Id}" : "Насекомое создано, ID не получен"; } finally { IsBusy = false; } } private async Task UpdateInsectAsync(Insect updated, Insect original) { IsBusy = true; updated.Id = original.Id; LogClientAction($"PUT /insects/{original.Id} ({FormatInsectDetails(updated)})"); try { var apiResult = await _apiClient.UpdateAsync(updated, CancellationToken.None); if (!apiResult.IsSuccess || apiResult.Data is null) { Status = $"Ошибка при обновлении насекомого ID {original.Id}: {apiResult.Error ?? "неизвестная ошибка"}"; return; } await RefreshAsync(); SelectedInsect = _allInsects.FirstOrDefault(e => e.Id == original.Id) ?? _allInsects.FirstOrDefault(); Status = $"Обновлено насекомое ID {original.Id}"; } finally { IsBusy = false; } } private async Task DeleteInsectOnServerAsync(Insect insect) { IsBusy = true; LogClientAction($"DELETE /insects/{insect.Id} ({FormatInsectDetails(insect)})"); try { var apiResult = await _apiClient.DeleteAsync(insect, CancellationToken.None); if (!apiResult.IsSuccess) { Status = $"Ошибка при удалении насекомого ID {insect.Id}: {apiResult.Error ?? "неизвестная ошибка"}"; return; } await RefreshAsync(); SelectedInsect = _allInsects.FirstOrDefault(); Status = $"Насекомое удалено (ID {insect.Id})"; } finally { IsBusy = false; } } private async Task RefreshAsync() { IsBusy = true; Status = "Загрузка списка насекомых..."; LogClientAction("GET /insects/list"); try { var response = await _apiClient.GetInsectsAsync(CancellationToken.None); if (!response.IsSuccess || response.Data is null) { Status = response.Error ?? "Ошибка загрузки списка"; _hasStorageError = true; _storageErrorMessage = response.Error; _allInsects.Clear(); FilteredInsects = Array.Empty<Insect>(); FilterSummary = "Фильтр: все; отобрано 0 из 0"; SelectedInsect = null; return; } _hasStorageError = false; _storageErrorMessage = null; var previousSelected = SelectedInsect; _allInsects.Clear(); var allowed = response.Data.Where(IsSupportedType).ToList(); _allInsects.AddRange(allowed); await ApplyFiltersAsync(); if (previousSelected != null) { SelectedInsect = _allInsects.FirstOrDefault(x => x.Id == previousSelected.Id); } Status = $"Получено {_allInsects.Count:N0} насекомых"; } finally { IsBusy = false; } } private async Task ApplyFiltersAsync() { _filterCts?.Cancel(); _filterCts = new CancellationTokenSource(); var token = _filterCts.Token; var snapshot = _allInsects.ToList(); var name = NameFilter?.Trim() ?? string.Empty; var mouthpiece = MouthpieceFilter?.Trim() ?? string.Empty; var min = MinId; var max = MaxId; try { var filtered = await Task.Run(() => FilterInsects(snapshot, name, mouthpiece, min, max, token), token); if (token.IsCancellationRequested) { return; } FilteredInsects = filtered; SelectedInsect = filtered.FirstOrDefault(x => x.Id == SelectedInsect?.Id) ?? filtered.FirstOrDefault(); FilterSummary = BuildFilterSummary(name, mouthpiece, min, max, filtered.Count, snapshot.Count); this.RaisePropertyChanged(nameof(CanEditOrDelete)); } catch (OperationCanceledException) { // ignore } } private List<Insect> FilterInsects(List<Insect> source, string name, string mouthpiece, int? minId, int? maxId, CancellationToken token) { var list = new List<Insect>(source.Count); foreach (var insect in source) { token.ThrowIfCancellationRequested(); if (!MatchesName(insect, name)) { continue; } if (!MatchesMouthpiece(insect, mouthpiece)) { continue; } if (!MatchesId(insect, minId, maxId)) { continue; } list.Add(insect); } return list; } private static bool MatchesName(Insect insect, string filter) { if (string.IsNullOrWhiteSpace(filter)) { return true; } return insect.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) == true; } private static bool MatchesMouthpiece(Insect insect, string filter) { if (string.IsNullOrWhiteSpace(filter)) { return true; } return insect.MouthpieceType?.Contains(filter, StringComparison.OrdinalIgnoreCase) == true; } private static bool MatchesId(Insect insect, int? min, int? max) { if (min.HasValue && insect.Id < min.Value) { return false; } if (max.HasValue && insect.Id > max.Value) { return false; } return true; } private static string BuildFilterSummary(string name, string mouthpiece, int? min, int? max, int filteredCount, int totalCount) { var parts = new List<string>(); if (!string.IsNullOrWhiteSpace(name)) { parts.Add($"название содержит \"{name}\""); } if (!string.IsNullOrWhiteSpace(mouthpiece)) { parts.Add($"рот. аппарат содержит \"{mouthpiece}\""); } if (min.HasValue || max.HasValue) { var minText = min?.ToString() ?? "-"; var maxText = max?.ToString() ?? "-"; parts.Add($"ID: {minText}..{maxText}"); } var core = parts.Count == 0 ? "нет" : string.Join("; ", parts); return $"Фильтр: {core}; показано {filteredCount:N0} из {totalCount:N0}"; } private async Task ResetFiltersAsync() { NameFilter = string.Empty; MouthpieceFilter = string.Empty; MinId = null; MaxId = null; await ApplyFiltersAsync(); } private static bool IsSupportedType(Insect insect) => insect is Ladybug or Necrophages; private static string FormatInsectDetails(Insect insect) { var core = $"Id={insect.Id}, Type={insect.TypeName}, Name=\"{insect.Name}\", HasWings={insect.HasWings}, Mouthpiece=\"{insect.MouthpieceType}\", BodySize={insect.BodySize}"; return insect switch { Ladybug ladybug => $"{core}, Mandible={ladybug.MandibleSize}, Spots={ladybug.NumberOfSpots}", Necrophages necro => $"{core}, Mandible={necro.MandibleSize}, Stage={necro.StageOfSuccession}", _ => core }; } private static void LogClientAction(string message) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}"); } private async Task<Insect?> RunDialogWithServerRetryAsync(ViewModelBase dialogVm, Func<Insect, Task<ApiResult<Insect>>> sender) { if (dialogVm is not IEditInsectDialogViewModel vm) { return null; } while (true) { var result = await ShowEditInsectDialog.Handle(dialogVm).FirstAsync(); if (result == null) { return null; } var apiResult = await sender(result); if (apiResult.IsSuccess && apiResult.Data != null) { vm.SetServerError(string.Empty); return apiResult.Data; } var err = apiResult.Error ?? "Ошибка сервера. Исправьте данные или отмените."; vm.SetServerError(err); Status = err; // диалог откроется вновь в следующей итерации } } }