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:
@@ -1,6 +1,8 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using JdeScoping.ConfigManager.Services;
|
||||||
|
using JdeScoping.ConfigManager.ViewModels;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -8,13 +10,22 @@ namespace JdeScoping.ConfigManager;
|
|||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the dependency injection service provider for the application.
|
||||||
|
/// </summary>
|
||||||
public static IServiceProvider Services { get; private set; } = null!;
|
public static IServiceProvider Services { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the Avalonia XAML loader.
|
||||||
|
/// </summary>
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the framework initialization is complete; configures services and sets the main window.
|
||||||
|
/// </summary>
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
@@ -23,7 +34,10 @@ public partial class App : Application
|
|||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new Views.MainWindow();
|
desktop.MainWindow = new Views.MainWindow
|
||||||
|
{
|
||||||
|
DataContext = Services.GetRequiredService<MainWindowViewModel>()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
@@ -31,8 +45,22 @@ public partial class App : Application
|
|||||||
|
|
||||||
private void ConfigureServices(IServiceCollection services)
|
private void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
// Logging
|
||||||
services.AddLogging(builder => builder
|
services.AddLogging(builder => builder
|
||||||
.AddConsole()
|
.AddConsole()
|
||||||
.SetMinimumLevel(LogLevel.Debug));
|
.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.Collections.ObjectModel;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
using JdeScoping.ConfigManager.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace JdeScoping.ConfigManager.ViewModels;
|
namespace JdeScoping.ConfigManager.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main window view model.
|
/// Main window view model.
|
||||||
/// This is a stub implementation for Task 11 - full implementation in Task 13.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MainWindowViewModel : ViewModelBase
|
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 string _configFolderPath = "No folder selected";
|
||||||
private bool _hasUnsavedChanges;
|
private bool _hasUnsavedChanges;
|
||||||
private string _validationStatus = "Valid";
|
private string _validationStatus = "Valid";
|
||||||
@@ -17,6 +25,9 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
private TreeNodeViewModel? _selectedNode;
|
private TreeNodeViewModel? _selectedNode;
|
||||||
private object? _selectedFormViewModel;
|
private object? _selectedFormViewModel;
|
||||||
|
|
||||||
|
private ConfigModel? _appSettings;
|
||||||
|
private PipelinesConfigModel? _pipelines;
|
||||||
|
|
||||||
public string ConfigFolderPath
|
public string ConfigFolderPath
|
||||||
{
|
{
|
||||||
get => _configFolderPath;
|
get => _configFolderPath;
|
||||||
@@ -44,7 +55,11 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
public TreeNodeViewModel? SelectedNode
|
public TreeNodeViewModel? SelectedNode
|
||||||
{
|
{
|
||||||
get => _selectedNode;
|
get => _selectedNode;
|
||||||
set => SetProperty(ref _selectedNode, value);
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedNode, value))
|
||||||
|
OnSelectedNodeChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? SelectedFormViewModel
|
public object? SelectedFormViewModel
|
||||||
@@ -63,15 +78,219 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
public ICommand ValidateCommand { get; }
|
public ICommand ValidateCommand { get; }
|
||||||
public ICommand TestConnectionCommand { 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
|
_configFileService = configFileService;
|
||||||
OpenFolderCommand = new RelayCommand(() => { });
|
_validationService = validationService;
|
||||||
SaveCommand = new RelayCommand(() => { });
|
_backupService = backupService;
|
||||||
ExitCommand = new RelayCommand(() => { });
|
_autoDiscoveryService = autoDiscoveryService;
|
||||||
UndoCommand = new RelayCommand(() => { });
|
_logger = logger;
|
||||||
RedoCommand = new RelayCommand(() => { });
|
|
||||||
ValidateCommand = new RelayCommand(() => { });
|
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
|
||||||
TestConnectionCommand = new RelayCommand(() => { });
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user