top of page
  • Black LinkedIn Icon
  • YouTube
  • Gateway Scripts
  • Whatsapp

Conquering WPF Reporting in ESAPI Applications

Introduction

When developing applications or scripts with the Eclipse Scripting API (ESAPI) in Varian's Eclipse environment, one of the most frequent requirements is to generate clear, concise, and professional-looking reports. Whether you need to produce summaries of patient plans, beam configurations, or treatment verification documents, having a reliable way to print or export these reports is crucial.


The WPF-compatible printing approaches covered in this post will assist in pinpointing the ideal printing strategy for your workflow, whether printing simple on-page summaries or navigating highly detailed, data-rich reports.

In Windows Presentation Foundation (WPF) context, there are multiple ways to tackle the problem of creating print-ready documents. Some developers rely on the native WPF FlowDocument for on-screen and physical printing, others prefer direct PDF generation using a library like PDFsharp, and still others opt for HTML-to-PDF workflow powered by XML and XSL transformations.


This blog post will explore these approaches in detail. Using an existing open-source application DoseMetricExample available on Github, we will create a printing service utilizing all three styles of print examples, highlighting the benefits and drawbacks to each. The code for this project is specifically on the print branch to allow readers to follow the progression through prior blog posts. The WPF-compatible printing approaches covered in this post will assist in pinpointing the ideal printing strategy for your workflow, whether printing simple on-page summaries or navigating highly detailed, data-rich reports.


FlowDocument Printing


A FlowDocument in WPF is a document-centric class that is highly flexible for displaying data in a flow-based layout. You can style it extensively, embed controls, and dynamically generate content based on your application's data. Because WPF natively supports printing FlowDocuments, it's straightforward approach for printing simple or moderately complex reports directly from the application.


One of your biggest challenges to using a FlowDocument, will be to develop the application in a way that would look appropriate printed to the PDF. If the application has many user control elements, this may not be the correct print option for your application. If the application is built to review data, and is meant to look similar when printing, adding the view component of the application to the FlowDocument is certainly an easy implementation.


How it Works

  1. Create a FlowDocument: Generate a FlowDocument in code or load it from XAML.

  2. Populate the Document with Data: In the script, gather relevant view components that should be displayed. Alternatively, create new Blocks from building tables, text blocks, or other elements.

  3. Use the DocumentPaginator: Convert the FlowDocument to a paginator and send it to the PrintDialog for printing or saving the XPS.


Below is the current version of the OnPrint method using the FlowDocument.

private void OnPrint(object parameter)
{
    //Create and configure the FlowDocument
    FlowDocument fd = new FlowDocument()
    {
        FontSize = 10,
        FontFamily = new FontFamily("Arial"),
    };
    ////Set styles to be more printer friendly.
    fd.Resources["TextBrush"] = Brushes.Black;
  
    //add custom controls to user interfaces.
    fd.Blocks.Add(new Paragraph(new Run { Text = "Plan Dosimetry Report", FontWeight = FontWeights.Bold }));
    fd.Blocks.Add(new BlockUIContainer(new DoseParametersView { DataContext = DoseParametersViewModel }));
    fd.Blocks.Add(new BlockUIContainer(new DoseMetricView { DataContext = DoseMetricViewModel }));
    //Export DVH plot as bitmap and add to a FlowDocument.
    Section dvhPage = new Section();//
    //dvhPage.BreakPageBefore = true;
    BitmapSource bmp = new PngExporter().ExportToBitmap(DVHViewModel.DVHPlotModel);
    dvhPage.Blocks.Add(new BlockUIContainer(new System.Windows.Controls.Image { Source = bmp, Height = 950, Width = 700 }));
    fd.Blocks.Add(dvhPage);
  
    //Set up a PrintDialog and document parameters
    PrintDialog printer = new PrintDialog();
    fd.PageHeight = 1056; //typically 11 inches at 96 dpi.
    fd.PageWidth = 816; // typically 8.5 inches at 96 dpi
    fd.PagePadding = new Thickness(50);
    fd.ColumnGap = 0;
    fd.ColumnWidth = 816;
    IDocumentPaginatorSource dps = fd;
    //Prompt user to select Printer and print.
    if (printer.ShowDialog() == true)
    {
        printer.PrintDocument(dps.DocumentPaginator, "Sample Report");
    }
}

