Skip to main content

Dynamic Columns and Designer Reports - Part One

· 10 min read
James A Brannan

DynamicPDF Designer is a powerful graphical editing tool for creating a DLEX XML specification that, when processed, creates a PDF document. You can also programmatically modify a DLEX specification to format a report dynamically. Here, we show how to dynamically change a report to remove columns when creating a columnar report.

This is part one of a two-part blog post. In this post we programmatically modify an existing DLEX. In the next post we create that DLEX from scratch.

Suppose you wished to create a columnar report with dynamically displayed columns. For one customer, you might want to show columns A, B, and C; for another, you might wish to display columns A and C only. Although you could create two separate DLEX documents, a better solution is to create one DLEX document and then programmatically remove and move relevant elements. Recall that the result from DynamicPDF Designer Online is a DLEX XML document. Therefore, you can programmatically modify that DLEX document dynamically.

<document xmlns="https://www.dynamicpdf.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.dynamicpdf.com https://www.dynamicpdf.com/schemas/DLEX20.xsd" version="2.0" author="" keywords="" subject="" title="" id="Document1">
<report id="Report1" dataName="lineItems" pageSize="letter" pageOrientation="portrait" leftMargin="50" topMargin="50" rightMargin="50" bottomMargin="50">
--- snip ---
</report>
</document>
caution

Creating and modifying a DLEX from scratch is tedious and error-prone. If possible, you should use Designer.

Let's illustrate how to programmatically modify an existing report to remove a column and then reposition the remaining columns.

info

Find the files for this post in the samples folder (blog-dynamic-columns) if you wish to add the files to your cloud storage space (File Manager - Sample Resources).

Before reviewing the code required, let's examine the DLEX in DynamicPDF Designer Online.

Designer

The report-with-cover-page.dlex - when opened in Designer - contains a DLEX template consisting of a page and a report. The Report section includes a Header and a Detail.

The JSON data is as follows and contains a ProductID, ProductName, QuantityPerUnit, UnitPrice, and Discontinued element.

