using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.SecureStoreManager.Constants;
using JdeScoping.SecureStoreManager.Services;
namespace JdeScoping.SecureStoreManager.ViewModels;
///
/// Main window view model containing all application logic.
///
public class MainWindowViewModel : ViewModelBase
{
private readonly ISecureStoreManager _storeManager;
private readonly IDialogService _dialogService;
private readonly IClipboardService _clipboardService;
private SecretItemViewModel? _selectedSecret;
private string _statusMessage = "Ready";
///
/// Initializes a new instance of the MainWindowViewModel.
///
/// The secure store manager service.
/// The dialog service for user interactions.
/// The clipboard service for copying secrets.
public MainWindowViewModel(
ISecureStoreManager storeManager,
IDialogService dialogService,
IClipboardService clipboardService)
{
_storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager));
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
Secrets = new ObservableCollection();
// 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 AsyncRelayCommand(ExecuteDeleteSecretAsync, CanEditOrDeleteSecret);
// Tools commands (async)
GenerateKeyFileCommand = new AsyncRelayCommand(ExecuteGenerateKeyFileAsync);
ExportKeyCommand = new AsyncRelayCommand(ExecuteExportKeyAsync, () => _storeManager.IsStoreOpen);
}
///
/// Gets the collection of secrets in the current store.
///
public ObservableCollection Secrets { get; }
///
/// Gets or sets the currently selected secret.
///
public SecretItemViewModel? SelectedSecret
{
get => _selectedSecret;
set
{
if (SetProperty(ref _selectedSecret, value))
{
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
}
}
}
///
/// Gets the current status message.
///
public string StatusMessage
{
get => _statusMessage;
private set => SetProperty(ref _statusMessage, value);
}
///
/// Gets the window title including the current store path.
///
public string WindowTitle
{
get
{
var title = "SecureStore Manager";
if (_storeManager.IsStoreOpen)
{
title += $" - {_storeManager.CurrentStorePath}";
if (_storeManager.HasUnsavedChanges)
title += " *";
}
return title;
}
}
///
/// Gets whether a store is currently open.
///
public bool IsStoreOpen => _storeManager.IsStoreOpen;
///
/// Gets whether there are unsaved changes.
///
public bool HasUnsavedChanges => _storeManager.HasUnsavedChanges;
// File Commands
///
/// Gets the command to create a new store.
///
public ICommand NewStoreCommand { get; }
///
/// Gets the command to open an existing store.
///
public ICommand OpenStoreCommand { get; }
///
/// Gets the command to save the current store.
///
public ICommand SaveCommand { get; }
///
/// Gets the command to close the current store.
///
public ICommand CloseStoreCommand { get; }
///
/// Gets the command to exit the application.
///
public ICommand ExitCommand { get; }
// Secret Commands
///
/// Gets the command to add a new secret.
///
public ICommand AddSecretCommand { get; }
///
/// Gets the command to edit the selected secret.
///
public ICommand EditSecretCommand { get; }
///
/// Gets the command to delete the selected secret.
///
public ICommand DeleteSecretCommand { get; }
// Tools Commands
///
/// Gets the command to generate a new key file.
///
public ICommand GenerateKeyFileCommand { get; }
///
/// Gets the command to export the store's key.
///
public ICommand ExportKeyCommand { get; }
///
/// Creates a new store with a key file. Called by the dialog.
///
/// The path where the store file will be created.
/// The path to a key file for encryption.
public async Task CreateNewStoreAsync(string storePath, string keyFilePath)
{
try
{
if (string.IsNullOrEmpty(keyFilePath))
{
throw new ArgumentException("Key file path must be provided.");
}
_storeManager.CreateStore(storePath, keyFilePath);
StatusMessage = $"Created store with key file: {keyFilePath}";
RefreshSecrets();
NotifyStoreChanged();
}
catch (Exception ex)
{
StatusMessage = $"Error creating store: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToCreateStoreFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
///
/// Opens an existing store with a key file. Called by the dialog.
///
/// The path to the store file to open.
/// The path to a key file for decryption.
public async Task OpenExistingStoreAsync(string storePath, string keyFilePath)
{
try
{
if (string.IsNullOrEmpty(keyFilePath))
{
throw new ArgumentException("Key file path must be provided.");
}
_storeManager.OpenStore(storePath, keyFilePath);
StatusMessage = "Opened store with key file";
RefreshSecrets();
NotifyStoreChanged();
}
catch (Exception ex)
{
StatusMessage = $"Error opening store: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToOpenStoreFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
///
/// Adds or updates a secret. Called by the dialog.
///
/// The secret key identifier.
/// The secret value to store.
/// True if this is a new secret, false if updating an existing one.
public async Task SaveSecretAsync(string key, string value, bool isNew)
{
try
{
_storeManager.SetSecret(key, value);
RefreshSecrets();
NotifyStoreChanged();
StatusMessage = isNew ? $"Added secret: {key}" : $"Updated secret: {key}";
}
catch (Exception ex)
{
StatusMessage = $"Error saving secret: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToSaveSecretFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
///
/// Checks for unsaved changes and prompts the user.
///
/// True if it's safe to proceed, false if the user cancelled.
public async Task PromptForUnsavedChangesAsync()
{
if (!_storeManager.HasUnsavedChanges)
return true;
var result = await _dialogService.ShowUnsavedChangesPromptAsync();
switch (result)
{
case UnsavedChangesResult.Save:
await ExecuteSaveAsync();
return true;
case UnsavedChangesResult.DontSave:
return true;
case UnsavedChangesResult.Cancel:
default:
return false;
}
}
private async Task ExecuteNewStoreAsync()
{
if (!await PromptForUnsavedChangesAsync())
return;
// The view will show the NewStoreDialog
OnRequestNewStoreDialog?.Invoke();
}
private async Task ExecuteOpenStoreAsync()
{
if (!await PromptForUnsavedChangesAsync())
return;
// The view will show the OpenStoreDialog
OnRequestOpenStoreDialog?.Invoke();
}
private async Task ExecuteSaveAsync()
{
try
{
_storeManager.Save();
NotifyStoreChanged();
StatusMessage = "Store saved";
}
catch (Exception ex)
{
StatusMessage = $"Error saving: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToSaveStoreFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
private bool CanSave() => _storeManager.IsStoreOpen && _storeManager.HasUnsavedChanges;
private async Task ExecuteCloseStoreAsync()
{
if (!await PromptForUnsavedChangesAsync())
return;
_storeManager.CloseStore();
Secrets.Clear();
NotifyStoreChanged();
StatusMessage = "Store closed";
}
private async Task ExecuteExitAsync()
{
if (!await PromptForUnsavedChangesAsync())
return;
OnRequestClose?.Invoke();
}
private void ExecuteAddSecret()
{
// The view will show the SecretEditDialog
OnRequestAddSecretDialog?.Invoke();
}
private void ExecuteEditSecret()
{
if (SelectedSecret == null)
return;
// The view will show the SecretEditDialog with existing values
OnRequestEditSecretDialog?.Invoke(SelectedSecret.Key, SelectedSecret.ActualValue);
}
private async Task ExecuteDeleteSecretAsync()
{
if (SelectedSecret == null)
return;
var confirmMessage = string.Format(DialogStrings.ConfirmDeleteFormat, SelectedSecret.Key);
var confirmed = await _dialogService.ShowConfirmationAsync(confirmMessage, DialogStrings.ConfirmDeleteTitle);
if (!confirmed)
return;
try
{
var key = SelectedSecret.Key;
_storeManager.RemoveSecret(key);
RefreshSecrets();
NotifyStoreChanged();
StatusMessage = $"Deleted secret: {key}";
}
catch (Exception ex)
{
StatusMessage = $"Error deleting secret: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToDeleteSecretFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
private bool CanEditOrDeleteSecret() => _storeManager.IsStoreOpen && SelectedSecret != null;
private async Task ExecuteGenerateKeyFileAsync()
{
var filePath = await _dialogService.ShowSaveFileDialogAsync(
DialogStrings.GenerateKeyFileTitle,
FileExtensions.KeyTypeName,
FileExtensions.KeyPattern,
FileExtensions.KeyExtension);
if (string.IsNullOrEmpty(filePath))
return;
try
{
_storeManager.GenerateKeyFile(filePath);
StatusMessage = $"Generated key file: {filePath}";
await _dialogService.ShowInfoAsync(
string.Format(DialogStrings.KeyFileGeneratedFormat, filePath),
DialogStrings.KeyGeneratedTitle);
}
catch (Exception ex)
{
StatusMessage = $"Error generating key: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToGenerateKeyFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
private async Task ExecuteExportKeyAsync()
{
var filePath = await _dialogService.ShowSaveFileDialogAsync(
DialogStrings.ExportKeyTitle,
FileExtensions.KeyTypeName,
FileExtensions.KeyPattern,
FileExtensions.KeyExtension);
if (string.IsNullOrEmpty(filePath))
return;
try
{
_storeManager.ExportKey(filePath);
StatusMessage = $"Exported key to: {filePath}";
await _dialogService.ShowInfoAsync(
string.Format(DialogStrings.KeyExportedFormat, filePath),
DialogStrings.KeyExportedTitle);
}
catch (Exception ex)
{
StatusMessage = $"Error exporting key: {ex.Message}";
await _dialogService.ShowErrorAsync(
string.Format(DialogStrings.FailedToExportKeyFormat, ex.Message),
DialogStrings.ErrorTitle);
}
}
private void RefreshSecrets()
{
Secrets.Clear();
if (!_storeManager.IsStoreOpen)
return;
foreach (var key in _storeManager.GetKeys())
{
var value = _storeManager.GetSecret(key);
Secrets.Add(new SecretItemViewModel(key, value, _clipboardService));
}
}
private void NotifyStoreChanged()
{
OnPropertyChanged(nameof(IsStoreOpen));
OnPropertyChanged(nameof(HasUnsavedChanges));
OnPropertyChanged(nameof(WindowTitle));
// Manually raise CanExecuteChanged for all commands
(SaveCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
(CloseStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
(AddSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
(ExportKeyCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
}
// Events for view to show dialogs (these require view-specific DataContext setup)
///
/// Raised when a new store creation dialog should be shown.
///
public event Action? OnRequestNewStoreDialog;
///
/// Raised when an open store dialog should be shown.
///
public event Action? OnRequestOpenStoreDialog;
///
/// Raised when a new secret dialog should be shown.
///
public event Action? OnRequestAddSecretDialog;
///
/// Raised when an edit secret dialog should be shown with the specified key and value.
///
public event Action? OnRequestEditSecretDialog;
///
/// Raised when the application should close.
///
public event Action? OnRequestClose;
}