/
d4nil
/
Konenkov_2413_software_engineering_
Обзор
Документация
Войти
/
d4nil
/
Konenkov_2413_software_engineering_
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
ProgressMonitor/ProgressWindow.cs
319 строк
10 KB
Konenkov Daniel
doc: изменить комментарии
16 дек 2025, 09:37
16 дек 2025, 09:37
ecfc27c
Код
Авторство
О чём код?
using System; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; namespace ProgressMonitor; /// <summary> /// Окно отображения прогресса с поддержкой отмены. /// </summary> public class ProgressWindow : Window, IProgressDialog { /// <summary> /// Минимальное время отображения окна, чтобы пользователь успел увидеть результат. /// </summary> private readonly TimeSpan _minimumVisible = TimeSpan.FromMilliseconds(750); /// <summary> /// Момент открытия окна. /// </summary> private DateTime _openedAt = DateTime.UtcNow; /// <summary> /// Связанный токен отмены, объединяющий пользовательский и внешний токены. /// </summary> private CancellationTokenSource? _linkedCts; /// <summary> /// Флаг завершения операции. /// </summary> private bool _isOperationCompleted; /// <summary> /// Флаг запроса закрытия окна пользователем. /// </summary> private bool _isCloseRequested; /// <summary> /// Создаёт окно прогресса без запуска операции генерации. /// </summary> /// <param name="parViewModel">Модель представления, если требуется переиспользование.</param> public ProgressWindow(ProgressWindowViewModel? parViewModel = null) { BuildLayout(); DataContext = parViewModel ?? new ProgressWindowViewModel(); AttachViewModelHandlers(); Closing += OnClosing; Closed += (_, __) => _isCloseRequested = true; } /// <summary> /// ViewModel окна прогресса. /// </summary> public ProgressWindowViewModel ViewModel => (ProgressWindowViewModel)DataContext!; /// <summary> /// Показывает окно как модальный диалог и выполняет длительную операцию. /// </summary> /// <param name="parOwner">Родительское окно приложения.</param> /// <param name="parWorker">Функция, выполняющая основную работу.</param> /// <param name="parExternalToken">Внешний токен отмены операции.</param> /// <returns>Задача отображения окна и выполнения работы.</returns> public async Task RunAsync(Window parOwner, Func<IProgressDialog, CancellationToken, Task> parWorker, CancellationToken parExternalToken = default) { ArgumentNullException.ThrowIfNull(parOwner); ArgumentNullException.ThrowIfNull(parWorker); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(parExternalToken); _linkedCts = linkedCts; void HandleCancel(object? _, EventArgs __) => linkedCts.Cancel(); ViewModel.CancelRequested += HandleCancel; ViewModel.CloseRequested += HandleCancel; var showTask = ShowDialog(parOwner); Exception? captured = null; try { await Task.Run(() => parWorker(this, linkedCts.Token), linkedCts.Token); } catch (OperationCanceledException) { // отмена пользователем } catch (Exception ex) { captured = ex; } finally { ViewModel.CancelRequested -= HandleCancel; ViewModel.CloseRequested -= HandleCancel; Close(true); } await showTask; if (captured is not null) { throw captured; } } /// <summary> /// Сбрасывает состояние ViewModel и задаёт параметры новой операции. /// </summary> /// <param name="parTitle">Заголовок окна.</param> /// <param name="parCaption">Основной текст.</param> /// <param name="parSubtitle">Дополнительная подпись.</param> /// <param name="parMaxProgress">Максимальное значение прогресса.</param> public void Initialize(string parTitle, string parCaption, string? parSubtitle, double parMaxProgress) { Dispatcher.UIThread.Post(() => { ViewModel.ResetButtonState(); ViewModel.Title = parTitle; ViewModel.Caption = parCaption; ViewModel.Subtitle = parSubtitle ?? string.Empty; ViewModel.MaxProgress = Math.Max(1, parMaxProgress); ViewModel.CurrentProgress = 0; _openedAt = DateTime.UtcNow; }); } /// <summary> /// Увеличивает прогресс и при необходимости обновляет подпись. /// </summary> /// <param name="parStep">Величина приращения прогресса.</param> /// <param name="parSubtitle">Новая подпись шага.</param> public void NextStep(double parStep = 1, string? parSubtitle = null) { Dispatcher.UIThread.Post(() => ViewModel.UpdateProgress(parStep, parSubtitle)); } /// <summary> /// Переводит окно в завершённое состояние и закрывает по необходимости. /// </summary> /// <param name="parAutoCloseWindow">Признак автоматического закрытия.</param> public void Close(bool parAutoCloseWindow = true) { _isOperationCompleted = true; Dispatcher.UIThread.Post(async () => { ViewModel.SwitchToCompletionState(); if (!parAutoCloseWindow) { return; } var missing = _minimumVisible - (DateTime.UtcNow - _openedAt); if (missing > TimeSpan.Zero) { await Task.Delay(missing); } TryCloseInternal(); }); } /// <summary> /// Обновляет интерфейс на сообщение о завершении и задаёт финальный подзаголовок. /// </summary> /// <param name="parSubtitle">Текст финального сообщения.</param> /// <returns>Задача, завершающаяся после обновления UI.</returns> public Task SwitchToCompletedAsync(string parSubtitle) { _isOperationCompleted = true; return Dispatcher.UIThread.InvokeAsync(() => { ViewModel.SwitchToCompletionState(); ViewModel.Subtitle = parSubtitle; }).GetTask(); } /// <summary> /// Обработчик закрытия окна. /// </summary> /// <param name="parSender">Источник события.</param> /// <param name="parArgs">Аргументы закрытия окна.</param> private void OnClosing(object? parSender, WindowClosingEventArgs parArgs) { if (_isOperationCompleted) { return; } if (!_isCloseRequested) { _linkedCts?.Cancel(); parArgs.Cancel = true; ViewModel.DisableCancellationButton(); } } /// <summary> /// Подписывает ViewModel на события отмены и закрытия /// </summary> private void AttachViewModelHandlers() { ViewModel.CancelRequested += (_, __) => { _linkedCts?.Cancel(); ViewModel.Subtitle = "Отмена операции..."; }; ViewModel.CloseRequested += (_, __) => { if (_isOperationCompleted) { TryCloseInternal(); } }; } /// <summary> /// Создаёт визуальное дерево окна /// </summary> private void BuildLayout() { Width = 420; Height = 220; MinWidth = 360; MinHeight = 200; CanResize = false; WindowStartupLocation = WindowStartupLocation.CenterScreen; this.Bind(TitleProperty, new Binding(nameof(ProgressWindowViewModel.Title))); var caption = new TextBlock { FontSize = 16, TextWrapping = TextWrapping.Wrap }; caption.Classes.Add("h3"); caption.Bind(TextBlock.TextProperty, new Binding(nameof(ProgressWindowViewModel.Caption))); var subtitle = new TextBlock { TextWrapping = TextWrapping.Wrap, Foreground = Brushes.Gray }; subtitle.Bind(TextBlock.TextProperty, new Binding(nameof(ProgressWindowViewModel.Subtitle))); var summary = new TextBlock { HorizontalAlignment = HorizontalAlignment.Right, FontSize = 12, Foreground = Brushes.Gray }; summary.Bind(TextBlock.TextProperty, new Binding(nameof(ProgressWindowViewModel.ProgressSummary))); var progressBar = new ProgressBar { Height = 24, Minimum = 0 }; progressBar.Bind(RangeBase.MaximumProperty, new Binding(nameof(ProgressWindowViewModel.MaxProgress))); progressBar.Bind(RangeBase.ValueProperty, new Binding(nameof(ProgressWindowViewModel.CurrentProgress))); var cancelButton = new Button { MinWidth = 100, HorizontalAlignment = HorizontalAlignment.Right }; cancelButton.Bind(ContentControl.ContentProperty, new Binding(nameof(ProgressWindowViewModel.CancelButtonText))); cancelButton.Bind(Button.CommandProperty, new Binding(nameof(ProgressWindowViewModel.CancelCommand))); cancelButton.Bind(Button.IsEnabledProperty, new Binding(nameof(ProgressWindowViewModel.IsCancelEnabled))); var grid = new Grid { RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"), ColumnDefinitions = new ColumnDefinitions("1*") }; grid.Children.Add(caption); Grid.SetRow(caption, 0); grid.Children.Add(subtitle); Grid.SetRow(subtitle, 1); grid.Children.Add(progressBar); Grid.SetRow(progressBar, 2); progressBar.Margin = new Thickness(0, 4, 0, 4); var bottom = new DockPanel { LastChildFill = true }; bottom.Children.Add(summary); DockPanel.SetDock(summary, Dock.Left); bottom.Children.Add(cancelButton); DockPanel.SetDock(cancelButton, Dock.Right); grid.Children.Add(bottom); Grid.SetRow(bottom, 3); Content = new Border { Padding = new Thickness(16), Child = grid }; } /// <summary> /// Безопасно закрывает окно через диспетчер UI. /// </summary> private void TryCloseInternal() { if (_isCloseRequested) { return; } _isCloseRequested = true; Dispatcher.UIThread.Post(() => base.Close()); } }