refactor: address code review findings across all projects

Apply comprehensive fixes from code reviews including:
- Extract shared utilities (SqlFormatHelper, CellValueConverter, DbDestinationBase)
- Add interface abstractions (IAuthenticationService, IDatabaseMigrator, IMisQueryBuilder)
- Implement SecureStore for encrypted secrets storage
- Fix error handling with proper HTTP status codes and logging
- Optimize double enumeration in DevEtlRegistry
- Add DataSync.Dev README for developer onboarding
- Extract filter panel base classes to reduce duplication
- Update code review docs to mark all issues as fixed
This commit is contained in:
Joseph Doherty
2026-01-19 11:05:36 -05:00
parent 08f5aa1447
commit 604bfe919c
148 changed files with 8696 additions and 1538 deletions
@@ -0,0 +1,116 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JdeScoping.SecureStoreManager.ViewModels"
x:Class="JdeScoping.SecureStoreManager.Views.MainWindow"
Title="{Binding WindowTitle}"
Height="500" Width="800"
MinHeight="400" MinWidth="600"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Window.KeyBindings>
<KeyBinding Gesture="Ctrl+N" Command="{Binding NewStoreCommand}" />
<KeyBinding Gesture="Ctrl+O" Command="{Binding OpenStoreCommand}" />
<KeyBinding Gesture="Ctrl+S" Command="{Binding SaveCommand}" />
<KeyBinding Gesture="Ctrl+W" Command="{Binding CloseStoreCommand}" />
<KeyBinding Gesture="Delete" Command="{Binding DeleteSecretCommand}" />
</Window.KeyBindings>
<Grid RowDefinitions="Auto,Auto,*,Auto">
<!-- Menu Bar -->
<Menu Grid.Row="0">
<MenuItem Header="_File">
<MenuItem Header="_New Store..." Command="{Binding NewStoreCommand}" InputGesture="Ctrl+N" />
<MenuItem Header="_Open Store..." Command="{Binding OpenStoreCommand}" InputGesture="Ctrl+O" />
<Separator />
<MenuItem Header="_Save" Command="{Binding SaveCommand}" InputGesture="Ctrl+S" />
<Separator />
<MenuItem Header="_Close Store" Command="{Binding CloseStoreCommand}" InputGesture="Ctrl+W" />
<Separator />
<MenuItem Header="E_xit" Command="{Binding ExitCommand}" InputGesture="Alt+F4" />
</MenuItem>
<MenuItem Header="_Secrets">
<MenuItem Header="_Add Secret..." Command="{Binding AddSecretCommand}" />
<MenuItem Header="_Edit Secret..." Command="{Binding EditSecretCommand}" />
<Separator />
<MenuItem Header="_Delete Secret" Command="{Binding DeleteSecretCommand}" InputGesture="Delete" />
</MenuItem>
<MenuItem Header="_Tools">
<MenuItem Header="_Generate Key File..." Command="{Binding GenerateKeyFileCommand}" />
<MenuItem Header="_Export Current Key..." Command="{Binding ExportKeyCommand}" />
</MenuItem>
</Menu>
<!-- Toolbar -->
<Border Grid.Row="1" BorderBrush="LightGray" BorderThickness="0,0,0,1" Padding="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<Button Content="New" Command="{Binding NewStoreCommand}" ToolTip.Tip="Create new store (Ctrl+N)" Padding="8,4" />
<Button Content="Open" Command="{Binding OpenStoreCommand}" ToolTip.Tip="Open existing store (Ctrl+O)" Padding="8,4" />
<Button Content="Save" Command="{Binding SaveCommand}" ToolTip.Tip="Save changes (Ctrl+S)" Padding="8,4" />
<Rectangle Width="1" Fill="LightGray" Margin="5,0" />
<Button Content="Add" Command="{Binding AddSecretCommand}" ToolTip.Tip="Add new secret" Padding="8,4" />
<Button Content="Edit" Command="{Binding EditSecretCommand}" ToolTip.Tip="Edit selected secret" Padding="8,4" />
<Button Content="Delete" Command="{Binding DeleteSecretCommand}" ToolTip.Tip="Delete selected secret (Delete)" Padding="8,4" />
</StackPanel>
</Border>
<!-- Main Content -->
<Grid Grid.Row="2" Margin="10">
<!-- Empty State -->
<TextBlock Text="No store open. Use File > New Store or File > Open Store to begin."
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="14" Foreground="Gray"
IsVisible="{Binding !IsStoreOpen}" />
<!-- Secrets Grid -->
<DataGrid ItemsSource="{Binding Secrets}"
SelectedItem="{Binding SelectedSecret}"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
GridLinesVisibility="Horizontal"
IsVisible="{Binding IsStoreOpen}"
x:Name="SecretsDataGrid"
DoubleTapped="DataGrid_DoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="Key" Binding="{Binding Key}" Width="200" />
<DataGridTextColumn Header="Value" Binding="{Binding DisplayValue}" Width="*" />
<DataGridTemplateColumn Header="Actions" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5">
<Button Content="{Binding IsValueVisible, Converter={StaticResource BoolToVisibilityIcon}}"
Command="{Binding ToggleVisibilityCommand}"
ToolTip.Tip="Show/Hide value"
Width="50" Height="24"
FontSize="11" />
<Button Content="Copy"
Command="{Binding CopyToClipboardCommand}"
ToolTip.Tip="Copy value to clipboard"
Width="50" Height="24"
FontSize="11" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Status Bar -->
<Border Grid.Row="3" Background="#F0F0F0" BorderBrush="LightGray" BorderThickness="0,1,0,0" Padding="10,5">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<TextBlock Text="Secrets: " />
<TextBlock Text="{Binding Secrets.Count}" />
</StackPanel>
<TextBlock Text="{Binding StatusMessage}" />
</DockPanel>
</Border>
</Grid>
</Window>
@@ -0,0 +1,187 @@
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!;
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
}
private void MainWindow_Loaded(object? sender, RoutedEventArgs e)
{
// Subscribe to dialog request events
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)
{
e.Cancel = true;
if (await ViewModel.PromptForUnsavedChangesAsync())
{
e.Cancel = false;
}
}
private void DataGrid_DoubleTapped(object? sender, TappedEventArgs e)
{
if (ViewModel.SelectedSecret != null)
{
ViewModel.EditSecretCommand.Execute(null);
}
}
private async void ShowNewStoreDialog()
{
var dialog = new NewStoreDialog();
var result = await dialog.ShowDialog<bool?>(this);
if (result == true)
{
var vm = dialog.ViewModel;
await ViewModel.CreateNewStoreAsync(
vm.StorePath,
vm.UseKeyFile ? vm.KeyFilePath : null,
vm.UsePassword ? vm.Password : null);
}
}
private async void ShowOpenStoreDialog()
{
var dialog = new OpenStoreDialog();
var result = await dialog.ShowDialog<bool?>(this);
if (result == true)
{
var vm = dialog.ViewModel;
await ViewModel.OpenExistingStoreAsync(
vm.StorePath,
vm.UseKeyFile ? vm.KeyFilePath : null,
vm.UsePassword ? vm.Password : null);
}
}
private async void ShowAddSecretDialog()
{
var dialog = new SecretEditDialog();
var result = await dialog.ShowDialog<bool?>(this);
if (result == true)
{
var vm = dialog.ViewModel;
await ViewModel.SaveSecretAsync(vm.Key, vm.Value, isNew: true);
}
}
private async void ShowEditSecretDialog(string key, string value)
{
var dialog = new SecretEditDialog(key, value);
var result = await dialog.ShowDialog<bool?>(this);
if (result == true)
{
var vm = dialog.ViewModel;
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);
}
}
}
@@ -0,0 +1,100 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JdeScoping.SecureStoreManager.ViewModels"
x:Class="JdeScoping.SecureStoreManager.Views.NewStoreDialog"
Title="Create New Store"
Height="400" Width="500"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<Window.DataContext>
<vm:NewStoreDialogViewModel />
</Window.DataContext>
<Grid Margin="15" RowDefinitions="Auto,Auto,Auto,*,Auto">
<!-- Store Path -->
<Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="Store Location" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding StorePath, Mode=TwoWay}"
Margin="0,0,5,0" />
<Button Grid.Column="1"
Content="Browse..."
Command="{Binding BrowseStorePathCommand}"
Padding="10,5" />
</Grid>
</StackPanel>
</Border>
<!-- Encryption Method -->
<Border Grid.Row="1" BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="Encryption Method" FontWeight="SemiBold" Margin="0,0,0,10" />
<RadioButton Content="Use Key File (recommended for production)"
IsChecked="{Binding UseKeyFile, Mode=TwoWay}"
Margin="0,5" />
<RadioButton Content="Use Password"
IsChecked="{Binding UsePassword, Mode=TwoWay}"
Margin="0,5" />
</StackPanel>
</Border>
<!-- Key File Settings -->
<Border Grid.Row="2"
BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10"
IsVisible="{Binding UseKeyFile}">
<StackPanel>
<TextBlock Text="Key File" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding KeyFilePath, Mode=TwoWay}"
Margin="0,0,5,0" />
<Button Grid.Column="1"
Content="Browse..."
Command="{Binding BrowseKeyFilePathCommand}"
Padding="10,5" />
</Grid>
</StackPanel>
</Border>
<!-- Password Settings -->
<Border Grid.Row="2"
BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10"
IsVisible="{Binding UsePassword}">
<StackPanel>
<TextBlock Text="Password" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Password:" VerticalAlignment="Center" Margin="0,5" />
<TextBox Grid.Row="0" Grid.Column="1"
x:Name="PasswordBox"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}"
Margin="0,5" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Confirm:" VerticalAlignment="Center" Margin="0,5" />
<TextBox Grid.Row="1" Grid.Column="1"
x:Name="ConfirmPasswordBox"
PasswordChar="*"
Text="{Binding ConfirmPassword, Mode=TwoWay}"
Margin="0,5" />
</Grid>
</StackPanel>
</Border>
<!-- Validation Error -->
<TextBlock Grid.Row="3"
Text="{Binding ValidationError}"
Foreground="Red"
FontSize="11"
Margin="0,5,0,0"
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
VerticalAlignment="Top" />
<!-- Buttons -->
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Button Content="Create" Click="CreateButton_Click" MinWidth="80" Padding="10,5" />
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,62 @@
using Avalonia.Controls;
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 NewStoreDialog : Window
{
public NewStoreDialogViewModel ViewModel => (NewStoreDialogViewModel)DataContext!;
public NewStoreDialog()
{
InitializeComponent();
Loaded += NewStoreDialog_Loaded;
}
private void NewStoreDialog_Loaded(object? sender, RoutedEventArgs e)
{
ViewModel.OnShowSaveFileDialog += ShowSaveFileDialogAsync;
}
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 void CreateButton_Click(object? sender, RoutedEventArgs e)
{
if (!ViewModel.IsValid)
{
var box = MessageBoxManager
.GetMessageBoxStandard(
"Validation Error",
ViewModel.ValidationError ?? "Please fill in all required fields.",
ButtonEnum.Ok,
MsBox.Avalonia.Enums.Icon.Warning);
await box.ShowWindowDialogAsync(this);
return;
}
Close(true);
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}
@@ -0,0 +1,94 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JdeScoping.SecureStoreManager.ViewModels"
x:Class="JdeScoping.SecureStoreManager.Views.OpenStoreDialog"
Title="Open Store"
Height="370" Width="500"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<Window.DataContext>
<vm:OpenStoreDialogViewModel />
</Window.DataContext>
<Grid Margin="15" RowDefinitions="Auto,Auto,Auto,*,Auto">
<!-- Store Path -->
<Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="Store File" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding StorePath, Mode=TwoWay}"
Margin="0,0,5,0" />
<Button Grid.Column="1"
Content="Browse..."
Command="{Binding BrowseStorePathCommand}"
Padding="10,5" />
</Grid>
</StackPanel>
</Border>
<!-- Decryption Method -->
<Border Grid.Row="1" BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="Decryption Method" FontWeight="SemiBold" Margin="0,0,0,10" />
<RadioButton Content="Use Key File"
IsChecked="{Binding UseKeyFile, Mode=TwoWay}"
Margin="0,5" />
<RadioButton Content="Use Password"
IsChecked="{Binding UsePassword, Mode=TwoWay}"
Margin="0,5" />
</StackPanel>
</Border>
<!-- Key File Settings -->
<Border Grid.Row="2"
BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10"
IsVisible="{Binding UseKeyFile}">
<StackPanel>
<TextBlock Text="Key File" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding KeyFilePath, Mode=TwoWay}"
Margin="0,0,5,0" />
<Button Grid.Column="1"
Content="Browse..."
Command="{Binding BrowseKeyFilePathCommand}"
Padding="10,5" />
</Grid>
</StackPanel>
</Border>
<!-- Password Settings -->
<Border Grid.Row="2"
BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10"
IsVisible="{Binding UsePassword}">
<StackPanel>
<TextBlock Text="Password" FontWeight="SemiBold" Margin="0,0,0,10" />
<Grid ColumnDefinitions="100,*">
<TextBlock Text="Password:" VerticalAlignment="Center" />
<TextBox Grid.Column="1"
x:Name="PasswordBox"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}"
Margin="0,5" />
</Grid>
</StackPanel>
</Border>
<!-- Validation Error -->
<TextBlock Grid.Row="3"
Text="{Binding ValidationError}"
Foreground="Red"
FontSize="11"
Margin="0,5,0,0"
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
VerticalAlignment="Top" />
<!-- Buttons -->
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Button Content="Open" Click="OpenButton_Click" MinWidth="80" Padding="10,5" />
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,62 @@
using Avalonia.Controls;
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 OpenStoreDialog : Window
{
public OpenStoreDialogViewModel ViewModel => (OpenStoreDialogViewModel)DataContext!;
public OpenStoreDialog()
{
InitializeComponent();
Loaded += OpenStoreDialog_Loaded;
}
private void OpenStoreDialog_Loaded(object? sender, RoutedEventArgs e)
{
ViewModel.OnShowOpenFileDialog += ShowOpenFileDialogAsync;
}
private async Task<string?> ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern)
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = title,
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
new FilePickerFileType("All Files") { Patterns = new[] { "*.*" } }
}
});
return files.Count > 0 ? files[0].Path.LocalPath : null;
}
private async void OpenButton_Click(object? sender, RoutedEventArgs e)
{
if (!ViewModel.IsValid)
{
var box = MessageBoxManager
.GetMessageBoxStandard(
"Validation Error",
ViewModel.ValidationError ?? "Please fill in all required fields.",
ButtonEnum.Ok,
MsBox.Avalonia.Enums.Icon.Warning);
await box.ShowWindowDialogAsync(this);
return;
}
Close(true);
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}
@@ -0,0 +1,50 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JdeScoping.SecureStoreManager.ViewModels"
x:Class="JdeScoping.SecureStoreManager.Views.SecretEditDialog"
Title="{Binding DialogTitle}"
Height="280" Width="450"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<Window.DataContext>
<vm:SecretEditDialogViewModel />
</Window.DataContext>
<Grid Margin="15" RowDefinitions="Auto,Auto,*,Auto">
<!-- Key -->
<Grid Grid.Row="0" ColumnDefinitions="80,*" Margin="0,0,0,10">
<TextBlock Grid.Column="0" Text="Key:" VerticalAlignment="Center" Margin="0,5,10,5" />
<TextBox Grid.Column="1"
Text="{Binding Key, Mode=TwoWay}"
IsEnabled="{Binding IsKeyEditable}"
Margin="0,5" />
</Grid>
<!-- Value -->
<Grid Grid.Row="1" ColumnDefinitions="80,*" Margin="0,0,0,10">
<TextBlock Grid.Column="0" Text="Value:" VerticalAlignment="Top" Margin="0,8,10,5" />
<TextBox Grid.Column="1"
Text="{Binding Value, Mode=TwoWay}"
TextWrapping="Wrap"
AcceptsReturn="True"
Height="80"
Margin="0,5" />
</Grid>
<!-- Validation Error -->
<TextBlock Grid.Row="2"
Text="{Binding ValidationError}"
Foreground="Red"
FontSize="11"
Margin="0,5,0,0"
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
VerticalAlignment="Top" />
<!-- Buttons -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Button Content="Save" Click="SaveButton_Click" MinWidth="80" Padding="10,5" />
<Button Content="Cancel" Click="CancelButton_Click" MinWidth="80" Padding="10,5" />
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,44 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using JdeScoping.SecureStoreManager.ViewModels;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
namespace JdeScoping.SecureStoreManager.Views;
public partial class SecretEditDialog : Window
{
public SecretEditDialogViewModel ViewModel => (SecretEditDialogViewModel)DataContext!;
public SecretEditDialog()
{
InitializeComponent();
}
public SecretEditDialog(string key, string value) : this()
{
DataContext = new SecretEditDialogViewModel(key, value);
}
private async void SaveButton_Click(object? sender, RoutedEventArgs e)
{
if (!ViewModel.IsValid)
{
var box = MessageBoxManager
.GetMessageBoxStandard(
"Validation Error",
ViewModel.ValidationError ?? "Please fill in all required fields.",
ButtonEnum.Ok,
MsBox.Avalonia.Enums.Icon.Warning);
await box.ShowWindowDialogAsync(this);
return;
}
Close(true);
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}