Klausur-Deckblatt: Fortschrittsbalken und Erzeugung im Hintergrund
CI / build-and-test (push) Canceled after 0s
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:
1 parent
24a30881f3
commit
e4a81e8974
9 files changed
+93
-23
No files matched your search
@@ -120,7 +120,7 @@ public sealed class ExamCoverSheetTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Generate_SchreibtEinSammelPdfMitEinerSeiteJeSchueler()
|
public async Task Generate_SchreibtEinSammelPdfMitEinerSeiteJeSchueler()
|
||||||
{
|
{
|
||||||
_results.Add(new ExamResult
|
_results.Add(new ExamResult
|
||||||
{ ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 });
|
{ ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 });
|
||||||
@@ -129,7 +129,7 @@ public sealed class ExamCoverSheetTests : IDisposable
|
|||||||
var vm = Build([store]);
|
var vm = Build([store]);
|
||||||
var output = Path.Combine(_directory, "deckblaetter.pdf");
|
var output = Path.Combine(_directory, "deckblaetter.pdf");
|
||||||
|
|
||||||
Assert.True(vm.Generate(output));
|
Assert.True(await vm.GenerateAsync(output));
|
||||||
|
|
||||||
using var document = UglyToad.PdfPig.PdfDocument.Open(output);
|
using var document = UglyToad.PdfPig.PdfDocument.Open(output);
|
||||||
Assert.Equal(2, document.NumberOfPages);
|
Assert.Equal(2, document.NumberOfPages);
|
||||||
@@ -145,10 +145,12 @@ public sealed class ExamCoverSheetTests : IDisposable
|
|||||||
// Ben ist noch nicht korrigiert: Linien zum Ausfüllen statt Punkten und Note.
|
// Ben ist noch nicht korrigiert: Linien zum Ausfüllen statt Punkten und Note.
|
||||||
Assert.Contains($"Gesamtpunkte:{ExamCoverPlaceholderBuilder.BlankPoints}von17Punkte", ben);
|
Assert.Contains($"Gesamtpunkte:{ExamCoverPlaceholderBuilder.BlankPoints}von17Punkte", ben);
|
||||||
Assert.Contains("1 davon ohne Punkte und Note", vm.StatusMessage);
|
Assert.Contains("1 davon ohne Punkte und Note", vm.StatusMessage);
|
||||||
|
Assert.False(vm.IsGenerating);
|
||||||
|
Assert.True(vm.CanGenerate);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Generate_OhnePunkteEintragen_BleibtAuchBeiKorrigiertenBlanko()
|
public async Task Generate_OhnePunkteEintragen_BleibtAuchBeiKorrigiertenBlanko()
|
||||||
{
|
{
|
||||||
_results.Add(new ExamResult
|
_results.Add(new ExamResult
|
||||||
{ ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 });
|
{ ExamId = _exam.Id, StudentId = _anna.Id, Points = [4.5, 12, 1], TotalPoints = 17.5 });
|
||||||
@@ -158,21 +160,21 @@ public sealed class ExamCoverSheetTests : IDisposable
|
|||||||
vm.FillResults = false;
|
vm.FillResults = false;
|
||||||
var output = Path.Combine(_directory, "blanko.pdf");
|
var output = Path.Combine(_directory, "blanko.pdf");
|
||||||
|
|
||||||
Assert.True(vm.Generate(output));
|
Assert.True(await vm.GenerateAsync(output));
|
||||||
|
|
||||||
using var document = UglyToad.PdfPig.PdfDocument.Open(output);
|
using var document = UglyToad.PdfPig.PdfDocument.Open(output);
|
||||||
Assert.DoesNotContain("17,5", Normalize(document.GetPage(1).Text));
|
Assert.DoesNotContain("17,5", Normalize(document.GetPage(1).Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Generate_ZuVieleAufgabenFuerEineSeite_WarntImStatus()
|
public async Task Generate_ZuVieleAufgabenFuerEineSeite_WarntImStatus()
|
||||||
{
|
{
|
||||||
_exam.Tasks = Enumerable.Range(1, 80).Select(n => new ExamTask { Nr = n, MaxPoints = 1 }).ToList();
|
_exam.Tasks = Enumerable.Range(1, 80).Select(n => new ExamTask { Nr = n, MaxPoints = 1 }).ToList();
|
||||||
var store = Store();
|
var store = Store();
|
||||||
Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover);
|
Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover);
|
||||||
var vm = Build([store]);
|
var vm = Build([store]);
|
||||||
|
|
||||||
Assert.True(vm.Generate(Path.Combine(_directory, "lang.pdf")));
|
Assert.True(await vm.GenerateAsync(Path.Combine(_directory, "lang.pdf")));
|
||||||
|
|
||||||
Assert.Contains("passen nicht auf eine Seite", vm.StatusMessage);
|
Assert.Contains("passen nicht auf eine Seite", vm.StatusMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,20 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
|
|||||||
/// <summary>Vorbelegt mit dem internen Klausurtitel, hier nur für diesen Druck änderbar — der
|
/// <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>
|
/// Titel der Klausur bleibt unberührt. Leer gelassen gilt wieder der interne Titel.</summary>
|
||||||
[ObservableProperty] private string _coverTitle;
|
[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 ExamTitle => _exam.Title;
|
||||||
public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}";
|
public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}";
|
||||||
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
|
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
|
||||||
public ObservableCollection<ExamCoverStudentChoice> Students { get; } = [];
|
public ObservableCollection<ExamCoverStudentChoice> Students { get; } = [];
|
||||||
public bool HasNoTemplates => Templates.Count == 0;
|
public bool HasNoTemplates => Templates.Count == 0;
|
||||||
|
public bool CanGenerate => !HasNoTemplates && !IsGenerating;
|
||||||
public string SuggestedFileName => SanitizeFileName($"Deckblätter_{_group.Name}_{_exam.Title}.pdf");
|
public string SuggestedFileName => SanitizeFileName($"Deckblätter_{_group.Name}_{_exam.Title}.pdf");
|
||||||
|
|
||||||
public ExamCoverSheetDialogViewModel(Exam exam, LearningGroup group, string subjectName,
|
public ExamCoverSheetDialogViewModel(Exam exam, LearningGroup group, string subjectName,
|
||||||
@@ -68,11 +76,12 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
|
|||||||
FillResults = Students.Any(s => s.IsCorrected);
|
FillResults = Students.Any(s => s.IsCorrected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Schreibt das Sammel-PDF und meldet den Erfolg.</summary>
|
/// <summary>Schreibt das Sammel-PDF und meldet den Erfolg. Gesetzt wird im Hintergrund, damit
|
||||||
public bool Generate(string outputPath)
|
/// der Dialog bei einem Klassensatz bedienbar bleibt und den Fortschritt zeigen kann.</summary>
|
||||||
|
public async Task<bool> GenerateAsync(string outputPath)
|
||||||
{
|
{
|
||||||
StatusMessage = "";
|
StatusMessage = "";
|
||||||
if (SelectedTemplate is null) return false;
|
if (SelectedTemplate is null || IsGenerating) return false;
|
||||||
var included = Students.Where(s => s.IsIncluded).ToList();
|
var included = Students.Where(s => s.IsIncluded).ToList();
|
||||||
if (included.Count == 0) { StatusMessage = "Kein Schüler ausgewählt."; return false; }
|
if (included.Count == 0) { StatusMessage = "Kein Schüler ausgewählt."; return false; }
|
||||||
try
|
try
|
||||||
@@ -82,8 +91,15 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
|
|||||||
var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider(
|
var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider(
|
||||||
ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults,
|
ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults,
|
||||||
_grading, loaded, today, s.Niveau, TeacherAbbreviation, CoverTitle))).ToList();
|
_grading, loaded, today, s.Niveau, TeacherAbbreviation, CoverTitle))).ToList();
|
||||||
var result = _renderer.RenderBatchToPdf(loaded, data);
|
ReportProgress(0, included.Count);
|
||||||
File.WriteAllBytes(outputPath, result.Pdf);
|
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 message = $"{included.Count} Deckblätter erzeugt.";
|
||||||
var blank = FillResults ? included.Count(s => !s.IsCorrected) : 0;
|
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)
|
catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException)
|
||||||
{ StatusMessage = $"Deckblätter konnten nicht erzeugt werden: {ex.Message}"; return false; }
|
{ 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)
|
private static string SanitizeFileName(string value)
|
||||||
|
|||||||
@@ -49,13 +49,21 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Text="{Binding StatusMessage}" FontSize="12" Margin="0,10,0,0" TextWrapping="Wrap"
|
<StackPanel Grid.Row="3">
|
||||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
<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">
|
<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"
|
<Button Grid.Column="2" Content="PDF erzeugen…" HorizontalAlignment="Stretch" Click="OnGenerate"
|
||||||
IsEnabled="{Binding !HasNoTemplates}"/>
|
IsEnabled="{Binding CanGenerate}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ namespace LehrerApp.Desktop.Views.Groups;
|
|||||||
|
|
||||||
public partial class ExamCoverSheetDialog : Window
|
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)
|
private async void OnGenerate(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -21,7 +27,7 @@ public partial class ExamCoverSheetDialog : Window
|
|||||||
});
|
});
|
||||||
if (file is null) return;
|
if (file is null) return;
|
||||||
var path = file.Path.LocalPath;
|
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 });
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,14 +61,17 @@ public sealed class ExamCoverTemplatingTests : IDisposable
|
|||||||
var tall = new DrawingValue(
|
var tall = new DrawingValue(
|
||||||
[new DrawStringEx(0, 290, 12, 170, "LANG", DrawingTextAlignment.AlignLeft, 10)], 300);
|
[new DrawStringEx(0, 290, 12, 170, "LANG", DrawingTextAlignment.AlignLeft, 10)], 300);
|
||||||
|
|
||||||
|
var progress = new RecordingProgress();
|
||||||
|
|
||||||
var result = new QuestTemplateRenderer().RenderBatchToPdf(template,
|
var result = new QuestTemplateRenderer().RenderBatchToPdf(template,
|
||||||
[
|
[
|
||||||
new DictionaryProvider(new Dictionary<string, PlaceholderValue> { ["Tabelle"] = ShortDrawing }),
|
new DictionaryProvider(new Dictionary<string, PlaceholderValue> { ["Tabelle"] = ShortDrawing }),
|
||||||
new DictionaryProvider(new Dictionary<string, PlaceholderValue> { ["Tabelle"] = tall }),
|
new DictionaryProvider(new Dictionary<string, PlaceholderValue> { ["Tabelle"] = tall }),
|
||||||
]);
|
], progress);
|
||||||
|
|
||||||
using var document = UglyToad.PdfPig.PdfDocument.Open(result.Pdf);
|
using var document = UglyToad.PdfPig.PdfDocument.Open(result.Pdf);
|
||||||
Assert.Equal([1, 2], result.PageCounts);
|
Assert.Equal([1, 2], result.PageCounts);
|
||||||
|
Assert.Equal([1, 2], progress.Reported);
|
||||||
Assert.Equal(3, document.NumberOfPages);
|
Assert.Equal(3, document.NumberOfPages);
|
||||||
Assert.Contains("TABELLE", document.GetPage(1).Text);
|
Assert.Contains("TABELLE", document.GetPage(1).Text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,3 +394,11 @@ internal sealed class DictionaryProvider(IReadOnlyDictionary<string, Placeholder
|
|||||||
{
|
{
|
||||||
public IReadOnlyDictionary<string, PlaceholderValue> GetValues() => values;
|
public IReadOnlyDictionary<string, PlaceholderValue> GetValues() => values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Zeichnet Meldungen synchron auf — <see cref="Progress{T}"/> würde sie ohne
|
||||||
|
/// SynchronizationContext erst später auf dem Threadpool zustellen.</summary>
|
||||||
|
internal sealed class RecordingProgress : IProgress<int>
|
||||||
|
{
|
||||||
|
public List<int> Reported { get; } = [];
|
||||||
|
public void Report(int value) => Reported.Add(value);
|
||||||
|
}
|
||||||
@@ -192,7 +192,10 @@ public sealed record BatchRenderResult(byte[] Pdf, IReadOnlyList<int> PageCounts
|
|||||||
public interface ITemplateBatchRenderer
|
public interface ITemplateBatchRenderer
|
||||||
{
|
{
|
||||||
/// <summary>Rendert dieselbe Vorlage je Datensatz und fügt alles zu einem PDF zusammen.</summary>
|
/// <summary>Rendert dieselbe Vorlage je Datensatz und fügt alles zu einem PDF zusammen.</summary>
|
||||||
BatchRenderResult RenderBatchToPdf(LoadedTemplate template, IReadOnlyList<ITemplateDataProvider> data);
|
/// <param name="progress">Bekommt nach jedem gesetzten Datensatz deren bisherige Anzahl. Nach
|
||||||
|
/// dem letzten folgt noch das Zusammenfügen, das sich nicht weiter unterteilen lässt.</param>
|
||||||
|
BatchRenderResult RenderBatchToPdf(LoadedTemplate template, IReadOnlyList<ITemplateDataProvider> data,
|
||||||
|
IProgress<int>? progress = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ITemplatePreviewRenderer
|
public interface ITemplatePreviewRenderer
|
||||||
|
|||||||
@@ -14,17 +14,23 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
|||||||
public byte[] RenderToPdf(LoadedTemplate template, ITemplateDataProvider data) =>
|
public byte[] RenderToPdf(LoadedTemplate template, ITemplateDataProvider data) =>
|
||||||
BuildDocument(template, ValidateData(template, data)).GeneratePdf();
|
BuildDocument(template, ValidateData(template, data)).GeneratePdf();
|
||||||
|
|
||||||
public BatchRenderResult RenderBatchToPdf(LoadedTemplate template, IReadOnlyList<ITemplateDataProvider> data)
|
public BatchRenderResult RenderBatchToPdf(LoadedTemplate template, IReadOnlyList<ITemplateDataProvider> data,
|
||||||
|
IProgress<int>? progress = null)
|
||||||
{
|
{
|
||||||
if (data.Count == 0) throw new ArgumentException("Mindestens ein Datensatz ist erforderlich.", nameof(data));
|
if (data.Count == 0) throw new ArgumentException("Mindestens ein Datensatz ist erforderlich.", nameof(data));
|
||||||
var documents = data.Select(x => BuildDocument(template, ValidateData(template, x))).ToList();
|
var documents = data.Select(x => BuildDocument(template, ValidateData(template, x))).ToList();
|
||||||
// Seitenzahl je Datensatz getrennt ermitteln: das zusammengeführte Dokument verrät nicht
|
// Seitenzahl je Datensatz getrennt ermitteln: das zusammengeführte Dokument verrät nicht
|
||||||
// mehr, welcher Datensatz unerwartet auf eine Folgeseite übergelaufen ist.
|
// mehr, welcher Datensatz unerwartet auf eine Folgeseite übergelaufen ist.
|
||||||
var pageCounts = documents.Select(x => PdfPageCounter.Count(x.GeneratePdf())).ToList();
|
var single = new List<byte[]>(documents.Count);
|
||||||
|
foreach (var document in documents)
|
||||||
|
{
|
||||||
|
single.Add(document.GeneratePdf());
|
||||||
|
progress?.Report(single.Count);
|
||||||
|
}
|
||||||
var pdf = documents.Count == 1
|
var pdf = documents.Count == 1
|
||||||
? documents[0].GeneratePdf()
|
? single[0]
|
||||||
: Document.Merge(documents).UseOriginalPageNumbers().GeneratePdf();
|
: Document.Merge(documents).UseOriginalPageNumbers().GeneratePdf();
|
||||||
return new(pdf, pageCounts);
|
return new(pdf, single.Select(PdfPageCounter.Count).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] RenderFirstPageToPng(LoadedTemplate template, ITemplateDataProvider data, int dpi = 120)
|
public byte[] RenderFirstPageToPng(LoadedTemplate template, ITemplateDataProvider data, int dpi = 120)
|
||||||
|
|||||||
@@ -4876,6 +4876,13 @@ beides vor dem ersten produktiven Zwei-Geräte-Einsatz empfehlenswert nachzuhole
|
|||||||
liefert zusätzlich die Seitenzahl je Datensatz; läuft ein Deckblatt wegen zu vieler Aufgaben
|
liefert zusätzlich die Seitenzahl je Datensatz; läuft ein Deckblatt wegen zu vieler Aufgaben
|
||||||
auf eine zweite Seite, warnt der Dialog.
|
auf eine zweite Seite, warnt der Dialog.
|
||||||
|
|
||||||
|
Nachgezogen (Nutzer-Wunsch): Fortschrittsbalken im Dialog, weil ein Klassensatz spürbar
|
||||||
|
dauert. `GenerateAsync` setzt im Hintergrund (`Task.Run`), `RenderBatchToPdf` meldet über
|
||||||
|
ein optionales `IProgress<int>` jeden gesetzten Datensatz; das abschließende
|
||||||
|
`Document.Merge` meldet nichts, dort läuft der Balken unbestimmt ("Sammel-PDF wird
|
||||||
|
zusammengefügt …"). Währenddessen sind beide Schaltflächen gesperrt und der Dialog lässt
|
||||||
|
sich nicht schließen. Ein einzelnes Deckblatt wird dabei nicht mehr doppelt gesetzt.
|
||||||
|
|
||||||
**TemplateDesigner:** "Als Klausur-Deckblatt einrichten" setzt `report-type=exam-cover` und
|
**TemplateDesigner:** "Als Klausur-Deckblatt einrichten" setzt `report-type=exam-cover` und
|
||||||
legt alle Deckblatt-Platzhalter mit Beispielwerten an; vorhandene Platzhalter bleiben, ein
|
legt alle Deckblatt-Platzhalter mit Beispielwerten an; vorhandene Platzhalter bleiben, ein
|
||||||
abweichender Typ wird gemeldet statt überschrieben. Neuer Sonderinhalt "Punktetabelle
|
abweichender Typ wird gemeldet statt überschrieben. Neuer Sonderinhalt "Punktetabelle
|
||||||
|
|||||||
Reference in new issue
Block a user