Part 1: Printing Invoice, Bill, Ticket, Reports in Vanilla ASP.NET Web Forms (Static Layout)

This is part of the series of introducing Vanilla ASP.NET Web Forms Architecture. Read more about this at the main menu: Main Article: [Introducing Vanilla ASP.NET Web Forms Architecture]

Part 1: Printing Static Layout Content (Tickets, PVC ID Card, etc…)

Summarized Introduction & Walkthrough

The basic steps:

First, programmatically generate the HTML document.

Then, for print preview, load the HTML document in an iframe.

For silent printing, load into an invisible iframe, insert the window.print() JavaScript function into the HTML body to initiate an auto-print.

The Auto-Print JavaScript:

// wait for all resource to fully loaded, includes images
window.onload = () => { window.print(); };

How about including JavaScript?

In the “ready-to-print” HTML document, do not include any JavaScript, as there is nothing to interact with aside from just serving it for printing. Print engine will not entertain JavaScript, therefore render the full static HTML for maximum printing rendering efficiency.

Visual Design – Static Layout (Ticket, Queue Number, Card etc…)

Example: Ticket, Queue Number, Card, etc…

Works on: Receipt Printer, PVC ID Card Printer (industrial practice tested)

*Note: The PVC Card Printer will render the print just fine. Printable directly from web browser (or web browser control, webview desktop/mobile app component).

Imagine we have a cinema ticket that will be printed on a paper (width: 150mm), which will look something like this:

Sample Ticket Layout

URL API Routing

Preparing the URL API Routing:

// Example of URL API format:

// Query String
// https://myweb.com/apiTicket?purchase_id=123&autoprint=1

// MVC alike URL
// https://myweb.com/apiTicket/123/1

// MVC alike URL Routing

using System.Web.Routing;

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // route the main api page
        RouteTable.Routes.MapPageRoute("api1", "apiTicket", "~/apiTicket.aspx");

        // route all other URL patterns

        // default auto-print = true
        RouteTable.Routes.MapPageRoute("api2", "apiTicket/{purchase_id}", "~/apiTicket.aspx");
        RouteTable.Routes.MapPageRoute("api3", "apiTicket/{purchase_id}/", "~/apiTicket.aspx");

        // manuall define auto-print
        RouteTable.Routes.MapPageRoute("api4", "apiTicket/{purchase_id}/{autoprint}", "~/apiTicket.aspx");
        RouteTable.Routes.MapPageRoute("api5", "apiTicket/{purchase_id}/{autoprint}/", "~/apiTicket.aspx");
    }
}

Ticket Generator Engine

Prepare the C# Class Print Info object (Gather the info that required to be printed only):

public class Ticket
{
    public int Id { get; set; }
    public string TicketNo { get; set; }
    public string MovieName { get; set; }
    public DateTime MovieDateTime { get; set; }
    public string HallName { get; set; }
    public string SeatType { get; set; }
    public string RowNo { get; set; }
    public string SeatNo { get; set; }
    public double Price { get; set; }
}

C#, The Ticket Generator Engine:

