Reporting in FireMonkey applications

By: Mathias Burbach

Abstract: A FireMonkey client application that can use FastReport on the server side to create PDF files that are streamed to the client and shown in the default PDF reader of the client machine.

Reporting in FireMonkey applications is not as straight forward as it has been for many years in VCL applications. Whilst many report generator vendors are exploring options for porting their products to FireMonkey, we had to solve the business case to provide PDF reports of invoices in a FireMonkey application talking to a DataSnap server right now. This DataSnap back-end running under Windows came to our rescue as we can create reports with FastReports, which comes with Delphi XE2. Those reports can be stored on the server side and streamed out to the FireMonkey client application as requested. Instead of trying to show the PDF file inside the FireMonkey application we decided to store it in a local folder, open the file with the standard PDF reader installed and delete the local PDF files when the client is shutdown.

Let's start with the server side.

Hide image
EmployeeReportMethods


The server connects to InterBase XE's sample database Employee.gdb, for which we created an alias named Employee. A simple query is used to select a subset of employees based on the employee number Emp_No:

Select First_Name, Last_Name, Emp_No
From Employee
Where Emp_No Between :FromEmpNo And :ToEmpNo
Order By First_Name, Last_Name

The query is hooked up to a TfrxDBDataSet, which is then used as the main dataset for the FastReport frxEmployeeReport. A TfrxPDFExport component is dropped into the same data module to be able to export the report to PDF. Here is the implementation of the server method to retrieve a report for a particular employee number range:

function TsvmEmployeeReport.GetEmployeeReport(const FromEmpNo, ToEmpNo: Word): TStream;
var
  sFileName: string;
  GUID: TGUID;
begin
  Result := nil;
  sFileName := srvContainer.ReportPath;
  ForceDirectories(sFileName);
  CreateGUID(GUID);
  sFileName := sFileName + Format('%s.pdf', [GUIDToString(GUID)]);
  conIBEmployee.Open;
  try
    qryEmployee.ParamByName('FromEmpNo').AsInteger := FromEmpNo;
    qryEmployee.ParamByName('ToEmpNo').AsInteger := ToEmpNo;
    frxPDFExport.FileName := sFileName;
    frxEmployeeReport.PrepareReport;
    frxEmployeeReport.Export(frxPDFExport);
  finally
    conIBEmployee.Close;
  end;
  if
FileExists(sFileName) then
    Result := TFileStream.Create(sFileName, fmOpenRead);
end;

First we ask the server container instance called srvContainer where to place the temporary PDF files on the server side (e.g. in ReportPath). Then we create a temporary file name for the PDF file using a GUID. In our real business scenario we kept the invoice files on the server side in folders ordered by year and month and did not delete the file after streaming it out. Once we have a file name we open the dbExpress connection to InterBase and set the parameters of the query qryEmployee. The PDF export filter of FastReport is instructed where to place the file (e.g. frxPDFExport.FileName := sFileName), the report is prepared (e.g. frxEmployeeReport.PrepareReport) and finally exported (e.g. frxEmployeeReport.Export(frxPDFExport)). Preparing the report will also open the query and once the FastReport export filter has done its job the query is closed by FastReport as well. In the end we should have a PDF file on the server side, that can be streamed out via a file stream as the return value of the DataSnap server method.

In order to avoid accumulating PDF files on the server side in the sample application we used the TDSTCPServerTransport.OnDisconnect event in the server container unit to delete all PDF files that are not in use any more (e.g. are not streamed out right now).

On the FireMonkey client side we designed a fairly simplistic form to choose a the employee number range and start the server call via a button:

Hide image
FireMonkey Client UI


All the DataSnap & stream handling was implemented in a data module on the client side after creating the client-side proxy classes for the DataSnap server.

procedure TdmoMain.OpenEmployeeReport(const FromEmpNo, ToEmpNo: Word);
const
  cBufferSize = 32 * 1024;
var
  sFileName: string;
  svr: TsvmEmployeeReportClient;
  stReportAsPDF: TStream;
  fsTempFile: TFileStream;
  Buffer: PByte;
  iBytesRead: Integer;
begin
  sFileName := EmptyStr;
  conAppServer.Open;
  try
    svr := TsvmEmployeeReportClient.Create(conAppServer.DBXConnection);
    try
      stReportAsPDF := svr.GetEmployeeReport(FromEmpNo, ToEmpNo);
      if Assigned(stReportAsPDF) then
      begin

        sFileName := GetReportsPath;
        ForceDirectories(sFileName);
        sFileName := sFileName + Format('%d-%d.pdf', [FromEmpNo, ToEmpNo]);
        fsTempFile := TFileStream.Create(sFileName, fmCreate);
        try
          GetMem(Buffer, cBufferSize);
          try
            repeat
              iBytesRead := stReportAsPDF.Read(Pointer(Buffer)^, cBufferSize);
              if iBytesRead > 0 then
                fsTempFile.WriteBuffer(Pointer(Buffer)^, iBytesRead);
            until iBytesRead < cBufferSize;
          finally
            FreeMem(Buffer, cBufferSize);
          end;
        finally

          fsTempFile.Free;
        end;
      end;

    finally

      svr.Free;
    end;
  finally

    conAppServer.Close;
  end;
  if
(sFileName <> EmptyStr)  and (FileExists(sFileName)) then
  begin

    {$IFDEF MSWINDOWS}
    ShellExecute(0, 'OPEN', PChar(sFileName), '', '', SW_SHOWNORMAL);
    {$ENDIF MSWINDOWS}
    {$IFDEF POSIX}
    _system(PAnsiChar('open ' + AnsiString(sFileName)));
    {$ENDIF POSIX}
  end;
end;


First we connect to the application server via a TSQLConnection called conAppServer in the sample application. Then we create an instance of the client-side proxy (e.g. TsvmEmployeeReportClient) to talk to the server report methods. The server method GetEmployeeReport is executed and the resulting stream is made available in stReportAsPDF. Unfortunately we don't know at this stage how big the stream will be in the end. This is why we need to read from it into a buffer (here 32KB of size) until we have come to an end. Each chunk of 32KB - or smaller at the end - is written to a file stream (e.g. fsTempFile). Please note that we do not need to free the stream result of the DataSnap method return value, as it will be freed by the DataSnap framework when freeing the client-side proxy instance. Last but not least, we need to open the downloaded PDF file via ShellExecute under Windows or a  _system 'open' call under Mac OS.

Again we need to clean up behind us when the client application is closed. In the OnDestroy event of the main datamodule we call our own DeleteAllTempFiles method, which is really simple to implement thanks to the new System.IOUtils unit:

procedure TdmoMain.DeleteAllTempFiles;
var
  FileNames: TStringDynArray;
  sFileName: string;
begin
  FileNames := TDirectory.GetFiles(GetReportsPath, '*.pdf');
  for sFileName in FileNames do
    System.SysUtils.DeleteFile(sFileName);
end;

As a result we have a FireMonkey client application that can use FastReport on the server side to create PDF files that are streamed to the client and shown in the default PDF reader of the client machine.

You can download the sample code from here..


Server Response from: ETNASC03