Habari Web Components: TDatamodule per Session

This program for Habari Web Components demonstrates how the internal Indy HTTP server instance can be used to hold custom data objects in memory for the duration of a HTTP session. Update: the code and a compiled application is now included in the demo download.

It also shows how the datamodule instance can be removed automatically when its session ends.

When run, the demo application will display a list of names (populated with some example values) and a HTML form which allows to enter another name and send it to the server.

HTML page

For every client session, the server will create and use a separate instance of the datamodule. You can verify this with different browsers, or with private mode browser windows.

The executable and the project source are available at http://cc.embarcadero.com/Item/28784

The Datamodule class

The Datamodule in this example acts as a container for people names stored in a TStrings object. This people list is accessible as a property with public visibility.

unit DemoDataMod;

  ...

  TDemoDataModule = class(TDataModule)
    procedure DataModuleCreate(Sender: TObject);
    procedure DataModuleDestroy(Sender: TObject);
  private
    FPeople: TStrings;
  public
    property People: TStrings read FPeople;
  end;

TDemoDataModule.DataModuleCreate creates an instance of TStringList, assigns it to the internal FPeople field, and adds some example data.
TDemoDataModule.DataModuleDestroy frees the TStringList instance. (Note that the datamodule has to be destroyed also when its owning session ends, this will be shown in the next section).

procedure TDemoDataModule.DataModuleCreate(Sender: TObject);
begin
  FPeople := TStringlist.Create;

  People.Add('Alice');
  People.Add('Bob');
  People.Add('Homer');
end;

procedure TDemoDataModule.DataModuleDestroy(Sender: TObject);
begin
  People.Free;
end;

The Web Component class

The Web Component interface uses the handlers for HTTP GET and POST requests, to route them to our own “catch-all” handler OnHandleRequest. This handler will process both the initial GET request (when the client navigates to the page) and all POST requests when the client submits the HTML form.

unit DataFormCmp;

  ...

  TDataFormPage = class(TdjWebComponent)
  private
    procedure OnHandleRequest(Request: TIdHTTPRequestInfo;
      Response: TIdHTTPResponseInfo);
  public
    procedure OnGet(Request: TIdHTTPRequestInfo; Response:
      TIdHTTPResponseInfo); override;
    procedure OnPost(Request: TIdHTTPRequestInfo; Response:
      TIdHTTPResponseInfo); override;
  end;

  ...

procedure TDataFormPage.OnGet(Request: TIdHTTPRequestInfo;
  Response: TIdHTTPResponseInfo);
begin
  OnHandleRequest(Request, Response);
end;

procedure TDataFormPage.OnPost(Request: TIdHTTPRequestInfo;
  Response: TIdHTTPResponseInfo);
begin
  OnHandleRequest(Request, Response);
end;

In OnHandleRequest, the datamodule either will be created and stored in the session if it does not exist or it will be retrieved from the session:

procedure TDataFormPage.OnHandleRequest(Request:
  TIdHTTPRequestInfo; Response: TIdHTTPResponseInfo);
const
  KEY = 'datamodule';
var
  Pos: Integer;
  DM: TDemoDataModule;
  Tmp: string;
  S: string;
begin
  Request.Session.Lock;
  try
    Pos := Request.Session.Content.IndexOf(KEY);
    if Pos <> -1 then
    begin
    DM := TDemoDataModule(Request.Session.Content.Objects[Pos]);
    end
    else
    begin
      WriteLn(Format('Create datamodule for session %s',
       [Request.Session.SessionID]));
      DM := TDemoDataModule.Create(nil);
      Request.Session.Content.AddObject(KEY, DM);
    end;
  finally
    Request.Session.Unlock;
  end;

If it is a POST request, the value of the input field will be extracted and added to the people list:

  if Request.CommandType = hcPOST then
  begin
    S := Utf8ToString(RawByteString(
           Request.Params.Values['textfield1']));
    if S <> '' then
    begin
      DM.People.Add(S);
    end;
  end;

We have all that is needed to build the HTML response now:

  • a reference to the datamodule instance which belongs to this session
  • the name which has been POSTed from the client is in the people list

Finally, the OnHandleRequest handler creates the HTML response. The names stored in the people list will be added to the HTML code (followed by the HTML form code, which is not shown here but included in the source code).

  Tmp := '<!DOCTYPE html>'

   ... // add HTML page code ...

  for S in DM.People do
  begin
    Tmp := Tmp + '  <p>' + S + '</p>' + #10;
  end;

  ... // more HTML page code ...

  Response.ContentText := Tmp;
  Response.ContentType := 'text/html';
  Response.CharSet := 'utf-8';

Housekeeping

In a server application, memory leaks would lead to out of memory errors sooner or later. Our code has to free the datamodule instance when the session ends. The Habari Web Component framework is build on top of Indy, so we can use the OnSessionEnd event of the internal TIdHTTPServer instance to do this. In the example project, we custom subclass of TdjHTTPConnector to add a handler for the OnSessionEnd event, and to set the session timeout to a lower value than the default.

program DataModuleDemo;

  ...

  TCleanUpConnector = class(TdjHTTPConnector)
  private
    procedure DoSessionCleanUp(Sender: TIdHTTPSession);
  public
    constructor Create(const Handler: IHandler); override;
  end;

{ TCleanUpConnector }

constructor TCleanUpConnector.Create(const Handler: IHandler);
begin
  inherited;

  HTTPServer.OnSessionEnd := DoSessionCleanUp;

  // 20 sec to demonstrate seesion cleanup
  HTTPServer.SessionTimeOut := 20000;
end;

procedure TCleanUpConnector.DoSessionCleanUp(Sender:
  TIdHTTPSession);
const
  KEY = 'datamodule';
var
  Pos: Integer;
begin
  Pos := Sender.Content.IndexOf(KEY);

  if (Pos <> -1) and Assigned(Sender.Content.Objects[Pos]) then
  begin
    WriteLn(Format('Destroy datamodule for session %s',
      [Sender.SessionID]));
    Sender.Content.Objects[Pos].Free;
  end
end;

Is it 100% thread safe?

Not yet: the People property of the datamodule is not protected against concurrent modificationss by different threads. This can cause problems when the client opens multiple connection to the server in the same session, and two connections read or write the People property.

Putting it all together

This excerpt of the demo project shows how the Web Component and the custom HTTP connector are wired in the Habari Web Components framework.

procedure Demo;
var
  Server: TdjServer;
  Connector: TdjHTTPConnector;
  Context: TdjWebAppContext;
begin
  Server := TdjServer.Create(8080);
  try
    Context := TdjWebAppContext.Create('demo', True);
    Context.Add(TDataFormPage, '/dataform.html');
    Server.Add(Context);

    Connector := TCleanUpConnector.Create(Server.Handler);

    Connector.Host := '127.0.0.1';
    Connector.Port := 8080;

    Server.AddConnector(Connector);

    SetShutdownHook(Server);

    Server.Start;

    // launch default web browser and navigate to dataform.html
    ShellExecute(0, 'open',
      'http://127.0.0.1:8080/demo/dataform.html', '', '', 0);
    WriteLn('Hit any key to terminate.');
    ReadLn;

  finally
    Server.Free;
  end;
end;

Server running

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s