Zwei Dinge unterscheiden das Windows-Programm von jeder Webseite, und beide stehen als Bruchstellen in der Liste aus Lektion M1.2. B7: Der Rechnungsdruck zeichnet mit PrintDocument und Graphics.DrawString auf einen Drucker,
den es im Browser nicht gibt. B8: Der Export schreibt eine Datei in einen Ordner auf einem Netzlaufwerk, den ein Server in Azure nicht sieht. Beides sind Naht N6 und N7 aus Lektion M1.4. Beides ist der Moment, in dem Modernisierer gern
etwas „nachbauen“, statt zu fragen, wozu es da war. Frau Aydin will keine PrintDocument-Seite; sie will eine Rechnung, die sie dem Kunden gibt. Die Buchhaltung will kein Netzlaufwerk; sie will jeden Morgen die Datei, die ihr Programm liest.
Diese Lektion ersetzt das Mittel und behält den Zweck.
Die Hintergrundarbeit für lange Aufträge kennen Sie aus K2, Modul 2.8 (Hintergrund und Integration); hier steht, was sie beim Auftragsbuch besonders macht.
Drucken: Browser oder PDF vom Server
Die Entscheidung kommt zuerst, weil sie alles Weitere bestimmt. Es gibt drei Wege zu einer Rechnung auf Papier oder als Datei:
| Weg | Wer erzeugt das Dokument | Wofür er sich eignet | Was er kostet |
|---|---|---|---|
| Die Seite druckt der Browser (Druckformat der Seite) | der Browser auf dem Rechner der Benutzerin | die Rechnung ansehen, drucken, als PDF speichern lassen | die Seite sieht auf jedem Rechner leicht anders aus; kein Dokument auf dem Server |
| PDF auf dem Server | der Server, mit einer Bibliothek oder einem Dienst | die Rechnung per Mail an den Kunden, Ablage im Archiv, Aufbewahrung | eine Abhängigkeit mehr, Schriftarten und Seitenumbruch sind Ihre Aufgabe |
Weiter PrintDocument im Windows-Programm |
das Windows-Programm | nichts Neues; es bleibt, solange das Programm bleibt | bleibt an Windows gebunden und stirbt mit dem Programm |
Für das Auftragsbuch gilt: Die Anzeige und der Druck laufen über die Seite, das ist der Weg dieser Lektion. Für eine Rechnung als Datei, die der Kunde per Mail bekommt oder die in ein Archiv muss, braucht es die zweite Zeile, und die Wahl der Bibliothek ist eine Entscheidung für ein ADR (Lektion M1.1), mit den Fragen, die die Tabelle nennt: Lizenz, Schriftarten, Pflege, Datenschutz, wenn ein Dienst den Inhalt sieht. Microsofts Dokumentation, die wir für diesen Kurs gelesen haben, nennt dafür keine Bibliothek, deshalb steht hier auch keine. Wichtig ist, dass die Daten der Rechnung aus der API kommen, wie immer sie später gedruckt werden: Dann ändert sich bei einem Wechsel des Verfahrens nur der Teil, der zeichnet.
Der Vertrag um die Rechnung erweitern
Alles, was Rechnungsdruck bisher zeichnete, liefert jetzt die API: Rechnungsnummer, Kundenanschrift, Titel, die Positionen aus dem Rechner und den Mehrwertsteuersatz. Das ist eine verträgliche Änderung nach der Tabelle aus Lektion M4.1: ein neuer Pfad,
GET /api/v1/auftraege/{id}/rechnung, und ein neuer Typ im Vertrag. Der Pfadtest wird rot, bis Sie den Pfad eintragen: Das ist gewollt.
// Alles, was auf einer Rechnung steht. Der Server stellt es zusammen; Seite und Windows-Druck zeigen es nur an.
public sealed record RechnungDto(
string Rechnungsnummer,
string Kunde,
string Strasse,
string Plz,
string Ort,
string Titel,
Rechnungsbetrag Betrag,
decimal Mehrwertsteuersatz);
Der Endpunkt gibt es nur für abgerechnete Aufträge: Eine Rechnung ohne Nummer wäre ein Entwurf, den es nach Lektion M4.1 nicht gibt, und die API sagt es mit 409.
gruppe.MapGet("/{id:int}/rechnung", RechnungLesen)
.WithName("RechnungLesen")
.WithSummary("Alles, was auf der Rechnung steht; nur für abgerechnete Aufträge.")
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status409Conflict);
private static async Task<Results<Ok<RechnungDto>, ProblemHttpResult>> RechnungLesen(
int id,
AuftragsbuchContext db,
IOptions<PreiseOptions> preise,
CancellationToken ct)
{
Auftrag? auftrag = await db.Auftraege.AsNoTracking().Include(a => a.Kunde).SingleOrDefaultAsync(a => a.AuftragId == id, ct);
if (auftrag is null)
{
return NichtGefunden(id);
}
if (auftrag.Status != AuftragsStatus.Abgerechnet || auftrag.Rechnungsnummer is null)
{
return TypedResults.Problem(title: "Der Auftrag ist noch nicht abgerechnet.", statusCode: StatusCodes.Status409Conflict);
}
PreiseOptions p = preise.Value;
return TypedResults.Ok(new RechnungDto(
auftrag.Rechnungsnummer,
auftrag.Kunde.Name,
auftrag.Kunde.Strasse,
auftrag.Kunde.Plz,
auftrag.Kunde.Ort,
auftrag.Titel,
p.Rechner().Berechnen(new Leistung(auftrag.Art, auftrag.Stunden, auftrag.Material)),
p.Mwst));
}
Der Client bekommt eine Methode dazu (RechnungLesenAsync in IAuftragsApi und in AuftragsApiClient, dieselbe Form wie die anderen), und die Attrappe in den Komponententests aus Lektion M4.3 folgt:
Task<ApiErgebnis<RechnungDto>> RechnungLesenAsync(int auftragId, CancellationToken ct = default);
public Task<ApiErgebnis<RechnungDto>> RechnungLesenAsync(int auftragId, CancellationToken ct = default) =>
SendenAsync<RechnungDto>(() => http.GetAsync($"api/v1/auftraege/{auftragId}/rechnung", ct), ct);
Die Seite
Die Rechnung ist eine Komponente der Bibliothek, wie die Liste; sie läuft damit im Browser und, über die Brücke aus Lektion M4.4, im Windows-Programm:
@inject IAuftragsApi Api
@inject IJSRuntime Js
<div class="rechnung-ansicht">
@if (_laedt)
{
<p role="status">Lade ...</p>
}
else if (_rechnung is null)
{
<p role="alert" class="fehler">@(_fehler is null ? "Die Rechnung konnte nicht geladen werden." : ApiMeldungen.Fuer(_fehler))</p>
}
else
{
<p class="nicht-drucken"><button type="button" @onclick="DruckenAsync">Drucken</button></p>
<article class="rechnung">
<h1>Rechnung @_rechnung.Rechnungsnummer</h1>
<p>
@_rechnung.Kunde<br />
@_rechnung.Strasse<br />
@_rechnung.Plz @_rechnung.Ort
</p>
<p>@_rechnung.Titel</p>
<table>
<caption>Beträge in Euro</caption>
<tbody>
<tr><th scope="row">Arbeit</th><td class="betrag">@Betrag(_rechnung.Betrag.Arbeit)</td></tr>
<tr><th scope="row">Anfahrt</th><td class="betrag">@Betrag(_rechnung.Betrag.Anfahrt)</td></tr>
<tr><th scope="row">Material</th><td class="betrag">@Betrag(_rechnung.Betrag.Material)</td></tr>
<tr><th scope="row">Netto</th><td class="betrag">@Betrag(_rechnung.Betrag.Netto)</td></tr>
<tr><th scope="row">Brutto (@((_rechnung.Mehrwertsteuersatz * 100).ToString("0.##")) % Mehrwertsteuer)</th><td class="betrag">@Betrag(_rechnung.Betrag.Brutto)</td></tr>
</tbody>
</table>
</article>
}
</div>
<style>
.rechnung .betrag { text-align: right; }
@@media print {
.nicht-drucken { display: none; }
}
</style>
@code {
[Parameter, EditorRequired]
public int AuftragId { get; set; }
private RechnungDto? _rechnung;
private ApiFehler? _fehler;
private bool _laedt = true;
protected override async Task OnParametersSetAsync()
{
_laedt = true;
ApiErgebnis<RechnungDto> ergebnis = await Api.RechnungLesenAsync(AuftragId);
_laedt = false;
(_rechnung, _fehler) = (ergebnis.IstOk ? ergebnis.Wert : null, ergebnis.Fehler);
}
// window.print gibt es in jedem Browser und in jeder WebView; der Druckdialog gehört dem Gastgeber.
private async Task DruckenAsync() => await Js.InvokeVoidAsync("window.print");
private static string Betrag(decimal betrag) => betrag.ToString("N2", CultureInfo.GetCultureInfo("de-DE"));
}
Drei Entscheidungen:
- Das Druckformat steht im Stil der Seite. Der Block
@media printblendet alles aus, was nicht auf das Papier gehört (hier die Schaltfläche). In Razor-Dateien wird das@einer Stilregel verdoppelt (@@media), weil@sonst Razor-Code einleitet. Das Format gehört zur Seite, nicht zum Drucker: Dieselbe Seite wird im Browser gedruckt oder als PDF gespeichert. - Der Druck ist ein Aufruf von
window.print. Die Komponente benutztIJSRuntime, die gemeinsame Abstraktion, die nach Microsofts Dokumentation in allen Hosting-Modellen und auf allen Plattformen verfügbar ist. Der Druckdialog gehört dem Gastgeber: Browser oder WebView. - Die Beträge stehen deutsch und ohne Währungszeichen in der Zelle. Die Überschrift der Tabelle sagt „Beträge in Euro“. Die Zahlen sind genau die der API; die Seite rechnet nichts.
Die Web-App bekommt dazu eine Seite, die nur die Komponente zeigt:
@page "/auftraege/{AuftragId:int}/rechnung"
@rendermode InteractiveServer
<PageTitle>Rechnung zu Auftrag @AuftragId</PageTitle>
<RechnungAnsicht AuftragId="AuftragId" />
@code {
[Parameter]
public int AuftragId { get; set; }
}