This code is part of my talk at Borcon 2002 entitled "Building WebSnap Components". Be sure to attend for more information on enhancing WebSnap.
The Obligatory Introduction
So far, I've
written a series of articles on building WebSnap applications,
paying most of the attention to how to get the existing WebSnap
components to work and do the things that you want to for your
website. However, now I think it is time to change the pace a little
bit and work on some custom WebSnap components. It is in building
these components that you can really bring out the power of WebSnap
in your applications.
The Much Maligned TAdapterPageProducer
One of the more powerful components in WebSnap is the
TAdapterPageProducer. It can be used to create complex web pages
using TAdapter components and their fields and actions. If you have
been following along with my earlier articles, you are familiar with
the component and what it can do. Some folks in the Borland
newsgroups have maligned unfairly, I think the
TAdapterPageProducer (You know who you are. ;-). It has its
weaknesses and limitations, yes, but I don't think it should be so
easily dismissed. Granted, it can't always do what a web developer
might need, but it can be quite powerful for producing HTML and
Javascript that can then be imported into a regular TPageProducer and
managed manually. I like the TAdapterPageProducer, though, and am
sticking with it, as I am confident that Borland's R&D lab has
great things cooked up for it in the future.
In any event, out of the box, there is a limited amount of
functionality that it provides, but that functionality can easily be
quickly enhanced by adding custom components to it that allow you to
add any type of HTML that you want almost anywhere within it. This
capability alone will add power to the TAdapterPageProducer right
away. In addition, we'll design these components in such a way that
makes it easy to build any type of HTML based components to add to a
TAdapterPageProducer. So away we go!
The Art of the Abstract
When ever you want to build a class that will be malleable and
able to have lots of descendants with similar functionality but
slightly different implementation, you should always consider
creating an abstract class. That's what I've done here. We are going
to want to create a set of components that do one thing
produce HTML. Each component you'll want will likely produce slightly
different HTML, but they'll all do that one thing. So maybe we can
create an abstract class where all you'd need to do to descend from
it would be to call a single method called, say, GetHTML. What a
great idea, eh? Well, let's do it:
TnxBaseWebSnapComponent = class(TWebContainedComponent, IWebContent)
protected
{ IWebContent }
function Content(Options: TWebContentOptions; ParentLayout: TLayout): string;
function GetHTML: string; virtual; abstract;
end;
Now, TnxBaseWebSnapComponent is a cool class. First, notice that it
descends from TWebContainedComponent, which is the parent class for
all components that fit into a TAdapterPageProducer. It basically
knows how to be owned, parented and managed by a
TAdapterPageProducer. In and of itself it doesn't do anything, but
provides the framework for being a TAdapterPageProducer component.
Next, you should have noticed that the class declares and
implements the IWebContent interface. That interface is declared like
this:
IWebContent = interface
['{1B3E1CD1-DF59-11D2-AA45-00A024C11562}']
function Content(Options: TWebContentOptions;
ParentLayout: TLayout): string;
end;
This is obviously a simple interface that when implemented, merely
asks for the content of the component, normally as HTML. Hey, that's
exactly what we need! What a coincidence, don't you think?
Thus, next you'll notice that our class does indeed implement the
IWebContent interface. The implementation of the single method goes
like this:
{ TnxBaseWebSnapComponent }
function TnxBaseWebSnapComponent.Content(Options: TWebContentOptions;
ParentLayout: TLayout): string;
var
Intf: ILayoutWebContent;
begin
if Supports(ParentLayout, ILayoutWebContent, Intf) then
begin
Result := Intf.LayoutField(GetHTML, nil)
end else
begin
Result := GetHTML;
end;
end;
This code basically just retrieves the HTML that your component will
generate via the call to the GetHTML call. It does it in two
different ways. If the ParentLayout parameter, i.e. the component
into which this HTML will be placed, implements the ILayoutWebContent
interface, then the ParentLayout will be responsible for fitting the
HTML into the TAdapterPageProducer. And example of this might be if
you were trying to put HTML into a special type of grid column, then
the grid column would be responsible for properly formatting the HTML
you are returning. Otherwise, just the HTML itself is returned.
Of course, the next thing that you'll notice in the
TnxBaseWebSnapComponent is the abstract virtual method, GetHTML.
Abstract, virtual methods scream to be overridden. Since this is an
abstract class, and the GetHTML call is made in the Content method,
the wonders of polymorphism will allow your descendant components to
worry about nothing other than implementing the GetHTML method. And
that's what we'll do right now.
Get Your HTML While its Still Hot!
Okay, probably the most basic thing you can do here is to simply
add HTML any HTML via this component. So we'll declare
the following component, TnxHTMLCode, which will do nothing more than
insert HTML into a TAdapterPageProducer.
TnxHTMLCode = class(TnxBaseWebSnapComponent)
private
FHTML: TStrings;
procedure SetHTML(const Value: TStrings);
protected
function GetHTML: string; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property HTML: TStrings read FHTML write SetHTML;
end;
This class implements the abstract method GetHTML and declares (and
manages) a single property of type TStrings named HTML. The GetHTML
method is so simple that I almost don't have to show it, but I will:
function TnxHTMLCode.GetHTML: string;
begin
Result := FHTML.Text;
end;
I don't need to explain this code, do I? <g> Thus, when this
component is added to the TAdapterPageProducer, and the HTML property
filled in, whatever is in that HTML property will be placed right
into the TAdapterPageProducer at the spot where the TnxHTMLCode
component resides. How could that be any simpler? For your further
edification, however, it is not that simple behind the scenes. What
happens is that the TAdapterPageProducer component will cycle through
all the components it holds, grabbing the IWebContent interface for
each, and then calling the Content method on that interface. Thus, it
is clear that any component that wants to be a TAdapterPageProducer
component needs to implement the IWebContent interface, right? See
how that works? Cool, huh? This is a great component because it
illustrates the use of interfaces, abstract classes, and
polymorphism.
Let's Get a Bit More Specific
Okay, now you see how a simple HTML component works, there are
other more specific uses that you might want to implement. For
instance, you might want to insert the contents of an existing HTML
into your web page. That's easily done, as you might guess. Here's
how I did it:
TnxWebFile = class(TnxBaseWebSnapComponent)
private
FHTMLFile: TFilename;
FHTMLDoc: TStrings;
FFileInDFM: Boolean;
procedure SetHTMLDoc(const Value: TStrings);
procedure SetHTMLFile(const Value: TFilename);
procedure SetFileInDFM(const Value: Boolean);
protected
function GetHTML: string; override;
function LoadFile: string;
public
constructor Create(AComponent: TComponent); override;
destructor Destroy; override;
published
property FileInDFM: Boolean read FFileInDFM write SetFileInDFM;
property HTMLDoc: TStrings read FHTMLDoc write SetHTMLDoc;
property HTMLFile: TFilename read FHTMLFile write SetHTMLFile;
end;
This class, again, overrides the GetHTML method to grab the HTML from
the file, and three properties that manage the HTML file. The first,
FileInDFM, determines whether the contents of the file are copied
into the HTMLFile property, or whether the file remains outside of
the binary application, where it can be changed manually and where
those changes will be immediately shown in the next page request. So,
that pretty much explains the other two properties, HTMLDoc and
HTMLFile.
The important implementations are done like this:
function TnxWebFile.GetHTML: string;
begin
if FileInDFM then
begin
Result := HTMLDoc.Text;
end else
begin
Result := LoadFile;
end;
end;
function TnxWebFile.LoadFile: string;
var
FS: TFileStream;
SS: TStringStream;
begin
if (HTMLFile <> '') and FileExists(HTMLFile) then
begin
FS := TFileStream.Create(HTMLFile, fmOpenRead);
SS := TStringStream.Create('');
try
SS.CopyFrom(FS, FS.Size);
Result := SS.DataString;
finally
FS.Free;
SS.Free;
end;
end else
begin
Result := ''; // if the file doesn't exist, then return nothing
end;
end;
procedure TnxWebFile.SetFileInDFM(const Value: Boolean);
begin
// Note: if this is True, then the contents of the file are added to the HTMLDoc property, and thus
// written to the DFM file. If that is the case, and you make changes to the file, they _won't_ be reflected
// in the component. If this is False, then the component will hunt up the file each time that it is asked for
// and sent as Content. In that case, you can change the file, and those changes will reflect in the next view
if FFileInDFM <> Value then
begin
FFileInDFM := Value;
HTMLDoc.Clear;
if FFileInDFM then
begin
HTMLDoc.Add(LoadFile);
end;
end;
end;
The GetHTML method returns either the contents of the HTMLDoc.Text
property, or the LoadFile method, depending on the value of the
FileInDFM property. LoadFile does what you'd expect it to given its
name, and returns a string with the contents of the file named in the
HTMLFile property. Finally, the SetFileInDFM method manages the
content in the HTMLDoc property, clearing or setting the values in it
depending on the SetFileInDFM also.
Now, when you add this component and set the HTMLFile property,
the contents of that file will be added to the TAdapterPageProducer.
You could use this to manage, say, an article like this one as an
external HTML file, but easily integrate it into a page template for
your site. As a matter of fact, that is exactly what I do with the
articles on my site.
One More Component
TnxHTMLCode shows how to add any
HTML to your web page. There may be times, however, when you want to
add specific types of HTML, and our component architecture can handle
that as well. For example, there may be times when you want to add a
Javascript code section to your page. Maybe you have a number of
different standard code snippets that you like to add to pages as
needed.
We can easily create a component that
will add a Javascript block. In fact, it will be designed almost the
same as the TnxHTMLCode component, but instead of an HTML property,
it will have a Javascript property of type TStrings, and its GetHTML
method will properly format the Javascript. The class declaration
will look like this:
TnxJavascriptEntry = class(TnxBaseWebSnapComponent)
private
FJavascript: TStrings;
procedure SetJavaScript(const Value: TStrings);
protected
function GetHTML: string; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Javascript: TStrings read FJavascript write SetJavaScript;
end;
The GetHTML function is implemented like so:
function TnxJavascriptEntry.GetHTML: string;
const
CRLF = #13#10;
JSTemplate = CRLF + '<SCRIPT LANGUAGE="JavaScript">' + CRLF +
'%s' + CRLF +
'</SCRIPT>'+ CRLF;
begin
Result := Format(JSTemplate, [FJavascript.Text]);
end;
Again, it's easy to create these components
because all you have to do is manage the properties you need to
customize your HTML, and then implement the GetHTML method to put it
all together. You could easily create components to add Delphi code
entries, links, graphics, or other elements to your HTML code. The
simple abstract class TnxBaseWebSnapComponent does all the plumbing
to keep the TAdapterPageProducer happy, and all you need to do is
worry about creating the appropriate HTML.
Registering All of This
Of course, like any component, you need to register all of these
classes with Delphi. However, unlike registering regular components
on the palette, registering WebSnap components is a bit more
complicated, and in the case of TAdapterPageProducer components, it
is a bit more complicated still. The declaration for this unit is
as follows:
unit nxHTMLCompsReg;
interface
procedure Register;
implementation
uses Classes, WebComp, MidItems, WebForm, nxHTMLComps;
type
THelper = class(TWebComponentsEditorHelper)
protected
function ImplCanAddClassHelper(AEditor: TComponent; AParent: TComponent; AClass: TClass): Boolean; override;
end;
var
nxHelper: THelper;
{ THelper }
function THelper.ImplCanAddClassHelper(AEditor: TComponent; AParent: TComponent; AClass: TClass): Boolean;
begin
// Return True to indicate that AParent is a valid parent of the components being registered.
Result := AEditor.InheritsFrom(TCustomLayoutGroup) or
AEditor.InheritsFrom(TCustomAdapterForm);
end;
procedure Register;
begin
RegisterWebComponents([TnxWebFile, TnxHTMLCode, TnxJavascriptEntry], nxHelper);
end;
initialization
// Helper is used at design-time to indicate which components can parent the THTMLCode components
nxHelper := THelper.Create;
finalization
UnregisterWebComponents([TnxHTMLCode, TnxWebFile, TnxJavascriptEntry]);
nxHelper.Free;
end.
The code is pretty straightforward, but there are some things to
note:
The unit has a Register procedure just like any design time
registration unit.
The three classed in question are registered with a call to
RegisterWebComponents in the Register procedure, but then must be
unregistered with a call to UnregisterWebComponents in the
finalization section of the unit
The call to RegisterWebComponents takes as its second
parameter the instance of a class that descends from THelper. In
this case, we declared TnxHelper. You need to implement just the
ImplCanAddClassHelper method, and return true if the classes being
registered can be added to the class type passed in to the function.
In this case, the components can be added to any TCustomLayoutGroup
or TCustomAdapterForm descendants. This class is the one that tells
the IDE which classes can become sub-components of other classes
i.e., it tells the IDE which classes to list when you select a
component in the Web Surface Designer and press the Add Component
button
The TnxHelper class instance is created in the initialization
section of the unit and destroyed after the components are
unregistered in the finalization section.
The Obligatory Conclusion
This is a pretty cool component, because the possibilities are
endless. You can easily create components that build all sorts of
HTML constructs based on properties the user can set. Simply use
those properties to build HTML based on a template and you are off to
the races. As an aside, a number of files in the
<delphi>sourceinternet directory have routines for managing
and building HTML, so take a look there and see what you can ferret
out.
As always, the
code for the components can be found on Code
Central.
Nick Hodges is the Chief Technology Officer at Lemanix Corporation, a consulting shop specializing in Delphi Development. He likes to read Stephanie Plum novels and keep track of the Minnesota Timberwolves. But mostly he like to hang with his family and enjoy their new home in St. Paul, MN.