{
"ReportCreatedFor": "Alex Smith",
"Products": [
{
"ProductID": 17,
"ProductName": "Alice Mutton",
"QuantityPerUnit": "20 - 1 kg tins",
"UnitPrice": 39,
"Discontinued": true
},
{
"ProductID": 3,
"ProductName": "Aniseed Syrup",
"QuantityPerUnit": "12 - 550 ml bottles",
"UnitPrice": 10,
"Discontinued": false
},
{
"ProductID": 40,
"ProductName": "Boston Crab Meat",
"QuantityPerUnit": "24 - 4 oz tins",
"UnitPrice": 18.4,
"Discontinued": false
},
--- snip ---

When Designer runs the report, it creates the following PDF document.

When the DLEX file is opened in a text or XML editor, we see the following XML.

<document xmlns="https://www.dynamicpdf.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.dynamicpdf.com https://www.dynamicpdf.com/schemas/DLEX20.xsd" version="2.0" author="" keywords="" subject="" title="" id="Document1">
<page id="Page1" pageSize="letter" pageOrientation="portrait" leftMargin="50" topMargin="50" rightMargin="50" bottomMargin="50">
<recordArea id="RecordArea1" x="0" y="464" width="512" height="66" align="center" fontSize="24" underline="false" vAlign="bottom" autoLeading="true" cleanParagraphBreaks="true" expandable="false" rightToLeft="false" splittable="false">
<text>This report was created for:
#ReportCreatedFor#</text>
</recordArea>
<image id="Image1" x="160.5" y="142" width="191" height="189" path="NorthwindLogo.gif"/>
</page>
<report id="Report1" dataName="Products" pageSize="letter" pageOrientation="portrait" leftMargin="50" topMargin="50" rightMargin="50" bottomMargin="50">
<template id="Template1"/>
<header id="Header" height="36">
<recordBox id="daterecordbox" x="0" y="0" width="512" height="12" font="HelveticaBold" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="false" rightToLeft="false" splittable="false" dataName="CurrentDateTime()" dataFormat="d MMM yyyy h:mm:ss tt"/>
<label id="Label4" x="0" y="0" width="512" height="12" align="center" font="HelveticaBold" underline="false" text="Northwind Product List"/>
<pageNumberingLabel id="PageNumberingLabel1" x="0" y="0" width="512" height="12" align="right" font="HelveticaBold" underline="false" text="Page %%CP%% of %%TP%%"/>
<label id="productnamelabel" x="2" y="21" width="122" height="14" font="TimesBold" fontSize="11" underline="false" text="Product Name"/>
<label id="qtyperunitlabel" x="128" y="21" width="156" height="14" font="TimesBold" fontSize="11" underline="false" text="Qty Per Unit"/>
<label id="unitpricelabel" x="288" y="21" width="65" height="14" align="right" font="TimesBold" fontSize="11" underline="false" text="Unit Price"/>
<label id="discontinuedlabel" x="358" y="21" width="100" height="14" align="center" font="TimesBold" underline="false" text="Discontinued"/>
<line id="Line1" x1="0" y1="36" x2="512" y2="36"/>
</header>
<detail id="Body" autoSplit="false" height="18">
<rectangle id="Rectangle1" x="0" y="0" width="512" height="18" borderColor="lightBlue" fillColor="lightBlue" showAlternateRow="even"/>
<recordBox id="productnamebox" x="2" y="3" width="122" height="14" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="true" rightToLeft="false" splittable="false" dataName="ProductName"/>
<recordBox id="qtyperunitbox" x="128" y="3" width="156" height="14" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="true" rightToLeft="false" splittable="false" dataName="QuantityPerUnit"/>
<recordBox id="unitpricebox" x="288" y="3" width="65" height="14" align="right" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="false" rightToLeft="false" splittable="false" dataName="UnitPrice" dataFormat="0.00##"/>
<symbol id="discontinuedsymbol" x="358" y="3" width="100" height="14" visibilityCondition="EQ(Discontinued,1)" symbolType="check1"/>
<recordBox id="productidbox" x="462" y="3" width="44" height="14" align="right" font="TimesItalic" fontSize="9" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="false" rightToLeft="false" splittable="false" dataName="ProductID"/>
</detail>
<footer id="Footer" height="0"/>
</report>
</document>

Suppose we removed the Qty Per Unit dynamically. To accomplish this removal, we need to remove the qtyperunitlabel and qtyperunitbox elements and then move the remaining labels left. The Header and Detail contain the following elements.

The labels and data fields, Product Name, Qty Per Unit, Unit Price, and Discontinued headings, all have the following x-axis values.

Removing the elements from the DLEX file requires code similar to the following pseudocode.

// move unitpricelabel.x & unitpricebox.x to 128
// move discontinuedlabel.x & discontinuedsymbol.x to 288

var x1 = qtyperunitlabel.x;
var x2 = unitpricelabel.x;
unitpricelabel.x = x1;
unitpricebox.x = x1
discontinuedlabel.x = x2;
discontinuedsymbol.x = x2;
delete(qtyperunitlabel);
delete(qtyperunitbox)

We can use the DynamicPDF API's C# client library combined with the C# .NET's System.Xml Namespace to implement the preceding pseudocode. But first, let's review the DynamicPDF API's pdf endpoint.

pdf API Endpoint

The pdf endpoint is a POST command that takes a JSON instructions document that specifies the processing that is to occur. The following JSON instructions are needed to process the request to produce the PDF using the DLEX document on the cloud and the layout data in your local filesystem.

{
"inputs":[
{
"type":"dlex",
"resourceName":"samples/blog-dynamic-columns/report-with-cover-page.dlex",
"layoutDataResourceName":"report-with-cover-page.json"
}]
}

The DLEX document, the embedded image, and the JSON data must all be downloaded to your local filesystem to modify the DLEX document.

tip

If following along, then download the resources to your local filesystem.

Call the pdf endpoint using HTTP POST (here we use cURL).

curl https://api.dynamicpdf.com/v1.0/pdf -H "Authorization:Bearer DP.xxx-api-key-xxx" 
-F "Instructions=@C:/temp/dynamicpdf-api-samples/columns/instructions.json"
-F "Resource=@C:/temp/dynamicpdf-api-samples/columns/report-with-cover-page.json"
-o simple-page.pdf
info

Refer to the pdf documentation (REST endpoint) topic for more information on the REST endpoint and the pdf documentation (client library) topic for more information on the client library use of this endpoint.

However, creating an instructions document and calling POST from scratch can prove challenging.

info

Refer to the Client Library Installation documentation topic for information on the client libraries.

The C# client library (dotnet-client-examples) is available on GitHub.

DynamicPDF offers client libraries for C# .NET, Java, Node.js, PHP, Go, and Perl to make calling the pdf endpoint easier. Let's use the C# client library. The following code accomplishes the same as the previous instructions.json file and cURL command.

public static void RunOriginal(string apiKey, string layoutDataPath, string dlexPath, string outputPath)
{
Pdf pdf = new Pdf();
pdf.ApiKey = apiKey;
LayoutDataResource layoutDataResource = new LayoutDataResource(layoutDataPath);
pdf.AddDlex("samples/blog-dynamic-columns/report-with-cover-page.dlex"", layoutDataResource);
PdfResponse pdfResponse = pdf.Process();
if (pdfResponse.IsSuccessful)
{
File.WriteAllBytes(outputPath, pdfResponse.Content);
}
else
{
Console.WriteLine(pdfResponse.ErrorJson);
}
}
Source: DynamicColumnsOne.cs

The example creates a new PDF instance and then a LayoutDataResource instance from the JSON data and then adds the DLEX from cloud storage and the layout data from the local filesystem. It then calls the endpoint using the Process method and returns the resultant PDF as binary.

info

Obtain the C# .NET client library from one of the following sources.

Modifying the DLEX

To modify the DLEX file, we must load the XML into an XmlDocument and then manipulate the relevant nodes.

caution

Realize the XSD defining the DLEX is complex. Simply clicking "generate object model" in your language/tool of choice will probably not work. Moreover, the schema has numerous dependencies between schema elements. A much easier solution is to fire up Designer, and in Designer, review the available properties for the element you wish to manipulate programmatically and use that. For example, reviewing a Label element's properties in the XSD requires navigating its dependencies. However, when using Designer, you can quickly identify a Label element's possible attributes.

The following labels are the relevant elements and their x values in the DLEX document.

<label id="productnamelabel" x="2" y="21" width="122" height="14" font="TimesBold" fontSize="11" underline="false" text="Product Name"/>
<label id="qtyperunitlabel" x="128" y="21" width="156" height="14" font="TimesBold" fontSize="11" underline="false" text="Qty Per Unit"/>
<label id="unitpricelabel" x="288" y="21" width="65" height="14" align="right" font="TimesBold" fontSize="11" underline="false" text="Unit Price"/>
<label id="discontinuedlabel" x="358" y="21" width="100" height="14" align="center" font="TimesBold" underline="false" text="Discontinued"/>

The following record boxes and symbol are the relevant elements and their x values in the DLEX document.

<recordBox id="productnamebox" x="2" y="3" width="122" height="14" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="true" rightToLeft="false" splittable="false" dataName="ProductName"/>
<recordBox id="qtyperunitbox" x="128" y="3" width="156" height="14" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="true" rightToLeft="false" splittable="false" dataName="QuantityPerUnit"/>
<recordBox id="unitpricebox" x="288" y="3" width="65" height="14" align="right" font="TimesRoman" fontSize="11" underline="false" autoLeading="true" cleanParagraphBreaks="true" expandable="false" rightToLeft="false" splittable="false" dataName="UnitPrice" dataFormat="0.00##"/>
<symbol id="discontinuedsymbol" x="358" y="3" width="100" height="14" visibilityCondition="EQ(Discontinued,1)" symbolType="check1"/>

The following example code illustrates using the XmlDocument class to modify the relevant elements, delete the qtyperunitlabel and qtyperunitbox, and then move the remaining elements on the x-axis.

public static XmlDocument ModifyDlexDocument(string dlexFile)
{
XmlDocument doc = new XmlDocument();

//declare namespace for xpath

XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("dpdf", "https://www.dynamicpdf.com");

//load xml document in XmlDocument

doc.Load(dlexFile);

//get the qtyperunitlabel's x value and then
//remove node.

string xpathExpression = "//dpdf:label[@id='qtyperunitlabel']";
XmlNode nodeToRemove = doc.DocumentElement.SelectSingleNode(
xpathExpression, nsmgr);
string xValue = nodeToRemove.Attributes["x"].Value;
nodeToRemove.ParentNode.RemoveChild(nodeToRemove);

//now remove qtyperunitbox node

xpathExpression = "//dpdf:recordBox[@id='qtyperunitbox']";
nodeToRemove = doc.DocumentElement.SelectSingleNode(
xpathExpression, nsmgr);
nodeToRemove.ParentNode.RemoveChild(nodeToRemove);

//replace unitpricelabel's x value
//with qtyperunitlabel's original x value

xpathExpression = "//dpdf:label[@id='unitpricelabel']";
XmlNode nodeToReplace = doc.DocumentElement.SelectSingleNode(
xpathExpression, nsmgr);
string x = nodeToReplace.Attributes["x"].Value;
nodeToReplace.Attributes["x"].Value = xValue;

//replace unitpricebox's x value
//with qtyperunitlabel's orinal x value

xpathExpression = "//dpdf:recordBox[@id='unitpricebox']";
nodeToReplace = doc.DocumentElement.SelectSingleNode(
xpathExpression, nsmgr);
nodeToReplace.Attributes["x"].Value = xValue;

//replace discontinuedlabel's x value
//with unitpricelabel's original x value

xpathExpression = "//dpdf:label[@id='discontinuedlabel']";
nodeToReplace = doc.DocumentElement.SelectSingleNode(xpathExpression, nsmgr);
nodeToReplace.Attributes["x"].Value = x;

//replace discontinuedsymbol's x val
//with unitpricelabel's original x value

xpathExpression = "//dpdf:symbol[@id='discontinuedsymbol']";
nodeToReplace = doc.DocumentElement.SelectSingleNode(xpathExpression, nsmgr);
nodeToReplace.Attributes["x"].Value = x;
return doc;
}
Source: DynamicColumnsOne.cs

The method above first declares the namespace so that the XPath statements can be processed. It then loads the DLEX document into an XmlDocument instance. It then uses XPath to return the qtyperunitlabel element as an XmlNode. After getting the x value, the XmlNode instance is removed.

The qtyperunitbox element is then removed. Then, the XPath statement replaces the x values of the remaining elements. The product ID is assumed to be appropriate flush right and is not modified.

After modifying the XMLDocument, we call the pdf endpoint, only this time passing the DLEX as binary. The following code calls the pdf endpoint programmatically using the C# .NET client library.

static void Main(string[] args)
{
//modify dlex then process into PDF

XmlDocument doc = ModifyDlexDocument(BASEPATH + "report-with-cover-page.dlex");
RunDlex("DP.xxx-api-key-xxx", BASEPATH + "report-with-cover-page.json", doc, BASEPATH + "report-with-cover-page-output.pdf");
}

public static void RunDlex(string apiKey, string layoutDataPath, XmlDocument doc, string outputPath)
{
Pdf pdf = new Pdf();
pdf.ApiKey = apiKey;

//load the JSON layout data

LayoutDataResource layoutDataResource = new LayoutDataResource(layoutDataPath);

//load the dlex file from XmlDocument as bytes

DlexResource dlexResource = new DlexResource(
Encoding.Default.GetBytes(doc.OuterXml));

//get the image embedded in dlex

pdf.AddAdditionalResource(BASEPATH + "NorthwindLogo.gif");
pdf.AddDlex(dlexResource, layoutDataResource);

//send request to cloudapi to process

PdfResponse pdfResponse = pdf.Process();

//save PDF if successful

if (pdfResponse.IsSuccessful)
{
File.WriteAllBytes(outputPath, pdfResponse.Content);
}
else
{
Console.WriteLine(pdfResponse.ErrorJson);
}
}
Source: DynamicColumnsOne.cs

A new Pdf instance is created, and then a LayoutdataResource instance is created from the JSON layout data. A new DlexResource instance is created from the modified DLEX XML encapsulated in the XmlDocument instance. The NorthwingLogo.gif image is also loaded as an additional resource because the image is embedded in the DLEX.

The Pdf instance then calls the Process method, sending the POST request to the pdf endpoint, and then returns the PDF as binary, producing the following report.

GitHub Example

The complete GitHub example is in the dotnet-client-examples solution.