feat(configmanager): add MainWindowViewModel with service wiring

Complete MainWindowViewModel implementation with:
- Constructor injection for IConfigFileService, IValidationService,
  IBackupService, IAutoDiscoveryService, and ILogger
- Properties: ConfigFolderPath, HasUnsavedChanges, ValidationStatus,
  ValidationStatusColor, SelectedNode, SelectedFormViewModel, TreeNodes
- Commands: OpenFolderCommand, SaveCommand, ExitCommand, UndoCommand,
  RedoCommand, ValidateCommand, TestConnectionCommand
- Methods: InitializeAsync, OpenFolderAsync, LoadConfigAsync,
  BuildTreeNodes, OnSelectedNodeChanged, SaveAsync, Validate,
  TestConnectionAsync

Update App.axaml.cs to register all services in DI container:
- IFileSystem -> FileSystem (singleton)
- IAutoDiscoveryService -> AutoDiscoveryService (singleton)
- IBackupService -> BackupService (singleton)
- IDiffService -> DiffService (singleton)
- IValidationService -> ValidationService (singleton)
- IConfigFileService -> ConfigFileService (scoped)
- MainWindowViewModel (transient)

Wire MainWindow.DataContext to resolved MainWindowViewModel.
This commit is contained in:
Joseph Doherty
2026-01-19 17:53:25 -05:00
parent 8caaa8ee54
commit c22e2ed877
2 changed files with 259 additions and 12 deletions
@@ -1,6 +1,8 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -8,13 +10,22 @@ namespace JdeScoping.ConfigManager;
public partial class App : Application
{
/// <summary>
/// Gets the dependency injection service provider for the application.
/// </summary>
public static IServiceProvider Services { get; private set; } = null!;
/// <summary>
/// Initializes the Avalonia XAML loader.
/// </summary>
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
/// <summary>
/// Called when the framework initialization is complete; configures services and sets the main window.
/// </summary>
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();
@@ -23,7 +34,10 @@ public partial class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new Views.MainWindow();
desktop.MainWindow = new Views.MainWindow
{
DataContext = Services.GetRequiredService<MainWindowViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
@@ -31,8 +45,22 @@ public partial class App : Application
private void ConfigureServices(IServiceCollection services)
{
// Logging
services.AddLogging(builder => builder
.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
// Services - File system abstraction
services.AddSingleton<IFileSystem, FileSystem>();
// Services - Configuration management
services.AddSingleton<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddSingleton<IBackupService, BackupService>();
services.AddSingleton<IDiffService, DiffService>();
services.AddSingleton<IValidationService, ValidationService>();
services.AddScoped<IConfigFileService, ConfigFileService>();
// ViewModels
services.AddTransient<MainWindowViewModel>();
}
}
@@ -1,15 +1,23 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Avalonia.Media;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.ViewModels;
/// <summary>
/// Main window view model.
/// This is a stub implementation for Task 11 - full implementation in Task 13.
/// </summary>
public class MainWindowViewModel : ViewModelBase
{
private readonly IConfigFileService _configFileService;
private readonly IValidationService _validationService;
private readonly IBackupService _backupService;
private readonly IAutoDiscoveryService _autoDiscoveryService;
private readonly ILogger<MainWindowViewModel>? _logger;
private string _configFolderPath = "No folder selected";
private bool _hasUnsavedChanges;
private string _validationStatus = "Valid";
@@ -17,6 +25,9 @@ public class MainWindowViewModel : ViewModelBase
private TreeNodeViewModel? _selectedNode;
private object? _selectedFormViewModel;
private ConfigModel? _appSettings;
private PipelinesConfigModel? _pipelines;
public string ConfigFolderPath
{
get => _configFolderPath;
@@ -44,7 +55,11 @@ public class MainWindowViewModel : ViewModelBase
public TreeNodeViewModel? SelectedNode
{
get => _selectedNode;
set => SetProperty(ref _selectedNode, value);
set
{
if (SetProperty(ref _selectedNode, value))
OnSelectedNodeChanged();
}
}
public object? SelectedFormViewModel
@@ -63,15 +78,219 @@ public class MainWindowViewModel : ViewModelBase
public ICommand ValidateCommand { get; }
public ICommand TestConnectionCommand { get; }
public MainWindowViewModel()
public MainWindowViewModel(
IConfigFileService configFileService,
IValidationService validationService,
IBackupService backupService,
IAutoDiscoveryService autoDiscoveryService,
ILogger<MainWindowViewModel>? logger)
{
// Stub commands - full implementation in Task 13
OpenFolderCommand = new RelayCommand(() => { });
SaveCommand = new RelayCommand(() => { });
ExitCommand = new RelayCommand(() => { });
UndoCommand = new RelayCommand(() => { });
RedoCommand = new RelayCommand(() => { });
ValidateCommand = new RelayCommand(() => { });
TestConnectionCommand = new RelayCommand(() => { });
_configFileService = configFileService;
_validationService = validationService;
_backupService = backupService;
_autoDiscoveryService = autoDiscoveryService;
_logger = logger;
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
SaveCommand = new AsyncRelayCommand(SaveAsync, () => HasUnsavedChanges);
ExitCommand = new RelayCommand(() => Environment.Exit(0));
UndoCommand = new RelayCommand(() => { }, () => false); // TODO: Implement undo/redo
RedoCommand = new RelayCommand(() => { }, () => false); // TODO: Implement undo/redo
ValidateCommand = new RelayCommand(Validate);
TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync);
_ = InitializeAsync();
}
/// <summary>
/// Design-time constructor for XAML previewer.
/// </summary>
public MainWindowViewModel() : this(
new ConfigFileService(new FileSystem()),
new ValidationService(),
new BackupService(new FileSystem()),
new AutoDiscoveryService(new FileSystem()),
null)
{
}
/// <summary>
/// Initializes the view model by auto-discovering and loading configuration.
/// </summary>
private async Task InitializeAsync()
{
var folder = await _autoDiscoveryService.FindConfigFolderAsync();
if (folder != null)
{
await LoadConfigAsync(folder);
}
}
/// <summary>
/// Opens a folder picker dialog to select a configuration folder.
/// </summary>
private async Task OpenFolderAsync()
{
// TODO: Show folder picker dialog
_logger?.LogInformation("Open folder requested");
await Task.CompletedTask;
}
/// <summary>
/// Loads configuration files from the specified folder.
/// </summary>
/// <param name="folderPath">Path to the configuration folder.</param>
private async Task LoadConfigAsync(string folderPath)
{
try
{
ConfigFolderPath = folderPath;
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
var pipelinesPath = Path.Combine(folderPath, "Pipelines", "pipelines.json");
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath);
if (File.Exists(pipelinesPath))
{
_pipelines = await _configFileService.LoadPipelinesAsync(pipelinesPath);
}
BuildTreeNodes();
Validate();
_logger?.LogInformation("Loaded configuration from {Path}", folderPath);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to load configuration from {Path}", folderPath);
}
}
/// <summary>
/// Builds the tree nodes representing the configuration structure.
/// </summary>
private void BuildTreeNodes()
{
TreeNodes.Clear();
// Settings folder
var settingsFolder = new TreeNodeViewModel("Settings", "gear", TreeNodeType.Folder) { IsExpanded = true };
settingsFolder.Children.Add(new TreeNodeViewModel("DataSync", "sync", TreeNodeType.SettingsSection) { SectionKey = "DataSync" });
settingsFolder.Children.Add(new TreeNodeViewModel("DataAccess", "database", TreeNodeType.SettingsSection) { SectionKey = "DataAccess" });
settingsFolder.Children.Add(new TreeNodeViewModel("Auth", "lock", TreeNodeType.SettingsSection) { SectionKey = "Auth" });
settingsFolder.Children.Add(new TreeNodeViewModel("Ldap", "users", TreeNodeType.SettingsSection) { SectionKey = "Ldap" });
settingsFolder.Children.Add(new TreeNodeViewModel("Search", "search", TreeNodeType.SettingsSection) { SectionKey = "Search" });
settingsFolder.Children.Add(new TreeNodeViewModel("ExcelExport", "file-spreadsheet", TreeNodeType.SettingsSection) { SectionKey = "ExcelExport" });
TreeNodes.Add(settingsFolder);
// Pipelines folder
var pipelinesFolder = new TreeNodeViewModel("Pipelines", "workflow", TreeNodeType.Folder) { IsExpanded = true };
if (_pipelines != null)
{
foreach (var (name, _) in _pipelines.Pipelines)
{
pipelinesFolder.Children.Add(new TreeNodeViewModel(name, "zap", TreeNodeType.Pipeline) { SectionKey = name });
}
}
TreeNodes.Add(pipelinesFolder);
}
/// <summary>
/// Called when the selected tree node changes; updates the form view model.
/// </summary>
private void OnSelectedNodeChanged()
{
// TODO: Load appropriate form ViewModel based on selected node
SelectedFormViewModel = null;
}
/// <summary>
/// Saves all configuration changes to disk.
/// </summary>
private async Task SaveAsync()
{
if (_appSettings == null) return;
try
{
var appSettingsPath = Path.Combine(ConfigFolderPath, "appsettings.json");
// Create backup before saving
if (File.Exists(appSettingsPath))
{
await _backupService.CreateBackupAsync(appSettingsPath);
}
// Save appsettings
await _configFileService.SaveAppSettingsAsync(appSettingsPath, _appSettings);
// Save pipelines if loaded
if (_pipelines != null)
{
var pipelinesPath = Path.Combine(ConfigFolderPath, "Pipelines", "pipelines.json");
if (File.Exists(pipelinesPath))
{
await _backupService.CreateBackupAsync(pipelinesPath);
}
await _configFileService.SavePipelinesAsync(pipelinesPath, _pipelines);
}
HasUnsavedChanges = false;
_logger?.LogInformation("Configuration saved");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to save configuration");
}
}
/// <summary>
/// Validates the current configuration and updates the status bar.
/// </summary>
private void Validate()
{
var errors = 0;
var warnings = 0;
if (_appSettings != null)
{
var result = _validationService.ValidateAppSettings(_appSettings);
errors += result.Errors.Count;
warnings += result.Warnings.Count;
}
if (_pipelines != null)
{
var result = _validationService.ValidatePipelines(_pipelines);
errors += result.Errors.Count;
warnings += result.Warnings.Count;
}
if (errors > 0)
{
ValidationStatus = $"Errors: {errors}, Warnings: {warnings}";
ValidationStatusColor = new SolidColorBrush(Color.Parse("#FF6B6B"));
}
else if (warnings > 0)
{
ValidationStatus = $"Warnings: {warnings}";
ValidationStatusColor = new SolidColorBrush(Color.Parse("#FFB84D"));
}
else
{
ValidationStatus = "Valid";
ValidationStatusColor = new SolidColorBrush(Color.Parse("#3DD68C"));
}
}
/// <summary>
/// Tests database connections defined in the configuration.
/// </summary>
private async Task TestConnectionAsync()
{
// TODO: Implement connection testing
_logger?.LogInformation("Test connection requested");
await Task.CompletedTask;
}
}