Fehlerbehandlung, Logging und feldbezogene Validierung (Kapitel 13.2)

Zentrale Exception-Behandlung (Dispatcher.UIThread.UnhandledException,
AppDomain, TaskScheduler) verhindert Abstürze und protokolliert Fehler
über AppLogger in eine rotierende Log-Datei im App-Datenverzeichnis.
Toast-Benachrichtigungen zeigen Erfolg/Fehler global an. Alle Dialoge
mit Formularfeldern zeigen Validierungsmeldungen jetzt direkt am
betroffenen Feld statt in einem Sammel-Label.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 12:38:30 +02:00
co-authored by Claude Sonnet 5
parent 273f459119
commit db1afff7e4
27 changed files with 578 additions and 102 deletions
@@ -28,7 +28,9 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private int? _examNumber;
[ObservableProperty] private string _notes = "";
[ObservableProperty] private string _returnedAtText = "";
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dateTextError = "";
[ObservableProperty] private string _returnedAtTextError = "";
[ObservableProperty] private double _totalPoints;
[ObservableProperty] private bool _hasCompetencyCatalog;
[ObservableProperty] private bool _useWeighting;
@@ -264,27 +266,34 @@ public partial class ExamDialogViewModel : ObservableObject
[RelayCommand]
private void Save()
{
if (string.IsNullOrWhiteSpace(Title)) { ValidationMessage = "Titel erforderlich."; return; }
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
TitleError = ""; DateTextError = ""; ReturnedAtTextError = ""; GradingKeyValidation = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
DateOnly date = default;
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out date))
{
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
return;
DateTextError = "Format TT.MM.JJJJ.";
valid = false;
}
DateOnly? returnedAt = null;
if (!string.IsNullOrWhiteSpace(ReturnedAtText))
{
if (!DateOnly.TryParseExact(ReturnedAtText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
{
ValidationMessage = "Rückgabedatum im Format TT.MM.JJJJ eingeben.";
return;
ReturnedAtTextError = "Format TT.MM.JJJJ.";
valid = false;
}
returnedAt = r;
else returnedAt = r;
}
var gradingKey = BuildGradingKeyEntries();
var gradingKeyError = _grading.ValidateGradingKey(gradingKey);
if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; return; }
GradingKeyValidation = "";
if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; valid = false; }
if (!valid) return;
Result = _editingExam ?? new Exam { GroupId = _groupId };
Result.Title = Title.Trim();
@@ -315,11 +315,16 @@ public partial class StudentGradesDialogViewModel : ObservableObject
private void Save(GradeEditItem item)
{
if (string.IsNullOrWhiteSpace(item.Value)) { item.ValidationMessage = "Wert darf nicht leer sein."; return; }
item.ValueError = ""; item.DateTextError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(item.Value)) { item.ValueError = "Wert darf nicht leer sein."; valid = false; }
if (!DateOnly.TryParseExact(item.DateText, "dd.MM.yyyy", null,
System.Globalization.DateTimeStyles.None, out _))
{ item.ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
item.ValidationMessage = "";
{ item.DateTextError = "Format TT.MM.JJJJ."; valid = false; }
if (!valid) return;
var grade = item.ToModel();
_grades.Save(grade);
item.IsNew = false;
@@ -345,7 +350,8 @@ public partial class GradeEditItem : ObservableObject
[ObservableProperty] private string _dateText;
[ObservableProperty] private double _weight;
[ObservableProperty] private string? _note;
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private string _valueError = "";
[ObservableProperty] private string _dateTextError = "";
// Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens.
public string CategoryName
@@ -405,7 +411,7 @@ public partial class CollectiveGradeDialogViewModel : ObservableObject
[ObservableProperty] private double _weight = 1.0;
[ObservableProperty] private string? _note;
[ObservableProperty] private string _statusMessage = "";
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private string _dateTextError = "";
// Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens.
public string CategoryName
@@ -428,8 +434,8 @@ public partial class CollectiveGradeDialogViewModel : ObservableObject
{
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null,
System.Globalization.DateTimeStyles.None, out var date))
{ ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
ValidationMessage = "";
{ DateTextError = "Format TT.MM.JJJJ."; return; }
DateTextError = "";
var count = 0;
foreach (var row in Rows)
@@ -618,7 +618,8 @@ public partial class AddGroupDialogViewModel : ObservableObject
[ObservableProperty] private int? _hoursPerWeek;
[ObservableProperty] private bool _isOwnClass;
[ObservableProperty] private bool _isDifferentiated;
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _gradeLevelError = "";
public List<string> SchoolYears { get; }
public List<string> GradingOptions { get; } = ["Noten 16", "Punkte 015"];
@@ -662,8 +663,13 @@ public partial class AddGroupDialogViewModel : ObservableObject
[RelayCommand]
private void Save()
{
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; }
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 113."; return; }
NameError = ""; GradeLevelError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Name)) { NameError = "Bezeichnung erforderlich."; valid = false; }
if (GradeLevel is < 1 or > 13) { GradeLevelError = "Klassenstufe 113."; valid = false; }
if (!valid) return;
string? subjectName = !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null;
Guid? subjectId = null;
@@ -671,7 +671,7 @@ public partial class AddSessionDialogViewModel : ObservableObject
[ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today);
[ObservableProperty] private string _comment = "";
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private string _dateTextError = "";
public ParticipationSession? Result { get; private set; }
@@ -681,9 +681,10 @@ public partial class AddSessionDialogViewModel : ObservableObject
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null,
System.Globalization.DateTimeStyles.None, out var date))
{
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
DateTextError = "Format TT.MM.JJJJ.";
return;
}
DateTextError = "";
Result = new ParticipationSession { Date = date, Comment = Comment.Trim() };
}
}
@@ -37,7 +37,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
[ObservableProperty] private string _progressText = "";
[ObservableProperty] private string _newSectionLabel = "";
[ObservableProperty] private string _newSectionEndDateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _sectionValidationMessage = "";
[ObservableProperty] private string _newSectionLabelError = "";
[ObservableProperty] private string _newSectionEndDateTextError = "";
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
[ObservableProperty] private string _rollupStatusMessage = "";
[ObservableProperty] private double _timelineZoom = 1.0;
@@ -322,13 +323,20 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
[RelayCommand]
private void CloseSection()
{
if (string.IsNullOrWhiteSpace(NewSectionLabel)) { SectionValidationMessage = "Bezeichnung erforderlich."; return; }
NewSectionLabelError = ""; NewSectionEndDateTextError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(NewSectionLabel)) { NewSectionLabelError = "Bezeichnung erforderlich."; valid = false; }
DateOnly end = default;
if (!DateOnly.TryParseExact(NewSectionEndDateText, "dd.MM.yyyy", null,
DateTimeStyles.None, out var end))
{ SectionValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
DateTimeStyles.None, out end))
{ NewSectionEndDateTextError = "Format TT.MM.JJJJ."; valid = false; }
if (!valid) return;
var start = ComputeOpenStart();
if (end < start) { SectionValidationMessage = "Enddatum liegt vor Abschnittsbeginn."; return; }
if (end < start) { NewSectionEndDateTextError = "Enddatum liegt vor Abschnittsbeginn."; return; }
var section = new ParticipationSection { GroupId = _groupId, Label = NewSectionLabel.Trim(), StartDate = start, EndDate = end };
_sectionRepo.Save(section);
@@ -350,7 +358,6 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
});
}
SectionValidationMessage = "";
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
BuildTimeline(CurrentStudentId);
BuildSections(CurrentStudentId);