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

391 lines
12 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security;
using System.Windows.Input;
namespace Zcbot.WindowsNode;
internal sealed class MainWindowViewModel : ObservableObject, IDisposable
{
private readonly IStartupRegistration startupRegistration;
private readonly IDiagnosticService diagnosticService;
private readonly AsyncCommand registerCommand;
private readonly AsyncCommand reconnectCommand;
private readonly AsyncCommand resetIdentityCommand;
private string statusText = "尚未注册";
private string statusDetail = "等待节点状态";
private string identityText = "尚未建立节点身份";
private string serverUrl = "http://127.0.0.1:8765";
private string nodeName = Environment.MachineName.ToLowerInvariant();
private string enrollmentCode = string.Empty;
private string operationMessage = string.Empty;
private string statusTone = "neutral";
private bool isRegistered;
private bool isBusy;
private bool isExiting;
private string currentPage = "overview";
private bool isStartupEnabled;
private NodeConfig? currentConfig;
internal MainWindowViewModel(
IStartupRegistration startupRegistration,
IDiagnosticService diagnosticService,
SoftwarePageViewModel softwarePage,
JobPageViewModel jobPage,
AnsysAcceptanceViewModel acceptancePage,
DataRootPageViewModel dataRootPage)
{
this.startupRegistration = startupRegistration;
this.diagnosticService = diagnosticService;
SoftwarePage = softwarePage;
JobPage = jobPage;
AcceptancePage = acceptancePage;
DataRootPage = dataRootPage;
try
{
isStartupEnabled = startupRegistration.IsEnabled;
}
catch (Exception exception) when (
exception is UnauthorizedAccessException or IOException or SecurityException)
{
operationMessage = $"无法读取自动启动设置:{exception.Message}";
}
CopyDiagnosticsCommand = new RelayCommand(CopyDiagnostics, () => IsRegistered);
HideCommand = new RelayCommand(() => HideRequested?.Invoke(), () => !IsExiting);
ExitCommand = new AsyncCommand(
() => ExitRequested?.Invoke() ?? Task.CompletedTask,
() => !IsExiting);
ShowOverviewCommand = new RelayCommand(() => Navigate("overview"));
ShowSoftwareCommand = new AsyncCommand(async () =>
{
Navigate("software");
await SoftwarePage.RefreshAsync();
AcceptancePage.RefreshStatus();
});
ShowJobsCommand = new RelayCommand(() =>
{
Navigate("jobs");
JobPage.Refresh();
});
ShowSettingsCommand = new RelayCommand(() =>
{
Navigate("settings");
DataRootPage.Refresh();
});
registerCommand = new AsyncCommand(RegisterAsync, CanRegister);
reconnectCommand = new AsyncCommand(ReconnectAsync, () => IsRegistered && !IsBusy);
resetIdentityCommand = new AsyncCommand(ResetIdentityAsync, () => IsRegistered && !IsBusy);
RegisterCommand = registerCommand;
ReconnectCommand = reconnectCommand;
ResetIdentityCommand = resetIdentityCommand;
}
internal event Func<string, bool>? CopyDiagnosticsRequested;
internal event Action? HideRequested;
internal event Func<Task>? ExitRequested;
internal event Func<EnrollOptions, Task>? RegisterRequested;
internal event Func<Task<bool>>? ReconnectRequested;
internal event Func<Task<bool>>? ResetIdentityRequested;
public string StatusText
{
get => statusText;
private set => SetProperty(ref statusText, value);
}
public string StatusDetail
{
get => statusDetail;
private set => SetProperty(ref statusDetail, value);
}
public string IdentityText
{
get => identityText;
private set => SetProperty(ref identityText, value);
}
public string ServerUrl
{
get => serverUrl;
set
{
if (SetProperty(ref serverUrl, value)) registerCommand.RaiseCanExecuteChanged();
}
}
public string NodeName
{
get => nodeName;
set
{
if (SetProperty(ref nodeName, value)) registerCommand.RaiseCanExecuteChanged();
}
}
public string EnrollmentCode
{
get => enrollmentCode;
set
{
if (SetProperty(ref enrollmentCode, value)) registerCommand.RaiseCanExecuteChanged();
}
}
public string OperationMessage
{
get => operationMessage;
private set
{
if (SetProperty(ref operationMessage, value))
{
OnPropertyChanged(nameof(HasOperationMessage));
}
}
}
public bool HasOperationMessage => !string.IsNullOrWhiteSpace(OperationMessage);
public string StatusTone
{
get => statusTone;
private set => SetProperty(ref statusTone, value);
}
public bool IsRegistered
{
get => isRegistered;
private set
{
if (SetProperty(ref isRegistered, value))
{
OnPropertyChanged(nameof(IsRegistrationVisible));
RaiseOperationCanExecuteChanged();
((RelayCommand)CopyDiagnosticsCommand).RaiseCanExecuteChanged();
}
}
}
public bool IsRegistrationVisible => !IsRegistered;
public bool IsBusy
{
get => isBusy;
private set
{
if (SetProperty(ref isBusy, value)) RaiseOperationCanExecuteChanged();
}
}
public bool IsExiting
{
get => isExiting;
private set
{
if (SetProperty(ref isExiting, value))
{
((RelayCommand)HideCommand).RaiseCanExecuteChanged();
((AsyncCommand)ExitCommand).RaiseCanExecuteChanged();
}
}
}
public bool IsOverviewPage => currentPage == "overview";
public bool IsSoftwarePage => currentPage == "software";
public bool IsJobsPage => currentPage == "jobs";
public bool IsSettingsPage => currentPage == "settings";
public string PageTitle => currentPage switch
{
"software" => "专业软件",
"jobs" => "本机任务",
"settings" => "运行设置",
_ => "节点概览",
};
public bool IsStartupEnabled
{
get => isStartupEnabled;
set
{
if (value == isStartupEnabled) return;
try
{
startupRegistration.SetEnabled(value);
SetProperty(ref isStartupEnabled, value);
OperationMessage = value
? "已启用登录 Windows 后自动启动"
: "已关闭登录 Windows 后自动启动";
}
catch (Exception exception) when (
exception is UnauthorizedAccessException or IOException or SecurityException)
{
OperationMessage = $"自动启动设置失败:{exception.Message}";
OnPropertyChanged();
}
}
}
public ICommand CopyDiagnosticsCommand { get; }
public ICommand HideCommand { get; }
public ICommand ExitCommand { get; }
public ICommand ShowOverviewCommand { get; }
public ICommand ShowSoftwareCommand { get; }
public ICommand ShowJobsCommand { get; }
public ICommand ShowSettingsCommand { get; }
public ICommand RegisterCommand { get; }
public ICommand ReconnectCommand { get; }
public ICommand ResetIdentityCommand { get; }
public SoftwarePageViewModel SoftwarePage { get; }
public JobPageViewModel JobPage { get; }
public AnsysAcceptanceViewModel AcceptancePage { get; }
public DataRootPageViewModel DataRootPage { get; }
internal void ApplyStatus(NodeStatus status, NodeConfig? config = null)
{
StatusText = status.State switch
{
NodeState.Online => "节点在线",
NodeState.Connecting => "正在连接",
NodeState.AuthenticationRequired => "需要重新注册",
NodeState.Offline => "节点离线",
NodeState.Stopped => "节点已停止",
_ => "尚未注册",
};
StatusTone = status.State switch
{
NodeState.Online => "success",
NodeState.Connecting => "warning",
NodeState.NotRegistered or NodeState.AuthenticationRequired => "error",
_ => "neutral",
};
StatusDetail = $"{status.Message} {status.ChangedAt:HH:mm:ss}";
IsRegistered = config is not null;
currentConfig = config;
IdentityText = config is null
? "尚未建立节点身份"
: $"节点:{config.NodeName}\nNode ID{config.NodeId}\n服务{config.ServerUrl}";
if (config is not null)
{
ServerUrl = config.ServerUrl.AbsoluteUri.TrimEnd('/');
NodeName = config.NodeName;
EnrollmentCode = string.Empty;
}
}
internal void BeginExit()
{
SoftwarePage.CancelInstall();
IsExiting = true;
StatusText = "正在退出";
StatusTone = "neutral";
StatusDetail = "正在等待本机任务与后台进程安全收尾";
}
public void Dispose()
{
SoftwarePage.Dispose();
AcceptancePage.Dispose();
}
private bool CanRegister() =>
!IsRegistered
&& !IsBusy
&& !string.IsNullOrWhiteSpace(ServerUrl)
&& !string.IsNullOrWhiteSpace(NodeName)
&& !string.IsNullOrWhiteSpace(EnrollmentCode);
private void CopyDiagnostics()
{
if (currentConfig is null) return;
OperationMessage = CopyDiagnosticsRequested?.Invoke(
diagnosticService.Build(currentConfig)) == true
? "诊断信息已复制;内容不包含 Node Token。"
: "无法写入剪贴板,请稍后重试。";
}
private async Task RegisterAsync()
{
if (RegisterRequested is null) return;
await RunOperationAsync(async () =>
{
var options = EnrollOptions.Parse([
"--server", ServerUrl,
"--name", NodeName,
"--code", EnrollmentCode]);
await RegisterRequested(options);
OperationMessage = "注册成功,正在连接节点";
}, "正在注册节点…");
}
private async Task ReconnectAsync()
{
if (ReconnectRequested is null) return;
await RunOperationAsync(async () =>
{
OperationMessage = await ReconnectRequested()
? "已开始重新连接"
: "本机尚未注册";
}, "正在等待任务收尾并重新连接…");
}
private async Task ResetIdentityAsync()
{
if (ResetIdentityRequested is null) return;
await RunOperationAsync(async () =>
{
if (await ResetIdentityRequested())
{
OperationMessage = "本机身份已清除,请使用新注册码注册";
}
else
{
OperationMessage = "已取消清除本机身份";
}
}, "正在清除本机身份…");
}
private async Task RunOperationAsync(Func<Task> operation, string progress)
{
IsBusy = true;
OperationMessage = progress;
try
{
await operation();
}
catch (Exception exception) when (
exception is NodeConfigurationException
or HttpRequestException
or IOException
or UnauthorizedAccessException)
{
OperationMessage = exception.Message;
}
finally
{
IsBusy = false;
}
}
private void RaiseOperationCanExecuteChanged()
{
registerCommand.RaiseCanExecuteChanged();
reconnectCommand.RaiseCanExecuteChanged();
resetIdentityCommand.RaiseCanExecuteChanged();
}
private void Navigate(string page)
{
if (currentPage == page) return;
currentPage = page;
OnPropertyChanged(nameof(IsOverviewPage));
OnPropertyChanged(nameof(IsSoftwarePage));
OnPropertyChanged(nameof(IsJobsPage));
OnPropertyChanged(nameof(IsSettingsPage));
OnPropertyChanged(nameof(PageTitle));
}
}