Klausur-Deckblatt: Fortschrittsbalken und Erzeugung im Hintergrund
CI / build-and-test (push) Canceled after 0s

Ein Klassensatz dauert spürbar und fror den Dialog ein. GenerateAsync
setzt jetzt per Task.Run, RenderBatchToPdf meldet jeden gesetzten
Datensatz über ein optionales IProgress<int>. Das abschließende
Document.Merge meldet nichts, dort läuft der Balken unbestimmt.
Währenddessen sind die Schaltflächen gesperrt und der Dialog bleibt
offen. Ein einzelnes Deckblatt wird nicht mehr doppelt gesetzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-08 09:00:15 +02:00
1 parent 24a30881f3
commit e4a81e8974
9 files changed
+93 -23

No files matched your search

@@ -31,12 +31,20 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
/// <summary>Vorbelegt mit dem internen Klausurtitel, hier nur für diesen Druck änderbar — der
/// Titel der Klausur bleibt unberührt. Leer gelassen gilt wieder der interne Titel.</summary>
[ObservableProperty] private string _coverTitle;
[ObservableProperty, NotifyPropertyChangedFor(nameof(CanGenerate))] private bool _isGenerating;
[ObservableProperty] private int _progressValue;
[ObservableProperty] private int _progressMaximum = 1;
/// <summary>Alle Deckblätter sind gesetzt, das Sammel-PDF entsteht — dieser Schritt meldet
/// keinen Zwischenstand, der Balken läuft deshalb unbestimmt.</summary>
[ObservableProperty] private bool _isMerging;
[ObservableProperty] private string _progressText = "";
public string ExamTitle => _exam.Title;
public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}";
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
public ObservableCollection<ExamCoverStudentChoice> Students { get; } = [];
public bool HasNoTemplates => Templates.Count == 0;
public bool CanGenerate => !HasNoTemplates && !IsGenerating;
public string SuggestedFileName => SanitizeFileName($"Deckblätter_{_group.Name}_{_exam.Title}.pdf");
public ExamCoverSheetDialogViewModel(Exam exam, LearningGroup group, string subjectName,
@@ -68,11 +76,12 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
FillResults = Students.Any(s => s.IsCorrected);
}
/// <summary>Schreibt das Sammel-PDF und meldet den Erfolg.</summary>
public bool Generate(string outputPath)
/// <summary>Schreibt das Sammel-PDF und meldet den Erfolg. Gesetzt wird im Hintergrund, damit
/// der Dialog bei einem Klassensatz bedienbar bleibt und den Fortschritt zeigen kann.</summary>
public async Task<bool> GenerateAsync(string outputPath)
{
StatusMessage = "";
if (SelectedTemplate is null) return false;
if (SelectedTemplate is null || IsGenerating) return false;
var included = Students.Where(s => s.IsIncluded).ToList();
if (included.Count == 0) { StatusMessage = "Kein Schüler ausgewählt."; return false; }
try
@@ -82,8 +91,15 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider(
ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults,
_grading, loaded, today, s.Niveau, TeacherAbbreviation, CoverTitle))).ToList();
var result = _renderer.RenderBatchToPdf(loaded, data);
File.WriteAllBytes(outputPath, result.Pdf);
ReportProgress(0, included.Count);
IsGenerating = true;
var progress = new Progress<int>(done => ReportProgress(done, included.Count));
var result = await Task.Run(() =>
{
var rendered = _renderer.RenderBatchToPdf(loaded, data, progress);
File.WriteAllBytes(outputPath, rendered.Pdf);
return rendered;
});
var message = $"{included.Count} Deckblätter erzeugt.";
var blank = FillResults ? included.Count(s => !s.IsCorrected) : 0;
@@ -96,6 +112,17 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
}
catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException)
{ StatusMessage = $"Deckblätter konnten nicht erzeugt werden: {ex.Message}"; return false; }
finally { IsGenerating = false; }
}
private void ReportProgress(int done, int total)
{
ProgressMaximum = total;
ProgressValue = done;
IsMerging = done >= total && total > 1;
ProgressText = IsMerging
? "Sammel-PDF wird zusammengefügt …"
: $"{done} von {total} Deckblättern gesetzt …";
}
private static string SanitizeFileName(string value)
@@ -49,13 +49,21 @@
</StackPanel>
</ScrollViewer>
<TextBlock Grid.Row="3" Text="{Binding StatusMessage}" FontSize="12" Margin="0,10,0,0" TextWrapping="Wrap"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<StackPanel Grid.Row="3">
<StackPanel Spacing="6" Margin="0,10,0,0" IsVisible="{Binding IsGenerating}">
<ProgressBar Minimum="0" Maximum="{Binding ProgressMaximum}" Value="{Binding ProgressValue}"
IsIndeterminate="{Binding IsMerging}" Height="6"/>
<TextBlock Text="{Binding ProgressText}" FontSize="12" Opacity="0.65"/>
</StackPanel>
<TextBlock Text="{Binding StatusMessage}" FontSize="12" Margin="0,10,0,0" TextWrapping="Wrap"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Grid Grid.Row="4" ColumnDefinitions="*,8,*" Margin="0,16,0,0">
<Button Grid.Column="0" Content="Schließen" HorizontalAlignment="Stretch" Click="OnClose"/>
<Button Grid.Column="0" Content="Schließen" HorizontalAlignment="Stretch" Click="OnClose"
IsEnabled="{Binding !IsGenerating}"/>
<Button Grid.Column="2" Content="PDF erzeugen…" HorizontalAlignment="Stretch" Click="OnGenerate"
IsEnabled="{Binding !HasNoTemplates}"/>
IsEnabled="{Binding CanGenerate}"/>
</Grid>
</Grid>
@@ -7,7 +7,13 @@ namespace LehrerApp.Desktop.Views.Groups;
public partial class ExamCoverSheetDialog : Window
{
public ExamCoverSheetDialog() => InitializeComponent();
public ExamCoverSheetDialog()
{
InitializeComponent();
// Solange das PDF entsteht, bleibt der Dialog offen — sonst liefe die Erzeugung ohne
// sichtbare Rückmeldung weiter.
Closing += (_, e) => e.Cancel = DataContext is ExamCoverSheetDialogViewModel { IsGenerating: true };
}
private async void OnGenerate(object? sender, RoutedEventArgs e)
{
@@ -21,7 +27,7 @@ public partial class ExamCoverSheetDialog : Window
});
if (file is null) return;
var path = file.Path.LocalPath;
if (vm.Generate(path))
if (await vm.GenerateAsync(path))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
}