β
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
π Issue Correlate
π Note Implementative
- ObservableValidator: Usare
[NotifyDataErrorInfo] con validation attributes
- Async Validation: Per validazioni che richiedono DB queries
- Centralized Messages: Creare
ErrorMessages.resx per localizzazione futura
- Logging: Integrare con Serilog o NLog
- Telemetry: Opzionale: Application Insights per production monitoring
β 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.cs2. Global Error Handler
File:
Services/IErrorHandlingService.cs3. Validation Attributes
File:
Validation/ValidationAttributes.cs4. Input Validation in ViewModels
Esempio:
ProjectFormViewModelcon Validazione5. Error Details Dialog
File:
Views/Dialogs/ErrorDetailsDialog.xamlπ¦ Usage negli Views
Esempio con Validation Behavior
β Acceptance Criteria
π Issue Correlate
π Note Implementative
[NotifyDataErrorInfo]con validation attributesErrorMessages.resxper localizzazione futura