Sunday, April 19, 2009

 

Apache 2.2.x modules with Delphi


If you want to create Apache modules with Delphi, you can start reading DrBob's Delphi 2006 and Apache Web Apps and this articles, they explain step by step how to create Apache modules with Dephi.

Apache 2.2.x

DrBob articles covers up to Apache 2.0.63, but the current version of the http server is 2.2.x, if you try to run them in this version, they won't work.

Don't worry, to make it work with the new version, you have to change and add a couple of lines to the same HTTPD2.pas file as follows:

Before start changing anything, copy the files HTTPD2.pas, ApacheTwoApp.pas and ApacheTwoHTTP.pas from the Delphi /source/win32/internet directory to the directory of your project, in this example c:\projects\apachemodule.

Changes

Find the text in the left column and replace by the one in the right.

Old HTTPD2.pas
New HTTPD2.pas
LibAPR = 'libapr.dll';     {do not localize}
LibAPR = 'libapr-1.dll';     {do not localize}
MODULE_MAGIC_COOKIE = $041503230;  (* "AP20" *)
{$EXTERNALSYM MODULE_MAGIC_COOKIE}
MODULE_MAGIC_NUMBER_MAJOR = 20020903; { Apache 2.0.42 }
{$EXTERNALSYM MODULE_MAGIC_NUMBER_MAJOR}
MODULE_MAGIC_NUMBER_MINOR = 0; (* 0...n *)
MODULE_MAGIC_COOKIE = $041503232; (* "AP22" *)
{$EXTERNALSYM MODULE_MAGIC_COOKIE}
MODULE_MAGIC_NUMBER_MAJOR = 20051115; { Apache 2.2.x }
{$EXTERNALSYM MODULE_MAGIC_NUMBER_MAJOR}
MODULE_MAGIC_NUMBER_MINOR = 0; (* 0...n *)
{$EXTERNALSYM apr_bucket_alloc_t}
{$EXTERNALSYM apr_bucket_alloc_t}
ap_conn_keepalive_e = (AP_CONN_UNKNOWN, AP_CONN_CLOSE, AP_CONN_KEEPALIVE);
(** Are we going to keep the connection alive for another request?
* @see ap_conn_keepalive_e *)
{keepalive: ap_conn_keepalive_e;}
(** Are we going to keep the connection alive for another request?
* @see ap_conn_keepalive_e *)
keepalive: ap_conn_keepalive_e;
(** The bucket allocator to use
for all bucket/brigade creations *)
bucket_alloc: Papr_bucket_alloc_t;
end;
  (** The bucket allocator to use
for all bucket/brigade creations *)
bucket_alloc: Papr_bucket_alloc_t;
// New for Apache 2.2
(** The current state of this connection *)
cs: Pointer;
(** Is there data pending in the input filters? *)
data_in_input_filters: Integer;
end;


Delphi 2009

To create an Apache module with Delphi 2009, go to File->New->Other->Delphi Projects->WebBroker->WebServer Application and Select CGI Stand-Alone executable, then click OK. This will create a new project, then go to the "uses" clause of this project and replace CGIApp by ApacheTwoApp. That's it.

Update: I added an example to this article, please, read it here.

Saturday, April 18, 2009

 

Paginating TListView - Part 3 of 3


To finish this series of paginating TListViews, I'll show a simple method of automatic assignment of data from a database query to properties of an object.

Introducing RTTI

Delphi and FreePascal provides the ability to get and set the values of object's properties at run-time, this is called Run Time Type Information, RTTI for short. This allows a great deal of flexibility at the moment of reducing code size and automating repetitive tasks.

In the last example, the function GetCurrentPage executes a database query, then iterates trough it's dataset assigning each property of each item the FCustomers collection. One problem here, is that this function only works for TCustomers, so if we want to use another collection, we need to create a GetCurrentPage_for_our_new_tcollection and so on.

Automating GetCurrentPage

To generalize our GetCurrentPage method, we need to RTTI-enable the TCustomer class by replacing the "public" keyword by "published". This change tells the compiler that it's properties will be available to the RTTI functions like SetPropValue.

