How to Write a Data Source Provider Class
Our goal in this tutorial is to show how to write a custom DSP (Data Source Provider) class.
The role of DSP class is to provide data for both design-time and run-time of reports and documents.
A DSP class inherits one of three possible base DSP classes:
- DocDataSourceProviderSrsReporting
- DocDataSourceProviderBasicReporting
- DocDataSourceProviderMailMerge
depending on which sub-framework of Docentric AX is used: SSRS reports, Basic reports or Mail Merge (Docentric Template Libraries, i.e. Word documents).
When the base DSP class is used and when a custom DSP class is needed
1. SSRS reports
If you improve only a SSRS report design then the default base DSP (Data Source Provider) for Docentric SSRS reports (DocDataSourceProviderSrsReporting) is used.
A custom DSP class should be used with SSRS reports when you want to:
- Add some additional data to an existing SSRS report without modifying the report artifacts.
- Define and fetch all needed data for a new custom SSRS report developed from scratch for which you don't want to spend time on creating temporary tables and also you want to have an ability to arbitrary shape your data.
- Define and manage custom placeholders used in print destinations.
2. Basic reports
The base DSP class for Docentric Basic Reports (DocDataSourceProviderBasicReporting) is an abstract class with the abstract generateXmlDataSource() method which means that this method has to be implemented for each Basic report. This also means that for Basic reports we have no other choice but to create and use custom DSP classes.
3. Word documents
If you use queries to describe Word document data sources (this is the only possibility in the standard Document Management framework) then the default base DSP (Data Source Provider) for query based Docentric Word documents (DocDataSourceProviderMailMergeQuery) is used. This class executes the data source query of a document and generates document data source.
On the other hand, if you want to gain more control in fetching and shaping your data, a custom DSP class is needed that inherits from the DocDataSourceProviderMailMerge class, a base class for Docentric Word documents. This is a recommended approach to take.
How do we use a custom DSP class
But we don’t have to worry about extending the proper base DSP class and implementing the particular methods. There is a wizard that generates a new Data Source Provider class for our Word document, Basic report or even SSRS report, in case we want to extend its data source.
1. The generateXmlDataSource() method
The only thing we really have to do is to implement one single method – generateXmlDataSource(). This method actually provides the data needed for a report or a document by collecting, calculating and (re)shaping the data as required.
2. The addDataFieldsForRdpTableRecord() method
Additionally for SSRS reports you will most likely be using the addDataFieldsForRdpTableRecord() method in order to add additional data or exclude particular data from the currently adding record of an RDP (report data provider) temporary table, or to change names and labels of particular fields or adding record itself. Learn here on how to use the addDataFieldsForRdpTableRecord() method for Docentric SSRS reports.
When is a DSP class invoked
Docentric AX Framework invokes the implementation of the generateXmlDataSource() and addDataFieldsForRdpTableRecord() methods of a DSP class (base or custom) and produces the main section of the Data Source Package (DDSP file) for a document or a report when:
- DDSP file is generated and saved to a physical file and afterwards used in the template design,
- a Word document is generated from the template or a SSRS or Basic report is executed.
Each DDSP file contains also two standard Data Sections automatically added by Docentric AX Framework:
- General Data Section containing data related to the current company, worker, etc.
- Parameter Data Section containing report parameters (applicable to reports only).
You can read more about Data Source Package structure here >> .
What we get if we use custom DSP classes
If we use custom DSP classes for customizing and developing SSRS reports and Word documents we get:
1. Greater flexibility
You do not have to deal only with tabular data (tables and fields) in a report or a document data source as we used to in AX world. With the use of Docentric AX APIs for building data sources, you can arbitrary shape and nest your data. And the good news is that Docentric AX Designer fantastically manages n-level nested lists/tables/collections.
2. Much faster development
Simply traverse through the needed data in X++ and shape/nest/calculate them as you like by using Docentric AX APIs described in this tutorial.
3. No need for modifying the built-in SSRS report artifacts
If you need to add some additional data fields or records to an existing SSRS report or to exclude unnecessary data, you don't have to customize the report built-in artifacts (temporary table, data provider, etc.). You can use custom Docentric DSP classes for SSRS reports instead.
Using Record Builder APIs
1. Adding Data Records and Fields to the Record Builder’s Internal Record Tree
When implementing the generateXmlDataSource() method, we use the DocXmlRecordBuilder class to build an XML report or document data source (Data Source Package) simply by traversing through the needed data and adding them to the DocXmlRecordBuilder instance (called Record Builder) as Data Records and Data Fields using various methods shown on the picture below. For example, we can add a table buffer as a Data Record, fields from a table field group or a table display method as Data Fields, etc. All added Data Records and Fields are stored in the Record Builder’s Internal Record Tree that is afterwards serialized into an XML fragment, with the following rule: Data Records are serialized as XML elements and Data Fields as XML attributes.
Data Records and Fields added to Record Builder’s Internal Record Tree can be either Table Buffer Based (we use addRecordXXX(), addField(), addFieldGroupXXX(), addDisplayMethod(), etc. methods) or Calculated (we use addCalculatedFieldXXX() and addCalculatedRecord() methods). Calculated Data Records and Fields enable us to reshape the data structure and to add calculated values.
1.1 Examples in code
Take a look at the following code fragment from the generateXmlDataSource() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
select firstOnly rfqCaseTable where rfqCaseTable.StatusHigh == PurchRFQStatus::Received; // Add a record. recordBuilder.addRecord(rfqCaseTable); // Add fields of different data types. recordBuilder.addField(fieldStr(PurchRFQCaseTable, RFQCaseId)); recordBuilder.addField(fieldStr(PurchRFQCaseTable, DeliveryDate)); // Add an enum field. recordBuilder.addField(fieldStr(PurchRFQCaseTable, RFQType)); // Add a dimension field. recordBuilder.addField(fieldStr(PurchRFQCaseTable, DefaultDimension)); // Add a field group – all fields from the field group are being added. recordBuilder.addFieldGroup(tableFieldGroupStr(PurchRFQCaseTable, Inventory)); // Add a field group as a child record. recordBuilder.addFieldGroupAsChildRecord(tableFieldGroupStr(PurchRFQCaseTable, Payment)); // Add a calculated field. projTable = ProjTable::find(rfqCaseTable.ProjId); recordBuilder.addCalculatedField('ProjectName', projTable.Name, 'Project name'); // Add a calculated field with the label of the other table field. hcmWorker = HcmWorker::find(rfqCaseTable.ResponsibleWorkerId); recordBuilder.addCalculatedFieldSysLbl('ResponsibleWorker', hcmWorker.name(), fieldStr(PurchRFQCaseTable, ResponsibleWorkerId)); // Add child records. while select rfqTable where rfqTable.RFQCaseId == rfqCaseTable.RFQCaseId { // Add record. recordBuilder.addRecord(rfqTable); // Add title fields. recordBuilder.addTitleFields(); // Add display method. recordBuilder.addDisplayMethod(tableMethodStr(PurchRFQTable, vendorStatus)); ... } |
The resulting XML is:
1 2 3 4 5 6 7 8 9 10 11 12 |
<PurchRFQCaseTable RFQCaseId="000002" DeliveryDate="2013-02-04" InventLocationId="12" InventSiteId="1" InventSiteName="Home Speakers Production" ProjectName="Midrange stereo install" ResponsibleWorker="Kevin Cook"> <Payment Payment="Net45" PaymMode="" /> <PurchRFQTable RFQId="000001" RFQName="Fabrikam Supplier" VendorStatus="Submitted: No action required" TotalAmount="15908.0000000000" ValidityDateStart="2013-02-04" VendAccount="US-104" /> <PurchRFQTable RFQId="000002" RFQName="Datum Receivers" VendorStatus="Submitted: No action required" TotalAmount="16700.0000000000" ValidityDateStart="2013-02-04" VendAccount="US-105" /> <PurchRFQTable RFQId="000003" RFQName="Humongous Insurance" VendorStatus="Submitted: No action required" TotalAmount="15720.0000000000" ValidityDateStart="2013-02-04" VendAccount="US-106" /> <RFQType Value="0" Name="Purch" Text="Purchase order" /> <DefaultDimension> <BusinessUnit DisplayValue="005" Description="Electronics" /> <CostCenter DisplayValue="020" Description="West" /> </DefaultDimension> </PurchRFQCaseTable> |
2. The main rule regarding Internal Record Tree structure
If the name of an adding Data Record is the same as the name of recordBuilder.currentRecord(), the Data Record is added on the same level as recordBuilder.currentRecord(), i.e. as its next sibling; otherwise, the Data Record is automatically added as a child of the recordBuilder.currentRecord().
For example:
1 2 3 4 5 6 7 8 9 10 11 |
// Add a record. recordBuilder.addRecord(rfqCaseTable); recordBuilder.addField(fieldStr(PurchRFQCaseTable, RFQCaseId)); // Add child records. while select rfqTable where rfqTable.RFQCaseId == rfqCaseTable.RFQCaseId { // Add record with Title fields. recordBuilder.addRecordWithTitleFields(rfqTable); } |
The resulting XML is:
1 2 3 4 5 |
<PurchRFQCaseTable RFQCaseId="000002"> <PurchRFQTable RFQId="000001" RFQName="Fabrikam Supplier"/> <PurchRFQTable RFQId="000002" RFQName="Datum Receivers"/> <PurchRFQTable RFQId="000003" RFQName="Humongous Insurance"/> </PurchRFQCaseTable> |
In the first pass of the while loop in the code above, when we were adding the first PurchRFQTable record to the Record Builder’s Internal Record Tree, the current record name was ‘PurchRFQCaseTable’ and hence the record has been added as a child record. In the second pass of the loop, when we were adding the second PurchRFQTable record, the current record name was ‘PurchRFQTable’. Therefore, the second PurchRFQTable record has been added as a sibling record to the first PurchRFQTable record. In the end, Internal Record Tree serialization resulted in the above XML fragment.
3. Navigating through internal record builder’s record tree
When we add a Data Record to Internal Record Tree, the current record pointer of the Record Builder recordBuilder.currentRecord() is moved to the newly added record. We can use goToXXX() navigation methods to change the current record pointer position.
Take a look at the previous example. If we invoke the recordBuilder.goToParentRecord() navigation method directly after adding of the PurchRFQCaseTable record:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Add a record. recordBuilder.addRecord(rfqCaseTable); recordBuilder.addField(fieldStr(PurchRFQCaseTable, RFQCaseId)); // Navigate to the parent record. recordBulder.goToParentRecord(); // Add child records. while select rfqTable where rfqTable.RFQCaseId == rfqCaseTable.RFQCaseId { // Add record with Title fields. recordBuilder.addRecordWithTitleFields(rfqTable); } |
The resulting XML will be:
1 2 3 4 |
<PurchRFQCaseTable RFQCaseId="000002"/> <PurchRFQTable RFQId="000001" RFQName="Fabrikam Supplier"/> <PurchRFQTable RFQId="000002" RFQName="Datum Receivers"/> <PurchRFQTable RFQId="000003" RFQName="Humongous Insurance"/> |
4. DocXmlRecord and DocXmlField classes
Result of the addRecordXXX() and addFieldXXX() methods is an instance of the DocXmlRecord or DocXmlField class, representing a Data Record or a Data Field added to Internal Record Tree of the Record Builder. After a addXXX() method is executed and a DocXmlRecord or DocXmlField instance is created, we can invoke some of the following methods: setRecordName(), setRecordLabelId() or setFieldName(), setFieldLabelId(), etc. These methods allow us to modify the names and labels of the corresponding XML elements and attributes in the resulting XML, after the serialization of Internal Record Tree takes place.
5. Execution context
5.1 Report parameters
A report parameter value can be retrieved through the method:
this.getParameter('<parameter_name>').parmValue().
This applies only to SSRS or Basic DSP classes.
5.2 Report execution context
Use methods this.getReportExecutionContextXXX() to access the report execution context table in a case of SSRS or Basic DSP classes.
5.3 Primary Table execution context
Use the methods this.getPrimaryTableXXX() to access a Primary table in case of Mail Merge (Word Documents) DSP classes.
6. Labels
All addRecordXXX() and addFieldXXX() methods automatically add the corresponding system labels, but at the same time allowing us to override them. These labels can be used later during template design. Also if we need to introduce some custom labels for a report or a document, this can be done through the report or document setup. On how to use labels and make multilingual documents and reports you can learn here >>.
7. Dumping Data Source XML
Use the following methods to dump the resulting XML of document or report data source:
1 2 3 4 5 |
// Dump Internal Record Tree of Record Builder to a string. info(recordBuilder.exportToXmlStr()); // Dump Internal Record Tree of Record Builder to a file on file system. recordBuilder.exportToXmlFile(@’C:\Temp\ReportDS.xml’); |
Using Data Records and Data Fields APIs
Alternatively, you can use the DocXmlRecord and DocXmlField classes alongside/instead of Record Builder to gain more control over building data source record tree.
For example, you can retrieve the current record from Record Builder and change the record name and label:
1 2 3 4 5 6 7 8 9 10 11 12 |
DocXmlField dataField; DocXmlRecord rootDataRecord = _recordBuilder.currentRecord(); DocXmlRecord purchTableAdditionalData = rootDataRecord.addChildRecord(purchTable); // Rename the record and change its label. purchTableAdditionalData.setRecordName('AdditionalData'); purchTableAdditionalData.setRecordLabel('Additional data for purchase order'); dataField = purchTableAdditionalData.addField(fieldStr(PurchTable, CashDiscPercent)); // Rename the field and change its label. dataField.setFieldName('CashDiscount'); dataField.setFieldLabelId(literalStr("@SYS4376")); |
In the method addDataFieldsForRdpTableRecord(DocXmlRecord _addingRecord, Common _rdpTableRecord, TableName _rdpTableName) you will deal with DocXmlRecord and DocXmlField objects directly instead of using Record Builder, but APIs are practically the same. Learn more here >>
Resources
Data source provider class Data source (DDSP file) TemplatingFeaturesOverview Template Subdocument
NOTE: This example can be used only with Docentric Template Library, for the Primary table PurchTable. Please see Document Template Libraries Examples for more details.
See also
Download examples >>
Docentric Data Source Packages (DDSP files) Overview >>
How to Extend Standard Data Sections of Data Source Package >>
How to Add Additional Data to a Docentric SSRS Report >>
How to Develop a New SSRS Report >>
How to Create a Word Document with DSP Class >>