Review der Codebase

This commit is contained in:
2026-08-13 19:57:48 +02:00
parent d987b93315
commit 27359794f7
9 changed files with 269 additions and 80 deletions
+34 -5
View File
@@ -15,6 +15,8 @@ namespace LehrerApp.Desktop;
public class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
private static ServiceProvider? _serviceProvider;
private static bool _exitHandlerAttached;
public override void Initialize() => AvaloniaXamlLoader.Load(this);
@@ -32,31 +34,58 @@ public class App : Application
promptVm.OnUnlocked = password =>
{
AppBootstrapper.DbPassword = password;
StartMainApp(desktop);
StartMainApp(desktop, showImmediately: true);
promptWindow.Close();
};
desktop.MainWindow = promptWindow;
}
else
{
StartMainApp(desktop);
StartMainApp(desktop, showImmediately: false);
}
}
base.OnFrameworkInitializationCompleted();
}
private static void StartMainApp(IClassicDesktopStyleApplicationLifetime desktop)
private static void StartMainApp(
IClassicDesktopStyleApplicationLifetime desktop, bool showImmediately)
{
Services = AppBootstrapper.BuildServices();
_serviceProvider = AppBootstrapper.BuildServices();
Services = _serviceProvider;
Services.GetRequiredService<AppLogger>().Info("Anwendung gestartet.");
GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>());
if (!_exitHandlerAttached)
{
desktop.Exit += (_, _) => DisposeServices();
_exitHandlerAttached = true;
}
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
WireCallbacks(mainVm);
var main = new MainWindow { DataContext = mainVm };
desktop.MainWindow = main;
main.Show();
if (showImmediately) main.Show();
}
private static void DisposeServices()
{
if (_serviceProvider is null) return;
try
{
_serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
}
catch (Exception ex)
{
AppBootstrapper.Logger.Error("Datenbank-Checkpoint beim Beenden fehlgeschlagen.", ex);
}
finally
{
_serviceProvider.Dispose();
_serviceProvider = null;
}
}
private static void WireCallbacks(MainWindowViewModel main)
+1 -1
View File
@@ -76,7 +76,7 @@ public static class AppBootstrapper
Environment.Exit(0);
}
public static IServiceProvider BuildServices()
public static ServiceProvider BuildServices()
{
var services = new ServiceCollection();
@@ -7,10 +7,10 @@ namespace LehrerApp.Desktop.Services;
/// Zentrale Exception-Behandlung (13.2.1): protokolliert jeden unerwarteten Fehler und zeigt
/// eine verständliche Meldung statt eines rohen Absturzes.
///
/// Reichweite ehrlich betrachtet: <see cref="Dispatcher"/>.UIThread.UnhandledException kann
/// Ausnahmen aus Befehlen/Ereignis-Handlern auf dem UI-Thread abfangen und die App am Leben
/// erhalten (Handled = true) — das deckt den weit überwiegenden Teil realer Abstürze in einer
/// Desktop-App ab. AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind
/// <see cref="Dispatcher"/>.UIThread.UnhandledException fängt Fehler aus Befehlen und
/// Ereignis-Handlern ab. Nur erwartbar wiederherstellbare I/O-, Netzwerk-, Timeout- und
/// Abbruchfehler werden behandelt; unbekannte Zustandsfehler dürfen die App beenden.
/// AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException sind
/// Sicherheitsnetze für Fehler außerhalb des UI-Threads; die App kann eine "IsTerminating"-
/// Ausnahme dort nicht mehr verhindern, aber wenigstens vollständig protokollieren, bevor sie endet.
/// </summary>
@@ -36,10 +36,18 @@ public static class GlobalExceptionHandler
private static void OnDispatcherUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
{
_logger?.Error("Unbehandelter Fehler auf dem UI-Thread.", e.Exception);
_notifications?.ShowError("Es ist ein unerwarteter Fehler aufgetreten. Details wurden protokolliert.");
e.Handled = true; // App am Leben halten statt abzustürzen.
e.Handled = IsRecoverable(e.Exception);
if (e.Handled)
_notifications?.ShowError("Der Vorgang ist fehlgeschlagen. Details wurden protokolliert.");
}
private static bool IsRecoverable(Exception exception) => exception is
IOException or
UnauthorizedAccessException or
HttpRequestException or
TimeoutException or
OperationCanceledException;
private static void OnAppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
_logger?.Error(
@@ -22,6 +22,14 @@ public partial class MainWindowViewModel : ObservableObject
public ObservableCollection<ToastItem> Toasts { get; }
public AppLockViewModel AppLock { get; }
public bool IsDashboardActive => ActiveNavItem == NavItem.Dashboard;
public bool IsGroupsActive => ActiveNavItem == NavItem.Groups;
public bool IsStudentsActive => ActiveNavItem == NavItem.Students;
public bool IsExamsActive => ActiveNavItem == NavItem.Exams;
public bool IsPlannerActive => ActiveNavItem == NavItem.Planner;
public bool IsWorkloadActive => ActiveNavItem == NavItem.Workload;
public bool IsSettingsActive => ActiveNavItem == NavItem.Settings;
public MainWindowViewModel(IServiceProvider services,
DashboardViewModel dashboard, SchoolYearService sy,
SyncStatusViewModel syncStatus, NotificationService notifications, AppLockViewModel appLock)
@@ -35,6 +43,17 @@ public partial class MainWindowViewModel : ObservableObject
AppLock.ApplyConfig();
}
partial void OnActiveNavItemChanged(NavItem value)
{
OnPropertyChanged(nameof(IsDashboardActive));
OnPropertyChanged(nameof(IsGroupsActive));
OnPropertyChanged(nameof(IsStudentsActive));
OnPropertyChanged(nameof(IsExamsActive));
OnPropertyChanged(nameof(IsPlannerActive));
OnPropertyChanged(nameof(IsWorkloadActive));
OnPropertyChanged(nameof(IsSettingsActive));
}
[RelayCommand]
private void NavigateTo(NavItem item)
{
+57 -37
View File
@@ -60,6 +60,15 @@
überschreiben kann lokale Werte hätten immer Vorrang vor Style-Settern. -->
<Style Selector="TextBlock.navicon">
<Setter Property="FontSize" Value="15"/>
<Setter Property="Width" Value="24"/>
</Style>
<Style Selector="Button.navitem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/>
</Style>
<Style Selector="Button.navitem.active">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<!-- Minimierter Zustand: Icons größer & zentriert, Beschriftungen ausgeblendet -->
<Style Selector="DockPanel.compact TextBlock.navlabel">
@@ -67,9 +76,20 @@
</Style>
<Style Selector="DockPanel.compact TextBlock.navicon">
<Setter Property="FontSize" Value="20"/>
<Setter Property="Width" Value="28"/>
</Style>
<Style Selector="DockPanel.compact Button.navitem">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="2,8"/>
</Style>
<Style Selector="DockPanel.compact StackPanel.navitems">
<Setter Property="Margin" Value="2,12,2,0"/>
</Style>
<Style Selector="DockPanel.compact StackPanel.navcontent">
<Setter Property="Spacing" Value="0"/>
</Style>
<Style Selector="DockPanel.compact Border.drawerheader">
<Setter Property="Padding" Value="8,20,8,14"/>
</Style>
<Style Selector="DockPanel.compact TextBlock.navsection">
<Setter Property="IsVisible" Value="False"/>
@@ -82,7 +102,7 @@
</Style>
</DockPanel.Styles>
<Border DockPanel.Dock="Top" Padding="16,20,16,14"
<Border Classes="drawerheader" DockPanel.Dock="Top" Padding="16,20,16,14"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="10">
@@ -106,14 +126,14 @@
</Border>
<ScrollViewer>
<StackPanel Margin="8,12,8,0" Spacing="2">
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<StackPanel Classes="navitems" Margin="8,12,8,0" Spacing="2">
<Button Classes="navitem" Classes.active="{Binding IsDashboardActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Dashboard}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📊" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📊" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Dashboard"/>
</StackPanel>
</Button>
@@ -121,43 +141,43 @@
<TextBlock Classes="navsection" Text="UNTERRICHT" FontSize="10" FontWeight="Bold"
Opacity="0.4" Margin="10,14,0,4"/>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsGroupsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Groups}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="🏫" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="🏫" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Lerngruppen"/>
</StackPanel>
</Button>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsStudentsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Students}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="👤" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="👤" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Schüler"/>
</StackPanel>
</Button>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsExamsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Exams}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📝" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📝" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Klausuren"/>
</StackPanel>
</Button>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsPlannerActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Planner}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📅" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="📅" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Planung"/>
</StackPanel>
</Button>
@@ -165,23 +185,23 @@
<TextBlock Classes="navsection" Text="VERWALTUNG" FontSize="10" FontWeight="Bold"
Opacity="0.4" Margin="10,14,0,4"/>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsWorkloadActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Workload}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⏱" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⏱" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Arbeitszeit"/>
</StackPanel>
</Button>
<Button Classes="navitem" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Background="Transparent"
Padding="10,8" CornerRadius="6"
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CornerRadius="6"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:NavItem.Settings}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⚙️" Width="20" TextAlignment="Center"/>
<StackPanel Classes="navcontent" Orientation="Horizontal" Spacing="10">
<TextBlock Classes="navicon" Text="⚙️" TextAlignment="Center"/>
<TextBlock Classes="navlabel" Text="Einstellungen"/>
</StackPanel>
</Button>