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; }