Skip to content

FASE 4 - Polish & Deploy: Validation UI e Error Handling GlobaleΒ #54

@artcava

Description

@artcava

βœ… FASE 4: Polish & Deploy - Validation & Error Handling

πŸ“‹ Obiettivo

Implementare sistema di validazione globale con feedback visivo user-friendly e gestione centralizzata degli errori.

🎯 Componenti da Implementare

1. Validation Behaviors (XAML Behaviors)

File: Behaviors/ValidationBehavior.cs

using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace PTRP.App.Behaviors;

public class ValidationBehavior : Behavior<FrameworkElement>
{
    public static readonly DependencyProperty ErrorMessageProperty =
        DependencyProperty.Register(
            nameof(ErrorMessage),
            typeof(string),
            typeof(ValidationBehavior),
            new PropertyMetadata(string.Empty, OnErrorMessageChanged));
    
    public string ErrorMessage
    {
        get => (string)GetValue(ErrorMessageProperty);
        set => SetValue(ErrorMessageProperty, value);
    }
    
    private TextBlock? _errorTextBlock;
    private Border? _errorBorder;
    
    protected override void OnAttached()
    {
        base.OnAttached();
        CreateErrorElements();
    }
    
    private void CreateErrorElements()
    {
        if (AssociatedObject.Parent is not Panel parent)
            return;
        
        var index = parent.Children.IndexOf(AssociatedObject);
        
        // Crea border per highlight errore
        _errorBorder = new Border
        {
            BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#DC3545")!),
            BorderThickness = new Thickness(2),
            CornerRadius = new CornerRadius(4),
            Visibility = Visibility.Collapsed
        };
        
        // Wrap element esistente
        parent.Children.RemoveAt(index);
        _errorBorder.Child = AssociatedObject;
        parent.Children.Insert(index, _errorBorder);
        
        // Crea TextBlock per messaggio errore
        _errorTextBlock = new TextBlock
        {
            Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#DC3545")!),
            FontSize = 12,
            Margin = new Thickness(0, 4, 0, 0),
            Visibility = Visibility.Collapsed
        };
        
        parent.Children.Insert(index + 1, _errorTextBlock);
    }
    
    private static void OnErrorMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ValidationBehavior behavior)
        {
            behavior.UpdateErrorDisplay();
        }
    }
    
    private void UpdateErrorDisplay()
    {
        var hasError = !string.IsNullOrWhiteSpace(ErrorMessage);
        
        if (_errorBorder != null)
        {
            _errorBorder.Visibility = hasError ? Visibility.Visible : Visibility.Collapsed;
        }
        
        if (_errorTextBlock != null)
        {
            _errorTextBlock.Text = ErrorMessage;
            _errorTextBlock.Visibility = hasError ? Visibility.Visible : Visibility.Collapsed;
        }
    }
}

2. Global Error Handler

File: Services/IErrorHandlingService.cs

namespace PTRP.Services;

public interface IErrorHandlingService
{
    void HandleError(Exception exception, string userMessage = "");
    void ShowValidationError(string message);
    void ShowWarning(string message);
    void ShowSuccess(string message);
}

public class ErrorHandlingService : IErrorHandlingService
{
    private readonly ILogger<ErrorHandlingService> _logger;
    private readonly ISnackbarMessageQueue _snackbarQueue;
    
    public ErrorHandlingService(
        ILogger<ErrorHandlingService> logger,
        ISnackbarMessageQueue snackbarQueue)
    {
        _logger = logger;
        _snackbarQueue = snackbarQueue;
    }
    
    public void HandleError(Exception exception, string userMessage = "")
    {
        // Log errore tecnico
        _logger.LogError(exception, "Application error occurred");
        
        // Determina messaggio user-friendly
        var displayMessage = string.IsNullOrWhiteSpace(userMessage)
            ? GetUserFriendlyMessage(exception)
            : userMessage;
        
        // Mostra snackbar errore
        Application.Current.Dispatcher.Invoke(() =>
        {
            _snackbarQueue.Enqueue(
                displayMessage,
                "DETTAGLI",
                () => ShowErrorDetailsDialog(exception),
                null,
                false,
                true,
                TimeSpan.FromSeconds(5));
        });
    }
    
    public void ShowValidationError(string message)
    {
        _snackbarQueue.Enqueue(
            message,
            null,
            null,
            null,
            false,
            true,
            TimeSpan.FromSeconds(3));
    }
    
