275 lines
8.7 KiB
C#
275 lines
8.7 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|