marco
cantu

Generating HTML in a WebSnap Application

Version 1.01 -- November 16, 2001 -- Copyright 2001 Marco Cantù, but open to contributions

As you probably know, Delphi 6 has introduced a brand new architecture for the development of Internet applications, called WebSnap. My book Mastering Delphi 6 and other sources (including quite a few articles on the Borland Community site) discuss a number of features of WebSnap, from the separation of pages for each request with a different pathname, to the management of sessions and users. Many sources discuss also how you can use the visual designer to generate the script that will be the starting point for generating the HTML.

I followed a similar path in my book, but when I actually started to try building a more complex application, beyond a classic and simple demo, I started bumping into many troubles. This article is a summary of the problems I found and of some of the possible solutions. It is a critical article, as the HTML generation of WebSnap has quite a few problems (from missing to wrong documentation, to arguable architectural decisions, to actual bugs. Even if I like Delphi, and I appreciate a lot of the new features of Delphi 6 (including ample portions of WebSnap) there is little to say but the fact that this area was not up to the level of the VCL code. It might be because we are used to a very high standard... in any case I already discussed some of the problems mentioned here with a member of Delphi's R&D team and I'm going to point to a few problems only to discuss alternative solutions, offering a contribution (at least, I hope).

HTML Generation in WebForm.pas

Before we get into the specific details I want to discuss, let me start by saying that I had troubles form the start with the idea of a compiled server-side application generating scripting to be expanded by the application itself (using an external scripting engine). Why not generate the HTML in the first place, using the compiled application? I still don't have a real answer to this question, but this is not the actual point I want to discuss here, or at least I don't want to discuss the theory, but only to focus on the practice.

When you use WebSnap's visual designer (Web Surface Designer, I think is the official name) you basically place a few components (forms, field groups and the actual fields, grids and columns) inside a hierarchy tree and can see a preview of the resulting HTML in a window below (you can see an example in Figure 1). If you've never tried to do this, you can as well stop reading, as you want to able to fully understand until you've done at least some experiments with this technique. Before I forget, this approach is quite similar to what Delphi 5 used (and Delphi 6 still provides) for the Internet Express architecture, in which scripting is executed on the client side.



Figure 1: WebSnap's visual designer

If we want to understand how the HTML code is generated we have to look to the WebForm unit (notice this was NOT updated by Delphi 6 patch, although I think it deserved it). The WebForm unit defines a number of classes and has about 5,000 lines of code. The structure is also not easy to grasp. I started by looking at the class used for each field, as the older Internet Express architecture had methods for generating the HTML right into a similar class. Looking at TCustomAdapterDisplayField (the parent of TAdapterDisplayField) you can see that there is a ControlContent protected function that determines the HTML output of the value of the files, by calling either the FormatInputControl or the FormatDisplayControl subfunctions. These in turn call one of the private methods that computer the HTML output adding the proper attributes to it.

Nowhere in the code of this class you can find the HTML code used to arrange the caption and the control output within a table cell. This support in fact is provided by the layout manager hosting the control. This layout manager is determined by the container of the control, and includes the HTML code used by the container to host the controls. For example, the TCustomAdapterFieldGroup class (the one I'm trying to use) has an ImplContent virtual function which simply calls the private FormatFields method. FormatFields in turn creates a form layout with the code:

FFormLayout := TAdapterFormLayout.Create(ParentLayout);
and then calls FormatField, which calls Content for the field, resulting in a call to TCustomAdapterDisplayField.ImplContent that receives the layout manager as parameter. The key element of this rather complex method (with a number of cute calls to GetAdapterFormLayout.GetAdapterFormLayout!) is the call to LayoutLabelAndField method of the layout itself. If we look at this code, or actually at the implementation method, we can see that the method produces the HTML inside a table cell for each of the possible positions of the label:
function TAdapterFormLayout.ImplLayoutLabelAndField(
  const HTMLLabel, HTMLField: string;
  Attributes: TLayoutAttributes): string;
begin
  if HTMLField = '' then
  begin
    Result := '';
    Exit;
  end;
  if Assigned(Attributes) then
    case Attributes.LabelPosition of
      lposLeft:
      begin
        Result := StartFields(2) +
          Format('%0:s>%1:s</td>'#13#10 + FieldIndent + 
            '<td %2:s>%3:s</td>'#13#10,
            [Attributes.LabelAttributes, HTMLLabel,
            Attributes.ControlAttributes, HTMLField]);
      end;
      lposAbove:
        Result :=
          StartFields(1) +
          Format('%0:s>%1:s</td></tr>'#13#10'<tr><td ' + sColSpanAttr + 
            '="2" %2:s>%3:s</td>'#13#10,
            [Attributes.LabelAttributes, HTMLLabel, 
            Attributes.ControlAttributes, HTMLField]);
      // and so on for the other positions

Now this code has a few problems for the above position, as it has a hard-coded new table line (<tr>) and a fixed colspan attribute. This confuses the management of tables lines (which has also a few troubles by itself). If you are interested in the details look into the code of StartFields (which adds the <td>tag and eventually also the <tr> tag in the NextColumn and StartRow methods, respectively -- notice in particular the the parameter of StartFields is passed to NextColumn but it later ignored there).

Marco's Html Components

This rather long preamble to demonstrate that as I wanted to customize the layout to display multiple short fields on a single line, as in Figure 2, I was at a loss. But as you can see I was able to achieve this effect. What I had to do, let me puzzled, but let's not jump at the conclusion.



Figure 2: The effect with multiple fields per row I badly wanted to obtain...

Of course I had to customize the ImplLayoutLabelAndField in a custom layout class, declared as follows:

type
  TMarcoAdapterFormLayout = class(TAdapterFormLayout)
    function ImplLayoutLabelAndField(const HTMLLabel, HTMLField: string;
      Attributes: TLayoutAttributes): string; override;
  end;

In the code of this method I used a simple appraoch, but you can of course customize it as you like:

function TMarcoAdapterFormLayout.ImplLayoutLabelAndField(const HTMLLabel,
HTMLField: string; Attributes: TLayoutAttributes): string;
var
  cells: Integer;
begin
  if HTMLField = '' then
  begin
    Result := '';
    Exit;
  end;
  if Assigned(Attributes) then
  begin
    // done to bypass the fact that the StartFields
    // parameter is actually ignored!
    cells := 2;
    case Attributes.LabelPosition of
      lposLeft:
      begin
        Result := StartFields(cells) +
           Format('%0:s>%1:s %2:s</td>'#13#10,
            [Attributes.ControlAttributes, HTMLLabel, HTMLField]);
      end;
      lposRight:
      begin
        Result := 'not supported';
      end;
      lposAbove:
        Result :=
          StartFields(cells) +
           Format('%0:s>%1:s <br> %2:s</td>'#13#10,
            [Attributes.ControlAttributes, HTMLLabel, HTMLField]);
      lposBelow:
        Result := 'not supported';
    else
      Assert(False, 'Unknown position');
    end;

    // take into account colspan: move ahead one more column!
    cells := 1;
    if Pos ('colspan=2', Attributes.ControlAttributes) > 0 then
      NextColumn(1);
  end
  else
    Result :=
        StartFields(2) + Format('>%0:s</td>'#13#10'<td>%1:s</td>'#13#10,
          [HTMLLabel, HTMLField]);
end;

This code is far from perfect, but it tries to address (often naively) some of the issues. But the trouble is how to make this layout to be used instead of a default one. As the layout is created directly inside of a private method I had to totally replace it in my derived field group class, where I had to add other methods used by it and even some large constant strings (I ended up by copy too many lines of code from the WebForm unit to mine):

type
  TMarcoAdapterFieldGroup = class (TAdapterFieldGroup)
  private
    FColumns: Integer;
    function FormatFields(ParentLayout: TLayout;
      Options: TWebContentOptions): string;
    procedure SetColumns(const Value: Integer);
  public
    function ImplContent(Options: TWebContentOptions; 
    ParentLayout: TLayout): string; override;
  published
    property Columns: Integer read FColumns write SetColumns;
  end;

I actually took advantage of this new class to add support for the number of columns of the grid, a property passed to the ColumnCount property of the TAdapterFormLayout which is later used in the NextColumn method to determine if a new line is needed. Finally, to be able to have a single field taking up two cells (the only value I really support!) I had to inherit a custom display field class, add the colspan property and use it in the GetLayoutAttributes method:

type
  TMarcoAdapterDisplayField = class (TAdapterDisplayField)
  private
    FColSpan: Integer;
    procedure SetColSpan(const Value: Integer);
  protected
    function GetLayoutAttributes: TLayoutAttributes; override;
  published
    property ColSpan: Integer read FColSpan write SetColSpan;
  end;

function TMarcoAdapterDisplayField.GetLayoutAttributes: TLayoutAttributes;
begin
  Result := inherited GetLayoutAttributes;
  Result.ControlAttributes := Result.ControlAttributes + ' colspan=' +
    IntToStr (FColSpan);
end;

The Simple Html Generation

Now if you try installing and using these components, they can be hardly considered an early beta. But as I got to this point I was very confused on how to proceed to make this code work better. So I stopped and started to think if there wasn't a simpler path to follow, using a limited but effective architecture instead of the one suggested by Borland.

I started once more from scratch, creating a field group and a display field of my own (well, mostly by copying code in the WebForm unit once more). I've also added a custom interface to let the field group ask the required information to the field, cell content and number of cells required:

type
  ISimpleProduceHtml = interface
    ['{98202025-053F-4D11-9DF3-244DC1C9A06A}']
    function GetHtml (Options: TWebContentOptions): string;
    function GetCols: Integer;
  end;

Now my custom fields implement this specific interface on top of the standard one, but all of the code for the HTML generation (for the field) is clearly in one location. This is the class:

type
  TSimpleAdapterDisplayField = class (TAdapterDisplayField, ISimpleProduceHtml)
  private
    FColSpan: Integer;
    FCellCustom: string;
    procedure SetColSpan(const Value: Integer);
    function GetColSpan: Integer;
    procedure SetCellCustom(const Value: string);
  protected
    function GetLayoutAttributes: TLayoutAttributes; override;
    function FormatCaption: string; override;
  public
    function GetHtml (Options: TWebContentOptions): string; virtual;
    function GetCols: Integer; virtual;
  published
    property CellColSpan: Integer read GetColSpan write SetColSpan;
    property CellCustom: string read FCellCustom write SetCellCustom;
  end;

The core method is now GetHtml, which has basically two alternative implementations I've sketched. But the idea is that it should be much easier to customize this code to suit your needs than the WebForm unit code:

  // single cell for everything...
  Result := '<td ' + GetLayoutAttributes.ControlAttributes + '>';
  case CaptionPosition of
    capLeft:
      Result := Result + FormatCaption + ' ' + ControlContent (Options);
    capRight:
      Result := Result + '<b>Attributes.LabelPosition right not supported</b>';
    capAbove:
      Result := Result + FormatCaption + ' <br> ' + ControlContent (Options);
    capBelow:
      Result := Result + '<b>Attributes.LabelPosition below not supported</b>';
  end;
  Result := Result + '</td>';

  // double cell if left
  case CaptionPosition of
    capLeft:
      Result := '<td>' + FormatCaption + '</td><td ' +
        GetLayoutAttributes.ControlAttributes + '>' + ControlContent (Options) + '</td>';
    capAbove:
      Result := '<td ' + GetLayoutAttributes.ControlAttributes + '>' +
        FormatCaption + ' <br> ' + ControlContent (Options) + '</td>';
  end;



Figure 3: The output of the components of my Simple architecture, displayed with a table border to see how the alignment is obtained

The first version uses one cell for both the label and the actual element, something not very nice for a left-aligned caption, as elements on following lines won't be aligned but will depend on the length of the caption. This is why I added the second version.

Notice that in both cases I use the control attributes in the td element, as some of the attributes like the alignment and vertical alignment pertain to the cell but are used (in Borland's code) in the input or other control (where they are totally ignored). Actually, one should complete the example by separating the two sets of attributes more clearly, determining the correct effect on the HTML of each adapter field property, if any. Of course I added the colspan attribute to the standard ones.

The interface I added to the field is used in the rewritten FormatFields, which doesn't use a layout any more:

function TSimpleAdapterFieldGroup.ImplContent(
  Options: TWebContentOptions; ParentLayout: TLayout): string;
begin
  Result := '<table ' + GetLayoutAttributes.ControlAttributes +
    FormatFields (Options) + '</table>';
  if (Adapter <> nil) and (AdapterMode <> '') then
    Result := Format(sSetAdapterMode,
      [MakeAdapterVariableName(Self), AdapterMode, Result]);
end;

function TSimpleAdapterFieldGroup.FormatFields(
  Options: TWebContentOptions): string;
var
  IHtml: ISimpleProduceHtml;
  I: Integer;
  VarName: string;
  colpos: integer;
begin
  colpos := 0;
  Result := '<tr>';
  for I := 0 to VisibleFields.Count - 1 do
  begin
    if Supports (IInterface(VisibleFields[I]), ISimpleProduceHtml, IHtml) then
    begin
      Result := Result + iHtml.GetHtml (Options);
      inc (colpos, ihtml.GetCols);
      // end of the row?
      if colpos >= Columns then
      begin
        colpos := 0;
        Result := Result + '</tr>' + #13#10 + '<tr>';
      end;
    end;
  end;
  Result := Result + '</tr>';
  ...

This code requests the HTML of each control and handles the table rows by computing the size of each cell and adding closing a row when required. Not that this code accounts for every possible situation, but it is certainly an improvement and is now quite easy to customize, as the entire HTML structure inside the field group is controlled in two places and the program flow is simplified by far.

The effect of this code can be seen in Figure 3, where I've added the border to the external table to make it clear where the various cells are placed (you'll want to avoid this in a real application obtaining the output of Figure 4).



Figure 4: The output of the components of my Simple architecture

The Hidden Alternative: XML Builder

Having explored these alternatives, I bumped also into a totally different solution. The WebSnap examples available with Delphi 6 include a folder called XmlBuilder where you can find some more visual designers you can install in Delphi. These visual designer are quire different from the ones commonly used in WebSnap, because they don't use server-side scripting at all! Instead they grab information from the adapters (database, session, application, and all) and generate an XML file, which is later processed by an XSL file to produce the final HTML. Even the classic template script with the page name or the login request is replaced by a combination of XML and XSL.

The AdapterXMLBuilder component installed by this extra package has no visual preview, but the same type of structure as you can see in Figure 5. I've attached it to a simple adapter with a field and an action and to the default template provided in Delphi6\Demos\WebSnap\XMLBiolife\template.xsl.



Figure 5: The editor of the AdapterXMLBuilder component

The effect of this adapter it to produce XML and show it right away in Delphi's editor, as you can see in Figure 6. The result of the application is a very simple page, with an input field and a button, but it is exactly the same page you'd obtain by attaching a traditional display adapter with scripting support.



Figure 6: The XML produced by the AdapterXMLBuilder editor in a simple program.

As I've spend a lot of time recently working with XML and XSLT I'd rather further push this direction rather than working with the server side scripting and related problems. Also, this is the only solution to sue the full power of WebSnap and still be able to separate the page and HTML layout and design from the program, as the XSL file is a totally separate file which provides you with 100 percent of control on the HTML. The drawback is that XSL is not that easy to start working with (even if terribly powerful after a while) and the demo XSL file is over 100 lines!

(Temporary) Conclusion

In the attached files WebSnapAddon.zip (18KB) you'll find both my sample components with a package to install them and a demo program of their use, plus the demo of XmlBuilder shown above. Feel free to use this code as you like, but publishing it and its description on your site or an article/book. Refer people to this site instead. If you are interested in continuing development and improving my sketchy code, feel free to go ahead. If I can help letting others work together, I'll be willing to devote a little time, but I wont' be able to really extend this code myself, as I'm working more on the XSLT side of HTML development nowadays. And someone wants to contribute to further versions of this paper, I'm all ears.

Again, this was not meant to be too critical of Borland, as I like other areas of WebSnap a lot (and most of the other new features of Delphi 6), but the HTML generation provided in the WebForm unit has problems and those classes are very hard to inherit from, as I've shown. I'm just trying to help you using WebSnap in the real world, as the HTML generation is OK for demos but doesn't really take you much further, in my opinion. I'd also like to thank a client (Barazzetta) who asked me to help in this direction, starting my exploration of the area.