public class engineTicket
{
    public static string GenerateTicket(int purchase_id, int autoprint)
    {
        // get the tickets (card, queue number, etc...)
        List<Ticket> lstTicket = Database.GetTickets(purchase_id);

        // invalid purchase_id
        if (lstTicket == null)
        {
            return null;
        }

        // no ticket
        if (lstTicket.Count == 0)
        {
            string errorHtml = ReportNoTicket();
            return errorHtml;
        }

        StringBuilder sb = new StringBuilder();

        // render the header
        sb.Append(@"
<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Cinema Ticket</title>
    <style>

        /* some very beautiful CSS */
        /* this css will be attached at the end of this article */

    </style>
</head>
<body>
        ");

        // perform auto-print
        if (autoprint == 1)
        {
            sb.Append("<script>window.onload = ()=> { window.print(); };</script>");
        }

        bool isFirst = true;

        // render the ticket
        foreach (var t in lstTicket)
        {
            // check for paper cut
            if (isFirst) 
            {
                // set the flag to perform next page
                isFirst = false;
            }
            else
            {
                // using OS Print Spooler Paper Cut Signal
                // which universally understood by Printer Driver
                // next page, form feed
                sb.Append("<div style='page-break-after: always;'></div>");
            }

            // render the content of ticket, card, quene no receipt
            sb.Append($@"
    <div class='ticket'>
        <div class='main-section'>
            <div class='cinema-header'>
                <div class='cinema-name'>STARLIGHT CINEMA</div>
            </div>

            <div class='movie-info'>
                <div class='movie-title'>{t.MovieName}</div>

                <div class='ticket-details'>
                    <div class='detail-item'>
                        <span class='detail-label'>Date</span>
                        <span class='detail-value'>{t.MovieDateTime:MMM dd, yyyy}</span>
                    </div>
                    <div class='detail-item'>
                        <span class='detail-label'>Time</span>
                        <span class='detail-value'>{t.MovieDateTime:hh:mm tt}</span>
                    </div>
                    <div class='detail-item'>
                        <span class='detail-label'>Hall</span>
                        <span class='detail-value'>{t.HallName}</span>
                    </div>
                    <div class='detail-item'>
                        <span class='detail-label'>Type</span>
                        <span class='detail-value'>{t.SeatType}</span>
                    </div>
                </div>
            </div>

            <div class='barcode-section'>
                <div class='barcode'>*{t.TicketNo}*</div>
                <div class='ticket-number'>{t.TicketNo}</div>
            </div>
        </div>

        <div class='stub-section'>
            <div class='stub-header'>ADMIT ONE</div>

            <div class='seat-info'>
                <div class='seat-number'>{t.SeatNo}</div>
                <div class='row-number'>{t.RowNo}</div>
            </div>

            <div class='price'>${t.Price:#,##0.00}</div>
        </div>
    </div>
            ");
        }

        // render the footer
        sb.Append("</body></html>");

        return sb.ToString();
    }

    // No ticket
    static string ReportNoTicket()
    {
        // sending the message to the iframe's parent window
        string errorHtml = @"
<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>No Tickets Found</title>
</head>
<body>
    <script>
        window.onload = function() {
            alert('No ticket found');
            // Try to communicate back to parent window if in iframe
            if (window.parent && window.parent !== window) {
                window.parent.postMessage('no-tickets-found', '*');
            }
        };
    </script>
    <div style='display: none;'>No tickets found</div>
</body>
</html>";

        return errorHtml;
    }
}

Notice that the barcode is encoded in this way, there are a beginning and ending asterisk symbol (*).

<div class='barcode'>*{t.TicketNo}*</div>

This presents a proper encoded barcode 39 format. In this article, it uses the font “Libre Barcode 39“, or you can also use the free 39 code font from barcodesinc.com.

Backend API Page

The Backend API page, create a blank ASP.NET Web Forms:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiTicker.aspx.cs" Inherits="myweb.apiTicker" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
        </div>
    </form>
</body>
</html>

Delete all the frontend markup and leave only the first line page directive:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiTicker.aspx.cs" Inherits="myweb.apiTicker" %>

The code behind:

protected void Page_Load(object sender, EventArgs e)
{
    // user authentication verification
    if (Session["login_user"] == null) 
    {
        // Option 1: 404 Page not found
        Response.StatusCode = 404;
        return;

        // Option 2: 401 Unauthorized
        Response.StatusCode = 401;
        return;
    }

    int purchase_id = 0;

    // auto-print by default
    int auto_print = 1;

    // ===========================
    // Obtain Request Data
    // ===========================

    // Method 1: Query String & Form Post
    // check ticket validation
    // get from query string or form post
    if (!int.TryParse(Request["purchase_id"], out purchase_id) || purchase_id <= 0)
    {
        // 400 Bad Request
        Response.StatusCode = 400;
        return;
    }

    if (Request["autoprint"] != null) 
    {
        int.TryParse(Request["autoprint"] + "", out auto_print);
    }

    // Method 2: JSON body request
    string jsonBody;
    using (var reader = new StreamReader(Request.InputStream, Encoding.UTF8))
    {
        jsonBody = reader.ReadToEnd();
    }
    var ticketRequest = JsonSerializer.Deserialize<TicketRequest>(jsonBody)
    purchase_id = ticketRequest.PurchaseId;
    auto_print = ticketRequest.AutoPrint;

    // ==================================
    // Generate the Ticket HTML document
    // ==================================
        
    var html = engineTicket.GenerateTicket(purchase_id, auto_print);

    if (html == null)
    {
        // 400 Bad Request
        Response.StatusCode = 400;
        return;
    }

    // write the HTML page output to the client's browser, webview control
    Response.Write(html);
}

Frontend Print Request

Auto-Print, No Preview Needed

<button type="button" onclick="printTicket();">Print Tickets</button>

<!-- hidden iframe -->
<iframe id="frameprint" src="" style="height:0; width:0; border:none; overflow:hidden;"></iframe>

<script>

    // Global state
    const urlApi = "/apiTicket";
    let auto_print = 1;
    let purchase_id = 123;
    let frameprint = document.querySelector("#frameprint");

    function printTicket() {

        let url = `/apiTicket?purchase_id=${purchase_id}&autoprint=${auto_print}`;
        frameprint.src = url;
    }

    // Listening any message from the iframe
    document.addEventListener('DOMContentLoaded', function() {
        window.addEventListener('message', handleIframeMessages);
    });
    
    function handleIframeMessages(event) {
        // Verify origin for security
        if (event.origin !== window.location.origin) return;
        
        switch(event.data) {
            case 'no-tickets-found':
                showNotification('No tickets found for this purchase ID', 'error');
                enableFormControls(true);
                document.querySelector("#frameprint").src = '';
                break;
                
            default:
                console.log('Unknown message from iframe:', event.data);
        }
    }

</script>

Print Preview, No Auto Print

Show the print preview in iframe:

<style>
    .div-print-preview {
        position: fixed;
        top: 20px;
        left: 20px;
        right: 20px;
        bottom: 20px;
        border: 2px solid gray;
        padding: 10px;
        display: none;
        z-index: 999999;
        background: white;
    }

    .div-print-preview-button {
        background: #35b1d4;
        color: white;
        display: inline-block;
        padding: 10px 20px;
        border: none;
        cursor: pointer;
    }

    .div-print-preview-iframe {
        position: absolute;
        top: 60px;
        left: 10px;
        right: 10px;
        bottom: 10px;
        width: auto;
        height: auto;
        border: 1px solid gray;
    }
</style>

<div id="divPrintPreview" class="div-print-preview">

    <button type="button" class="div-print-preview-button"
        onclick="printTicket();">Print</button>

    <button type="button" class="div-print-preview-button"
        onclick="closePrintPreview();">Close</button>

    <div class="div-print-preview-iframe"></div>
</div>

<script>

    // Global state
    const urlApi = "/apiTicket";
    let auto_print = 0;     // don't auto print
    let purchase_id = 123;
    let divPrintPreview = document.querySelector("#divPrintPreview");
    let frameprint = document.querySelector("#frameprint");

    function printTicket() {
        let url = `/apiTicket?purchase_id=${purchase_id}&autoprint=${auto_print}`;
        frameprint.src = url;
        divPrintPreview.style.display = "block"; // display the preview container
    }

    function closePrintPreview() {
        divPrintPreview.style.display = "none"; // hide the preview container
        frameprint.src = ""; // clear the preview
    }

</script>

The CSS for The Ticket

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: #f5f5f5;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 0;
}

.ticket {
    width: 150mm;
    height: 70mm;
    background: white;
    border: 2px solid #2c2c2c;
    border-radius: 10px;
    display: flex;
    overflow: hidden;
    box-shadow: 0 10px 30px rgba(0,0,0,0.1);
    position: relative;
    transition: transform 0.3s ease;
}

.ticket:hover {
    transform: translateY(-3px);
    box-shadow: 0 15px 40px rgba(0,0,0,0.15);
}

.ticket::before {
    content: '';
    position: absolute;
    top: 0;
    left: 75%;
    width: 1px;
    height: 100%;
    background: repeating-linear-gradient(
        to bottom,
        transparent,
        transparent 5px,
        #888 5px,
        #888 8px
    );
}

.main-section {
    width: 75%;
    padding: 20px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    color: #2c2c2c;
    background: white;
}

.cinema-header {
    border-bottom: 2px solid #e0e0e0;
    padding-bottom: 10px;
    margin-bottom: 15px;
}

.cinema-name {
    font-size: 24px;
    font-weight: 900;
    letter-spacing: 2px;
    color: #1a1a1a;
    text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}

.movie-title {
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 10px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #2c2c2c;
}

.ticket-details {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    font-size: 12px;
}

.detail-item {
    display: flex;
    flex-direction: column;
}

.detail-label {
    font-size: 10px;
    color: #888;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 2px;
}

.detail-value {
    font-weight: bold;
    font-size: 14px;
    color: #2c2c2c;
}

.barcode-section {
    margin-top: 15px;
    padding-top: 10px;
    border-top: 1px solid #e0e0e0;
    display: flex;
    align-items: center;
    gap: 10px;
}

.barcode {
    flex: 1;
    height: 30px;
    font-family: "Libre Barcode 39", system-ui;
    font-size: 50px;
    line-height: 30px;
    color: #2c2c2c;
    border: 1px solid #e0e0e0;
    border-radius: 2px;
    text-align: center;
    background: white;
}

.ticket-number {
    font-size: 10px;
    color: #666;
    letter-spacing: 1px;
    font-family: 'Courier New', monospace;
}

.stub-section {
    width: 25%;
    background: #f8f8f8;
    border-left: 1px dashed #888;
    padding: 20px 15px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    color: #2c2c2c;
    position: relative;
}

.stub-section::before {
    content: '';
    position: absolute;
    left: -1px;
    top: -2px;
    bottom: -2px;
    width: 1px;
    background: white;
    z-index: 1;
}

.stub-header {
    writing-mode: vertical-rl;
    text-orientation: mixed;
    font-size: 10px;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 2px;
    font-weight: 600;
}

.seat-info {
    text-align: center;
    padding: 10px;
    background: white;
    border: 2px solid #2c2c2c;
    border-radius: 8px;
    min-width: 60px;
}

.seat-number {
    font-size: 24px;
    font-weight: 900;
    color: #2c2c2c;
    margin-bottom: 5px;
}

.row-number {
    font-size: 11px;
    color: #666;
    text-transform: uppercase;
}

.price {
    font-size: 18px;
    font-weight: bold;
    padding: 8px 12px;
    background: #2c2c2c;
    color: white;
    border-radius: 20px;
}

/* Decorative corner marks */
.ticket::after {
    content: '✂';
    position: absolute;
    left: calc(75% - 10px);
    top: 10px;
    color: #888;
    font-size: 12px;
    transform: rotate(-90deg);
}

/* Optional pattern overlay for texture */
.main-section::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 25%;
    bottom: 0;
    background-image: 
        repeating-linear-gradient(
            45deg,
            transparent,
            transparent 10px,
            rgba(0,0,0,0.01) 10px,
            rgba(0,0,0,0.01) 20px
        );
    pointer-events: none;
}

@media print {
    body {
        background: white;
    }

    .ticket {
        box-shadow: none;
        border: 1px solid #2c2c2c;
    }
}

/* Animation for ticket entry */
@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateX(-30px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

.ticket {
    animation: slideIn 0.5s ease-out;
}

/* Define the printed page size and margins specifically for tickets */
@page {
    /* Might not require if the printer already has default paper settings */
    /* Set page size to match ticket dimensions */
    size: 150mm 70mm;

    /* This is important */    
    /* Remove top and bottom printing URL and timestamp */
    margin: 0;
    
    /* (Optional) */
    /* Remove default browser print headers/footers */
    @top-center { content: none; }
    @bottom-center { content: none; }
}

Feature image credit: Photo by Amy-Leigh Barnard on Unsplash