FlowDocument Pros:

  • Built-in WPF Support: No external libraries required.

  • Flow-based layout: Great for multi-page textual documents.

  • Easy print preview and direct printing: The PrintDialog class integrates naturally with WPF.


FlowDocument Cons:

  • Limited PDF export options: By default, FlowDocument to PrintDialog supports XPS or physical printers, but not PDF export (unless you have a PDF printer driver installed).

    • Additionally it has been noticed that some PDF printers do worse with FlowDocuments, and may require, in the case of Adobe PDF printers, for the user to remove change advanced settings within the printer for characters to be read correctly.

  • Complex layout: for highly custom layouts or forms, you may need more robust layout handling.


PDFSharp Printing


Overview

PDFSharp is a popular open-source library for creating and manipulating PDF documents in .NET. If you need direct PDF generation (without going through an XPS intermediary or virtual printer), PDFsharp is the straightforward solution.


How it Works

  1. Reference PDFsharp: add the PDFsharp NuGet package to your project.

  2. Create a PDF Document: Programmatically create a new PdfDocument object.

  3. Add Pages and Draw Content: Use PDFsharp's drawing API (xGraphics) to place text, images, shapes, etc. on each page.

  4. Save as PDF: After constructing the document, you can save it to disk or memory.


The code snippet below demonstrates the essentials of manually laying out text and shapes using PDFsharp. For straightforward, single-page summaries, it may be enough, but