    public void ShowWarning(string message)
    {
        _snackbarQueue.Enqueue(
            message,
            null,
            null,
            null,
            false,
            false,
            TimeSpan.FromSeconds(4));
    }
    
    public void ShowSuccess(string message)
    {
        _snackbarQueue.Enqueue(
            message,
            null,
            null,
            null,
            false,
            false,
            TimeSpan.FromSeconds(2));
    }
    
    private string GetUserFriendlyMessage(Exception exception)
    {
        return exception switch
        {
            UnauthorizedAccessException => "Non hai i permessi per eseguire questa operazione.",
            FileNotFoundException => "Il file richiesto non Γ¨ stato trovato.",
            DbUpdateException => "Errore durante il salvataggio dei dati. Riprova.",
            HttpRequestException => "Errore di connessione. Verifica la tua rete.",
            _ => "Si Γ¨ verificato un errore imprevisto. Contatta il supporto se il problema persiste."
        };
    }
    
    private void ShowErrorDetailsDialog(Exception exception)
    {
        // Mostra dialog con stack trace per debug
        var dialog = new ErrorDetailsDialog
        {
            DataContext = new ErrorDetailsViewModel(exception)
        };
        
        dialog.ShowDialog();
    }
}

3. Validation Attributes

File: Validation/ValidationAttributes.cs

using System.ComponentModel.DataAnnotations;

namespace PTRP.Validation;

public class RequiredIfAttribute : ValidationAttribute
{
    private readonly string _dependentProperty;
    private readonly object _targetValue;
    
    public RequiredIfAttribute(string dependentProperty, object targetValue)
    {
        _dependentProperty = dependentProperty;
        _targetValue = targetValue;
    }
    
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        var dependentProperty = validationContext.ObjectType.GetProperty(_dependentProperty);
        if (dependentProperty == null)
            return ValidationResult.Success;
        
        var dependentValue = dependentProperty.GetValue(validationContext.ObjectInstance);
        
        if (Equals(dependentValue, _targetValue) && 
            (value == null || string.IsNullOrWhiteSpace(value.ToString())))
        {
            return new ValidationResult(ErrorMessage ?? $"{validationContext.DisplayName} Γ¨ obbligatorio.");
        }
        
        return ValidationResult.Success;
    }
}

public class FutureDateAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        if (value is DateTime dateTime && dateTime <= DateTime.Now)
        {
            return new ValidationResult(ErrorMessage ?? "La data deve essere futura.");
        }
        
        return ValidationResult.Success;
    }
}

public class DateRangeAttribute : ValidationAttribute
{
    private readonly string _startDateProperty;
    
    public DateRangeAttribute(string startDateProperty)
    {
        _startDateProperty = startDateProperty;
    }
    
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        var startProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
        if (startProperty == null)
            return ValidationResult.Success;
        
        var startDate = startProperty.GetValue(validationContext.ObjectInstance) as DateTime?;
        var endDate = value as DateTime?;
        
        if (startDate.HasValue && endDate.HasValue && endDate < startDate)
        {
            return new ValidationResult(
                ErrorMessage ?? "La data di fine deve essere successiva alla data di inizio.");
        }
        
        return ValidationResult.Success;
    }
}

4. Input Validation in ViewModels

Esempio: ProjectFormViewModel con Validazione

using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace PTRP.ViewModels;

public partial class ProjectFormViewModel : ObservableValidator
{
    [ObservableProperty]
    [Required(ErrorMessage = "Il titolo del progetto Γ¨ obbligatorio")]
    [MinLength(3, ErrorMessage = "Il titolo deve contenere almeno 3 caratteri")]
    [NotifyDataErrorInfo]
    private string _title = string.Empty;
    
    [ObservableProperty]
    [Required(ErrorMessage = "La data di inizio Γ¨ obbligatoria")]
    [FutureDate(ErrorMessage = "La data di inizio deve essere futura")]
    [NotifyDataErrorInfo]
    private DateTime? _startDate;
    
    [ObservableProperty]
    [Required(ErrorMessage = "La data di fine Γ¨ obbligatoria")]
    [DateRange(nameof(StartDate), ErrorMessage = "La data di fine deve essere successiva alla data di inizio")]
    [NotifyDataErrorInfo]
    private DateTime? _endDate;
    
    [ObservableProperty]
    [RequiredIf(nameof(ProjectType), "Personalizzato", ErrorMessage = "Specifica il tipo personalizzato")]
    [NotifyDataErrorInfo]
    private string _customType = string.Empty;
    
