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:
@@ -6,9 +6,7 @@
|
||||
Height="500" Width="800"
|
||||
MinHeight="400" MinWidth="600"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Window.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Window.DataContext>
|
||||
<!-- DataContext is set via DI in App.axaml.cs -->
|
||||
|
||||
<Window.KeyBindings>
|
||||
<KeyBinding Gesture="Ctrl+N" Command="{Binding NewStoreCommand}" />
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.SecureStoreManager.ViewModels;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
|
||||
private MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -21,35 +18,22 @@ public partial class MainWindow : Window
|
||||
|
||||
private void MainWindow_Loaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// Subscribe to dialog request events
|
||||
if (ViewModel == null)
|
||||
return;
|
||||
|
||||
// Subscribe to dialog request events (these open dialogs with their own DataContext)
|
||||
ViewModel.OnRequestNewStoreDialog += ShowNewStoreDialog;
|
||||
ViewModel.OnRequestOpenStoreDialog += ShowOpenStoreDialog;
|
||||
ViewModel.OnRequestAddSecretDialog += ShowAddSecretDialog;
|
||||
ViewModel.OnRequestEditSecretDialog += ShowEditSecretDialog;
|
||||
ViewModel.OnRequestClose += () => Close();
|
||||
|
||||
// Subscribe to async dialog events
|
||||
ViewModel.OnShowError += ShowErrorAsync;
|
||||
ViewModel.OnShowInfo += ShowInfoAsync;
|
||||
ViewModel.OnShowUnsavedChangesPrompt += ShowUnsavedChangesPromptAsync;
|
||||
ViewModel.OnShowDeleteConfirmation += ShowDeleteConfirmationAsync;
|
||||
ViewModel.OnShowSaveFileDialog += ShowSaveFileDialogAsync;
|
||||
|
||||
// Subscribe to clipboard events for secrets
|
||||
ViewModel.Secrets.CollectionChanged += (s, e) =>
|
||||
{
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (SecretItemViewModel secret in e.NewItems)
|
||||
{
|
||||
secret.OnCopyToClipboard += CopyToClipboardAsync;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async void MainWindow_Closing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
return;
|
||||
|
||||
e.Cancel = true;
|
||||
if (await ViewModel.PromptForUnsavedChangesAsync())
|
||||
{
|
||||
@@ -59,7 +43,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void DataGrid_DoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (ViewModel.SelectedSecret != null)
|
||||
if (ViewModel?.SelectedSecret != null)
|
||||
{
|
||||
ViewModel.EditSecretCommand.Execute(null);
|
||||
}
|
||||
@@ -67,6 +51,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void ShowNewStoreDialog()
|
||||
{
|
||||
if (ViewModel == null) return;
|
||||
|
||||
var dialog = new NewStoreDialog();
|
||||
var result = await dialog.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
@@ -81,6 +67,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void ShowOpenStoreDialog()
|
||||
{
|
||||
if (ViewModel == null) return;
|
||||
|
||||
var dialog = new OpenStoreDialog();
|
||||
var result = await dialog.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
@@ -95,6 +83,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void ShowAddSecretDialog()
|
||||
{
|
||||
if (ViewModel == null) return;
|
||||
|
||||
var dialog = new SecretEditDialog();
|
||||
var result = await dialog.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
@@ -106,6 +96,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private async void ShowEditSecretDialog(string key, string value)
|
||||
{
|
||||
if (ViewModel == null) return;
|
||||
|
||||
var dialog = new SecretEditDialog(key, value);
|
||||
var result = await dialog.ShowDialog<bool?>(this);
|
||||
if (result == true)
|
||||
@@ -114,74 +106,4 @@ public partial class MainWindow : Window
|
||||
await ViewModel.SaveSecretAsync(vm.Key, vm.Value, isNew: false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowErrorAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(title, message, ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
}
|
||||
|
||||
private async Task ShowInfoAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(title, message, ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Info);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
}
|
||||
|
||||
private async Task<UnsavedChangesResult> ShowUnsavedChangesPromptAsync()
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(
|
||||
"Unsaved Changes",
|
||||
"You have unsaved changes. Do you want to save before continuing?",
|
||||
ButtonEnum.YesNoCancel,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
|
||||
var result = await box.ShowWindowDialogAsync(this);
|
||||
|
||||
return result switch
|
||||
{
|
||||
ButtonResult.Yes => UnsavedChangesResult.Save,
|
||||
ButtonResult.No => UnsavedChangesResult.DontSave,
|
||||
_ => UnsavedChangesResult.Cancel
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> ShowDeleteConfirmationAsync(string key)
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(
|
||||
"Confirm Delete",
|
||||
$"Are you sure you want to delete the secret '{key}'?\n\nThis action cannot be undone.",
|
||||
ButtonEnum.YesNo,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
|
||||
var result = await box.ShowWindowDialogAsync(this);
|
||||
return result == ButtonResult.Yes;
|
||||
}
|
||||
|
||||
private async Task<string?> ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension)
|
||||
{
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = title,
|
||||
DefaultExtension = defaultExtension,
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType("All Files") { Patterns = new[] { "*.*" } }
|
||||
}
|
||||
});
|
||||
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
private async Task CopyToClipboardAsync(string text)
|
||||
{
|
||||
if (Clipboard != null)
|
||||
{
|
||||
await Clipboard.SetTextAsync(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
<Window.DataContext>
|
||||
<vm:NewStoreDialogViewModel />
|
||||
</Window.DataContext>
|
||||
<!-- DataContext is set in code-behind -->
|
||||
|
||||
<Grid Margin="15" RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<!-- Store Path -->
|
||||
@@ -93,7 +91,7 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
|
||||
<Button Content="Create" Click="CreateButton_Click" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Create" Click="CreateButton_Click" IsEnabled="{Binding IsValid}" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
using JdeScoping.SecureStoreManager.ViewModels;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
@@ -14,6 +15,7 @@ public partial class NewStoreDialog : Window
|
||||
public NewStoreDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new NewStoreDialogViewModel();
|
||||
Loaded += NewStoreDialog_Loaded;
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ public partial class NewStoreDialog : Window
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType("All Files") { Patterns = new[] { "*.*" } }
|
||||
new FilePickerFileType(FileExtensions.AllFilesTypeName) { Patterns = new[] { FileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,8 +46,8 @@ public partial class NewStoreDialog : Window
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(
|
||||
"Validation Error",
|
||||
ViewModel.ValidationError ?? "Please fill in all required fields.",
|
||||
DialogStrings.ValidationErrorTitle,
|
||||
ViewModel.ValidationError ?? DialogStrings.DefaultValidationError,
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
<Window.DataContext>
|
||||
<vm:OpenStoreDialogViewModel />
|
||||
</Window.DataContext>
|
||||
<!-- DataContext is set in code-behind -->
|
||||
|
||||
<Grid Margin="15" RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<!-- Store Path -->
|
||||
@@ -87,7 +85,7 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
|
||||
<Button Content="Open" Click="OpenButton_Click" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Open" Click="OpenButton_Click" IsEnabled="{Binding IsValid}" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
using JdeScoping.SecureStoreManager.ViewModels;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
@@ -14,6 +15,7 @@ public partial class OpenStoreDialog : Window
|
||||
public OpenStoreDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new OpenStoreDialogViewModel();
|
||||
Loaded += OpenStoreDialog_Loaded;
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ public partial class OpenStoreDialog : Window
|
||||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType("All Files") { Patterns = new[] { "*.*" } }
|
||||
new FilePickerFileType(FileExtensions.AllFilesTypeName) { Patterns = new[] { FileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,8 +46,8 @@ public partial class OpenStoreDialog : Window
|
||||
{
|
||||
var box = MessageBoxManager
|
||||
.GetMessageBoxStandard(
|
||||
"Validation Error",
|
||||
ViewModel.ValidationError ?? "Please fill in all required fields.",
|
||||
DialogStrings.ValidationErrorTitle,
|
||||
ViewModel.ValidationError ?? DialogStrings.DefaultValidationError,
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
<Window.DataContext>
|
||||
<vm:SecretEditDialogViewModel />
|
||||
</Window.DataContext>
|
||||
<!-- DataContext is set in code-behind -->
|
||||
|
||||
<Grid Margin="15" RowDefinitions="Auto,Auto,*,Auto">
|
||||
<!-- Key -->
|
||||
@@ -43,7 +41,7 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
|
||||
<Button Content="Save" Click="SaveButton_Click" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Save" Click="SaveButton_Click" IsEnabled="{Binding IsValid}" MinWidth="80" Padding="10,5" />
|
||||
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -13,10 +13,12 @@ public partial class SecretEditDialog : Window
|
||||
public SecretEditDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new SecretEditDialogViewModel();
|
||||
}
|
||||
|
||||
public SecretEditDialog(string key, string value) : this()
|
||||
public SecretEditDialog(string key, string value)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new SecretEditDialogViewModel(key, value);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user