Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/PTRP.App/App.xaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Application x:Class="PTRP.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:PTRP.App.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
Expand All @@ -14,10 +15,24 @@

<!-- Application Resources -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />

<!-- Custom Converters (Issue #94) -->
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<converters:ProjectStateToColorConverter x:Key="ProjectStateToColorConverter" />

<BitmapImage x:Key="AppIcon" UriSource="/favicon.ico" />

<!-- ViewModel to View DataTemplates will be registered by NavigationService at runtime -->
<!-- ========================================
ViewModel to View Mapping
Issue #94: View resolution via ViewLocator (code-behind)

DataTemplates removed because Views require DI in constructors.
ViewLocator (src/PTRP.App/Infrastructure/ViewLocator.cs) handles
View instantiation with proper dependency injection.

Mapping managed in MainWindow.xaml.cs via PropertyChanged event.
======================================== -->

</ResourceDictionary>
</Application.Resources>
Expand Down
8 changes: 7 additions & 1 deletion src/PTRP.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -112,6 +114,9 @@ private void ConfigureServices(ServiceCollection services)
services.AddSingleton<INavigationService, NavigationService>(); // Issue #46: Navigation Service
services.AddScoped<IConfigurationService, ConfigurationService>(); // Issue #49: Configuration Service

// Registra ViewLocator (Issue #94: DI-based View resolution)
services.AddSingleton<ViewLocator>();

// Registra i ViewModels
services.AddSingleton<MainViewModel>(); // Singleton per condividere stato app
// TODO: Issue #49 - Uncomment when implemented
Expand All @@ -125,11 +130,12 @@ private void ConfigureServices(ServiceCollection services)
services.AddTransient<SyncViewModel>(); // Issue #52: Sync ViewModel
services.AddTransient<ConflictResolutionViewModel>(); // Issue #52: Conflict Resolution ViewModel

// Registra le Views
// Registra le Views (Issue #94: Views with DI-based constructors)
services.AddScoped<MainWindow>();
services.AddScoped<PatientListView>(); // Issue #51/#74: Patient List View
services.AddScoped<EducatorListView>(); // Issue #63: Educator List View
services.AddScoped<ProjectListView>(); // Issue #64: Project List View
services.AddScoped<SyncView>(); // Issue #52: Sync View
}

/// <summary>
Expand Down
60 changes: 22 additions & 38 deletions src/PTRP.App/Converters/NullToVisibilityConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,35 @@
using System.Windows;
using System.Windows.Data;

namespace PTRP.App.Converters
namespace PTRP.App.Converters;

/// <summary>
/// Converts null to Visibility.Collapsed and non-null to Visibility.Visible.
/// Used throughout the app for conditional visibility based on object presence.
/// </summary>
public class NullToVisibilityConverter : IValueConverter
{
/// <summary>
/// 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
/// </summary>
public class NullToVisibilityConverter : IValueConverter
{
/// <summary>
/// Converts null/empty value to Visibility.
/// </summary>
/// <param name="value">Value to check (object, string, etc.)</param>
/// <param name="targetType">Target type (Visibility)</param>
/// <param name="parameter">Optional parameter ("Invert" to reverse logic)</param>
/// <param name="culture">Culture info</param>
/// <returns>Visibility.Visible if value is not null/empty, Visibility.Collapsed otherwise</returns>
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;
}

/// <summary>
/// Not implemented (one-way binding only).
/// </summary>
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");
}
}
58 changes: 23 additions & 35 deletions src/PTRP.App/Converters/ProjectStateToColorConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Converts TherapyProjectState enum to a Color brush for UI display.
/// Used in ProjectListView for status badges.
/// </summary>
public class ProjectStateToColorConverter : IValueConverter
{
/// <summary>
/// Converts project state string to color brush for badge display.
/// Used in PatientListView DataGrid to show colored status badges.
/// </summary>
public class ProjectStateToColorConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
/// <summary>
/// Converts project state to color brush.
/// </summary>
/// <param name="value">Project state string (Active, Suspended, Completed, Deceased, None)</param>
/// <param name="targetType">Target type (Brush)</param>
/// <param name="parameter">Optional parameter</param>
/// <param name="culture">Culture info</param>
/// <returns>SolidColorBrush for the badge background</returns>
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;

/// <summary>
/// Not implemented (one-way binding only).
/// </summary>
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");
}
}
73 changes: 73 additions & 0 deletions src/PTRP.App/Infrastructure/ViewLocator.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public class ViewLocator
{
private readonly IServiceProvider _serviceProvider;

public ViewLocator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}

/// <summary>
/// Creates a View instance for the given ViewModel, resolving dependencies via DI.
/// Sets the ViewModel as the View's DataContext.
/// </summary>
/// <param name="viewModel">The ViewModel instance</param>
/// <returns>A UserControl instance with DataContext set to the ViewModel, or null if no matching View found</returns>
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<PatientListView>(),

// Educators Module (requires IServiceProvider in constructor)
EducatorListViewModel => _serviceProvider.GetRequiredService<EducatorListView>(),

// Projects Module (requires IServiceProvider in constructor)
ProjectListViewModel => _serviceProvider.GetRequiredService<ProjectListView>(),

// 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<SyncView>(),

// Unknown ViewModel - return null
_ => null
};

if (view != null)
{
view.DataContext = viewModel;
}

return view;
}
}
6 changes: 3 additions & 3 deletions src/PTRP.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@
</Grid>
</Border>

<!-- DYNAMIC CONTENT AREA -->
<ContentControl Grid.Row="1"
Content="{Binding CurrentViewModel}"
<!-- DYNAMIC CONTENT AREA - ViewLocator managed in code-behind (Issue #94) -->
<ContentControl x:Name="ContentArea"
Grid.Row="1"
Margin="0"/>

<!-- SNACKBAR for notifications (MessageQueue set in code-behind) -->
Expand Down
40 changes: 37 additions & 3 deletions src/PTRP.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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)
/// </summary>
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private readonly ViewLocator _viewLocator;

/// <summary>
/// Costruttore - riceve il ViewModel via Dependency Injection
/// Costruttore - riceve il ViewModel e ViewLocator via Dependency Injection
/// </summary>
public MainWindow(MainViewModel viewModel)
public MainWindow(MainViewModel viewModel, ViewLocator viewLocator)
{
InitializeComponent();

_viewModel = viewModel;
_viewLocator = viewLocator;

// Imposta il ViewModel come DataContext
DataContext = _viewModel;
Expand All @@ -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;
}

/// <summary>
/// Intercetta i cambiamenti di CurrentViewModel e risolve la View tramite ViewLocator.
/// Issue #94: Permette Views con costruttori DI invece di DataTemplate statici.
/// </summary>
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;
}
}
}

/// <summary>
Expand Down Expand Up @@ -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);
}
}
Loading