    [RelayCommand]
    private async Task SaveAsync()
    {
        // Valida tutti i campi
        ValidateAllProperties();
        
        if (HasErrors)
        {
            _errorHandlingService.ShowValidationError(
                "Correggi gli errori evidenziati prima di salvare.");
            return;
        }
        
        try
        {
            // Salvataggio...
            await _projectRepository.SaveAsync(/* ... */);
            _errorHandlingService.ShowSuccess("Progetto salvato con successo!");
        }
        catch (Exception ex)
        {
            _errorHandlingService.HandleError(ex, "Impossibile salvare il progetto.");
        }
    }
}

5. Error Details Dialog

File: Views/Dialogs/ErrorDetailsDialog.xaml

<Window x:Class="PTRP.App.Views.Dialogs.ErrorDetailsDialog"
        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"
        Title="Dettagli Errore"
        Width="600" 
        Height="400"
        WindowStartupLocation="CenterOwner">
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <!-- Header -->
        <Border Grid.Row="0" 
                Background="{StaticResource ErrorBrush}" 
                Padding="20">
            <StackPanel Orientation="Horizontal">
                <materialDesign:PackIcon Kind="AlertCircle" 
                                         Width="32" 
                                         Height="32"
                                         Foreground="White"/>
                <TextBlock Text="Dettagli Errore" 
                           Style="{StaticResource H2Style}"
                           Foreground="White"
                           VerticalAlignment="Center"
                           Margin="12,0,0,0"/>
            </StackPanel>
        </Border>
        
        <!-- Content -->
        <ScrollViewer Grid.Row="1" 
                      VerticalScrollBarVisibility="Auto" 
                      Padding="20">
            <StackPanel>
                <TextBlock Text="Messaggio" 
                           Style="{StaticResource BodyStyle}"
                           FontWeight="SemiBold"
                           Margin="0,0,0,8"/>
                <TextBox Text="{Binding ExceptionMessage}" 
                         IsReadOnly="True"
                         TextWrapping="Wrap"
                         Background="#F5F5F5"
                         BorderThickness="0"
                         Padding="12"
                         Margin="0,0,0,16"/>
                
                <TextBlock Text="Stack Trace" 
                           Style="{StaticResource BodyStyle}"
                           FontWeight="SemiBold"
                           Margin="0,0,0,8"/>
                <TextBox Text="{Binding StackTrace}" 
                         IsReadOnly="True"
                         TextWrapping="Wrap"
                         FontFamily="Consolas"
                         FontSize="11"
                         Background="#F5F5F5"
                         BorderThickness="0"
                         Padding="12"
                         VerticalScrollBarVisibility="Auto"
                         Height="200"/>
            </StackPanel>
        </ScrollViewer>
        
        <!-- Footer -->
        <Border Grid.Row="2" 
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="0,1,0,0"
                Padding="20">
            <StackPanel Orientation="Horizontal" 
                        HorizontalAlignment="Right">
                <Button Style="{StaticResource MaterialDesignOutlinedButton}"
                        Command="{Binding CopyToClipboardCommand}"
                        Content="Copia negli Appunti"
                        Margin="0,0,12,0"/>
                <Button Style="{StaticResource MaterialDesignRaisedButton}"
                        Content="Chiudi"
                        IsCancel="True"/>
            </StackPanel>
        </Border>
    </Grid>
</Window>

πŸ“¦ Usage negli Views

Esempio con Validation Behavior

<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}">
    <i:Interaction.Behaviors>
        <behaviors:ValidationBehavior ErrorMessage="{Binding TitleError}" />
    </i:Interaction.Behaviors>
</TextBox>

βœ… Acceptance Criteria

  • ValidationBehavior implementato e testato
  • ErrorHandlingService con metodi Show(Error/Warning/Success)
  • Custom validation attributes (RequiredIf, FutureDate, DateRange)
  • Integration con ObservableValidator (CommunityToolkit.Mvvm)
  • ErrorDetailsDialog per stack trace
  • Global exception handler in App.xaml.cs
  • Snackbar messages per feedback visivo
  • Tutti i form usano validation behavior
  • Error messages in italiano
  • Unit tests per validation attributes

πŸ”— Issue Correlate

πŸ“ Note Implementative

  1. ObservableValidator: Usare [NotifyDataErrorInfo] con validation attributes
  2. Async Validation: Per validazioni che richiedono DB queries
  3. Centralized Messages: Creare ErrorMessages.resx per localizzazione futura
  4. Logging: Integrare con Serilog o NLog
  5. Telemetry: Opzionale: Application Insights per production monitoring

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions