Simple scripting with NetCLX extension components

By: John Kaster

Abstract: C++ Builder 6, Delphi 6 and Kylix all have new RTTI functions that make simple scripting much easier to implement. John K shares some producer components that show how.

The WebBroker/NetCLX technology available in Delphi, C++ Builder and Kylix is very flexible and quite extensible. In fact, WebSnap is built on top of it. However, any non-trivial NetCLX application starts to rely very heavily on the OnHTMLTag event for things that should be easier or more convenient to do. In Delphi 6, Kylix 1, and C++ Builder 6, some significant Run-Time Type Information (RTTI) improvements are included beyond the new RTTI support for interfaces. See TypInfo.pas to see what I'm talking about. After looking at these new routines, I decided that I had finally found a relatively easy way to create some handy NetCLX extension components. Of course, there's always more components to write, but those contained in this package seem to be a good starting point.

There are several "Delphi scripting" solutions available from third parties. All that I'm aware of have written their own evaluators for what they provide, and in fact go quite a bit beyond what these components do in some cases. These components don't provide a symbol table, branching or looping evaluators, function calls, or many of the other things you might expect from scripting. That said, these components are actually amazingly flexible and powerful for "scripting" certain parts of Borland's RAD products, particularly web applications where you sometimes simply want to retrieve or set the value of a component property via script.

The components provided here are provided as is, with no warranty on usability. That said, I use these components quite a bit in the web server applications written for the Borland Community site. They work quite well for my needs. Hopefully they will be useful to you as well. You can download the components from CodeCentral. They are at http://codecentral.borland.com/codecentral/ccweb.exe/listing?id=16760.

Functional description

These components are "property producers" that support RTTI on published properties so you can include the values of the published properties in your HTML template code. You can set published property values, or retrieve published property values. You can refer to the published property of a component in your html template code and retrieve or assign its value, to make a primitive kind of scripting.

RTTI reference syntax

The RTTI reference syntax is added as a new standard tag named property. If this is the name of a field in a dataset, you will run into problems with using it, which is why the alternative syntax of using an equals sign is supported.

Attribute pairs

Consider the following syntax:

<#property assign=Component1.Property1 value="Some text" result=Component2.Property2>

When a tag begins with <#property, the TagParams list will be examined for the following three values:

AttributeDescription
Assign Name of the property to which a value will be assigned. This must be a property reference to a published property. If searching the owner is enabled, components may be referenced by name as well.
Value The value to assign to the named property. This can either be another property reference or a variant value (expressed as a string, of course).
Result The text result to return after evaluating the assign and value requests. If it is omitted, no text will be assigned as the result of the tag evaluation.

Self is also used recognized for component references, and will override the "SearchOwner" property setting.

Shorthand

A shorthand syntax is also supported, where an equals sign "=" can be used in place of the property keyword. An assignment can also be made with the shorthand syntax using either "=" or ":=" after the property reference, followed by either another property reference or a constant expression. The following syntaxes are valid:


<#= Expression1[:=Expression2]>
<#= Expression1[=Expression2]>
The following tags have the same results:


<#property assign=Component1.Property1 value="Some text" result=Component2.Property2>
<#= assign=Component1.Property1 value="Some text" result=Component2.Property2>
<#= Component1.Property1="Some Text">
<#= Component1.Property1:="Some Text">
<#= Component1.Property1 = "Some Text">
<#= Component1.Property1 := "Some Text">

The shorthand syntax also can be used when only the resultant value is needed. Both:

<#= result=Component2.Property2>

and

<#= Component2.Property2>

produce the same result.

There are some limits on the RTTI reference syntax, and hence the scripting you can do. I'm simply using the existing RTTI routines to get property reference results. There is an ID parser that separates out the various ids in the reference, but no sub-element references can be made. For example, ListBox1.Items[1] will not be resolvable. (I have a comment in the code for where one could be inserted.) Also, while a specific component may be published, properties of it may not. For example, SQLQuery1.SQL.Text will not work, because Text is not a published property, but only public.

The following example shows various valid ways to combine the two different references syntaxes.


<#property assign=Edit1.Text value="This is the edit text">
<#property assign=Edit1.Text value="This is changed text" result=Edit1.Text>
<#= Edit1.Text := "This is some other text">
<#= Button2.Caption := "Enabled?">
<#= CheckBox1.Checked:=True>
<#= Form1.Caption:=Edit1.Text>
<#= Button2.Enabled:=CheckBox1.Checked>

You can use the demonstration project EvalTest to test the above syntax examples. If you're opening it with Kylix, it should automatically convert the project to a CLX application for you. You may find stepping into the code with the debugger very valuable if you're not familiar with the available RTTI calls.

The complete source code for the packages on Windows and Linux and the components themselves is provided. The following components are available in the included files:

PropProd.pas

TPropertyParser

All the components in this package use this component, either as an ancestor or a contained component. It implements the IExpressionParser interface for Delphi-like property references, which is designed to support multiple kinds of expression parsers. Here is the declaration section from PropProd.pas for IExpressionParser, TExpressionParser, and TPropertyParser:


