Friday, July 23, 2010

How-to store FastReport.NET report templates in database

Some applications require the storing of report templates in a database. This simplifies template support - all reports are stored in one place, and allows to differentiate the access rights to different templates. I noted only one shortcoming of this technique - slight complication of the code in the client application.
For example, we need to create Access database (mdb) named "reports" and a table for storing our report templates. I named this table "reports_table". Three tables are enough in our data table:
№ Field name Type Description
1 Id counter Auto-increment, index
2 ReportName text Name of report
3 ReportFile BLOB Binary data with template

You can store templates in any other database similar to the one in our example.
To do this:
• open the designer;
• show custom form with a list of templates from our database instead of standard dialog when users open a file in the designer;
• load template in the designer after selecting it in the custom form;
• Save the template in a database where the user saves a file in the designer.
Despite the apparent complexity, the tasks above will be simple because FastReport.NET has special features.
Create a new project in Visual Studio first. Then you need to drag-n-drop "Report" object from toolbox to the empty form. Also drag the " EnvironmentSettings" object (properties of reporting engine).

You can setup many settings of the report and the designer in "environmentSettings1" object. You have to override the event handlers CustomOpenDialog, CustomOpenReport, CustomSaveDialog and CustomSaveReport.
Event handler CustomOpenDialog allows releasing any algorithm of the load report template in the designer - load template from the database field and load it in the designer or set e.Cancel = true; when loading fails.
The problem reduces to showing a dialogue with a list of files from which you want to select the report template from the database and load the template in the designer. Of course, if the user presses the Cancel button in this dialog, we set e.Cancel = true. The loading process is clear but saving code need some details.
The user can choose two ways to save a file - simply by clicking "Save", in this case report should be rewritten to the old place, or the second option - "Save as...”, the user is expected to select path of file. Storage location of report templates cannot be changed in our case. You need to save template in the old place (database field) in both ways - capture the CustomSaveDialog event by empty function and save template in database in the CustomSaveReport event.
Now you need to add a new data source to the project - reports.mdb.Drop the button on form for launching of the reports designer.

Write in the Click event handler:
private void button1_Click(object sender, EventArgs e)
{
report1.Design();
}

Create an additional dialog form for template selection:

Assign the buttons OK and Cancel corresponding to DialogResult.
Do not forget to bind your data source to DataGrid - select the field name of the record and make the index field invisible.
You need to save the selected index when you click on the ‘OK’ button:
public int reportID;
...
private void OKBtn_Click(object sender, EventArgs e)
{
// save id of report for use in future
reportID = (int)dataGridView1.CurrentRow.Cells[0].Value;
}

We return to our first form. It is time to handle the events of opening and saving a report. We make loading the first thing to do:
// define variable for store of report ID
private int reportID;
// define array byte[] for store of template
private byte[] blob;

....
private void environmentSettings1_CustomOpenDialog(object sender,
FastReport.Design.OpenSaveDialogEventArgs e)
{
using (ReportListForm reportListForm = new ReportListForm())
{
// show dialog for report selection
if (reportListForm.ShowDialog() == DialogResult.OK)
{
// get report ID
reportID = reportListForm.reportID;
// load report in array from BLOB
blob =
(byte[])this.reports_tableTableAdapter.GetDataByID(reportID).Rows[0]["ReportFile"];
// read file name of report for designers title
e.FileName =
(string)this.reports_tableTableAdapter.GetDataByID(reportID).Rows[0]["ReportName"];
}
else
// cancel loading
e.Cancel = true;
}
}

Second handler CustomOpenReport for loading the template in designer should look like this:
private void environmentSettings1_CustomOpenReport(object sender,
FastReport.Design.OpenSaveReportEventArgs e)
{
using (MemoryStream stream = new MemoryStream())
{
// skip all garbage created by MS Access in begin of blob field - we seek the tag of XML
int start = 0;
for (int i = 0; i < blob.Length - 1; i++)
{
if (blob[i] == (byte)'<' && blob[i + 1] == (byte)'?')
{
start = i;
break;
}
}
// copy of blob content in stream
stream.Write(blob, start, blob.Length - start);
stream.Position = 0;
// load template in designer
e.Report.Load(stream);
}
}

