Dynamic Columns and Designer Reports - Part One
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>
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.
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>
Obtain the XML schema at https://www.dynamicpdf.com/schemas/DLEX20.xsd
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.
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.dpdf.io/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
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.
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);
}
}
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.
Obtain the C# .NET client library from one of the following sources.
- The C# .NET client library is available using the NuGet package manager at https://www.nuget.org/packages/DynamicPDF.API.
- The source is available on GitHub at https://github.com/dynamicpdf-api/dotnet-client.
Modifying the DLEX
To modify the DLEX file, we must load the XML into an XmlDocument
and then manipulate the relevant nodes.
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;
}
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);
}
}
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.