type

  TGetValueEvent = procedure (Sender: TObject; const Expression: string;
    var Value: variant) of object;
  TSetValueEvent = procedure (Sender: TObject; const Expression: string; Value: variant)
    of object;

  TParserErrorHandling = (peException, peVerbose, peSilent);

  TCustomParser = class(TComponent);

  IExpressionParser = interface
    ['{26BE6216-5848-4327-86D1-75F1EA339378}']
    function FindLastObject: TObject;
    function Evaluate(const AObject: TObject; const AExpression: string;
      const ASearchOwner: boolean = False) : variant;
    procedure AssignValue(const AObject: TObject;
      const AExpression: string; const AValue: string;
      const ASearchOwner: boolean = False);
    function CheckSyntax: integer;
  end;

  TExpressionParser = class(TCustomParser, IExpressionParser)
  private
    FObjectRef: TObject;
    FSearchOwner: boolean;
    FIdentPos: integer;
    FExpression: string;
    FComponent: TObject;
    FIdents: TIdentArray;
    FValue: variant;
    FErrorHandling: TParserErrorHandling;
    procedure SetSearchOwner(const Value: boolean);
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
    function GetValue: variant; virtual;
    function GetObjectRef: TObject; virtual;
    procedure SetValue(const Value: variant); virtual;
    procedure SetExpression(const Value: string); virtual;
    procedure SetComponent(const Value: TObject); virtual;
  public
    property Idents: TIdentArray read FIdents;
    property IdentPos: integer read FIdentPos;
    property ErrorHandling: TParserErrorHandling read FErrorHandling write FErrorHandling;
    property Expression: string read FExpression write SetExpression;
    property Component: TObject read FComponent write SetComponent;
    property ObjectRef: TObject read GetObjectRef;
    property Value: variant read GetValue write SetValue;
    property SearchOwner: boolean read FSearchOwner write SetSearchOwner;

    procedure Clear;
    { IExpressionParser }
    procedure AssignValue(const AObject: TObject; const AExpression: String;
      const AValue: String; const ASearchOwner: boolean = False); overload; virtual;
    function Evaluate(const AObject: TObject; const AExpression: String;
      const ASearchOwner: Boolean = False): Variant; overload; virtual;
    function FindLastObject: TObject; virtual;
    function CheckSyntax: integer; virtual;

    procedure AssignValue(const AExpression: string; const AValue: string);
      overload;
    procedure AssignValue(const AValue: string); overload;

    function Evaluate(const AExpression: string): variant; overload;
    function Evaluate: variant; overload;
  end;

  TPropertyParser = class(TExpressionParser)
  public
    constructor Create(AOwner: TComponent); override;
    procedure AssignValue(const AObject: TObject; const AExpression: String;
      const AValue: String; const ASearchOwner: boolean = False); override;
    function Evaluate(const AObject: TObject; const AExpression: String;
      const ASearchOwner: Boolean = False): Variant; override;
    function FindLastObject: TObject; override;
    function CheckSyntax: integer; override;

  published
    property Expression;
    property Component;
    property SearchOwner;
  end;

It would probably be a good idea to change the name of TPropertyParser in the future to TDelphiPropertyParser and create a TCPPPropertyParser as well.

TPropertyProducer

This is the ancestor of most of the other components in this package. It implements the RTTI expression lookups for resolving the property references from the HTML template tags.


  TPropertyProducer = class(TCustomPageProducer)
  private
    FOnGetValue: TGetValueEvent;
    FOnSetValue: TSetValueEvent;
    FParser: TPropertyParser;
    procedure SetOnGetValue(const Value: TGetValueEvent);
    procedure SetOnSetValue(const Value: TSetValueEvent);
    procedure SetSearchOwner(const Value: boolean);
    function GetSearchOwner: boolean;
    function GetErrorHandling: TParserErrorHandling;
    procedure SetErrorHandling(const Value: TParserErrorHandling);
  protected
    function HandleTag(const TagString: string;
      TagParams: TStrings): string; override;
  public
    property Parser: TPropertyParser read FParser;
    constructor Create(AOwner: TComponent); override;
    function Lookup(const AExpression: string) : variant; virtual;
    procedure AssignValue(const AExpression, AValue : string); virtual;
  published
    property HTMLDoc;
    property HTMLFile;
    property StripParamQuotes;
    property OnHTMLTag;
    {$ifdef MSWINDOWS}
    property ScriptEngine;
    {$endif}
    property ErrorHandling: TParserErrorHandling
      read GetErrorHandling write SetErrorHandling default peException;
    property SearchOwner : boolean read GetSearchOwner write SetSearchOwner;
    property OnSetValue : TSetValueEvent read FOnSetValue write SetOnSetValue;
    property OnGetValue : TGetValueEvent read FOnGetValue write SetOnGetValue;
  end;

TComponentPropProducer

