feat: page-based navigation shell + Tests page wizard
Replace the monolithic MainWindow with a SelectedPage-driven shell (Dashboard / Pump / Bench / Tests / Results / Settings). The Tests page gets the Plan -> Preconditions -> Running -> Done wizard from ui-structure.md \u00a74, backed by a 7-item precondition gate and shared sub-views (PhaseCardView / TestSectionView / GraphicIndicatorView) extracted from the now-deleted monolithic TestPanelView. New VMs / views: - Tests wizard: TestPreconditions, PhaseCard, GraphicIndicator, TestSection, TestPlan, TestRunning, TestDone - Dashboard panels: DashboardConnection, DashboardReadings, DashboardAlarms, InterlockBanner, ResultHistory - Pump / bench panels: PumpIdentificationPanel, PumpLiveData, UnlockPanel, BenchDriveControl, BenchReadings, RelayBank, TemperatureControl, DtcList, AuthGate - Dialogs: generic ConfirmDialog, UserManageDialog, UserPromptDialog Supporting changes: - IsOilPumpOn exposed on MainViewModel for precondition evaluation - RequiresAuth added to TestDefinition (XML round-trip) - BipStatusDefinition + CompletedTestRun models - ~35 new Test.* localization keys (en + es) - Settings moved from modal dialog to full page - Pause / Retry / Skip stubs in TestRunningView; full spec in docs/gap-test-running-controls.md for follow-up implementation - docs/ui-structure.md captures the wizard design Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
ViewModels/Dialogs/ConfirmDialogViewModel.cs
Normal file
51
ViewModels/Dialogs/ConfirmDialogViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic Yes/No (or Confirm/Cancel) modal dialog view-model.
|
||||
///
|
||||
/// <para>Reusable across abort-test, skip-phase, delete-pump, and any future
|
||||
/// binary decision prompt. Caller sets <see cref="Title"/>, <see cref="Message"/>,
|
||||
/// <see cref="ConfirmText"/>, <see cref="CancelText"/> before showing the dialog,
|
||||
/// then inspects <see cref="Accepted"/> after the dialog closes.</para>
|
||||
/// </summary>
|
||||
public sealed partial class ConfirmDialogViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>Window title.</summary>
|
||||
[ObservableProperty] private string _title = string.Empty;
|
||||
|
||||
/// <summary>Body message — may be multi-line.</summary>
|
||||
[ObservableProperty] private string _message = string.Empty;
|
||||
|
||||
/// <summary>Text shown on the positive-action button (defaults to a localised "OK").</summary>
|
||||
[ObservableProperty] private string _confirmText = "OK";
|
||||
|
||||
/// <summary>Text shown on the cancel button (defaults to a localised "Cancel").</summary>
|
||||
[ObservableProperty] private string _cancelText = "Cancel";
|
||||
|
||||
/// <summary>True when the operator clicked <see cref="ConfirmText"/>; false on cancel/close.</summary>
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
/// <summary>Raised when the dialog should close itself.</summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
/// <summary>Accepts the prompt — sets <see cref="Accepted"/> to true and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
Accepted = true;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Cancels the prompt — leaves <see cref="Accepted"/> false and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Accepted = false;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Infrastructure.Kwp;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the application settings dialog.
|
||||
/// Loads a local copy of every <see cref="AppSettings"/> property so that
|
||||
/// Cancel discards all changes.
|
||||
/// </summary>
|
||||
public sealed partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
// ── Dialog result ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the user clicked Accept.</summary>
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
/// <summary>Raised to close the owning dialog window.</summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
// ── Collections ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Available language codes for the language dropdown.</summary>
|
||||
public ObservableCollection<string> AvailableLanguages { get; } = new() { "ESP", "ENG" };
|
||||
|
||||
/// <summary>RPM-voltage lookup table, editable via DataGrid.</summary>
|
||||
public ObservableCollection<RpmVoltageRelation> Relations { get; } = new();
|
||||
|
||||
/// <summary>Available FTDI device serial numbers for K-Line port selection.</summary>
|
||||
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||
|
||||
// ── General ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedLanguage = "ESP";
|
||||
[ObservableProperty] private int _daysKeepLogs = 7;
|
||||
|
||||
// ── Safety ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _tempMax = 45;
|
||||
[ObservableProperty] private int _tempMin = 35;
|
||||
[ObservableProperty] private int _securityRpmLimit = 2500;
|
||||
[ObservableProperty] private int _maxPressureBar = 26;
|
||||
[ObservableProperty] private double _toleranceUpExtension = 0.08;
|
||||
[ObservableProperty] private double _tolerancePfpExtension = 0.1;
|
||||
[ObservableProperty] private bool _defaultIgnoreTin = true;
|
||||
|
||||
// ── PID ───────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private double _pidP = 0.1;
|
||||
[ObservableProperty] private double _pidI = 0.1;
|
||||
[ObservableProperty] private double _pidD = 0.04;
|
||||
[ObservableProperty] private int _pidLoopMs = 250;
|
||||
|
||||
// ── Motor ─────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _encoderResolution = 4096;
|
||||
[ObservableProperty] private double _voltageForMaxRpm = 10;
|
||||
[ObservableProperty] private int _maxRpm = 2500;
|
||||
[ObservableProperty] private bool _rightRelayValue = true;
|
||||
|
||||
// ── Company ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _companyName = string.Empty;
|
||||
[ObservableProperty] private string _companyInfo = string.Empty;
|
||||
[ObservableProperty] private string _reportLogoPath = string.Empty;
|
||||
|
||||
// ── K-Line ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedKLinePort = string.Empty;
|
||||
|
||||
// ── Advanced (refresh intervals) ──────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _refreshBenchInterfaceMs = 20;
|
||||
[ObservableProperty] private int _refreshWhileReadingMs = 1500;
|
||||
[ObservableProperty] private int _refreshCanBusReadMs = 2;
|
||||
[ObservableProperty] private int _refreshPumpRequestMs = 250;
|
||||
[ObservableProperty] private int _refreshPumpParamsMs = 4;
|
||||
[ObservableProperty] private int _blinkIntervalMs = 1000;
|
||||
[ObservableProperty] private int _flasherIntervalMs = 800;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────
|
||||
|
||||
/// <param name="configService">Configuration service for loading/saving settings.</param>
|
||||
/// <param name="localizationService">Localization service for language switching.</param>
|
||||
public SettingsViewModel(IConfigurationService configService, ILocalizationService localizationService)
|
||||
{
|
||||
_config = configService;
|
||||
_loc = localizationService;
|
||||
|
||||
var s = configService.Settings;
|
||||
|
||||
// General
|
||||
_selectedLanguage = s.Language;
|
||||
_daysKeepLogs = s.DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
_tempMax = s.TempMax;
|
||||
_tempMin = s.TempMin;
|
||||
_securityRpmLimit = s.SecurityRpmLimit;
|
||||
_maxPressureBar = s.MaxPressureBar;
|
||||
_toleranceUpExtension = s.ToleranceUpExtension;
|
||||
_tolerancePfpExtension = s.TolerancePfpExtension;
|
||||
_defaultIgnoreTin = s.DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
_pidP = s.PidP;
|
||||
_pidI = s.PidI;
|
||||
_pidD = s.PidD;
|
||||
_pidLoopMs = s.PidLoopMs;
|
||||
|
||||
// Motor
|
||||
_encoderResolution = s.EncoderResolution;
|
||||
_voltageForMaxRpm = s.VoltageForMaxRpm;
|
||||
_maxRpm = s.MaxRpm;
|
||||
_rightRelayValue = s.RightRelayValue;
|
||||
|
||||
// Company
|
||||
_companyName = s.CompanyName;
|
||||
_companyInfo = s.CompanyInfo;
|
||||
_reportLogoPath = s.ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
_selectedKLinePort = s.KLinePort;
|
||||
|
||||
// Advanced
|
||||
_refreshBenchInterfaceMs = s.RefreshBenchInterfaceMs;
|
||||
_refreshWhileReadingMs = s.RefreshWhileReadingMs;
|
||||
_refreshCanBusReadMs = s.RefreshCanBusReadMs;
|
||||
_refreshPumpRequestMs = s.RefreshPumpRequestMs;
|
||||
_refreshPumpParamsMs = s.RefreshPumpParamsMs;
|
||||
_blinkIntervalMs = s.BlinkIntervalMs;
|
||||
_flasherIntervalMs = s.FlasherIntervalMs;
|
||||
|
||||
// Deep-copy the RPM-voltage relation table
|
||||
foreach (var r in s.Relations)
|
||||
Relations.Add(new RpmVoltageRelation(r.Voltage, r.Rpm));
|
||||
|
||||
// Enumerate connected FTDI devices
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Copies all local values back to AppSettings, saves, and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Accept()
|
||||
{
|
||||
var s = _config.Settings;
|
||||
|
||||
// General
|
||||
s.DaysKeepLogs = DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
s.TempMax = TempMax;
|
||||
s.TempMin = TempMin;
|
||||
s.SecurityRpmLimit = SecurityRpmLimit;
|
||||
s.MaxPressureBar = MaxPressureBar;
|
||||
s.ToleranceUpExtension = ToleranceUpExtension;
|
||||
s.TolerancePfpExtension = TolerancePfpExtension;
|
||||
s.DefaultIgnoreTin = DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
s.PidP = PidP;
|
||||
s.PidI = PidI;
|
||||
s.PidD = PidD;
|
||||
s.PidLoopMs = PidLoopMs;
|
||||
|
||||
// Motor
|
||||
s.EncoderResolution = EncoderResolution;
|
||||
s.VoltageForMaxRpm = VoltageForMaxRpm;
|
||||
s.MaxRpm = MaxRpm;
|
||||
s.RightRelayValue = RightRelayValue;
|
||||
s.Relations = Relations.Select(r => new RpmVoltageRelation(r.Voltage, r.Rpm)).ToList();
|
||||
|
||||
// Company
|
||||
s.CompanyName = CompanyName;
|
||||
s.CompanyInfo = CompanyInfo;
|
||||
s.ReportLogoPath = ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
s.KLinePort = SelectedKLinePort;
|
||||
|
||||
// Advanced
|
||||
s.RefreshBenchInterfaceMs = RefreshBenchInterfaceMs;
|
||||
s.RefreshWhileReadingMs = RefreshWhileReadingMs;
|
||||
s.RefreshCanBusReadMs = RefreshCanBusReadMs;
|
||||
s.RefreshPumpRequestMs = RefreshPumpRequestMs;
|
||||
s.RefreshPumpParamsMs = RefreshPumpParamsMs;
|
||||
s.BlinkIntervalMs = BlinkIntervalMs;
|
||||
s.FlasherIntervalMs = FlasherIntervalMs;
|
||||
|
||||
// Language — switch if changed (also persists via LocalizationService)
|
||||
if (SelectedLanguage != _loc.CurrentLanguage)
|
||||
_loc.SetLanguage(SelectedLanguage);
|
||||
|
||||
_config.SaveSettings();
|
||||
|
||||
Accepted = true;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Discards changes and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Accepted = false;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Opens a file dialog to select a company logo image.</summary>
|
||||
[RelayCommand]
|
||||
private void BrowseLogo()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = _loc.GetString("Dialog.Settings.BrowseLogoTitle"),
|
||||
Filter = "Image files|*.png;*.jpg;*.jpeg;*.bmp|All files|*.*"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(ReportLogoPath))
|
||||
{
|
||||
try { dlg.InitialDirectory = System.IO.Path.GetDirectoryName(ReportLogoPath); }
|
||||
catch { /* ignore invalid path */ }
|
||||
}
|
||||
|
||||
if (dlg.ShowDialog() == true)
|
||||
ReportLogoPath = dlg.FileName;
|
||||
}
|
||||
|
||||
/// <summary>Re-enumerates connected FTDI devices into <see cref="AvailablePorts"/>.</summary>
|
||||
[RelayCommand]
|
||||
private void RefreshPorts()
|
||||
{
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
/// <summary>Appends a new empty row to the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void AddRelation()
|
||||
{
|
||||
Relations.Add(new RpmVoltageRelation(0.0, 0));
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected row from the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void RemoveRelation(RpmVoltageRelation? relation)
|
||||
{
|
||||
if (relation != null)
|
||||
Relations.Remove(relation);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Populates <see cref="AvailablePorts"/> with serial numbers of connected
|
||||
/// FTDI devices. Fails silently if the FTDI driver DLL is not present.
|
||||
/// </summary>
|
||||
private void EnumerateFtdiDevices()
|
||||
{
|
||||
AvailablePorts.Clear();
|
||||
try
|
||||
{
|
||||
uint count = FtdiInterface.GetDevicesCount();
|
||||
if (count == 0) return;
|
||||
|
||||
var list = new FT_DEVICE_INFO_NODE[count];
|
||||
FtdiInterface.GetDeviceList(list);
|
||||
|
||||
foreach (var device in list)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(device.SerialNumber))
|
||||
AvailablePorts.Add(device.SerialNumber);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// FTDI DLL not loaded or no devices — leave list empty.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
179
ViewModels/Dialogs/UserManageViewModel.cs
Normal file
179
ViewModels/Dialogs/UserManageViewModel.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Services;
|
||||
using HC_APTBS.Views.Dialogs;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the user management dialog.
|
||||
/// Invokes incremental methods on <see cref="IConfigurationService"/>
|
||||
/// (<c>AddUser</c>, <c>RemoveUser</c>, <c>ChangeUserPassword</c>) so that
|
||||
/// hashes of untouched accounts are preserved. Each action persists immediately;
|
||||
/// the dialog has no Accept/Cancel split — Close simply dismisses the window.
|
||||
/// </summary>
|
||||
public sealed partial class UserManageViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Creates the ViewModel and populates <see cref="Users"/> from the service.</summary>
|
||||
public UserManageViewModel(IConfigurationService config, ILocalizationService loc)
|
||||
{
|
||||
_config = config;
|
||||
_loc = loc;
|
||||
ReloadUsers();
|
||||
}
|
||||
|
||||
// ── Bindable state ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Usernames currently stored in the configuration.</summary>
|
||||
public ObservableCollection<string> Users { get; } = new();
|
||||
|
||||
/// <summary>Currently selected username in the DataGrid, or null when nothing is selected.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ChangePasswordCommand))]
|
||||
private string? _selectedUser;
|
||||
|
||||
/// <summary>Raised to close the owning dialog window.</summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Prompts for a username and password, then adds the user via the service.</summary>
|
||||
[RelayCommand]
|
||||
private void Add()
|
||||
{
|
||||
var prompt = new UserPromptDialog(
|
||||
_loc.GetString("Dialog.UserManage.Prompt.AddTitle"),
|
||||
usernameVisible: true)
|
||||
{
|
||||
Owner = Application.Current?.Windows.Count > 0
|
||||
? GetActiveWindow()
|
||||
: null
|
||||
};
|
||||
|
||||
if (prompt.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
string username = prompt.EnteredUsername?.Trim() ?? string.Empty;
|
||||
string password = prompt.EnteredPassword ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
||||
return;
|
||||
}
|
||||
if (username.IndexOfAny(new[] { ':', ',' }) >= 0)
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.InvalidChars", "Dialog.UserManage.Error.InvalidCharsTitle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_config.AddUser(username, password))
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.Duplicate", "Dialog.UserManage.Error.DuplicateTitle");
|
||||
return;
|
||||
}
|
||||
|
||||
ReloadUsers();
|
||||
SelectedUser = username;
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected user after confirmation, refusing when only one user remains.</summary>
|
||||
[RelayCommand(CanExecute = nameof(HasSelection))]
|
||||
private void Remove()
|
||||
{
|
||||
string user = SelectedUser!;
|
||||
var confirm = MessageBox.Show(
|
||||
string.Format(_loc.GetString("Dialog.UserManage.Confirm.Remove"), user),
|
||||
_loc.GetString("Dialog.UserManage.Confirm.RemoveTitle"),
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (confirm != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
if (!_config.RemoveUser(user))
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.LastUser", "Dialog.UserManage.Error.LastUserTitle");
|
||||
return;
|
||||
}
|
||||
|
||||
ReloadUsers();
|
||||
}
|
||||
|
||||
/// <summary>Prompts for a new password for the selected user and applies it.</summary>
|
||||
[RelayCommand(CanExecute = nameof(HasSelection))]
|
||||
private void ChangePassword()
|
||||
{
|
||||
string user = SelectedUser!;
|
||||
|
||||
var prompt = new UserPromptDialog(
|
||||
string.Format(_loc.GetString("Dialog.UserManage.Prompt.ChangeTitle"), user),
|
||||
usernameVisible: false,
|
||||
prefillUsername: user)
|
||||
{
|
||||
Owner = GetActiveWindow()
|
||||
};
|
||||
|
||||
if (prompt.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
string newPassword = prompt.EnteredPassword ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(newPassword))
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_config.ChangeUserPassword(user, newPassword))
|
||||
{
|
||||
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Closes the dialog.</summary>
|
||||
[RelayCommand]
|
||||
private void Close() => RequestClose?.Invoke();
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private bool HasSelection() => !string.IsNullOrEmpty(SelectedUser);
|
||||
|
||||
/// <summary>Reloads the user list from the service, preserving selection when possible.</summary>
|
||||
private void ReloadUsers()
|
||||
{
|
||||
string? previous = SelectedUser;
|
||||
Users.Clear();
|
||||
foreach (var name in _config.GetUsers())
|
||||
Users.Add(name);
|
||||
|
||||
SelectedUser = (previous != null && Users.Contains(previous)) ? previous : null;
|
||||
}
|
||||
|
||||
private void ShowError(string messageKey, string titleKey)
|
||||
{
|
||||
MessageBox.Show(
|
||||
_loc.GetString(messageKey),
|
||||
_loc.GetString(titleKey),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Stop);
|
||||
}
|
||||
|
||||
/// <summary>Returns the topmost active window for use as a dialog owner, or null if none.</summary>
|
||||
private static Window? GetActiveWindow()
|
||||
{
|
||||
foreach (Window w in Application.Current.Windows)
|
||||
{
|
||||
if (w.IsActive) return w;
|
||||
}
|
||||
return Application.Current.MainWindow;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user