zcbot/windows-node/Zcbot.WindowsNode/Presentation/ViewModels/SoftwarePageViewModel.cs

275 lines
8.7 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<SoftwareCardViewModel, Task> chooseLocation,
Func<SoftwareCardViewModel, Task> clearLocation,
Func<SoftwareCardViewModel, Task> 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<SoftwarePathKind, string, string, string?>? ChooseLocationRequested;
internal event Func<string, bool>? ConfirmInstallRequested;
public ObservableCollection<SoftwareCardViewModel> 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<string>(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);
}
}
}
}