using System.Collections.ObjectModel; using System.ComponentModel; using System.Security; using System.Windows.Input; namespace Zcbot.WindowsNode; internal sealed class SoftwareCardViewModel : ObservableObject { private SoftwareManagementSnapshot snapshot; private string locationText; private string statusText; internal SoftwareCardViewModel( SoftwareManagementSnapshot snapshot, Func chooseLocation, Func clearLocation, Func installRuntime) { this.snapshot = snapshot; locationText = snapshot.LocationText; statusText = snapshot.StatusText; ChooseLocationCommand = new AsyncCommand(() => chooseLocation(this), CanChange); ClearLocationCommand = new AsyncCommand(() => clearLocation(this), CanChange); InstallRuntimeCommand = new AsyncCommand(() => installRuntime(this), CanInstall); } public string Id => snapshot.Id; public string DisplayName => snapshot.DisplayName; internal SoftwarePathKind PathKind => snapshot.PathKind; internal string RequiredRelativePath => snapshot.RequiredRelativePath; public string LocationText { get => locationText; private set => SetProperty(ref locationText, value); } public string StatusText { get => statusText; set => SetProperty(ref statusText, value); } public bool LocationAvailable => snapshot.LocationAvailable; public bool RuntimeInstalled => snapshot.RuntimeInstalled; public bool HasActiveJobs => snapshot.HasActiveJobs; internal bool IsPageBusy { get; set; } public ICommand ChooseLocationCommand { get; } public ICommand ClearLocationCommand { get; } public ICommand InstallRuntimeCommand { get; } internal void Apply(SoftwareManagementSnapshot value) { snapshot = value; LocationText = value.LocationText; StatusText = value.StatusText; OnPropertyChanged(nameof(LocationAvailable)); OnPropertyChanged(nameof(RuntimeInstalled)); OnPropertyChanged(nameof(HasActiveJobs)); RaiseCanExecuteChanged(); } internal void RaiseCanExecuteChanged() { ((AsyncCommand)ChooseLocationCommand).RaiseCanExecuteChanged(); ((AsyncCommand)ClearLocationCommand).RaiseCanExecuteChanged(); ((AsyncCommand)InstallRuntimeCommand).RaiseCanExecuteChanged(); } private bool CanChange() => !IsPageBusy; private bool CanInstall() => !IsPageBusy && LocationAvailable && !HasActiveJobs; } internal sealed class SoftwarePageViewModel : ObservableObject, IDisposable { private readonly ISoftwareManagementService service; private CancellationTokenSource? installStop; private bool isBusy; private string message = "打开页面后检测本机软件与运行环境。"; internal SoftwarePageViewModel(ISoftwareManagementService service) { this.service = service; RefreshCommand = new AsyncCommand(RefreshAsync, () => !IsBusy); CancelInstallCommand = new RelayCommand(CancelInstall, () => installStop is not null); } internal event Func? ChooseLocationRequested; internal event Func? ConfirmInstallRequested; public ObservableCollection Cards { get; } = []; public bool IsBusy { get => isBusy; private set { if (!SetProperty(ref isBusy, value)) return; foreach (var card in Cards) { card.IsPageBusy = value; card.RaiseCanExecuteChanged(); } ((AsyncCommand)RefreshCommand).RaiseCanExecuteChanged(); ((RelayCommand)CancelInstallCommand).RaiseCanExecuteChanged(); } } public bool IsInstalling => installStop is not null; public string Message { get => message; private set => SetProperty(ref message, value); } public ICommand RefreshCommand { get; } public ICommand CancelInstallCommand { get; } internal async Task RefreshAsync() { if (IsBusy) return; IsBusy = true; Message = "正在检测本机软件与运行环境…"; try { await RefreshCoreAsync(CancellationToken.None); Message = "检测完成"; } catch (Exception exception) when ( exception is IOException or InvalidDataException or InvalidOperationException or SecurityException or UnauthorizedAccessException or Win32Exception) { Message = $"检测失败:{exception.Message}"; } finally { IsBusy = false; } } internal void CancelInstall() => installStop?.Cancel(); public void Dispose() { installStop?.Cancel(); installStop?.Dispose(); installStop = null; } private async Task ChooseLocationAsync(SoftwareCardViewModel card) { var selected = ChooseLocationRequested?.Invoke( card.PathKind, card.DisplayName, card.RequiredRelativePath); if (string.IsNullOrWhiteSpace(selected)) return; await ChangeLocationAsync( () => service.SaveLocation(card.Id, selected), "应用位置已保存"); } private Task ClearLocationAsync(SoftwareCardViewModel card) => ChangeLocationAsync( () => service.ClearLocation(card.Id), "已恢复自动检测"); private async Task ChangeLocationAsync(Action change, string successMessage) { IsBusy = true; try { change(); await RefreshCoreAsync(CancellationToken.None); Message = successMessage; } catch (Exception exception) when ( exception is ArgumentException or IOException or InvalidDataException or InvalidOperationException or SecurityException or UnauthorizedAccessException) { Message = $"应用位置无效:{exception.Message}"; } finally { IsBusy = false; } } private async Task InstallRuntimeAsync(SoftwareCardViewModel card) { if (card.HasActiveJobs) { Message = $"{card.DisplayName} 仍有任务在执行,暂不能更换运行环境。"; return; } if (ConfirmInstallRequested?.Invoke(card.DisplayName) != true) return; installStop?.Dispose(); installStop = new CancellationTokenSource(); OnPropertyChanged(nameof(IsInstalling)); IsBusy = true; Message = $"正在安装 {card.DisplayName} 运行环境…"; var progress = new Progress(line => { card.StatusText = line; Message = line; }); try { var result = await service.InstallRuntimeAsync( card.Id, progress, installStop.Token); await RefreshCoreAsync(CancellationToken.None); Message = $"{result.Detail} {result.RuntimePath}"; } catch (OperationCanceledException) { Message = "安装已取消,原运行环境保持不变。"; await RefreshCoreAsync(CancellationToken.None); } catch (Exception exception) when ( exception is IOException or InvalidDataException or InvalidOperationException or SecurityException or UnauthorizedAccessException or Win32Exception) { Message = $"安装失败:{exception.Message}"; } finally { installStop.Dispose(); installStop = null; OnPropertyChanged(nameof(IsInstalling)); IsBusy = false; } } private async Task RefreshCoreAsync(CancellationToken cancellationToken) { var snapshots = await service.RefreshAsync(cancellationToken); foreach (var snapshot in snapshots) { var card = Cards.SingleOrDefault(item => item.Id == snapshot.Id); if (card is null) { card = new SoftwareCardViewModel( snapshot, ChooseLocationAsync, ClearLocationAsync, InstallRuntimeAsync); card.IsPageBusy = IsBusy; Cards.Add(card); } else { card.Apply(snapshot); } } } }