The WebSnap framework is highly atomized thanks to the heavy use of
interfaces. This makes it easy to create new implementations of common tasks.
For example, WebSnap defines an interface (IWebUserList) for tracking user names,
passwords, permissions, and display-names, and includes a basic implementation
that tracks all that data in memory. So what if we want database
functionality? Just re-implement the IWebUserList interface, as I did for
the TDBWebUserList
component that's a part of the WebSnap
Pack, and it integrates right in with the rest of WebSnap!
So let's take a walk through techniques used in the free WebSnap
Pack, which extends WebSnap by re-implementing some interfaces and takes
advantage of some presently un-documented features.
TDBWebUserList
To create a database-enabled Web User
List, the obvious answer was to implement the IWebUserList interface:
IWebUserList = interface
['{0877DEAF-AB5D-11D4-A503-00C04F6BB853}']
function ValidateUser(Strings: TStrings): Variant;
function CheckAccessRights(UserID: Variant; Rights: string): Boolean;
end;
Diving into WebSnap's implementation of this interface, I found one
inadequacy: There's no way to get a user's display name! Was I scared? Did I give
up? No! I simply did some extending by creating my own descended interface,
IudWebUserList:
IudWebUserList = interface(IWebUserList)
['{0C7E6E80-3F82-47C6-B37E-04BEA4FAEE4A}']
function UserDisplayName(AUserID: variant): string;
end;
Tada! Now we have an interface to implement, and it's still compatible with
what WebSnap expects. Once this interface was in place, it only needed to be
implemented, and it snapped right in to WebSnap, thanks to interfaces.
Adding Caching
I solicited feedback on TDBWebUserList from the community. Graham Colwell suggested that
I add caching for logged-in users to speed things up. That way, it wouldn't be
necessary to access and refresh the database each time data about the user
(such as permission information) was needed. The solution seems pretty simple:
just keep a list of logged-in users and access that list instead of the
database. This brought up one problem, however: How can my component know when the
user logs out so that it can delete him or her from the cache?
Back to diving through the documentation (also known as Borland's source code)! I
couldn't find anything in the help, so I just started browsing WebSnap's source.
Then I found the answer: INotifyWebActivate! The INotifyWebActivate
interface does what you might expect: It lets its implementer be notified
of activation and deactivation of the WebSnap application. INotifyWebActivate is
defined as follows:
INotifyWebActivate = interface
['{CE18BE42-1358-11D4-ABF4-F18FFAD12B3C}']
procedure NotifyActivate;
procedure NotifyDeactivate;
end;
After implementing this undocumented interface, my component was magically
notified of the activation and deactivation of my WebSnap application! Great! All it
took, after discovering this interface, was a simple routine to determine whether the
user is logged in or not, and to delete them from the cache if they're not.
TudEndUserSessionAdapter
The TudEndUserSessionAdapter component was added to the WebSnap Pack to take
advantage of the TDBWebUserList's IudWebUserList interface and fix the
EndUser.DisplayName bug (The TEndUserSessionAdapter displays the user name
instead of the display-name).
To do this, I took a look at the TEndUserSessionAdapter and discovered that
the the user name is stored in the Session object, but the display-name is not.
Simple fix: Just set a new Session variable for the user name. When the
display-name is requested from script, just read the session object and return
the value:
function TudCustomEndUserSessionAdapter.GetUserName: string;
begin
if WebContext <> nil then
Result := WebContext.Session.Values[sUserName];
if Result = '' then
Result := UserID;
end;
Pretty simple, eh? See...this WebSnap stuff isn't too hard after all!
TudLoginFormAdapter
The TudLoginFormAdapter is by far the easiest addition to the WebSnap Pack.
It simply takes the existing TLoginFormAdapter and publishes the NextPage
property. Here is the full source code:
type
TudLoginFormAdapter = class(TLoginFormAdapter)
published
property NextPage;
end;
The NextPage property allows you to set where a user goes once he or she is
logged in. For example, you may want to re-direct administrators to an administration
page, or customers to a support page, depending on
permissions.
Here's a quick code example:
procedure TLoginPage.udLoginFormAdapterLogin(Sender: TObject; UserID: Variant);
begin
if HomePage.DBWebUserList.CheckAccessRights(UserID, 'admin') then
udLoginFormAdapter.NextPage := AdminPage.Name
else if HomePage.DBWebUserList.CheckAccessRights(UserID, 'user') then
udLoginFormAdapter.NextPage := UserPage.Name
else
udLoginFormAdapter.NextPage := VisitorPage.Name;
end;
Of course, because the NextPage property is published, you can set the value
at design time.
TudStringsValuesList
The TudStringsValuesList was introduced for two reasons: to fix a Borland
TStringsValuesList bug in which the OnPrepareStrings event is never called, and
to provide access, in Delphi, to the current string in script iteration. Even
though this required a complete rewrite, it was not very difficult. All it took
was some copying and pasting, and a few minor changes to the code.
Fixing the bug. Fixing the OnPrepareStrings bug, was simple. Take a look
at this snippet of the original Borland code for TStringsValuesList:
procedure TCustomStringsValuesList.SetStrings(const Value: TStrings);
begin
FStrings.Assign(Value);
end;
function TCustomStringsValuesList.GetStrings: TStrings;
begin
if not (csLoading in ComponentState) and not StringsPrepared then
PrepareStrings;
Result := FStrings;
end;
procedure TCustomStringsValuesList.PrepareStrings;
begin
StringsPrepared := True;
if Assigned(OnPrepareStrings) then
OnPrepareStrings(Self);
end;
procedure TCustomStringsValuesList.ImplNotifyDeactivate;
begin
inherited;
StringsPrepared := False;
end;
The SetStrings and GetStrings methods are for the Strings property of the
TStringsValuesList. If you look at the GetStrings method, you'll notice that it
calls the PrepareStrings method only if the strings are not prepared and the
component is not being loaded.
What is supposed to happen is, when the strings are needed for the first
time, PrepareStrings should be called. When the request is deactivated,
ImplNotifyDeactivate should set StringsPrepared back to false so that
PrepareStrings would be called again on the next request. This
doesn't happen, and here's why:
function TCustomStringsValuesList.ImplGetListName: string;
begin
if FIndex < FStrings.Count then
Result := FStrings.Names[FIndex]
else
Result := '';
end;
function TCustomStringsValuesList.ImplGetListValue: Variant;
var
S: string;
begin
if FIndex < FStrings.Count then
begin
S := FStrings.Names[FIndex];
if S <> '' then
Result := FStrings.Values[S]
else
Result := FStrings[FIndex]
end
else
Result := Unassigned;
end;
function TCustomStringsValuesList.ImplNextIteration(
var OwnerData: Pointer): Boolean;
begin
Inc(FIndex);
Result := FIndex < FStrings.Count;
end;
When the strings are needed in script, the code is accessing the
internal FStrings variable, thus circumventing the GetStrings method! Whoops!
To correct this problem, simply change the first call of FStrings from FStrings
to Strings in each of the methods that access the strings.
Adding a CurrentIteration property. The purpose of the
CurrentIteration property is so that you can tell, in your
Delphi code, where your page's script is in its current iteration. To do
that is simple. In the iteration implementation methods, just assign the
returned string:
function TudStringsValuesList.ImplStartIterator(var OwnerData: Pointer):Boolean;
begin
FIndex := 0;
Result := FIndex < Strings.Count;
if Result then
FCurrentIteration := Strings[FIndex];
end;
function TudStringsValuesList.ImplNextIteration(var OwnerData:Pointer): Boolean;
begin
Inc(FIndex);
Result := FIndex < Strings.Count;
if Result then
FCurrentIteration := Strings[FIndex];
end;
If the iteration is successful, result is true. And if the result is true, we
set the CurrentIteration property to the value of that iteration.
Wrapping Up
Say it with me: WebSnap is a lean, mean, web-serving machine. When
you get a chance, pop open the hood of WebSnap and see just what a fine
piece of work Borland has done. And if you see room for improvement, feel free
to contribute to the WebSnap Pack! It's there for the community.
Jimmy Tharpe is a skinny little
20-year-old kid who has nothing better to do that run his own company
and play with Delphi all day long.