Review der Codebase
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user