The second step is to include the TypInfo unit in the "uses" clause. This unit contains all the RTTI functions. I recommend further reading about it.

Now, the last step. Just replacing the lines 20 to 25 of GetCurrentPage function with this:


lCustomer := FCustomers.Add;
for I := 0 to IbQuery.Fields.Count - 1 do
SetPropValue(lCustomer,
IbQuery.Fields[I].FieldName,
IbQuery.Fields[I].Value);


Warning: This code works because the properties of TCustomer class have the same name as the fields returned by the query I used in the example.

Now, I'll remove the references to FCustomers in the function:


procedure TForm1.GetCurrentPage(ACurrentPage: Integer;
ACollection: TCollection);
var
lFrom: Integer;
lTo: Integer;
I: Integer;
lItem: TCollectionItem;

begin
(* Do the query *)
lFrom := ((ACurrentPage * cPageSize) - cPageSize) + 1;
lTo := (ACurrentPage * cPageSize) + 1;
IbQuery1.Close;
IbQuery1.SQL.Text :=
'select CustId, FirstName, LastName from customers ' +
'rows ' + IntToStr(lFrom) + ' to ' + IntToStr(lTo);
IbQuery1.Open;
(* Fill the collection *)
ACollection.Clear;
while not IbQuery1.Eof do
begin
lItem := ACollection.Add;
for I := 0 to IbQuery1.Fields.Count - 1 do
SetPropValue(lItem,
IbQuery1.Fields[I].FieldName,
IbQuery1.Fields[I].Value);
IbQuery1.Next;
end;
IbQuery1.Close;
end;


You can improve this by adding a new parameter for the Query to be executed inside the function. Also you have to adapt the ListView1Data event by this:


procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
lCurrPage: Integer;
lPos: Integer;
begin
(* Get current page index *)
lCurrPage := Item.Index div cPageSize;
(* Get the position in the current page *)
lPos := Item.Index - (lCurrPage * cPageSize);

(* Page changed? refresh the data *)
if FCurrentPage - 1 <> lCurrPage then
begin
FCurrentPage := lCurrPage + 1;
GetCurrentPage(FCurrentPage, FCustomers);
end;

(* Paint the ListView's item with our TCollection's items *)
Item.Caption := FCustomers[lPos].LastName;
Item.SubItems.Add(FCustomers[lPos].FirstName);
end;


This is the end of the series. This example could be improved even more, one way that comes to my mind is the creating a TListView descendant component.

Monday, April 06, 2009

 

Binding TCollections to paginating ListViews


In my previous article, I wrote about a method to paginate data shown in a ListView.
Now, I want to extend its reach by replacing TDataSets by TCollections.

What is a TCollection?

A TCollection is a container, where objects of only one type can be stored in it.
The difference with other containers like TList and TObjectList is they can
contain any kind of pointer, in the case of TList, and heterogenous objects in
a TObjectList, on the other hand, in a TCollection,
only objects of a specific class can be used.

A collection of Customers

To create a collection of Customers, we have to derive a class from TCollectionItem,
for example TCustomer, and another class from TCollection, for example
TCustomers, as follows:


TCustomer = class(TCollectionItem)
private
FCustId: Integer;
FLastName: string;
FFirstName: string;
public
property CustId: Integer read FCustId write FCustId;
property FirstName: string read FFirstName write FFirstName;
property LastName: string read FLastName write FLastName;
end;

TCustomers = class(TCollection)
private
function GetItem(AIndex: Integer): TCustomer;
public
function Add: TCustomer;
property Items[AIndex: Integer]: TCustomer read GetItem; default;
end;


Now lets define the methods Add and GetItem:


function TCustomers.GetItem(AIndex: Integer): TCustomer;
begin
result := TCustomer(inherited GetItem(AIndex));
end;

function TCustomers.Add: TCustomer;
begin
result := TCustomer(inherited Add);
end;