We used mdb-database to store report templates. MS Access write special header in begin of BLOB field. We need to skip this header and find begin of xml file (report templates stored in xml).
We turn to the organization and save the template using the designer. As written above, we need to override the event handler CustomSaveDialog by an empty function to prevent a user from unnecessary actions:
private void environmentSettings1_CustomSaveDialog(object sender,
FastReport.Design.OpenSaveDialogEventArgs e)
{
// empty
}

Event handler CustomSaveReport should look like this:
private void environmentSettings1_CustomSaveReport(object sender,
FastReport.Design.OpenSaveReportEventArgs e)
{
// create stream for store of template
using (MemoryStream stream = new MemoryStream())
{
// save template in stream
e.Report.Save(stream);
// copy stream in array
byte[] blob = stream.ToArray();
// save array in BLOB
this.reports_tableTableAdapter.UpdateReport(blob, reportID);
}
}
The methods GetDataByID and UpdateReport are created in the dataset designer- you can see them in the example project.
Compile and run application, then press a button to open the designer:

Click menu item "Open":

Select any report template and press OK.

You can change the report and save the template after that- new template will be stored in database.
Full source code of this example is available in an attached zip archive (databases with reports are included).

Reporting in Delphi threads

Sometimes we need create many reports in multiple threads simultaneously. This may be the development of a web service, or output information from an existing multithreaded application in a certain needed document format (PDF as example).
I noted that FastReport VCL library is a better solution for document generation in multiple formats. This component library is easy to use and has a convenient report designer which allows you to easily connect to different data sources, among which may be internal application data - arrays, sets of parameters, etc.
Traditional use of FastReport does not give any difficulties, but now we have to use this report generator in a multithreaded application. The output file format will be PDF.
TfrxReport class has a description of several properties which need to be set immediately after the creation of the object.
You need to remember that the object must work in a thread without the creation of dialogs, progress bars and other visual information.
Here is an example of creating and setting an object of class TfrxReport before the execution of the report:
// create report
FReport := TfrxReport.Create(nil);
// disable all messages
FReport.EngineOptions.SilentMode := True;
// enable safe work in threads
FReport.EngineOptions.EnableThreadSafe := True;
// disable cache
FReport.EngineOptions.UseFileCache := false;
// disable progress bar
FReport.ShowProgress := False;

Some reports have integrated dialog forms, and showing them should be banned for obvious reasons. We need to override an event handler TfrxReport.Engine.OnRunDialog by the procedure ShowReportDialog for to any dialogs.
// handle all dialogs by ShowReportDialog
FReport.Engine.OnRunDialog := ShowReportDialog;

Our procedure will be executed instead of showing each report dialog. We can change the state of any control in a dialog, but we will leave this procedure empty.
procedure TTestThread.ShowReportDialog(Page: TfrxDialogPage);
begin
// empty
end;

Then we create an object of TfrxPDFExport and disable the showing of dialog window and progress bar.
PDF := TfrxPDFExport.Create(nil);
PDF.ShowDialog := False;
PDF.ShowProgress := False;

All operations on the creation and exporting of report objects can be done in the constructor of the thread. The destructor of the thread should look like:
destructor TTestThread.Destroy;
begin
// destroy all created objects
PDF.Free;
FReport.Free;
inherited;
end;

Necessary objects are created and configured. Now you can load the report template from a file and run a report in the implementation of the main thread procedure Execute. There, it will also be exported to the desired format.

// load report template
FReport.LoadFromFile(FFileName);
// set report variables
FReport.Variables['ThreadID'] := QuotedStr(FId);
// run report
if FReport.PrepareReport then
begin
// save result in PDF
PDF.FileName := FOutPath + '\report_'+ FId +
'_' + FormatDateTime('YYYYYMMDDHHMMSS', Now) + '.pdf';
FReport.Export(PDF);
end;

Try building reports without using RichText objects because by so doing you can get an unstable application.

Do not forget to include ActiveX controls in uses module, and add a call to CoInitialize (nil); in the procedure Execute before creating a report if the report is connected to ADO. Call to CoUninitialize at the end of the thread procedure.

// thread function
procedure TTestThread.Execute;
begin
// initialize COM library in current thread
CoInitialize(nil);
try
// load report template from the file
FReport.LoadFromFile(FFileName);
...
...
...
finally
// Uninitialize COM
CoUninitialize;
end;
end;
end;

You can see an attached example with the application which creates 10 reports in multiple threads and write many PDF files.