Two things set the Windows program apart from any web page, and both appear as break points in the list from lesson M1.2. B7: the invoice printing draws onto a printer with PrintDocument and Graphics.DrawString, and there is no such printer in the browser. B8: the export writes a file into a folder on a network drive that a server in Azure cannot see. Both are seams N6 and N7 from lesson M1.4. Both are the moment when modernizers like to "rebuild" something instead of asking what it was there for. Ms. Aydin does not want a PrintDocument page; she wants an invoice she can hand to the customer. Accounting does not want a network drive; it wants, every morning, the file its program reads. This lesson replaces the means and keeps the purpose.

You know background work for long jobs from K2, module 2.8 (Background and integration); this lesson covers what makes it special in the Order Book.

Printing: browser or PDF from the server

The decision comes first because it determines everything else. There are three ways to an invoice on paper or as a file:

Way Who produces the document What it is suited for What it costs
The browser prints the page (the page's print layout) the browser on the user's machine viewing the invoice, printing it, having it saved as PDF the page looks slightly different on every machine; no document on the server
PDF on the server the server, with a library or a service the invoice by email to the customer, filing in the archive, retention one more dependency, fonts and page breaks are your job
Keep using PrintDocument in the Windows program the Windows program nothing new; it stays as long as the program stays stays tied to Windows and dies with the program

For the Order Book: the display and the printing go through the page, and that is the way of this lesson. For an invoice as a file that the customer receives by email or that has to go into an archive, you need the second row, and the choice of library is a decision for an ADR (lesson M1.1), with the questions the table names: license, fonts, maintenance, data protection if a service sees the content. The Microsoft documentation we read for this course names no library for this, so none is named here either. What matters is that the invoice's data comes from the API, however it is printed later: then, if you change the method, only the part that draws changes.

Extending the contract with the invoice

Everything InvoicePrint used to draw is now delivered by the API: invoice number, customer address, title, the line items from the calculator, and the VAT rate. This is a compatible change according to the table from lesson M4.1: a new path, GET /api/v1/orders/{id}/invoice, and a new type in the contract. The path test turns red until you enter the path: that is intended.

C#
// Everything that is on an invoice. The server assembles it; the page and the Windows printing only display it.
public sealed record InvoiceDto(
    string InvoiceNumber,
    string Customer,
    string Street,
    string PostalCode,
    string City,
    string Title,
    InvoiceAmount Amount,
    decimal VatRate);

The endpoint exists only for invoiced orders: an invoice without a number would be a draft, which does not exist according to lesson M4.1, and the API says so with 409.

C#
group.MapGet("/{id:int}/invoice", ReadInvoice)
    .WithName("ReadInvoice")
    .WithSummary("Everything that is on the invoice; only for invoiced orders.")
    .ProducesProblem(StatusCodes.Status404NotFound)
    .ProducesProblem(StatusCodes.Status409Conflict);
C#
private static async Task<Results<Ok<InvoiceDto>, ProblemHttpResult>> ReadInvoice(
    int id,
    OrderBookContext db,
    IOptions<PricesOptions> prices,
    CancellationToken ct)
{
    Order? order = await db.Orders.AsNoTracking().Include(o => o.Customer).SingleOrDefaultAsync(o => o.OrderId == id, ct);
    if (order is null)
    {
        return NotFound(id);
    }

    if (order.Status != OrderStatus.Invoiced || order.InvoiceNumber is null)
    {
        return TypedResults.Problem(title: "The order has not been invoiced yet.", statusCode: StatusCodes.Status409Conflict);
    }

    PricesOptions p = prices.Value;
    return TypedResults.Ok(new InvoiceDto(
        order.InvoiceNumber,
        order.Customer.Name,
        order.Customer.Street,
        order.Customer.PostalCode,
        order.Customer.City,
        order.Title,
        p.Calculator().Calculate(new Service(order.Kind, order.Hours, order.Material)),
        p.Vat));
}

The client gets one more method (ReadInvoiceAsync in IOrderApi and in OrderApiClient, the same form as the others), and the fake in the component tests from lesson M4.3 follows:

C#
Task<ApiResult<InvoiceDto>> ReadInvoiceAsync(int orderId, CancellationToken ct = default);
C#
public Task<ApiResult<InvoiceDto>> ReadInvoiceAsync(int orderId, CancellationToken ct = default) =>
    SendAsync<InvoiceDto>(() => http.GetAsync($"api/v1/orders/{orderId}/invoice", ct), ct);

The page

The invoice is a component of the library, like the list; so it runs in the browser and, through the bridge from lesson M4.4, in the Windows program:

Razor
@inject IOrderApi Api
@inject IJSRuntime Js

<div class="invoice-view">
    @if (_loading)
    {
        <p role="status">Loading ...</p>
    }
    else if (_invoice is null)
    {
        <p role="alert" class="error">@(_error is null ? "The invoice could not be loaded." : ApiMessages.For(_error))</p>
    }
    else
    {
        <p class="no-print"><button type="button" @onclick="PrintAsync">Print</button></p>

        <article class="invoice">
            <h1>Invoice @_invoice.InvoiceNumber</h1>
            <p>
                @_invoice.Customer<br />
                @_invoice.Street<br />
                @_invoice.PostalCode @_invoice.City
            </p>
            <p>@_invoice.Title</p>
            <table>
                <caption>Amounts in euros</caption>
                <tbody>
                    <tr><th scope="row">Labor</th><td class="amount">@Money(_invoice.Amount.Labor)</td></tr>
                    <tr><th scope="row">Call-out</th><td class="amount">@Money(_invoice.Amount.CallOut)</td></tr>
                    <tr><th scope="row">Material</th><td class="amount">@Money(_invoice.Amount.Material)</td></tr>
                    <tr><th scope="row">Net</th><td class="amount">@Money(_invoice.Amount.Net)</td></tr>
                    <tr><th scope="row">Gross (@((_invoice.VatRate * 100).ToString("0.##"))% VAT)</th><td class="amount">@Money(_invoice.Amount.Gross)</td></tr>
                </tbody>
            </table>
        </article>
    }
</div>

<style>
    .invoice .amount { text-align: right; }

    @@media print {
        .no-print { display: none; }
    }
</style>

@code {
    [Parameter, EditorRequired]
    public int OrderId { get; set; }

    private InvoiceDto? _invoice;
    private ApiError? _error;
    private bool _loading = true;

    protected override async Task OnParametersSetAsync()
    {
        _loading = true;
        ApiResult<InvoiceDto> result = await Api.ReadInvoiceAsync(OrderId);
        _loading = false;
        (_invoice, _error) = (result.IsOk ? result.Value : null, result.Error);
    }

    // window.print exists in every browser and in every WebView; the print dialog belongs to the host.
    private async Task PrintAsync() => await Js.InvokeVoidAsync("window.print");

    private static string Money(decimal amount) => amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE"));
}

Three decisions:

  • The print layout is in the page's style. The @media print block hides everything that does not belong on the paper (here the button). In Razor files the @ of a style rule is doubled (@@media) because @ otherwise starts Razor code. The layout belongs to the page, not to the printer: the same page is printed in the browser or saved as PDF.
  • Printing is a call to window.print. The component uses IJSRuntime, the shared abstraction that, according to Microsoft's documentation, is available in all hosting models and on all platforms. The print dialog belongs to the host: browser or WebView.
  • The amounts are in German format and without a currency symbol in the cell. The table's caption says "Amounts in euros". The numbers are exactly those of the API; the page calculates nothing.

The web app gets a page for this that shows only the component:

Razor
@page "/orders/{OrderId:int}/invoice"
@rendermode InteractiveServer

<PageTitle>Invoice for order @OrderId</PageTitle>

<InvoiceView OrderId="OrderId" />

@code {
    [Parameter]
    public int OrderId { get; set; }
}