Save the above code in a unit called customers.pas, then create a new application,
and add the newly created unit to the "uses" clause of the main form, then add
the attribute FCustomers: TCustomers; to the private section of the form:


type
TForm1 = class(TForm)
private
FCustomers: TCustomers;
public
{ Public declarations }
end;



Now, override the OnCreate and OnDestroy methods of the main form to
instantiate and free the collection:


procedure TForm1.FormCreate(Sender: TObject);
begin
FCustomers := TCustomers.Create(TCustomer);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
FCustomers.Free;
end;


From database to collection

Ok, the next step is to fill the collection with data. As I explained in my last
article, place the database connectivity components in a DataModule or just
in the main form (this is just an example) and name the TQuery as IbQuery1.

Good, the next step is to adapt the FormCreate method of the last article
to this:


procedure TForm1.FormCreate(Sender: TObject);
begin
(* Create an instance of the customers collection *)
FCustomers := TCustomers.Create(TCustomer);
(* Get the total amount of customers *)
IbQuery1.Close;
IbQuery1.SQL.Text := 'select count(*) from customers';
IbQuery1.Open;
ListView1.Items.Count := IbQuery1.Fields[0].Value;
// query for the first page
FCurrentPage := 1;
GetCurrentPage(FCurrentPage);
end;


Now, to fill our TCollection with data from the database, just modify
our GetCurrentPage method as this:


procedure TForm1.GetCurrentPage(ACurrentPage: Integer);
var
lFrom: Integer;
lTo: Integer;
I: Integer;

begin
(* Do the query *)
lFrom := ((ACurrentPage * cPageSize) - cPageSize) + 1;
lTo := (ACurrentPage * cPageSize) + 1;
IbQuery1.Close;
IbQuery1.SQL.Text :=
'select CustId, FirstName, LastName from customers ' +
'rows ' + IntToStr(lFrom) + ' to ' + IntToStr(lTo);
IbQuery1.Open;
(* Fill the collection *)
FCustomers.Clear;
while not IbQuery1.Eof do
begin
with FCustomers.Add do
begin
CustId := IbQuery1.FieldByName('CustId').AsInteger;
FirstName := IbQuery1.FieldByName('FirstName').AsString;
LastName := IbQuery1.FieldByName('LastName').AsString;
end;
IbQuery1.Next;
end;
IbQuery1.Close;
end;


Let me tell you that assigning data by querying with FieldByName is very slow,
but simple enough for the purpouse of this article, in a future post, I'll
show an improved version.

The last step, is to place the TListView in the form, set its properties as
shown in my last article and create the new OnData method,
adapted to our TCollection:


procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
lCurrPage: Integer;
lPos: Integer;
begin
(* Get current page index *)
lCurrPage := Item.Index div cPageSize;
(* Get the position in the current page *)
lPos := Item.Index - (lCurrPage * cPageSize);

(* Page changed? refresh the data *)
if FCurrentPage - 1 <> lCurrPage then
begin
FCurrentPage := lCurrPage + 1;
GetCurrentPage(FCurrentPage);
end;

(* Paint the ListView's item with our TCollection's items *)
Item.Caption := FCustomers[lPos].LastName;
Item.SubItems.Add(FCustomers[lPos].FirstName);
end;


That's it.

My next article, will be focused on creating an automated object binding method
using RTTI, to avoid repetitive assignments by hand.

Sunday, April 05, 2009

 

ListView and Pagination


This is one of those things I needed for ages, but didn't bothered to implement
because I thought it could require an unnecessary amount of work, and allways
opted for a less perfectionist method.

I'm talking about a method to retrieve data in Pages, then browse it in a ListView
transparently for the user.

I know TDbGrid allows a similar behavior when connected to *some* database connectors,
such as ADO, but what about a general method of browsing paged data independently
of the database engine?.

Let's start by creating an application and a data module, containing a database
connection, a dataset and a transaction. I'll assume you know how to create
connections to databases, datasets and how to do queries.

Now, supposse the TDataSet is a TIbQuery component that allows to query an Interbase
or Firebird database, and we have a table called Customers, with one million records.
The table has three fields, CustId, FirstName and LastName.