private void OnPrintSharp(object obj)
{
    // Create a new PDF document
    PdfDocument pdfDocument = new PdfDocument();
    pdfDocument.Info.Title = "Plan Dosimetry Report";
     // Add a page
    PdfPage pdfPage = pdfDocument.AddPage();
    // Optional: set up page size/orientation (default is A4; here we use Letter)
    pdfPage.Size = PdfSharp.PageSize.Letter;
    pdfPage.Orientation = PdfSharp.PageOrientation.Portrait;

      // Create XGraphics object for drawing on the page
    XGraphics gfx = XGraphics.FromPdfPage(pdfPage);

    // Define some fonts
    XFont titleFont = new XFont("Arial", 14, XFontStyleEx.Bold);
    XFont bodyFont = new XFont("Arial", 10, XFontStyleEx.Regular);


   // Draw a header/title
    double margin = 40;
    double yPosition = margin;
    gfx.DrawString("Plan Dosimetry Report", titleFont, XBrushes.Black,
                   new XRect(0, yPosition, pdfPage.Width.Value, 
pdfPage.Height.Value),
                   XStringFormats.TopCenter);
    yPosition += 40;
    gfx.DrawString("Dose Prescription Data", titleFont, XBrushes.Black,
                   new XRect(0, yPosition, pdfPage.Width.Value/2, pdfPage.Height.Value),
                   XStringFormats.TopCenter);
    gfx.DrawString("Plan Dosimetry Report", titleFont, XBrushes.Black,
                   new XRect(pdfPage.Width.Value/2, yPosition, pdfPage.Width.Value/2, pdfPage.Height.Value),
                   XStringFormats.TopCenter);
    yPosition += 20;
    // Print some summary text from your ViewModel (for example, Rx data).
    foreach (var parameter in DoseParametersViewModel.RxData)
    {
        gfx.DrawRectangle(XPens.Black, new XRect(margin, yPosition-5,
           pdfPage.Width.Value / 2 - margin, 20));
        gfx.DrawString(parameter.Property + ": " + parameter.Value, bodyFont, XBrushes.Black,
                       new XRect(margin, yPosition, pdfPage.Width.Value - 2 * margin, pdfPage.Height.Value),
                       XStringFormats.TopLeft);
        yPosition += 20;
    }
    yPosition -= 20*DoseParametersViewModel.RxData.Count;
    foreach (var parameter in DoseParametersViewModel.CalculationParameters)
    {
        gfx.DrawRectangle(XPens.Black, new XRect(pdfPage.Width.Value/2, yPosition-5,
            pdfPage.Width.Value/2 - margin, 20));
        gfx.DrawString(parameter.Property + ": " + parameter.Value, bodyFont, XBrushes.Black,
                       new XRect(margin+pdfPage.Width.Value/2, yPosition, pdfPage.Width.Value - 2 * margin, pdfPage.Height.Value),
                       XStringFormats.TopLeft);
        yPosition += 20;
    }
    yPosition += 10;
    // Display dose metrics.
    gfx.DrawString("Plan Dose Metrics", titleFont, XBrushes.Black,
                   new XRect(margin, yPosition, pdfPage.Width.Value, pdfPage.Height.Value),
                   XStringFormats.TopCenter);
    yPosition += 20;
    foreach (var metric in DoseMetricViewModel.DoseMetrics)
    {
        if (metric.ToleranceMet == ToleranceEnum.Pass)
        {
            gfx.DrawRectangle(XBrushes.LightGreen,margin,yPosition,pdfPage.Width.Value-2*margin,15);
        }
        if(metric.ToleranceMet == ToleranceEnum.Fail)
        {
            gfx.DrawRectangle(XBrushes.LightPink, margin, yPosition, pdfPage.Width.Value - 2 * margin, 15);
        }
        gfx.DrawString($"Structure: {metric.Structure}, {metric.Metric} = {metric.OutputValue:F2} {metric.OutputUnit}",
                       bodyFont, XBrushes.Black,
                       new XRect(margin+12, yPosition, pdfPage.Width.Value - 2 * margin, pdfPage.Height.Value),
                       XStringFormats.TopLeft);
        gfx.DrawLine(XPens.Black, margin, yPosition + 12, pdfPage.Width.Value - 2 * margin, yPosition+12);
        yPosition += 15;
    }
    // Export your DVH plot to a BitmapSource, then embed it as an image
    //    (Requires OxyPlot.Wpf; using PngExporter or similar approach).
    BitmapSource dvhBitmap = new PngExporter().ExportToBitmap(DVHViewModel.DVHPlotModel);

    // Convert the BitmapSource into an XImage via a MemoryStream
    XImage dvhXImage = null;
    using (var ms = new MemoryStream())
    {
        // Encode the BitmapSource to PNG in memory
        var encoder = new PngBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(dvhBitmap));
        encoder.Save(ms);
        ms.Seek(0, SeekOrigin.Begin);

       // Create XImage from the MemoryStream
        dvhXImage = XImage.FromStream(ms);
    }

    // Optionally, start a new page if the current one doesn't have enough space
    if (yPosition + 300 > pdfPage.Height.Value - margin)
    {
        // Add a new page and reset yPosition, gfx
        pdfPage = pdfDocument.AddPage();
        pdfPage.Size = PdfSharp.PageSize.Letter;
        gfx = XGraphics.FromPdfPage(pdfPage);
        yPosition = margin;
    }

    // Draw the DVH image at an arbitrary size
    gfx.DrawImage(dvhXImage, margin, yPosition, 400, 300);
    yPosition += 320;
    // Prompt user to select a location for the PDF
    SaveFileDialog sfd = new SaveFileDialog
    {
               Filter = "PDF Documents (*.pdf)|*.pdf",
        FileName = "PlanDosimetryReport.pdf"
    };
    if (sfd.ShowDialog() == true)
    {
        // Save the PDF to disk
        pdfDocument.Save(sfd.FileName);
        // Optionally open the PDF
        // System.Diagnostics.Process.Start(new ProcessStartInfo(sfd.FileName) { UseShellExecute = true });
    }
}

PDFSharp Pros:

  • Direct PDF Output: No need to install virtual PDF printer.

  • Control: PDFSharp's APIs give precise control over layout, fonts, images, etc.

  • Open Source: Freely available for .NET Applications


PDFSharp Cons:

  • Manual layout: The layout calculations must be handled by the developer (or build a layer on top of PDFSharp).

  • Minimal built-in structures: Text formatting, tables, and styling must be defined manually.


Migradoc is a higher-level document generator that leverages the same underlying PDF rendering engine as PDFsharp, but shields developers from the low-level complexities of manual drawing. Instead of calculating exact coordinates or managing page breaks yourself, you can build documents by adding sections, paragraphs, tables, and images. MigraDoc automatically handles text-wrapping, pagination, and table layout, letting you create professional-looking reports or forms with far less boiler plate code. This makes it especially helpful for developers who need more sophisticated or text-heavy output without micromanaging every drawing option.