This component adds support for searching the Owner of the current component to resolve property reference values. This means you can refer to the properties of other components on the same web module (or other container) that contains the ComponentPropProducer.


  TComponentPropProducer = class(TPropertyProducer)
  private
    procedure SetComponent(const Value: TComponent);
    function GetComponent: TComponent;
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
  public
    function Lookup(const AExpression: string) : variant; override;
    procedure AssignValue(const AExpression, AValue : string); override;
  published
    property Component : TComponent read GetComponent write SetComponent;
  end;

TPropertyPageProducer

This component simply allows the other property producers to retrieve the content of a TPageProducer.


  TPropertyPageProducer = class(TPageProducer)
  published
    property GetContent: string read Content;
  end;

DSParamTblProd.pas

TDataSetParamTableProducer

This is a descendant of the TDataSetTableProducer component that implements RTTI resolution and automatic assignment of query parameters. The DataSetParam components make it extremely easy to set up parameterized queries for your web server pages and get the results from them. You just include the value for the parameter as part of the request, and the parameter will be assigned automatically. This means you can do things like:

http://myserver/myapp.exe/customer?custid=10

and the parameter "custid" of the DataSetParam producer will automatically be assigned to 10.

Field references are resolved by field name, as the standard DataSet Producers do as well, except memos are handled gracefully as HTML output rather than (MEMO) or (memo). RTTI references are also supported.


  TDataSetParamTableProducer = class(TDataSetTableProducer)
  private
    { Private declarations }
  protected
    { Protected declarations }
    function FormatCell(CellRow: Integer; CellColumn: Integer;
      CellData: String; const Tag: String; const BgColor: THTMLBgColor;
      Align: THTMLAlign; VAlign: THTMLVAlign;
      const Custom: String): String; override;
  public
    { Public declarations }
    procedure AssignParameters(Request: TWebRequest = nil);
    function Content: String; override;
  published
    { Published declarations }
    property GetContent: string read Content;
  end;

The GetContent property is published so it can be accessed from an RTTI producer.

TDataSetParamPageProducer

This component works like a TDataSetPageProducer with the following enhancements:

  • It supports RTTI lookups
  • It handles "memo" fields correctly for HTML output
  • It supports specifying the number of records to process
  • It supports parameters passed in the URL
  • It supports top and bottom text files for setting up result set tables, headers, footers and so on.

  TDataSetParamPageProducer = class(TComponentPropProducer)
  private
    FRecsToDisplay: integer;
    FDataSet: TDataSet;
    FHighlightText: string;
    FHighlightTag: string;
    FHighlight: boolean;
    FHTMLBottomFile: string;
    FHTMLTopFile: string;
    procedure SetRecsToDisplay(const Value: integer);
    procedure SetHighlightTag(const Value: string);
    procedure SetHighlightText(const Value: string);
    { Private declarations }
  protected
    procedure DoTagEvent(Tag: TTag; const TagString: String;
      TagParams: TStrings; var ReplaceText: String); override;
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
    { Protected declarations }
  public
    { Public declarations }
    procedure AssignParameters(Request: TWebRequest = nil);
    function Content: String; override;
  published
    { Published declarations }
    property HighlightText: string read FHighlightText write SetHighlightText;
    property HighlightTag: string read FHighlightTag write SetHighlightTag;
    property HTMLTopFile: string read FHTMLTopFile write FHTMLTopFile;
    property HTMLBottomFile: string read FHTMLBottomFile write FHTMLBottomFile;
    property DataSet: TDataSet read FDataSet write FDataSet;
    property RecsToDisplay: integer read FRecsToDisplay write SetRecsToDisplay;
    property GetContent: string read Content;
  end;

RedirProd.pas

TRedirectProducer

This producer provides a convenient way to display a message, then redirect the user to another URL. The title, description, URL and delay for the message can all be specified as properties of the component and will be automatically plugged into the HTML content when you request the content from the producer. A future version of this component should also support the HTML metatag for redirecting, since not everyone enables JavaScript for their browser.


  TRedirectProducer = class(TPropertyProducer)
  private
    FTimeout: integer;
    FURL: string;
    FTitle: string;
    FDescription: TStrings;
    procedure SetDescription(const Value: TStrings);
    procedure SetTimeout(const Value: integer);
    procedure SetTitle(const Value: string);
    procedure SetURL(const Value: string);
    function GetDescriptionText: string;
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    { Published declarations }
    property DescriptionText : string read GetDescriptionText;
    property Description : TStrings read FDescription write SetDescription;
    property Title : string read FTitle write SetTitle;
    property Timeout : integer read FTimeout write SetTimeout default 15000;
    property URL : string read FURL write SetURL;
  end;

Installation

The source code is written to work on both Windows and Linux, but different design time packages are provided for installation on the two operating systems. Both include the run-time package NetCLXExt.dpk

Windows

Open dclNetCLXExtWin.dpk and click Install.

Linux

Open dclNetCLXExt.dpk and click Install.


Server Response from: ETNASC03