ListView in Virtual Mode

Place a TListView in the main form, then set this properties:

ViewStyle = vsReport
Columns = (3 columns Id, FirstName, LastName)
OwnerData = True

The OwnerData property setted to True, means the ListView will not be a data repository
by itself, it won't contain any data. To show it on screen,
it will rely on its OnData method, who will be in charge of getting data from
the dataset and paint the rows of the ListView.

First attempt

Let's do a first essay by getting All the data from the database. TListView
in Virtual Mode, must know in advance how much data it will show, so
first of all, override the Form's OnCreate method with this:


procedure TForm1.FormCreate(Sender: TObject);
begin
IbQuery1.Close;
IbQuery1.SQL.Text := 'select count(*) from customers';
IbQuery1.Open;
ListView1.Items.Count := IbQuery1.Fields[0].Value;
// Re-Set the query
IbQuery1.Close;
IbQuery1.SQL.Text := 'select CustId, FirstName, LastName from customers';
IbQuery1.Open;
end;


The second step is to override the OnData method of the TListView with
this code:


procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
begin
(* Move the DataSet's pointer to Item.Index, and paint the ListView's item *)
IbQuery1.RecNo := Item.Index + 1;
Item.Caption := IbQuery1.Fields[0].Value;
Item.SubItems.Add(IbQuery1.fields[1].Value);
end;


In small datasets, this code will perform really god, but as the dataset
becomes huge, it will start getting slower, and slower.

Pagination

What I want to accomplish, is to divide the data in pages of say 100 records,
then show each page at a time. Querying only 100 records each time, is
practically instantaneous, so let's show the first page, then, when the user
tries to browse after the 100 nt record, re-query the database for the
101 to 200 and so on.

Almost every database engine has a method to query just a slice of the data,
MySql has "limit nn to mm", Firebird has "Rows nn to mm", MsSql has "Top", etc.
So, I'll add a new method to my program, to let query only a given page:


procedure TForm1.GetCurrentPage(ACurrentPage: Integer);
var
lFrom: Integer;
lTo: Integer;

begin
lFrom := ((ACurrentPage * cPageSize) - cPageSize) + 1;
lTo := (ACurrentPage * cPageSize) + 1;
IbQuery1.Close;
IbQuery1.SQL.Text :=
'select CustId, FirstName, LastName from customers ' +
'rows ' + IntToStr(lFrom) + ' to ' + IntToStr(lTo);
IbQuery1.Open;
end;


Add the constant "const cPageSize = 100;" just after the "implementation" section
of the unit, or simply replace cPageSize by 100. Also add a the internal attribute
FCurrentPage: Integer; in the private section of the form.

Now, go back to the OnCreate method, and change it by this:


procedure TForm1.FormCreate(Sender: TObject);
begin
IbQuery1.Close;
IbQuery1.SQL.Text := 'select count(*) from customers';
IbQuery1.Open;
ListView1.Items.Count := IbQuery1.Fields[0].Value;
// query for the first page
FCurrentPage := 1;
GetCurrentPage(FCurrentPage);
end;


The last step is to slightly modify the OnData method with this:


procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var
lCurrPage: Integer;
lPos: Integer;
begin
(* Get current page index *)
lCurrPage := Item.Index div cPageSize;
(* Get the position in the current page *)
lPos := Item.Index - (lCurrPage * cPageSize);

(* Page changed? refresh the data *)
if FCurrentPage - 1 <> lCurrPage then
begin
FCurrentPage := lCurrPage + 1;
GetDataPage(FCurrentPage);
end;

(* Paint the ListView's item *)
IbQuery1.RecNo := lPos + 1;
Item.Caption := IbQuery1.Fields[0].Value;
Item.SubItems.Add(IbQuery1.fields[1].Value);
end;


I hope you enyoyed this as much as I did when I wrote it. When I'll find
time, I'll post a modified version using TCollections instead of
TDataSets, just as I do in my projects.

This page is powered by Blogger. Isn't yours?