XSL/HTML to PDF Printing


Overview

Another popular approach is to generate an HTML representation of your report (often via XSLT transformations). Since HTML is widely used, you can take advantage of a variety of HTML-to-PDF converters (e.g. wkhtmltopdf, iTextSharp, and other libraries). This can be especially helpful if you already have a template in XSL or an XML structure that you transform into HTML.


How it Works

  1. Prepare an XML Data Source: You might extract data from ESAPI into an XML format.

  2. Apply XSLT to Generate HTML: Use an XSL file to transform your XML Data into a stylized HTML report.

  3. Convert HTML to PDF: Pass the generated HTML to a tool like wkhtmltopdf, iText, or another library that can handle HTML to PDF conversion.


In this next example, we will apply steps 1 & 2 in the description above. First, we need a report.xsl file to define the style of the report. In a new Resources folder, create a file called report.xsl. It is important to have this file set to copy to the output directory from the properties of the file. Here is an example xsl file.


<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
	<head>
	<title>Plan Dosimetry Report</title>
	<style>
	/* Basic styling for table, headings, etc. */
	body { font-family: Arial, sans-serif; }
	table { border-collapse: collapse; width: 90%; margin: 20px auto; }
	th, td { border: 1px solid #000; padding: 8px; }
	th { background-color: #f0f0f0; }
	.pass { background-color: lightgreen; }
	.fail { background-color: lightpink; }
	</style>
	</head>
<body>
	<h1 style="text-align:center;">Plan Dosimetry Report</h1>

 
	<!-- Example for "DoseParameters" or "CalculationParameters" -->
	<h2>Dose Prescription Data</h2>
	<table border="1">
		<tr>
			<th>Property</th>
			<th>Value</th>
		</tr>
		<xsl:for-		each select="DoseMetricsReport/reportData/doseParameters/parameter">
		<tr>
			<td>
				<xsl:value-of select="property"/>
			</td>
			<td>
				<xsl:value-of select="value"/>
			</td>
		</tr>
		</xsl:for-each>
	</table>
	<h2>Dose Calculation Parameters</h2>
	<table border="1">
		<tr>
			<th>Property</th>
			<th>Value</th>
		</tr>
		<xsl:for-each select="DoseMetricsReport/reportData/calculationParameters/parameter">
		<tr>
			<td>
				<xsl:value-of select="property"/>
			</td>
			<td>
				<xsl:value-of select="value"/>
			</td>
		</tr>
	</xsl:for-each>
	</table>
	<!-- Example for "DoseMetrics" with pass/fail highlighting -->
	<h2>Plan Dose Metrics</h2>
	<table>
		<tr>
			<th>Structure</th>
			<th>Metric</th>
			<th>Value</th>
			<th>Status</th>
		</tr>
		<xsl:for-each select="DoseMetricsReport/reportData/doseMetrics/metric">
		<xsl:variable name="tolerance" select="toleranceMet" />
		<tr>
			<td>
				<xsl:value-of select="structure"/>
			</td>
			<td>
				<xsl:value-of select="metricName"/>
			</td>
			<td>
				<xsl:value-of select="outputValue"/>
				<xsl:value-of select="outputUnit"/>
			</td>
			<td>
				<xsl:choose>
					<xsl:when test="$tolerance='Pass'">
					<span class="pass">PASS</span>
				</xsl:when>
				<xsl:otherwise>
					<span class="fail">FAIL</span>
				</xsl:otherwise>
				</xsl:choose>
			</td>
		</tr>
		</xsl:for-each>
	</table>
	<h2>DVH</h2>
	<img>
		<xsl:attribute name="src">
		<xsl:text>data:image/png;base64,</xsl:text>
		<xsl:value-of select="DoseMetricsReport/reportData/chartImage"/>
		</xsl:attribute>
		<xsl:attribute name="alt">DVH Chart</xsl:attribute>
	</img>
	</body>
</html>
</xsl:template>
</xsl:stylesheet>
 

As you notice, in the style sheet, the xsl:value-of attributes are looking for data in a hierarhy of objects. For example DoseMetricsReport.reportData.doseMetrics.metric would be the path for a dose metric. The next task is to build an XML file that has those potential metrics embedded. The class pasted below has 3 methods.

  • A ConvertXmlToHtml method that transforms an XML file to html via the above XSL style sheet.

  • A DumpReportXML method that constructs the XML file from the DoseParametersViewModel and the DoseMetricViewModel.

  • A GetPlotImageBase64 method that converts an oxyplot bitmap to base64 encoded string.


public class XslToHtmlConverter
{
    private DoseParametersViewModel _doseParametersViewModel;    private DoseMetricViewModel _doseMetricViewModel;
    private DVHViewModel _dvhViewModel;
    /// <summary>
    /// Transforms an XML file to an HTML file via an XSL stylesheet.
    /// Once the HTML file is generated, you can open it in a browser
    /// and print to a virtual printer or a physical printer.
    /// </summary>
    public void ConvertXmlToHtml(DoseParametersViewModel doseParametersViewModel, DoseMetricViewModel doseMetricViewModel, DVHViewModel dVHViewModel,
        string xmlFilePath, string xslFilePath, string outputHtmlPath)
    {
        _doseParametersViewModel = doseParametersViewModel;
        _doseMetricViewModel = doseMetricViewModel;
        _dvhViewModel = dVHViewModel;
        // 1) Transform XML -> HTML
        XslCompiledTransform transform = new XslCompiledTransform();
        transform.Load(xslFilePath);
        DumpReportXML(xmlFilePath);
        using (XmlReader reader = XmlReader.Create(xmlFilePath))
        using (XmlWriter writer = XmlWriter.Create(outputHtmlPath))
        {
            transform.Transform(reader, writer);
        }

        // 2) (Optional) Open the resulting HTML automatically
        // Comment this out if you don't want to automatically launch the file
        try
        {
            Process.Start(new ProcessStartInfo(outputHtmlPath)
            {
                UseShellExecute = true
            });
        }
        catch
        {
            // Handle exceptions if desired (file not found, etc.)
        }
    }
    /// <summary>
    /// Generates an XML file from the data within the viewmodels
    /// </summary>
    /// <param name="xmlFilePath">Path to XMLfile output. </param>
    private void DumpReportXML(string xmlFilePath)
    {
        XmlWriterSettings settings = new XmlWriterSettings();
        settings.Indent = true;
        settings.IndentChars = ("\t");
        System.IO.MemoryStream mStream = new System.IO.MemoryStream();
        XmlWriter writer = XmlWriter.Create(mStream, settings);
        writer.WriteStartDocument(true);
        writer.WriteStartElement("DoseMetricsReport");
        writer.WriteAttributeString("created", DateTime.Now.ToString());

        writer.WriteStartElement("reportData");
        writer.WriteStartElement("doseParameters");
        foreach (var parameter in _doseParametersViewModel.RxData)
        {
            writer.WriteStartElement("parameter");
            writer.WriteElementString("property", parameter.Property);
            writer.WriteElementString("value", parameter.Value);
            writer.WriteEndElement();//</parameter>
        }
        writer.WriteEndElement(); // </doseparatmers>
  
        writer.WriteStartElement("calculationParameters");
        foreach (var parameter in _doseParametersViewModel.CalculationParameters)
        {
            writer.WriteStartElement("parameter");
            writer.WriteElementString("property", parameter.Property);
            writer.WriteElementString("value", parameter.Value);
            writer.WriteEndElement();//</parameter>
        }
        writer.WriteEndElement(); // </calculationParameters>
        writer.WriteStartElement("doseMetrics");
        foreach (var parameter in _doseMetricViewModel.DoseMetrics)
        {
            writer.WriteStartElement("metric");
            writer.WriteElementString("tolerance", parameter.Tolerance);
            writer.WriteElementString("toleranceMet", parameter.ToleranceMet.ToString());
            writer.WriteElementString("structure", parameter.Structure);
            writer.WriteElementString("metricName", parameter.Metric);
            writer.WriteElementString("outputValue", parameter.OutputValue.ToString("F2"));
            writer.WriteElementString("outputUnit", parameter.OutputUnit);
            writer.WriteEndElement();//</parameter>
        }
        writer.WriteEndElement(); // </doseMetrics>

        string getPlotAsString = GetPlotImageBase64(_dvhViewModel.DVHPlotModel);
        writer.WriteElementString("chartImage", getPlotAsString);
  
        writer.WriteEndElement(); // </reportData>
        writer.WriteEndElement(); // reportData

        writer.WriteEndDocument();
        writer.Flush();
        mStream.Flush();

        // write the XML file report.
        using (System.IO.FileStream file = new System.IO.FileStream(xmlFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write))
        {
            // Have to rewind the MemoryStream in order to read its contents.
            mStream.Position = 0;
            mStream.CopyTo(file);
            file.Flush();
            file.Close();
        }
  
        writer.Close();
        mStream.Close();
    }
    /// <summary>
    /// Get Base 64 string from oxyplot bitmap
    /// </summary>
    /// <param name="plotModel">PlotModel to print.</param>
    /// <returns></returns>
    public string GetPlotImageBase64(OxyPlot.PlotModel plotModel)
    {
        // 1) Export the plot to a BitmapSource
        BitmapSource bitmap = new PngExporter().ExportToBitmap(plotModel);
  
        // 2) Convert BitmapSource to a byte[] in PNG format
        using (var ms = new MemoryStream())
        {
            var encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(bitmap));
            encoder.Save(ms);
            ms.Seek(0, SeekOrigin.Begin);
            byte[] pngData = ms.ToArray();
  
            // 3) Convert to Base64 string
            return Convert.ToBase64String(pngData);
        }
    }
}

The XML file produced by the DumpReportXML method can be viewed in the C:\temp folder.

A new method in the MainViewModel has been included to call our new HTML printing service and provide the necessary input values for it. The additional code in the MainViewModel will look as so.

 private void OnPrintHtml(object obj)
 {
     string xmlFile = @"C:\temp\reportData.xml";
     string xslFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),"Resources", "report.xsl");//@"C:\temp\report.xsl";
     string htmlOutput = @"C:\temp\PlanDosimetryReport.html";

     XslToHtmlConverter converter = new XslToHtmlConverter();
     converter.ConvertXmlToHtml(DoseParametersViewModel, DoseMetricViewModel, DVHViewModel, xmlFile, xslFile, htmlOutput);
 }

