/
dOJer113
/
Insect
Обзор
Документация
Войти
/
dOJer113
/
Insect
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
lab_5
UI/ViewModels/MainWindowViewModel.cs
597 строк
19 KB
aleks
lab 5
14 дек 2025, 19:32
14 дек 2025, 19:32
395af5a
Код
Авторство
О чём код?
using System; using ReactiveUI; using System.Collections.ObjectModel; using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; using Avalonia.Threading; using System.Linq; using insects; using insects.Factory; using insects.Factory.Method; using UI.ViewModels.Insects; using ProgressOverlayWindow; using UI; namespace UI.ViewModels; /// <summary> /// Главная ViewModel приложения для управления коллекцией насекомых /// </summary> public class MainWindowViewModel : ViewModelBase { /// <summary> /// Методы создания фабрик насекомых /// </summary> private readonly IFactoryMethod[] _factoryMethods; /// <summary> /// Текущая активная фабрика насекомых /// </summary> private IFactory? _currentFactory; /// <summary> /// Индекс выбранного типа фабрики /// </summary> private int _selectedFactoryIndex = 0; /// <summary> /// Флаг выполнения генерации. /// </summary> private bool _isGenerationInProgress; /// <summary> /// Фильтр по типу ротового аппарата. /// </summary> private string _mouthpieceFilter = string.Empty; /// <summary> /// Фильтр минимального размера тела. /// </summary> private decimal? _minBodySizeFilter; /// <summary> /// Фильтр максимального размера тела. /// </summary> private decimal? _maxBodySizeFilter; /// <summary> /// Активное описание фильтра. /// </summary> private string _filterSummary = "Фильтр: отсутствует"; /// <summary> /// Источник отмены текущей фильтрации. /// </summary> private CancellationTokenSource? _filterCts; /// <summary> /// Базовый список всех насекомых. /// </summary> private readonly List<AbstractInsect> _allInsects = []; /// <summary> /// Стабильный порядок элементов для сохранения сортировки при фильтрации. /// </summary> private readonly Dictionary<AbstractInsect, long> _insectOrder = new(); /// <summary> /// Счётчик вставки для формирования стабильного порядка. /// </summary> private long _orderCounter; /// <summary> /// Объект синхронизации для коллекций. /// </summary> private readonly object _syncRoot = new(); /// <summary> /// Ограничение вывода в таблицу. /// </summary> private const int DisplayLimit = 10_000; private void AddInsect(AbstractInsect parInsect) { lock (_syncRoot) { _allInsects.Add(parInsect); if (!_insectOrder.ContainsKey(parInsect)) { _orderCounter++; _insectOrder[parInsect] = _orderCounter; } } } private void AddInsects(IEnumerable<AbstractInsect> parInsects) { lock (_syncRoot) { foreach (var insect in parInsects) { _allInsects.Add(insect); if (!_insectOrder.ContainsKey(insect)) { _orderCounter++; _insectOrder[insect] = _orderCounter; } } } } private void RemoveInsect(AbstractInsect parInsect) { lock (_syncRoot) { _allInsects.Remove(parInsect); _insectOrder.Remove(parInsect); } } /// <summary> /// Коллекция насекомых /// </summary> public ObservableCollection<AbstractInsect> Insects { get; } = []; /// <summary> /// Выбранное насекомое в списке /// </summary> private AbstractInsect? _selectedInsect; /// <summary> /// Выбранное насекомое /// </summary> public AbstractInsect? SelectedInsect { get => _selectedInsect; set { this.RaiseAndSetIfChanged(ref _selectedInsect, value); this.RaisePropertyChanged(nameof(CanEditOrDelete)); } } /// <summary> /// Индекс выбранного типа фабрики /// </summary> public int SelectedFactoryIndex { get => _selectedFactoryIndex; set => this.RaiseAndSetIfChanged(ref _selectedFactoryIndex, value); } /// <summary> /// Доступность операций редактирования и удаления /// </summary> public bool CanEditOrDelete => SelectedInsect != null; /// <summary> /// Флаг наличия выбранной фабрики /// </summary> public bool IsFactorySelected => _currentFactory != null; /// <summary> /// Признак блокировки генерации. /// </summary> public bool IsGenerationInProgress { get => _isGenerationInProgress; private set { this.RaiseAndSetIfChanged(ref _isGenerationInProgress, value); this.RaisePropertyChanged(nameof(CanGenerate)); } } /// <summary> /// Разрешена ли генерация. /// </summary> public bool CanGenerate => IsFactorySelected && !IsGenerationInProgress; /// <summary> /// Текстовый фильтр ротового аппарата. /// </summary> public string MouthpieceFilter { get => _mouthpieceFilter; set => this.RaiseAndSetIfChanged(ref _mouthpieceFilter, value); } /// <summary> /// Минимальный размер тела. /// </summary> public decimal? MinBodySizeFilter { get => _minBodySizeFilter; set => this.RaiseAndSetIfChanged(ref _minBodySizeFilter, value); } /// <summary> /// Максимальный размер тела. /// </summary> public decimal? MaxBodySizeFilter { get => _maxBodySizeFilter; set => this.RaiseAndSetIfChanged(ref _maxBodySizeFilter, value); } /// <summary> /// Текущее описание примененного фильтра. /// </summary> public string FilterSummary { get => _filterSummary; private set => this.RaiseAndSetIfChanged(ref _filterSummary, value); } /// <summary> /// Минимальная граница размера тела. /// </summary> public decimal BodySizeMinBound => FilterBounds.BodySizeMin; /// <summary> /// Максимальная граница размера тела. /// </summary> public decimal BodySizeMaxBound => FilterBounds.BodySizeMax; /// <summary> /// Взаимодействие для отображения диалога редактирования /// </summary> public Interaction<ViewModelBase, AbstractInsect?> ShowEditInsectDialog { get; } /// <summary> /// Взаимодействие для отображения окна прогресса. /// </summary> public Interaction<Func<IProgressOverlayWindow, CancellationToken, Task>, bool> ShowProgressDialog { get; } /// <summary> /// Команда добавления жука /// </summary> public ReactiveCommand<Unit, Unit> AddBeetleCommand { get; } /// <summary> /// Команда добавления бабочки /// </summary> public ReactiveCommand<Unit, Unit> AddButterflyCommand { get; } /// <summary> /// Команда редактирования насекомого /// </summary> public ReactiveCommand<Unit, Unit> EditCommand { get; } /// <summary> /// Команда удаления насекомого /// </summary> public ReactiveCommand<Unit, Unit> DeleteCommand { get; } /// <summary> /// Команда генерации тестовых данных /// </summary> public ReactiveCommand<Unit, Unit> GenerateTestInsectsCommand { get; } /// <summary> /// Команда применения фильтра. /// </summary> public ReactiveCommand<Unit, Unit> ApplyFilterCommand { get; } /// <summary> /// Конструктор по умолчанию /// </summary> public MainWindowViewModel() { ShowEditInsectDialog = new Interaction<ViewModelBase, AbstractInsect?>(); ShowProgressDialog = new Interaction<Func<IProgressOverlayWindow, CancellationToken, Task>, bool>(); _factoryMethods = [ new GlasscoverLadybugFactoryMethod(), new MonarchNecrophagesFactoryMethod(), new PrototypeFactoryMethod(GetGlassCoverLadyBugPrototypesAsync), new PrototypeFactoryMethod(GetMonarchNecrophagesPrototypesAsync), ]; AddBeetleCommand = ReactiveCommand.CreateFromTask(AddBeetleImplementation, this.WhenAnyValue(x => x.IsFactorySelected)); AddButterflyCommand = ReactiveCommand.CreateFromTask(AddButterflyImplementation, this.WhenAnyValue(x => x.IsFactorySelected)); EditCommand = ReactiveCommand.CreateFromTask(EditInsectCommand, this.WhenAnyValue(x => x.CanEditOrDelete)); DeleteCommand = ReactiveCommand.CreateFromTask(DeleteInsectCommand, this.WhenAnyValue(x => x.CanEditOrDelete)); GenerateTestInsectsCommand = ReactiveCommand.CreateFromTask( GenerateTestInsectsCommandImpl, this.WhenAnyValue(x => x.CanGenerate)); ApplyFilterCommand = ReactiveCommand.CreateFromTask(ApplyFilterAsync); this.WhenAnyValue(x => x.SelectedFactoryIndex) .Select(index => Observable.FromAsync(() => UpdateFactoryAsync(index))) .Concat() .Subscribe(); this.WhenAnyValue(x => x.IsFactorySelected, x => x.IsGenerationInProgress) .Subscribe(_ => this.RaisePropertyChanged(nameof(CanGenerate))); UpdateFilterSummary(string.Empty, null, null, _allInsects.Count); } /// <summary> /// Обновление текущей фабрики по выбранному индексу /// </summary> private async Task UpdateFactoryAsync(int index) { if (index < 0 || index >= _factoryMethods.Length) { _currentFactory = null; } else { _currentFactory = await _factoryMethods[index].CreateFactoryAsync(); } this.RaisePropertyChanged(nameof(IsFactorySelected)); this.RaisePropertyChanged(nameof(CanGenerate)); } /// <summary> /// Получение прототипов насекомых /// </summary> private async Task<(Butterfly, Beetle)> GetPrototypesAsync( Func<ViewModelBase> createButterflyVm, Func<Butterfly> createDefaultButterfly, Func<ViewModelBase> createBeetleVm, Func<Beetle> createDefaultBeetle) { var butterflyVm = createButterflyVm(); var butterflyPrototype = await ShowEditInsectDialog.Handle(butterflyVm) as Butterfly ?? createDefaultButterfly(); var beetleVm = createBeetleVm(); var beetlePrototype = await ShowEditInsectDialog.Handle(beetleVm) as Beetle ?? createDefaultBeetle(); return (butterflyPrototype, beetlePrototype); } /// <summary> /// Получение прототипов стеклокрылок и божьих коровок /// </summary> private Task<(Butterfly, Beetle)> GetGlassCoverLadyBugPrototypesAsync() => GetPrototypesAsync( () => new GlassCoverViewModel(), () => new GlassCover(), () => new LadybugViewModel(), () => new Ladybug()); /// <summary> /// Получение прототипов монархов и некрофагов /// </summary> private Task<(Butterfly, Beetle)> GetMonarchNecrophagesPrototypesAsync() => GetPrototypesAsync( () => new MonarchViewModel(), () => new Monarch(), () => new NecrophagesViewModel(), () => new Necrophages()); /// <summary> /// Добавление новой бабочки /// </summary> private async Task AddButterflyImplementation() { if (_currentFactory == null) return; var insect = _currentFactory.CreateButterfly(); ViewModelBase? viewModel = insect switch { GlassCover glassCover => new GlassCoverViewModel(glassCover, Mode.Create), Monarch monarch => new MonarchViewModel(monarch, Mode.Create), _ => null }; if (viewModel is null) return; var result = await ShowEditInsectDialog.Handle(viewModel); if (result != null) { AddInsect(result); await ApplyFilterAsync(); SelectedInsect = result; } } /// <summary> /// Добавление нового жука /// </summary> private async Task AddBeetleImplementation() { if (_currentFactory == null) return; var insect = _currentFactory.CreateBeetle(); ViewModelBase? viewModel = insect switch { Ladybug ladybug => new LadybugViewModel(ladybug, Mode.Create), Necrophages necrophages => new NecrophagesViewModel(necrophages, Mode.Create), _ => null }; if (viewModel is null) return; var result = await ShowEditInsectDialog.Handle(viewModel); if (result != null) { AddInsect(result); await ApplyFilterAsync(); SelectedInsect = result; } } /// <summary> /// Редактирование выбранного насекомого /// </summary> private async Task EditInsectCommand() { ViewModelBase? editViewModel = SelectedInsect switch { GlassCover glassCover => new GlassCoverViewModel(glassCover, Mode.Edit), Ladybug ladybug => new LadybugViewModel(ladybug, Mode.Edit), Monarch monarch => new MonarchViewModel(monarch, Mode.Edit), Necrophages necrophages => new NecrophagesViewModel(necrophages, Mode.Edit), _ => null }; if (editViewModel is null) return; var result = await ShowEditInsectDialog.Handle(editViewModel); if (result != null) { SelectedInsect!.Assign(result); } } /// <summary> /// Удаление выбранного насекомого /// </summary> private async Task DeleteInsectCommand() { if (SelectedInsect is null) return; ViewModelBase? deleteViewModel = SelectedInsect switch { GlassCover glassCover => new GlassCoverViewModel(glassCover, Mode.Delete), Ladybug ladybug => new LadybugViewModel(ladybug, Mode.Delete), Monarch monarch => new MonarchViewModel(monarch, Mode.Delete), Necrophages necrophages => new NecrophagesViewModel(necrophages, Mode.Delete), _ => null }; if (deleteViewModel is null) return; var result = await ShowEditInsectDialog.Handle(deleteViewModel); if (result != null) { Insects.Remove(SelectedInsect!); RemoveInsect(SelectedInsect!); SelectedInsect = null; await ApplyFilterAsync(); } } /// <summary> /// Генерация тестовых данных насекомых /// </summary> private async Task GenerateTestInsectsCommandImpl() { if (_currentFactory is null) return; IsGenerationInProgress = true; try { List<AbstractInsect>? generated = null; await ShowProgressDialog.Handle(async (progress, token) => { generated = await InsectGenerator.GenerateAsync(_currentFactory, progress, token); }); if (generated != null) { AddInsects(generated); await ApplyFilterAsync(); } } catch (OperationCanceledException) { } finally { IsGenerationInProgress = false; } } /// <summary> /// Применяет фильтр к коллекции. /// </summary> private async Task ApplyFilterAsync() { _filterCts?.Cancel(); _filterCts = new CancellationTokenSource(); var token = _filterCts.Token; var mouthpiece = MouthpieceFilter ?? string.Empty; var min = MinBodySizeFilter.HasValue ? Math.Clamp(MinBodySizeFilter.Value, FilterBounds.BodySizeMin, FilterBounds.BodySizeMax) : (decimal?)null; var max = MaxBodySizeFilter.HasValue ? Math.Clamp(MaxBodySizeFilter.Value, FilterBounds.BodySizeMin, FilterBounds.BodySizeMax) : (decimal?)null; if (min.HasValue && max.HasValue && min > max) { (min, max) = (max, min); } const decimal tolerance = 0.0001m; var previousSelection = SelectedInsect; try { var filtered = await Task.Run(() => { return _allInsects.Where(insect => { token.ThrowIfCancellationRequested(); var matchesMouthpiece = string.IsNullOrWhiteSpace(mouthpiece) || insect.MouthpieceType.Contains(mouthpiece, StringComparison.OrdinalIgnoreCase); var matchesMin = !min.HasValue || insect.BodySize >= min.Value - tolerance; var matchesMax = !max.HasValue || insect.BodySize <= max.Value + tolerance; return matchesMouthpiece && matchesMin && matchesMax; }) .OrderBy(insect => _insectOrder.GetValueOrDefault(insect)) .ToList(); }, token); await Dispatcher.UIThread.InvokeAsync(() => { Insects.Clear(); foreach (var insect in filtered.Take(DisplayLimit)) { Insects.Add(insect); } var selected = previousSelection != null && filtered.Contains(previousSelection) ? previousSelection : filtered.FirstOrDefault(); SelectedInsect = selected; UpdateFilterSummary(mouthpiece, min, max, filtered.Count); }, DispatcherPriority.Background, token); } catch (OperationCanceledException) { } } private void UpdateFilterSummary(string parMouthpiece, decimal? parMin, decimal? parMax, int parCount) { var pieces = new List<string>(); if (!string.IsNullOrWhiteSpace(parMouthpiece)) { pieces.Add($"Жвалы contains \"{parMouthpiece}\""); } if (parMin.HasValue) { pieces.Add($"Размер ≥ {parMin.Value}"); } if (parMax.HasValue) { pieces.Add($"Размер ≤ {parMax.Value}"); } FilterSummary = pieces.Count == 0 ? "Фильтр: отсутствует" : $"Фильтр: {string.Join(", ", pieces)} | Найдено: {parCount:N0}"; } }