refactor(securestoremanager): add platform service abstractions and constants
Implement deferred code review findings: - Add IDialogService/IClipboardService interfaces for testable platform operations - Create AvaloniaDialogService and AvaloniaClipboardService implementations - Extract dialog strings and file extensions to centralized Constants classes - Refactor ViewModels to use DI instead of event delegates - Update tests to use mock services
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// An async command implementation that properly handles async operations.
|
||||
/// </summary>
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AsyncRelayCommand that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The async action to execute.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute)
|
||||
: this(execute, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AsyncRelayCommand with a CanExecute predicate.
|
||||
/// </summary>
|
||||
/// <param name="execute">The async action to execute.</param>
|
||||
/// <param name="canExecute">The predicate to determine if the command can execute.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
}
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
return;
|
||||
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the CanExecuteChanged event.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
@@ -23,25 +24,41 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
set => SetProperty(ref _storePath, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _storePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set => SetProperty(ref _keyFilePath, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => SetProperty(ref _password, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _password, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
set => SetProperty(ref _confirmPassword, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _confirmPassword, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseKeyFile
|
||||
@@ -52,6 +69,7 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
if (SetProperty(ref _useKeyFile, value))
|
||||
{
|
||||
if (value) UsePassword = false;
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,10 +82,17 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
if (SetProperty(ref _usePassword, value))
|
||||
{
|
||||
if (value) UseKeyFile = false;
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
public ICommand BrowseStorePathCommand { get; }
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
@@ -93,18 +118,18 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return "Store path is required.";
|
||||
return DialogStrings.StorePathRequired;
|
||||
|
||||
if (UseKeyFile && string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return "Key file path is required.";
|
||||
return DialogStrings.KeyFilePathRequired;
|
||||
|
||||
if (UsePassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Password))
|
||||
return "Password is required.";
|
||||
return DialogStrings.PasswordRequired;
|
||||
|
||||
if (Password != ConfirmPassword)
|
||||
return "Passwords do not match.";
|
||||
return DialogStrings.PasswordsDoNotMatch;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,7 +148,11 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog("Choose Store Location", "SecureStore Files", "*.json", ".json");
|
||||
var path = await OnShowSaveFileDialog(
|
||||
DialogStrings.ChooseStoreLocation,
|
||||
FileExtensions.StoreTypeName,
|
||||
FileExtensions.StorePattern,
|
||||
FileExtensions.StoreExtension);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
StorePath = path;
|
||||
@@ -135,7 +164,11 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog("Choose Key File Location", "Key Files", "*.key", ".key");
|
||||
var path = await OnShowSaveFileDialog(
|
||||
DialogStrings.ChooseKeyFileLocation,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
@@ -163,19 +196,31 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
set => SetProperty(ref _storePath, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _storePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set => SetProperty(ref _keyFilePath, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => SetProperty(ref _password, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _password, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseKeyFile
|
||||
@@ -186,6 +231,7 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
if (SetProperty(ref _useKeyFile, value))
|
||||
{
|
||||
if (value) UsePassword = false;
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,10 +244,17 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
if (SetProperty(ref _usePassword, value))
|
||||
{
|
||||
if (value) UseKeyFile = false;
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
public ICommand BrowseStorePathCommand { get; }
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
@@ -227,22 +280,22 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return "Store path is required.";
|
||||
return DialogStrings.StorePathRequired;
|
||||
|
||||
if (!System.IO.File.Exists(StorePath))
|
||||
return "Store file does not exist.";
|
||||
return DialogStrings.StoreFileNotFound;
|
||||
|
||||
if (UseKeyFile)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return "Key file path is required.";
|
||||
return DialogStrings.KeyFilePathRequired;
|
||||
|
||||
if (!System.IO.File.Exists(KeyFilePath))
|
||||
return "Key file does not exist.";
|
||||
return DialogStrings.KeyFileNotFound;
|
||||
}
|
||||
|
||||
if (UsePassword && string.IsNullOrWhiteSpace(Password))
|
||||
return "Password is required.";
|
||||
return DialogStrings.PasswordRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -260,7 +313,10 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
if (OnShowOpenFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowOpenFileDialog("Select Store File", "SecureStore Files", "*.json");
|
||||
var path = await OnShowOpenFileDialog(
|
||||
DialogStrings.SelectStoreFile,
|
||||
FileExtensions.StoreTypeName,
|
||||
FileExtensions.StorePattern);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
StorePath = path;
|
||||
@@ -272,7 +328,10 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
if (OnShowOpenFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowOpenFileDialog("Select Key File", "Key Files", "*.key");
|
||||
var path = await OnShowOpenFileDialog(
|
||||
DialogStrings.SelectKeyFile,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
@@ -303,7 +362,11 @@ public class SecretEditDialogViewModel : ViewModelBase
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
set => SetProperty(ref _key, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _key, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Value
|
||||
@@ -329,9 +392,15 @@ public class SecretEditDialogViewModel : ViewModelBase
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Key))
|
||||
return "Key is required.";
|
||||
return DialogStrings.KeyRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
using JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
@@ -10,33 +11,36 @@ namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ISecureStoreManager _storeManager;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private SecretItemViewModel? _selectedSecret;
|
||||
private string _statusMessage = "Ready";
|
||||
|
||||
public MainWindowViewModel() : this(new Services.SecureStoreManager())
|
||||
public MainWindowViewModel(
|
||||
ISecureStoreManager storeManager,
|
||||
IDialogService dialogService,
|
||||
IClipboardService clipboardService)
|
||||
{
|
||||
}
|
||||
|
||||
public MainWindowViewModel(ISecureStoreManager storeManager)
|
||||
{
|
||||
_storeManager = storeManager;
|
||||
_storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager));
|
||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
|
||||
Secrets = new ObservableCollection<SecretItemViewModel>();
|
||||
|
||||
// File commands
|
||||
NewStoreCommand = new RelayCommand(ExecuteNewStore);
|
||||
OpenStoreCommand = new RelayCommand(ExecuteOpenStore);
|
||||
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
|
||||
CloseStoreCommand = new RelayCommand(ExecuteCloseStore, () => _storeManager.IsStoreOpen);
|
||||
ExitCommand = new RelayCommand(ExecuteExit);
|
||||
// File commands (async)
|
||||
NewStoreCommand = new AsyncRelayCommand(ExecuteNewStoreAsync);
|
||||
OpenStoreCommand = new AsyncRelayCommand(ExecuteOpenStoreAsync);
|
||||
SaveCommand = new AsyncRelayCommand(ExecuteSaveAsync, CanSave);
|
||||
CloseStoreCommand = new AsyncRelayCommand(ExecuteCloseStoreAsync, () => _storeManager.IsStoreOpen);
|
||||
ExitCommand = new AsyncRelayCommand(ExecuteExitAsync);
|
||||
|
||||
// Secret commands
|
||||
AddSecretCommand = new RelayCommand(ExecuteAddSecret, () => _storeManager.IsStoreOpen);
|
||||
EditSecretCommand = new RelayCommand(ExecuteEditSecret, CanEditOrDeleteSecret);
|
||||
DeleteSecretCommand = new RelayCommand(ExecuteDeleteSecret, CanEditOrDeleteSecret);
|
||||
DeleteSecretCommand = new AsyncRelayCommand(ExecuteDeleteSecretAsync, CanEditOrDeleteSecret);
|
||||
|
||||
// Tools commands
|
||||
GenerateKeyFileCommand = new RelayCommand(ExecuteGenerateKeyFile);
|
||||
ExportKeyCommand = new RelayCommand(ExecuteExportKey, () => _storeManager.IsStoreOpen);
|
||||
// Tools commands (async)
|
||||
GenerateKeyFileCommand = new AsyncRelayCommand(ExecuteGenerateKeyFileAsync);
|
||||
ExportKeyCommand = new AsyncRelayCommand(ExecuteExportKeyAsync, () => _storeManager.IsStoreOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +54,14 @@ public class MainWindowViewModel : ViewModelBase
|
||||
public SecretItemViewModel? SelectedSecret
|
||||
{
|
||||
get => _selectedSecret;
|
||||
set => SetProperty(ref _selectedSecret, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedSecret, value))
|
||||
{
|
||||
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -134,7 +145,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error creating store: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to create store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToCreateStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +179,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error opening store: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to open store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToOpenStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +200,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error saving secret: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to save secret:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToSaveSecretFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,10 +215,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
if (!_storeManager.HasUnsavedChanges)
|
||||
return true;
|
||||
|
||||
if (OnShowUnsavedChangesPrompt == null)
|
||||
return true;
|
||||
|
||||
var result = await OnShowUnsavedChangesPrompt();
|
||||
var result = await _dialogService.ShowUnsavedChangesPromptAsync();
|
||||
|
||||
switch (result)
|
||||
{
|
||||
@@ -216,7 +230,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteNewStore()
|
||||
private async Task ExecuteNewStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
@@ -225,7 +239,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
OnRequestNewStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async void ExecuteOpenStore()
|
||||
private async Task ExecuteOpenStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
@@ -234,11 +248,6 @@ public class MainWindowViewModel : ViewModelBase
|
||||
OnRequestOpenStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async void ExecuteSave()
|
||||
{
|
||||
await ExecuteSaveAsync();
|
||||
}
|
||||
|
||||
private async Task ExecuteSaveAsync()
|
||||
{
|
||||
try
|
||||
@@ -250,13 +259,15 @@ public class MainWindowViewModel : ViewModelBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error saving: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to save store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToSaveStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanSave() => _storeManager.IsStoreOpen && _storeManager.HasUnsavedChanges;
|
||||
|
||||
private async void ExecuteCloseStore()
|
||||
private async Task ExecuteCloseStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
@@ -267,7 +278,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
StatusMessage = "Store closed";
|
||||
}
|
||||
|
||||
private async void ExecuteExit()
|
||||
private async Task ExecuteExitAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
@@ -290,15 +301,13 @@ public class MainWindowViewModel : ViewModelBase
|
||||
OnRequestEditSecretDialog?.Invoke(SelectedSecret.Key, SelectedSecret.ActualValue);
|
||||
}
|
||||
|
||||
private async void ExecuteDeleteSecret()
|
||||
private async Task ExecuteDeleteSecretAsync()
|
||||
{
|
||||
if (SelectedSecret == null)
|
||||
return;
|
||||
|
||||
if (OnShowDeleteConfirmation == null)
|
||||
return;
|
||||
|
||||
var confirmed = await OnShowDeleteConfirmation(SelectedSecret.Key);
|
||||
var confirmMessage = string.Format(DialogStrings.ConfirmDeleteFormat, SelectedSecret.Key);
|
||||
var confirmed = await _dialogService.ShowConfirmationAsync(confirmMessage, DialogStrings.ConfirmDeleteTitle);
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
@@ -313,18 +322,22 @@ public class MainWindowViewModel : ViewModelBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error deleting secret: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to delete secret:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToDeleteSecretFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditOrDeleteSecret() => _storeManager.IsStoreOpen && SelectedSecret != null;
|
||||
|
||||
private async void ExecuteGenerateKeyFile()
|
||||
private async Task ExecuteGenerateKeyFileAsync()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
var filePath = await _dialogService.ShowSaveFileDialogAsync(
|
||||
DialogStrings.GenerateKeyFileTitle,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
|
||||
var filePath = await OnShowSaveFileDialog("Generate Key File", "Key Files", "*.key", ".key");
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
@@ -332,21 +345,27 @@ public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
_storeManager.GenerateKeyFile(filePath);
|
||||
StatusMessage = $"Generated key file: {filePath}";
|
||||
await (OnShowInfo?.Invoke($"Key file generated successfully:\n\n{filePath}", "Key Generated") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowInfoAsync(
|
||||
string.Format(DialogStrings.KeyFileGeneratedFormat, filePath),
|
||||
DialogStrings.KeyGeneratedTitle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error generating key: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to generate key file:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToGenerateKeyFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteExportKey()
|
||||
private async Task ExecuteExportKeyAsync()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
var filePath = await _dialogService.ShowSaveFileDialogAsync(
|
||||
DialogStrings.ExportKeyTitle,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
|
||||
var filePath = await OnShowSaveFileDialog("Export Key", "Key Files", "*.key", ".key");
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
@@ -354,12 +373,16 @@ public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
_storeManager.ExportKey(filePath);
|
||||
StatusMessage = $"Exported key to: {filePath}";
|
||||
await (OnShowInfo?.Invoke($"Key exported successfully:\n\n{filePath}", "Key Exported") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowInfoAsync(
|
||||
string.Format(DialogStrings.KeyExportedFormat, filePath),
|
||||
DialogStrings.KeyExportedTitle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error exporting key: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to export key:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToExportKeyFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +395,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
foreach (var key in _storeManager.GetKeys())
|
||||
{
|
||||
var value = _storeManager.GetSecret(key);
|
||||
Secrets.Add(new SecretItemViewModel(key, value));
|
||||
Secrets.Add(new SecretItemViewModel(key, value, _clipboardService));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,35 +406,18 @@ public class MainWindowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(WindowTitle));
|
||||
|
||||
// Manually raise CanExecuteChanged for all commands
|
||||
(SaveCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CloseStoreCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(SaveCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CloseStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(AddSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(ExportKeyCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(ExportKeyCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
// Events for view to show dialogs (sync)
|
||||
// Events for view to show dialogs (these require view-specific DataContext setup)
|
||||
public event Action? OnRequestNewStoreDialog;
|
||||
public event Action? OnRequestOpenStoreDialog;
|
||||
public event Action? OnRequestAddSecretDialog;
|
||||
public event Action<string, string>? OnRequestEditSecretDialog;
|
||||
public event Action? OnRequestClose;
|
||||
|
||||
// Events for view to show dialogs (async)
|
||||
public event Func<string, string, Task>? OnShowError;
|
||||
public event Func<string, string, Task>? OnShowInfo;
|
||||
public event Func<Task<UnsavedChangesResult>>? OnShowUnsavedChangesPrompt;
|
||||
public event Func<string, Task<bool>>? OnShowDeleteConfirmation;
|
||||
public event Func<string, string, string, string, Task<string?>>? OnShowSaveFileDialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from unsaved changes prompt.
|
||||
/// </summary>
|
||||
public enum UnsavedChangesResult
|
||||
{
|
||||
Save,
|
||||
DontSave,
|
||||
Cancel
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
@@ -8,13 +9,15 @@ namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
public class SecretItemViewModel : ViewModelBase
|
||||
{
|
||||
private readonly string _actualValue;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private bool _isValueVisible;
|
||||
private const string MaskedValue = "********";
|
||||
|
||||
public SecretItemViewModel(string key, string value)
|
||||
public SecretItemViewModel(string key, string value, IClipboardService clipboardService)
|
||||
{
|
||||
Key = key;
|
||||
_actualValue = value;
|
||||
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
|
||||
ToggleVisibilityCommand = new RelayCommand(ToggleVisibility);
|
||||
CopyToClipboardCommand = new RelayCommand(CopyToClipboard);
|
||||
}
|
||||
@@ -60,10 +63,9 @@ public class SecretItemViewModel : ViewModelBase
|
||||
public ICommand CopyToClipboardCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when clipboard copy is requested.
|
||||
/// The view subscribes to this to perform the actual clipboard operation.
|
||||
/// Event raised when clipboard copy fails.
|
||||
/// </summary>
|
||||
public event Func<string, Task>? OnCopyToClipboard;
|
||||
public event Action<string>? OnCopyFailed;
|
||||
|
||||
private void ToggleVisibility()
|
||||
{
|
||||
@@ -74,14 +76,11 @@ public class SecretItemViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OnCopyToClipboard != null)
|
||||
{
|
||||
await OnCopyToClipboard(_actualValue);
|
||||
}
|
||||
await _clipboardService.SetTextAsync(_actualValue);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Clipboard operations can fail in some scenarios
|
||||
OnCopyFailed?.Invoke($"Failed to copy to clipboard: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user