The provided report appears as HTML format in the browser. This can then be printed by the user simply by using CTRL+P and printing the browser to a PDF.

XSLT/HTML Pros

  • Separation of content and Presentation: XSLT is a powerful templating engine for XML data.

  • Extensive Styling with CSS: You can use standard web technologies (HTML and CSS) to style your reports.

  • Widely-supported PDF conversion: Many tools, both open-source and commercial, can handle HTML to PDF.


XSLT/HTML Cons

  • Additional tooling required: HTML-to-PDF converter must be integrated into the project. The solution for this may not be pure C# (such as wkhtmltopdf which is a separate executable).

  • Complex transformations: Debugging XSLT can be tedious if you have a large or complicated structure.


Comparison at a Glance

Method

Use Case

Pros

Cons

FlowDocument

Quick WPF printing, simple docs, or direct to XPS

• Built-in WPF support


 • Easy to style with XAML


 • Quick to implement

• No direct PDF output (unless using PDF printer)


 • Not ideal for complex PDFs

PDFsharp

Direct PDF generation with custom layout

• Full control over PDF creation


 • No intermediate steps

• Manual layout for each page


 • Requires knowledge of PDFsharp APIs

XSL/HTML to PDF

Complex or large-scale templating, existing XML data

• Powerful templating with XSLT


 • CSS styling


 • Many HTML-to-PDF tools

• Requires external libraries or tooling


 • Debugging XSLT can be tricky

Conclusion

When deciding how to print or generate PDFs from an ESAPI script—or any WPF application—you’ll want to weigh your project’s requirements and constraints:

  • FlowDocument is ideal for a quick WPF-native printing flow and if you don’t mind XPS output or installing a PDF printer driver.

  • PDFsharp is perfect if you need direct PDF files and low-level control over the layout, or if you want a code-first approach without involving XAML.

  • XSL/HTML shines when you have an XML-based data source or want a flexible, web-style layout with the option to integrate modern front-end techniques.

Each method has its own learning curve, trade-offs, and dependencies. Hopefully, this comparison helps you determine which path is right for your ESAPI project or general WPF reporting needs.

Happy printing!

ความคิดเห็น


©2035 by NWS. Powered and secured by Wix

bottom of page