diff --git a/src/PTRP.App/App.xaml b/src/PTRP.App/App.xaml index 47436be..ee5ba55 100644 --- a/src/PTRP.App/App.xaml +++ b/src/PTRP.App/App.xaml @@ -1,7 +1,8 @@ + xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:converters="clr-namespace:PTRP.App.Converters"> @@ -14,10 +15,24 @@ + + + + + - + diff --git a/src/PTRP.App/App.xaml.cs b/src/PTRP.App/App.xaml.cs index d87584a..99a61ed 100644 --- a/src/PTRP.App/App.xaml.cs +++ b/src/PTRP.App/App.xaml.cs @@ -9,9 +9,11 @@ using PTRP.Data; using PTRP.Data.Repositories; using PTRP.Data.Repositories.Interfaces; +using PTRP.App.Infrastructure; using PTRP.App.Views.Patients; using PTRP.App.Views.Educators; using PTRP.App.Views.Projects; +using PTRP.App.Views.Sync; using System.IO; using System.Windows; @@ -112,6 +114,9 @@ private void ConfigureServices(ServiceCollection services) services.AddSingleton(); // Issue #46: Navigation Service services.AddScoped(); // Issue #49: Configuration Service + // Registra ViewLocator (Issue #94: DI-based View resolution) + services.AddSingleton(); + // Registra i ViewModels services.AddSingleton(); // Singleton per condividere stato app // TODO: Issue #49 - Uncomment when implemented @@ -125,11 +130,12 @@ private void ConfigureServices(ServiceCollection services) services.AddTransient(); // Issue #52: Sync ViewModel services.AddTransient(); // Issue #52: Conflict Resolution ViewModel - // Registra le Views + // Registra le Views (Issue #94: Views with DI-based constructors) services.AddScoped(); services.AddScoped(); // Issue #51/#74: Patient List View services.AddScoped(); // Issue #63: Educator List View services.AddScoped(); // Issue #64: Project List View + services.AddScoped(); // Issue #52: Sync View } /// diff --git a/src/PTRP.App/Converters/NullToVisibilityConverter.cs b/src/PTRP.App/Converters/NullToVisibilityConverter.cs index b74c121..cffbf4b 100644 --- a/src/PTRP.App/Converters/NullToVisibilityConverter.cs +++ b/src/PTRP.App/Converters/NullToVisibilityConverter.cs @@ -3,51 +3,35 @@ using System.Windows; using System.Windows.Data; -namespace PTRP.App.Converters +namespace PTRP.App.Converters; + +/// +/// Converts null to Visibility.Collapsed and non-null to Visibility.Visible. +/// Used throughout the app for conditional visibility based on object presence. +/// +public class NullToVisibilityConverter : IValueConverter { /// - /// Converts null/empty values to Visibility enum. - /// Used to hide UI elements when data is not available. + /// When true, inverts the logic: null = Visible, non-null = Collapsed /// - public class NullToVisibilityConverter : IValueConverter - { - /// - /// Converts null/empty value to Visibility. - /// - /// Value to check (object, string, etc.) - /// Target type (Visibility) - /// Optional parameter ("Invert" to reverse logic) - /// Culture info - /// Visibility.Visible if value is not null/empty, Visibility.Collapsed otherwise - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - bool isNull = value == null; - - // Check for empty strings - if (!isNull && value is string str) - { - isNull = string.IsNullOrWhiteSpace(str); - } + public bool Invert { get; set; } = false; - // Check for parameter to invert logic - bool invert = parameter is string param && param.Equals("Invert", StringComparison.OrdinalIgnoreCase); + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool isNull = value == null; - if (invert) - { - return isNull ? Visibility.Visible : Visibility.Collapsed; - } - else - { - return isNull ? Visibility.Collapsed : Visibility.Visible; - } + if (Invert) + { + return isNull ? Visibility.Visible : Visibility.Collapsed; } - - /// - /// Not implemented (one-way binding only). - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + else { - throw new NotImplementedException("NullToVisibilityConverter is one-way only."); + return isNull ? Visibility.Collapsed : Visibility.Visible; } } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException("NullToVisibilityConverter does not support ConvertBack"); + } } diff --git a/src/PTRP.App/Converters/ProjectStateToColorConverter.cs b/src/PTRP.App/Converters/ProjectStateToColorConverter.cs index e529698..a64dcd5 100644 --- a/src/PTRP.App/Converters/ProjectStateToColorConverter.cs +++ b/src/PTRP.App/Converters/ProjectStateToColorConverter.cs @@ -2,45 +2,33 @@ using System.Globalization; using System.Windows.Data; using System.Windows.Media; +using PTRP.Models.Enums; -namespace PTRP.App.Converters +namespace PTRP.App.Converters; + +/// +/// Converts TherapyProjectState enum to a Color brush for UI display. +/// Used in ProjectListView for status badges. +/// +public class ProjectStateToColorConverter : IValueConverter { - /// - /// Converts project state string to color brush for badge display. - /// Used in PatientListView DataGrid to show colored status badges. - /// - public class ProjectStateToColorConverter : IValueConverter + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - /// - /// Converts project state to color brush. - /// - /// Project state string (Active, Suspended, Completed, Deceased, None) - /// Target type (Brush) - /// Optional parameter - /// Culture info - /// SolidColorBrush for the badge background - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is not string state) - return new SolidColorBrush(Colors.Gray); - - return state switch - { - "Active" => new SolidColorBrush(Color.FromRgb(76, 175, 80)), // Material Green 500 - "Suspended" => new SolidColorBrush(Color.FromRgb(255, 193, 7)), // Material Amber 500 - "Completed" => new SolidColorBrush(Color.FromRgb(158, 158, 158)), // Material Grey 500 - "Deceased" => new SolidColorBrush(Color.FromRgb(244, 67, 54)), // Material Red 500 - "None" => new SolidColorBrush(Color.FromRgb(189, 189, 189)), // Material Grey 400 - _ => new SolidColorBrush(Colors.Gray) - }; - } + if (value is not TherapyProjectState status) + return Brushes.Gray; - /// - /// Not implemented (one-way binding only). - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + return status switch { - throw new NotImplementedException("ProjectStateToColorConverter is one-way only."); - } + TherapyProjectState.Active => new SolidColorBrush(Color.FromRgb(40, 167, 69)), // Green #28A745 + TherapyProjectState.Suspended => new SolidColorBrush(Color.FromRgb(255, 193, 7)), // Yellow #FFC107 + TherapyProjectState.Completed => new SolidColorBrush(Color.FromRgb(0, 123, 255)), // Blue #007BFF + TherapyProjectState.Deceased => new SolidColorBrush(Color.FromRgb(108, 117, 125)), // Gray #6C757D + _ => Brushes.Gray + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException("ProjectStateToColorConverter does not support ConvertBack"); } } diff --git a/src/PTRP.App/Infrastructure/ViewLocator.cs b/src/PTRP.App/Infrastructure/ViewLocator.cs new file mode 100644 index 0000000..3483f16 --- /dev/null +++ b/src/PTRP.App/Infrastructure/ViewLocator.cs @@ -0,0 +1,73 @@ +using System; +using System.Windows.Controls; +using Microsoft.Extensions.DependencyInjection; +using PTRP.App.Views.Educators; +using PTRP.App.Views.Patients; +using PTRP.App.Views.Projects; +using PTRP.App.Views.Setup; +using PTRP.App.Views.Sync; +using PTRP.ViewModels; +using PTRP.ViewModels.Educators; +using PTRP.ViewModels.Patients; +using PTRP.ViewModels.Projects; + +namespace PTRP.App.Infrastructure; + +/// +/// Locates and instantiates Views for ViewModels using Dependency Injection. +/// Replaces DataTemplate approach when Views require constructor parameters. +/// Issue #94: Enables MVVM navigation with DI-based View constructors. +/// +public class ViewLocator +{ + private readonly IServiceProvider _serviceProvider; + + public ViewLocator(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + /// Creates a View instance for the given ViewModel, resolving dependencies via DI. + /// Sets the ViewModel as the View's DataContext. + /// + /// The ViewModel instance + /// A UserControl instance with DataContext set to the ViewModel, or null if no matching View found + public UserControl? CreateViewForViewModel(object? viewModel) + { + if (viewModel == null) + return null; + + UserControl? view = viewModel switch + { + // First Run / Setup + FirstRunViewModel => new FirstRunView(), + + // Patients Module (requires IServiceProvider in constructor) + PatientListViewModel => _serviceProvider.GetRequiredService(), + + // Educators Module (requires IServiceProvider in constructor) + EducatorListViewModel => _serviceProvider.GetRequiredService(), + + // Projects Module (requires IServiceProvider in constructor) + ProjectListViewModel => _serviceProvider.GetRequiredService(), + + // ProjectFormView is created manually with specific patient context, + // so it's not navigated to directly - it's opened in dialogs + ProjectFormViewModel => new ProjectFormView(), + + // Sync Module (requires IServiceProvider in constructor) + SyncViewModel => _serviceProvider.GetRequiredService(), + + // Unknown ViewModel - return null + _ => null + }; + + if (view != null) + { + view.DataContext = viewModel; + } + + return view; + } +} diff --git a/src/PTRP.App/MainWindow.xaml b/src/PTRP.App/MainWindow.xaml index 8687b24..b77579a 100644 --- a/src/PTRP.App/MainWindow.xaml +++ b/src/PTRP.App/MainWindow.xaml @@ -340,9 +340,9 @@ - - + diff --git a/src/PTRP.App/MainWindow.xaml.cs b/src/PTRP.App/MainWindow.xaml.cs index 7f6cc06..d351d79 100644 --- a/src/PTRP.App/MainWindow.xaml.cs +++ b/src/PTRP.App/MainWindow.xaml.cs @@ -1,6 +1,9 @@ +using System; +using System.ComponentModel; +using System.Windows; using MaterialDesignThemes.Wpf; +using PTRP.App.Infrastructure; using PTRP.ViewModels; -using System.Windows; namespace PTRP.App; @@ -12,19 +15,22 @@ namespace PTRP.App; /// 1. Collegamento del ViewModel (binding) /// 2. Setup MessageQueue per Snackbar /// 3. Gestione eventi notifica dal ViewModel +/// 4. Risoluzione View tramite ViewLocator (Issue #94) /// public partial class MainWindow : Window { private readonly MainViewModel _viewModel; + private readonly ViewLocator _viewLocator; /// - /// Costruttore - riceve il ViewModel via Dependency Injection + /// Costruttore - riceve il ViewModel e ViewLocator via Dependency Injection /// - public MainWindow(MainViewModel viewModel) + public MainWindow(MainViewModel viewModel, ViewLocator viewLocator) { InitializeComponent(); _viewModel = viewModel; + _viewLocator = viewLocator; // Imposta il ViewModel come DataContext DataContext = _viewModel; @@ -34,6 +40,33 @@ public MainWindow(MainViewModel viewModel) // Subscribe to notification events _viewModel.NotificationRequested += OnNotificationRequested; + + // Subscribe to CurrentViewModel changes to resolve Views via ViewLocator + _viewModel.PropertyChanged += OnViewModelPropertyChanged; + } + + /// + /// Intercetta i cambiamenti di CurrentViewModel e risolve la View tramite ViewLocator. + /// Issue #94: Permette Views con costruttori DI invece di DataTemplate statici. + /// + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MainViewModel.CurrentViewModel)) + { + // Risolvi la View per il ViewModel corrente tramite ViewLocator + var view = _viewLocator.CreateViewForViewModel(_viewModel.CurrentViewModel); + + // Imposta la View nel ContentControl + if (view != null) + { + ContentArea.Content = view; + } + else + { + // ViewModel sconosciuto o non implementato - mostra placeholder + ContentArea.Content = null; + } + } } /// @@ -77,6 +110,7 @@ private void OnNotificationRequested(object? sender, NotificationEventArgs e) protected override void OnClosed(EventArgs e) { _viewModel.NotificationRequested -= OnNotificationRequested; + _viewModel.PropertyChanged -= OnViewModelPropertyChanged; base.OnClosed(e); } }