Rave Report, PDF and Intraweb - Felix John COLIBRI. |
- abstract : how to produce PDF reports, and have an Intraweb site generate and display .PDF pages, with multi-user access
- key words : Rave Reports - .PDF report - Intraweb site - DirectDataView -
DriverDataView - Critical Section - Session management
- software used : Windows XP Home, Delphi 2006, Rave Report BE 6.5, Intraweb 8
- hardware used : Pentium 2.800Mhz, 512 M memory, 140 G hard disc
- scope : Delphi 7, Delphi 8 Delphi 2005, 2006, Turbo Delphi, Turbo 2007, Rad Studio 2007
- level : Delphi developer
- plan :
1 - Rave Report, PDF and Intraweb Intraweb is a very efficient tool for building web sites. Applications, mainly in the business area, also require some report output. And with the current
versions of Delphi, Rave Report is the candidate of choice. This article will present how to build Rave reports in PDF format from an Intraweb page.
2 - Rave and PDF reports
2.1 - simple Rave Report Let's first create a simple Rave report hooked to some Table. We will use the EMPLOYEE Table, and to keep the data source simple, we will use the .XML file. Therefore: Note that the tDbGrid is not necessary for the rest of this project, but allows us to easily check that everything is correctly initialized.
Now drop the Rave components on the Delphi side: | from the Rave tab, drop a tRvProject on the Form |
| then drop a tRvDatasetConnection, and connect its DataSet connection to ClientDataset1 |
2.2 - Using the Rave Designer To build our report, we will use the Rave Designer
For those unfamiliar with the Rave Designer: - 1 is the menu
- 2 the toolbar
- 3 the Palette
- 4 the Object Inspector
- 5 the design area
- 6 the component Treeview
Also note that the Rave Designer is a separate .EXE (which can be launched from Delphi's BIN directory), which has a life of its own. It can be used independently from Delphi, and when we build reports we will have save the
report in a file with a .RAV extension, and we will have to reference this file in Delphi to use this report layout.
We will create the Dataview which allows us to get access to our tDataSet:
| select "File | New" | | a new rave design project is created |
| select "File | New Data Object" | | a dialog with different data view connections is presented
| | select "Direct Data View" | |
a dialog with the only tRvDataSetConnection available, which is RvDataSetConnection1, is displayed | |
select this connection and click "Finish" | | a new DataView1 is added to the TreeView "DataViewDictionary", with all
the fields of ClientDataSet1 |
We will build a report layout with the rows of this DataView1:
To check that everything is correctly setup, we will preview the result:
And now the more important step before we leave the report, we must save this report layout in a .RAV file:
| select "File | Save" | |
a "Save Dialog" is presented, usually with a path totally unrelated to our current Delphi project, like the path you used 3 weeks ago for another Rave Designer project |
| navigate to our Delphi folder and save the report, as, for instance, R_EMPLOYEE.RAV |
At this stage you can close the Rave Designer. Note however that this report is named "Report1" (at the top of the TreeView), as this name will be used later to build the .PDF file.
2.3 - PDF output report
Rave report has many possible outputs: - the printer, obviously
- a .PDF file
- a .RTF file
- and some other like .HTML, internal Rave format (layout+data) etc
To produce a .PDF report, the steps are the following: | go back to the Delphi project |
| drop a tRvSystem component on the Form. Then - to produce a .PDF report file, in its DefaultDest property select rdFile
- to avoid the display of the "Output Options Dialog", select SystemSetups, click the + expand icon, and toggle ssAlowSetup to False
|
| select RvProject1 - select its Engine property and connect it to RvSystem1
- select its ProjectFile property, click the ellipsis ... and select the R_EMPLOYEE.RAV file
| |
drop a tRvRenderPDF component, and toggle Active to True (this is the default anyway) | |
drop a tButton on the Form, create its OnClick event, and type the following code:
procedure TForm1.output_pdf_Click(Sender: TObject);
begin with RvSystem1 do
begin DoNativeOutput:= False;
RenderObject:= RvRenderPDF1;
OutputFileName:= 'my_report.pdf';
end; // with RvSystem1 RvProject1.Execute;
end; // output_pdf_Click | | |
compile, run, click "output_pdf" | | the MY_REPORT.PDF is saved in the current directory (where the .EXE is) |
| click on the MY_REPORT.PDF file | | Acrobat is loaded, and its content displayed:
|
3 - Intraweb Rave Report 3.1 - The Intraweb Project
Let's setup the basic Intraweb site. We will use a standalone project, which avoids the IIS configuration steps. Therefore: | create a new folder |
| start a new Intraweb project by selecting "File | New | Other | Delphi Projects | Intraweb | Intraweb Application" |
| the Intraweb "Application Wizzard" is displayed | |
keep its default values ("Standalone Application" and "Create User Session") and in the "Project Directory" copy the name of your folder | |
save this project | | compile and run it | |
the project's Server is displayed | | type F9 | |
the browser is displayed with an empty page The firewall will eventually block the loading of the browser, but accepting this new page will unlock this |
We will place a tDataset and a tIwDbGrid on our page:
3.2 - Displaying a .PDF file Second step: how to present a .PDF file by clicking on an Intraweb link or button. The main points are
- create a FILES/ sub directory and place the .PDFs in this folder
- create a pop-up web page by code which will display the .PDF report
Therefore |
create the FILES/ sub directory in the folder containing your Intraweb .EXE | |
copy the previous .PDF, in our case MY_REPORT.PDF into this folder | | from the "IW Standard" tab, add a tIwButton to the Form, create its
OnClick event, and use the following code:
procedure TIWForm1.displaypdfClick(Sender: TObject);
var l_pdf_file_name: string;
l_popup_page_name: string;
l_popup_page_options: string;
l_pdf_url: string; begin
l_pdf_file_name:= 'my_report.pdf'; l_popup_page_name:= 'the_report';
l_popup_page_options:= 'scrollbars=yes,width=400,height=600';
l_pdf_url:= WebApplication.AppURLBase+ '/files/'+ l_pdf_file_name;
AddToInitProc('NewWindow("' + l_pdf_url
+ '","'+ l_popup_page_name
+ '","'+ l_popup_page_options + '");');
end; // displaypdfClick | | |
compile, run, click F9, click "displaypdf" | | the report is displayed in a pop-up page: |
3.3 - Generating the Report
To generate a simple .PDF report, we will use the same technique as the one used with a simple Win32 project, using a tRvProject, tRvDataSetConnection, tRvSystem, tRvRenderPdf:
| from the "Rave" tab, drop the tRvProject, tRvDataSetConnection, tRvSystem, tRvRenderPdf and connect them exactly as in the previous example |
| drop a tIwButton, and, in its OnClick event, create the .PDF file in the FILES/ subdirectory:
procedure TIWForm1.CreatePdfClick(Sender: TObject);
begin with RvSystem1 do
begin DoNativeOutput:= false;
RenderObject:= RvRenderPDF1;
OutputFileName:= 'FILES\my_report_3.pdf';
end; // with RvSystem1 RvProject1.Execute;
end; // CreatePdfClick | |
3.4 - Multi User .PDF generation
The previous approach works, but since Rave is not thread save, when several users want to generate .PDF files, we run into race problems. The solution recommended on the Intraweb web site is to protect the generation
of the report with some synchronizing object, like a critical section, and to create the tRvDataSetConnection for each report.
Let's try this solution. There are several points to take care of
- the critical section is created in the Initialization section of the Unit, and freed in the Finalization section
- if the reports are confidential, we must ensure that one User cannot read
the .PDF build for another User. The easiest way to do this is by creating separate folders, using the IntraWeb SessionId, which is a "non-guessable" value. During a Session, this value is retrieved using
WebApplication.AppID.
- this User directory must be created before the .PDF is generated, and deleted after the use of the .PDF. We can use the tIwServerControllerBase.OnCloseSession event to perform this
Here is our project: | copy the previous project to a new folder, and remove the two tIwButtons and their events |
| in the Project Manager, select the ServerController, and in the Object
Inspector, toggle IwServerController.AllowSubFolder to True | |
declare a global tCriticalSection variable, initialize it and free it in the Initialization and Finalization section of the Unit:
unit u_irp_safe_page; interface // -- ...ooo...
implementation uses SyncObjs; {$R *.dfm}
var // -- critical section to serialize the .PDF generation
g_c_rave_critical_section: TCriticalSection= Nil;
// -- ...ooo... initialization
TIWForm1.SetAsMainForm;
g_c_rave_critical_section:= TCriticalSection.Create;
finalization g_c_rave_critical_section.Free;
end | | |
drop a tIwButton and create the OnClick event which will generate and display the .PDF report
procedure TIWForm1.GenerateAndDisplayClick(Sender: TObject);
var l_pdf_file_name: string;
l_session_id_segment: String; procedure generate_pdf;
var l_c_rave_dataset_connection: TRvDataSetConnection;
begin ClientDataSet1.Close;
ClientDataSet1.IndexFieldNames:= g_sort_column;
ClientDataSet1.Open; RVProject1.ProjectFile:=
WebApplication.ApplicationPath+ '..\_data\r_employee.rav';
with RvSystem1 do begin
DoNativeOutput:= false;
RenderObject:= RvRenderPDF1;
OutputFileName:= IWServerController.FilesDir
+ l_session_id_segment+ '\'+ l_pdf_file_name;
end; // with RvSystem1
// -- create the user specific path
ForceDirectories(ExtractFileDir(RvSystem1.OutputFileName));
// -- enter into the critical section
g_c_rave_critical_section.Enter;
// -- create a new RvDataSetConnectoin
l_c_rave_dataset_connection:= TRvDataSetConnection.Create(self);
try
l_c_rave_dataset_connection.Name:= 'my_rave_dataset_connection';
l_c_rave_dataset_connection.DataSet:= ClientDataset1;
l_c_rave_dataset_connection.Name:= 'dataset_connection_'+ l_session_id_segment;
l_c_rave_dataset_connection.DataSet:= ClientDataset1;
RvProject1.ExecuteReport('Report1');
finally l_c_rave_dataset_connection.Free;
g_c_rave_critical_section.leave;
RvSystem1.OutputFileName:= '';
end; // try/finally end; // generate_pdf
procedure display_pdf;
var l_pdf_url: String;
l_popup_page_name: string;
l_popup_page_options: string;
l_popup_parameter: String; begin
l_pdf_url:= WebApplication.AppURLBase+ '/files/'
+ l_session_id_segment+ '/'+ l_pdf_file_name;
IwLabel2.Caption:= l_pdf_url;
l_popup_page_name:= 'the_report';
l_popup_page_options:= 'scrollbars=yes,width=500,height=300';
l_popup_parameter:= 'NewWindow("' + l_pdf_url
+ '", "'+ l_popup_page_name
+ '","'+ l_popup_page_options
+ '");'; AddToInitProc(l_popup_parameter);
end; // display_pdf begin // GenerateAndDisplayClick
l_session_id_segment:= WebApplication.AppID;
l_pdf_file_name:= 'safe_report_'
+ g_sort_column+ '_'+ l_session_id_segment+ '.pdf';
generate_pdf; display_pdf;
end; // GenerateAndDisplayClick | | |
in the Project Manager, create the OnCloseSession event, and erase all PDF's of this session:
procedure TIWServerController.IWServerControllerBaseCloseSession(
ASession: TIWApplication);
const k_path_delimiter= '\';
var l_output_file_dir: string;
l_search_record: TSearchRec; begin
l_output_file_dir:= GServerController.FilesDir+ ASession.AppID;
// -- CleanUp session's files directory
if FindFirst(l_output_file_dir+ k_path_delimiter+ '*.*',
faAnyFile, l_search_record)= 0 then
begin repeat
DeleteFile(
l_output_file_dir+ k_path_delimiter+ l_search_record.Name);
until FindNext(l_search_record)<> 0;
FindClose(l_search_record); end;
RemoveDir(l_output_file_dir); end; // IWServerControllerBaseCloseSession
| | | compile, run, type F9, click "generate_and_display" |
| the PDF is displayed in a separate page, and, if you look at the FILES/ subfolder, you will see the session folder with the .PDF in it |
| close the Intraweb Server | | the session folder is removed |
We also tried to generate several kinds of reports, by changing some parameters of the tDataSet: the order, filtering rows etc (but NOT the column structure, which is hard coded in the .RAV)
| drop a tIwListbox, with the list of some columns, and create the event which will sort the ClientDataSet1:
var g_sort_column: String= '';
procedure TIWForm1.IWListbox1Click(Sender: TObject);
begin with IwListBox1 do
case ItemIndex of
0 : g_sort_column:= 'EmpNo';
1 : g_sort_column:= 'LastName';
2 : g_sort_column:= 'PhoneExt';
else g_sort_column:= '';
end; // case
ClientDataSet1.IndexFieldNames:= g_sort_column;
end; // IWListbox1Click | | |
run, select a PhoneExt sort, click "generate_and_display" | | the PDF is displayed sorted in PhoneExt order |
| you may repeat the operation with an other sort order |
Please note that:
- IwServerController.AllowSubFolder only exists for Intraweb versions after 5.1
- the code within the critical section should be kept as short as possible
- there are many possible options for the pop-up page. We could use code like:
const k_options= 'toolbar=no,status=no,menubar=yes,scrollbars=yes'
+ ',resizable=yes,location=no,directories=no'
+ ',width=550,height=230';
k_format_mask= 'NewWindow("%s", "%s", "%s");';
AddToInitProc(Format(k_format_mask, [my_url, my_page_name, k_options));
|
To our big dismay, we did not succeed to start another user. The result was that the second user always received the last report produced by the first user.
So it still escapes us why we had to use a critical section with no apparent benefit.
4 - Multi User Intraweb .PDF report 4.1 - Other solutions
The newsgroups tell us that there are two other possibilities: - either directly create the report using code (no Rave Designer)
- or use a special Rave DataView, the Driver Data View
We will try the second approach. DriverDataViews are Rave Designer's DataViews which allow us to create reports directly from the Rave Designer: we do NOT need to use a tDataSet
on the Form. So the Rave Designer directly reads the data from some source. Not too difficult, knowing that the Rave Designer is written in Delphi. But
at the same time, we can still set the Sql request of this report from the Delphi application. And the DriverDataViews have been specially written to allow multi-user access.
4.2 - Driver Data View
Here is how to create a Rave report using a DriverDataView:
4.3 - Displaying the Report The steps are identical as the ones with a DirectDataView, but, to prepare us
for a multi-report application we will set the Sql query from Delphi: | go back to the Delphi project |
| select RvProject1 and initialize ProjectFile with R_DRIVER_EMPLOYEE.RAV | |
drop a tButton, and in its Click event, and set the DriverDataView Sql request. The tDriverDataView is found using tRvProject.ProjMan.FindRaveComponent
procedure TForm1.display_report_Click(Sender: TObject);
var l_c_rave_driver_data_view: TRaveDriverDataView; begin
With RvProject1 do begin
Open; // -- get a link to the DriverDataView
l_c_rave_driver_data_view:=
ProjMan.FindRaveComponent('DriverDataView1', nil) As TRaveDriverDataView;
// -- modify the Sql request
l_c_rave_driver_data_view.Query:=
'SELECT * '
+ ' FROM employee'
+ ' ORDER BY PhoneExt ' ;
Execute; end; // With RvProject1
end; // display_report_Click | | |
add RVData and RvDriverDataView to the USES clause | |
add RvDlBDE to the USES clause (or whatever .DLL is required by Rave to access your Sql engine) | | run, click "display_report |
| the preview dialog is presented, and the preview displayed |
We can also generate a .PDF report on disk:
| add another tRvProject, and initialize ProjectFile with R_DRIVER_EMPLOYEE.RAV | |
add a tRvSystem component, link the RvReport2.Engine to it, and initialize DefaultDest and SystemSetups | |
add a tRvRenderPdf component | | add a tButton, and write the code to generate the .PDF:
procedure TForm1.output_pdf_Click(Sender: TObject);
begin With RvProject2 do
begin Open;
with ProjMan.FindRaveComponent('DriverDataView1', nil)
As TRaveDriverDataView do
Query:= 'SELECT * '
+ ' FROM employee'
+ ' ORDER BY PhoneExt ' ;
end; // with RvProject2
with RvSystem1 do begin
DoNativeOutput:= False;
RenderObject:= RvRenderPDF1;
OutputFileName:= 'my_report_4.pdf';
end; // with RvSystem1 RvProject2.Execute;
end; // output_pdf_Click | | |
run, click "output_pdf_" |
4.4 - Multi User Intraweb PDF reports And now, lets try the multi user version again: |
copy the Intraweb+Rave previous project in a new folder | | add the RVData RvDriverDataView and RvDlBDE to the USES clause |
| replace the previous "GenerateAndDisplay" code with the following one:
procedure TIWForm1.GenerateAndDisplayClick(Sender: TObject);
var l_pdf_file_name: string;
l_session_id_segment: String; procedure generate_pdf;
begin With RvProject1 do
begin
ProjectFile:= WebApplication.ApplicationPath
+ '..\_data\r_driver_employee.rav'; Open;
with ProjMan.FindRaveComponent('DriverDataView1', nil)
As TRaveDriverDataView do
Query:= 'SELECT * '
+ ' FROM employee'
+ ' ORDER BY '+ g_sort_column
; end; // with RvProject1
with RvSystem1 do begin
DoNativeOutput:= False;
RenderObject:= RvRenderPDF1;
OutputFileName:= IWServerController.FilesDir
+ l_session_id_segment+ '\'+ l_pdf_file_name;
end; // with RvSystem1
// -- create the user specific path
ForceDirectories(ExtractFileDir(RvSystem1.OutputFileName));
iwLabel1.Caption:= 'not ok'; try
RvProject1.ExecuteReport('Report1');
iwLabel1.Caption:= 'ok'; finally
RvSystem1.OutputFileName:= '';
iwLabel1.Caption:= iwLabel1.Caption+ ' unlocked';
end; // try/finally end; // generate_pdf
procedure display_pdf;
var l_pdf_url: String;
l_popup_page_name: string;
l_popup_page_options: string;
l_popup_parameter: String; begin
l_pdf_url:= WebApplication.AppURLBase+ '/files/'
+ l_session_id_segment+ '/'+ l_pdf_file_name;
IwLabel2.Caption:= l_pdf_url;
l_popup_page_name:= 'the_report';
l_popup_page_options:= 'scrollbars=yes,width=500,height=300';
l_popup_parameter:= 'NewWindow("' + l_pdf_url
+ '", "'+ l_popup_page_name
+ '","'+ l_popup_page_options
+ '");'; AddToInitProc(l_popup_parameter);
end; // display_pdf begin // GenerateAndDisplayClick
l_session_id_segment:= WebApplication.AppID;
l_pdf_file_name:= 'multi_report_'
+ g_sort_column+ '_'+ l_session_id_segment+ '.pdf';
generate_pdf; display_pdf;
end; // GenerateAndDisplayClick | | |
run, click F9, click "GenerateAndDisplay" | | select the Intraweb Server and "Tools | Copy Start URL" |
| start another version of your browser (to simulate another user), paste the URL, and generate some report pages |
4.5 - Rave Architecture
Here is a diagram summarizing the properties we used from Rave in our Delphi code:
The organization of the Intraweb components was already presented in our
Intraweb Architecture paper.
5 - Intraweb and Rave comments 5.1 - Rave and Intraweb
The commercial presentation is proud to announce the "Rave reports from Intraweb". As we experience, this is not all that easy. We derived all our knowledge from the Delphi newsgroups. That's where you do appreciate a good newsgroup spider.
Just a couple of additions: - Chad Z. HOWER wrote the original DriverDataViews, to allow reports despite the thread safety problems of Rave
- some Rave support messages mentioned that what we presented only works for
Standalone Intraweb. Is is untested if we uses a .DLL (Isapi etc)
On the documentation side, both are very much alike - demo programs which are somehow sensitive to the version (if you try to run
them with another Delphi version than the version used for writing the original demo might not compile). Well, that's part of standard Delphi adjustments
- the help is quite bizare: in Object Oriented Programs, you have a list of Classes, each with Properties, Events and Methods. Both helps are not exactly organized along those lines
- some of the .PDF manuals are not clearly dated, and it might be difficult to link whatever manual you find on the web with the components you have on your Palette
- neither company is very eager to add external links to articles about their product. Maybe there are overflooded with articles. Or perhaps they estimate that our articles are not good enough. Or they do not like the color of our
pages ... And the same goes for adding a link to companies offering trainings for their products. Well, we will try again this time, and we will see what happens !
- however, both companies are very present in the Delphi newsgroups, trying to help out Delphi developers stuck with their so-so documentation. So, your best alternative is to post your questions there (and certainly on
their own newsgroups, but we did not visit those).
5.2 - Rave Training and Intraweb Training We are regularily orgainizing Rave Reports
Trainings, as well as IntraWeb trainings, anywhere in the world (of interest, of course, if there are several developers). You may contact us at fcolibri@felix-colibri.com for more details.
6 - Download the Sources Here are the source code files: The .ZIP file(s) contain: - the main program (.BDSPROJ, .DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP:
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder. The Pascal code uses the Alsacian notation, which prefixes identifier by
program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will
be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate:
the more traffic and Google references we get, the more articles we will write.
7 - The author Felix John COLIBRI works at the Pascal
Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly
active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi
Xe_n migrations, refactoring), Delphi Consulting and Delph
training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions. |