Sie sind auf Seite 1von 38



Volume 9 Number 11

Greater Delphi

Product Review: RoboHelp Office X4

Junk Mail Solution

Lets face it; when it comes to the current e-mail spam pandemic,
Outlooks Rules Wizard isnt up to the task. The good news is that Outlook
is an Automation Server and therefore readily tweakable by a Delphi
developer. Thats where Bill Todd comes in with his Junk Mail Solution.
Theres plenty to learn, or you can just download it and put it to use.

November 2003 vol. 9, no. 11

The Complete Monthly Guide to Delphi Development

Junk Mail Solution

Delphi and Outlook Separate the Good from the Bad


Bill Todd on
InterBase 7.1

Delphi Tech

Windows Services

Creating Multi-threaded Windows Services

Delphi Informant

Matthew Hess demonstrates techniques that allow developers to build

powerful service applications that can spawn multiple worker threads
to do the actual work of the service, while leaving the main service
thread available for incoming requests.

Trading Up to Advantage
Database Server 7.0
Using the Framework:
Queues, Stacks, & ArrayLists

Special C#PRO
Preview Section,

vs. VS .NET

Dynamic Delphi


Trading Up to Advantage Database Server 7.0

Cover Art by Arthur A. Dugoni Jr.

Thinking of moving up to a full-fledged database server with triggers,

stored procedures, transaction processing, etc? Bill Todd gets you started
with one of the premium options for Delphi database developers, with
step-by-step directions and working demonstration projects to download.

.NET Developer


Using the Framework: Part I

Xavier Pacheco begins a new series on Microsoft .NET Framework

development by introducing classes and interfaces in the
System.Collections namespace, including working demonstration
projects of three of the classes: Stack, Queue, and ArrayList.

Columns & Rows

Special Preview Edition

InterBase 7.1

2 C#Builder vs. VS .NET

33 RoboHelp Office X4

6 Query Client-Side Data


Bill Todd reports on whats new with InterBase 7.1, the latest
version of Borlands cross-platform, self-maintaining, embeddable
database. Its a free upgrade with an impressive list of new features,
including savepoints, Borland Data Provider (BDP) support, multiprocessor support, an integrated performance monitor, and more.

Product Review by Alan C. Moore, Ph.D.

2 Toolbox
37 File | New by Alan C. Moore, Ph.D.


With Borlands new C#Builder, VS .NET now

has a real competitor. Both are great tools,
but which will work best for you? Joe Mayo
compares and grades these IDEs to help
you pick the right editor for the job.

When getting data in .NET, youre in, youre

out, you free up those precious server
resources as quickly as possible so you
have a truly scalable system. Youve got
to do some things on the client side like
searching and selecting data that you
used to do on the server. Brian Noyes helps
you explore your options for locating and
extracting the info you need.

Create Visual Product Walkthroughs with Expo Walkthrough

InstallShield Software recently

announced Expo Walkthrough, a nextgeneration solution for business users
who need to quickly create guided
product tours. Expo Walkthrough is
a visual communication tool that lets
users build, edit, and distribute visual
product tours quickly and easily.
Capturing, storyboarding, annotating,
and publishing tools guide the user
through the creation of a Macromedia
Flash-animated walkthrough that can be
deployed via the Web, intranet, CD-ROM,
HTML help files, or e-mail.
Visually displaying an application,
process, or idea reveals much more
than written documentation. Business
users constantly require concise, visual explanations for documentation,
testing, support, and other purposes,
but dont have the time to learn a
complicated tool. InstallShield created
eHelp Introduces
RoboHelp Studio
eHelp Corp. announced RoboHelp
Studio, a suite that combines RoboHelp Office (Help authoring) and
RoboDemo (software to create interactive tutorials and demos that can be
integrated into a Help system). RoboHelp Studio is designed for technical
writers and software developers, but
is useful for IT support, technical support, or Help desk applications.
Help systems created with RoboHelp
Studio provide end users with visual
step-by-step tutorials that complement
text-based instructions. Also, developers can add audio voice-overs.
Developers and writers can create professional Help systems for
desktop and Web-based applications,
including .NET. Users can output
these Help systems into any standard
Help format. Choose from a variety
of authoring environments, such as
Microsoft Word, the built-in HTML
editor, or any popular HTML editor,
or import existing content from Word,
FrameMaker, HTML projects, or existing Help projects.
eHelp Corp.
Price: See Web site for pricing and upgrade options.
Web Site:

Expo Walkthrough
to literally walk
through software,
insert comments,
and publish everything that happens.
Expo Walkthrough
works behind the
scenes, capturing
everything a user
does on screen.
Once a user is
finished capturing screen shots
and mouse movements, easy-to-use
storyboarding and
annotation tools help create a visual
walkthrough in less time than it takes
to write about it. Hit publish and anyone with a Web browser can view the
Flash-animated output.

InstallShield Software Corp.

Price: US$399; a maintenance plan and future
upgrades can also be purchased.
Contact: (800) 809-5659;
outside the US, (847) 466-6000
Web Site:

Gnostice Develops PDF Document Management

Solution for Delphi and C++Builder
Gnostice announced PDFtoolkit, a
VCL component set for Delphi and
C++Builder developers to manage
PDF documents from within applications. The developer can place the
PDFDocument component on a Form
or DataModule, set properties, and
then call methods to fill or read form
data, compress, secure by setting a
password, append or merge multiple
PDF documents, rubber stamp, add
bookmarks, and perform many other
tasks. The output can be received in a
memory stream or a disk file.
The form processing feature of
PDFtoolkit enables filling and reading
of form data from PDF forms or AcroForms. PDFtoolkit can be queried for
a list of the form fields present in the
supplied PDF document, then values
can be set for each field, accessing the
field by name or index, and finally a
new PDF document can be written out
with all the fields filled. Field values
from filled PDF forms can be extracted,
again by providing a source PDF document and a string list to store the read
fields and values.
PDFtoolkit supports a variety of
PDF document management and
manipulation functions that enable a
developer to implement all PDF management tasks within the application,


including: merge a list of PDF documents; insert pages from one document to another; append pages to
the end of a document; delete pages
from a document; and extract pages
from one document by specifying
a range of pages to extract into the
second document. Documents can be
encrypted with owner and user passwords, and written to disk or memory
in compressed or uncompressed form.
Access permissions can also be set on
the user password to control printing, copying of text and graphics, and
modifying of the document.
PDFtoolkit supports the association
of pages with thumbnail images to
which users can easily relate simply
by specifying the page number and
the image with which to associate
the page. Physical markings can be
underlaid or overlaid as watermarks
or stampings to designate a document
for specific uses and restrictions,
such as Draft or Confidential.
Markings can contain text or images,
or both. You can even combine multiple underlay/overlay markings to
create composite markings.
Gnostice Information Technologies
Price: Check the Gnostice Web site.
Web Site:

Actual Transparent Windows 2.0 Available

3DChart 7.0 Available

Actual Tools announced Actual Transparent Windows 2.0, a Windows shell

enhancement application that allows
setting any level of transparency for
any individual window on the system.
You can set the individual transparency rate from 0 percent (fully transparent) to 100 percent (solid) for each
window. Actual Transparent Windows
also has the option to set transparency so it occurs only when a window
becomes inactive.
Actual Transparent Windows
adds a new dimension to desktop
space management, letting you
organize simultaneously open
windows without having to minimize and restore them. Desktop
space saved by eliminating
opaque windows can be used for
placing additional icons. Another
great use of this program is for
setting transparency depending
on the importance of a task an
application performs.

Nevron released 3DChart 7.0, an ActiveX

component that displays 2D and 3D
business, scientific, and presentation
charts. 3DChart is a 32-bit ActiveX
component for Visual Basic and other
ActiveX-compatible environments
(including Delphi, Visual C++, Microsoft Office, and many others), and it
fully supports the .NET environment.
3DChart supports more than 23 charting types with many different styles and
logic variations. All chart type combinations can be simultaneously displayed.
3DChart also boasts seamless ADO database and ASP server integration. Built-in
advanced interactivity features include:
client- and server-side events, tooltips,
zoom, offset, scroll, and more. With
87 automation objects containing 557
properties and 442 methods, 3DChart
provides flexibility in development.
3DChart has the ability to display
multiple charts in the same charting
canvas and describe them on one or
more legends.
3DChart offers direct connection
to any ADO data source, as well as
seamless ASP and ASP.NET integration. Configuring the chart to work
on ASP and ASP.NET servers is done
with a helper application, so no code
is required.
3DChart offers advanced control over
the axis scaling and position. There is no
scale mode not supported, and the axes
can be displayed at any position. Also,
annotations can be positioned on arbitrary positions, enabling you to increase
the readability of the created images.
Using 3DChart you can export to JPEG,
PNG, Bitmap, Targa, TIFF, or native
Chart format. The component can also
export PNG, TIFF, and Targa images
with transparent background. The chart
data can be exported to HTML, XML,
and tab-delimited text files, and there is
support for SVG and HTML image maps.
Also, 3DChart can generate WML output
for WAP devices.
3DChart is compatible with Windows
95/98/NT 4.0/2000/XP, and supports
a Windows XP look and feel, as well
as Unicode and localization to different

Actual Transparent Windows saves all

your settings so that you need to set the
transparency only once for each window.
Actual Transparent Windows supports
English, German, Spanish, French, Italian, Portuguese, Swedish, Catalan, Turkish, and Polish, and runs under Windows 2000/XP.
Actual Tools
Price: US$19.95
Web Site:

QuickHelp for Windows 1.0 Available

Excel Software announced QuickHelp
for Windows, a development tool for
creating and deploying application Help
to Windows 95 through XP, most Linux
distributions, Mac OS 9, and Mac OS
X. Using QuickHelp you can manage,
author, and test Help topics in a single
tabbed window running on your platform of choice, then deploy the Help
system to virtually any computer. Help
information resides in an XML file distributed with a native viewer executable
for each platform.
QuickHelp consists of a QuickHelp
Builder for authoring Help systems
and a QuickHelp Viewer for deploying them to end-users. For the enduser, QuickHelp supports a table of
contents, an index with an automatic
search field, word searches across topics, color-highlighted topic text with
hypertext links, formatted images, and
context-sensitive Help from application
menus and dialogs. From the Contents
panel, the user can view, expand, or
collapse topics. Use the Index panel
to locate topics based on index words.
Forward and backward buttons navigate recently viewed topics.

For the developer, QuickHelp provides an authoring environment that

combines the Contents and Index
panels with the Edit and General
panels for editing the Help file. The
Edit panel is used to add, edit, delete,
or move topics within the Contents
tree. Formatted text, images, and links
between topics can be quickly added.
Each topic has fields for defining index
words and context-sensitive identifier
strings that link topics to application
menus and dialogs. Topics or portions
of a topic can be conditionally visible based on the current platform or
other variables. This makes it easy to
customize your Help system for different platforms or product builds from a
single source. The General panel has
buttons to save, open, close, verify,
import, or export Help topics. QuickHelp handles administrative activities
like maintaining links between topics,
reordering topics, and locating bad
links, missing indexes, or images.
Excel Software
Price: US$195
Web Site:


Nevron LLC
Price: One developer license, US$399.
Web Site:





By Bill Todd

Using Outlook as an Automation Server to Manage
Your Unwanted E-mail

hate junk mail. Unfortunately, I receive

between 100 and 150 junk messages a day.
I use Outlook 2000 as my mail client. Ive

looked at others, including Outlook Express and

Agent, but none of them have tools for managing
junk mail that are usable with the volume of
messages I receive.

The good news is that Outlook is an Automation server, so I

decided to write an Automation client in Delphi that would
simplify processing the junk that appears in my inbox.
I put together a list of requirements for a junk mail manager.
I arrived at these requirements from my frustration with the
tools Outlook provides, with a little common sense thrown
in. It must:


Junk Senders

E-mail addresses from which I never want

to receive e-mail.

Junk Content

Phrases that identify a message as

probably junk mail if they appear in the
subject or body of the message.


E-mail addresses from which I always

want to receive mail, even if the
mail contains phrases from the junk
content list.

Exception Content

Phrases that identify a message that I

want to see no matter who it is from or
what else it contains.

Figure 1: Junk mail control lists.


Allow me to easily search and maintain the four lists

shown in the table in Figure 1.
Perform the following functions with a single mouse click:
Scan the Outlook inbox and move all messages from
senders in my exceptions list to a new message folder.
Move all messages that contain phrases from my
exception content list to the new message folder.
Delete all messages from senders in my junk senders list.
Move all messages that contain phrases from my junk
content list to a filtered messages folder, so I can
check them to see if they are really junk.
Make the comparison case-insensitive when comparing
phrases in the junk content and exception content lists
to the text in the message subject and message body.
Add all the messages in my inbox to my junk senders
list, and delete the messages, with a single mouse click.
Add the messages in my filtered messages folder to
the junk senders list and delete the messages with a
single mouse click.
Add all the senders in my new messages folder to my
exceptions list with a single mouse click.
Never add a sender who is in my exceptions list to the
junk senders list.
Automatically delete all senders in the exceptions list
from the junk senders list each time the program starts.
Let me add domains to the junk senders list and the
exceptions list. For example, if I add *
to the exceptions list, all messages from any address at will be moved to my new messages folder. If I
add * to my junk senders list, all messages from
any address at will be deleted.
Let me add multiple logically anded phrases to the
junk content and exception content lists, so all the
phrases must appear in a message before it is classified
as junk or as an exception.
Add all the e-mail addresses in my Outlook contacts list to
the exceptions list. Later versions of Outlook will do this
automatically, but Outlook 2000 and earlier versions dont.



Junk Mail Solution

ExceptionListFile=exception list.txt
JunkListFile=junk senders.txt

Figure 3: The OLBYPASS.INI file.

Figure 2: The main form.

The Trick
The bad news is that the Outlook object model doesnt make
the senders e-mail address available as a property of the message object. To prevent VBA scripts from being used as a vehicle for viruses, the Microsoft security update for Office disables
many functions that used to be available through automation
and which we need to write the junk mail handler.
The Extended MAPI functions provide a way to bypass these
restrictions. The security update doesnt affect Extended MAPI
because Extended MAPI isnt accessible from the VBA scripting
language. I used a product named Outlook Redemption to provide an easy-to-use Automation interface to the Extended MAPI
functions I needed. Read more about Outlook Redemption (and
download a free version) at
You must download and install Outlook Redemption to run the
program that accompanies this article (see end of article for
details on how to download the accompanying program).
Creating the Program
Figure 2 shows the programs main form. The program, named
OLBYPASS.EXE, uses an INI file with the same name to store
configuration information. Figure 3 shows the contents of the
INI file. The four entries in the Files section specify the names
for the files that hold the exceptions list, junk senders list, junk
content list, and exception content list, respectively. All the files
are located in the directory where the EXE resides.
The Folders section contains the name of the filtered message
folder and the new message folder. I use names that begin with
*_ so they will sort at the top of the list of subfolders under the
Outlook Inbox folder. The Settings section contains a single
parameter named ContentDelimiter. This is the character used
to delimit strings you want anded together in the junk content and exception content list. For example, if you dont want
to get messages that contain both the phrase auto insurance
and save money and if ContentDelimiter is set to the vertical
bar character, you would add this line to the junk content list:
auto insurance|save money


procedure TMainForm.FormCreate(Sender: TObject);

Top := 0;
Left := 0;
SplashForm.Msg('Loading Lists...');
SplashForm.Msg('Opening Outlook...');
SplashForm.Msg('Cleaning Junk Senders...');

Figure 4: The main forms OnCreate event handler.

procedure TMainForm.LoadLists;
ExceptionList := TStringList.Create;
ExceptionList.Duplicates := dupIgnore;
ExceptionList.Sorted := True;
JunkList := TStringList.Create;
JunkList.Duplicates := dupIgnore;
JunkList.Sorted := True;
ContentList := TStringList.Create;
ContentList.Duplicates := dupIgnore;
ContentList.Sorted := True;
ContentExceptList := TStringList.Create;
ContentExceptList.Duplicates := dupIgnore;
ContentExceptList.Sorted := True;

Figure 5: The LoadLists method.

Figure 4 shows the main forms OnCreate event handler.

It begins by setting the position of the form, then calls
the LoadIniFile method. LoadIniFile uses a TIniFile object
to read the settings from the INI file into private member
variables that have been added to the main forms class. If
you want to see this code, download the application that
accompanies this article. The application has a splash form
that displays progress messages as it loads. The call to
SplashForm.Msg displays a new message on the splash form.
The call to LoadLists, shown in Figure 5, creates and
loads the four string lists used by the application. The
StringList instance variables are declared as private



Junk Mail Solution

procedure TMainForm.OpenOutlook;
// Get the Outlook Application object.
OutlookApp := CreateOleObject('Outlook.Application');
// Get the MAPI NameSpace object.
MapiNamespace := OutlookApp.GetNameSpace('MAPI');
// Get Personal folder from the MAPI folders collection.
Personal := MapiNamespace.Folders('Personal Folders');
// Get the Inbox and Deleted Items folders.
Inbox := Personal.Folders('Inbox');
Contacts := Personal.Folders('Contacts');
FilteredFolder := Inbox.Folders(FilteredFolderName);
NewFolder := Inbox.Folders(NewFolderName);
DeletedItems := Personal.Folders('Deleted Items');

Figure 6: The OpenOutlook method.

procedure TMainForm.CleanJunkSenders;
{ Deletes any entry from the junk senders list
that's in the exceptions list. }
I: Integer;
P: Integer;
for I := 0 to ExceptionList.Count - 1 do begin
P := JunkList.IndexOf(ExceptionList[I]);
if (P <> -1) then

Figure 7: The CleanJunkSenders method.

member variables of the main form, so the lists are

accessible to all methods of the main form.
Figure 6 shows the OpenOutlook method. This method
begins by calling CreateOleObject to open an Automation
connection to Outlook. If Outlook is already open, this call
will connect to the running Outlook instance; otherwise,
it will open a hidden instance of Outlook. Like all the
variables used to store references to Outlook objects, the
OutlookApp variable is of type Variant and is declared as a
private member variable of the main form.
The next statement calls Outlooks GetNameSpace method
to get a reference to the MAPI namespace. The next
statement gets a reference to the Personal Folders collection
from the MAPI namespace Folders collection. The next
two statements get references to the Inbox folder and the
Contacts folder from the Personal Folders Folders collection.
The next two statements get references to the new
messages folder and the filtered messages folder described
in the INI file. You must create these folders manually
under the Inbox folder in Outlook. The last statement of
this method gets a reference to the Deleted Items folder.
As you can see, the trick to doing anything with Outlook
via Automation is to understand the Outlook object model,
and the properties and methods of each object. The best
place to start is the Outlook VBA help file. For Outlook 2000
this file is named VBAOUTL9.CHM. This help file contains
a diagram of the Outlook object model, as well as detailed
information about the properties and methods of each


procedure TMainForm.CleanInbox;
{ Move messages in the Inbox to appropriate folder. }
I: Integer;
MailMsg: Variant;
SenderAddress: string;
SenderDomain: string;
if (EditingList <> elNone) then begin
'You cannot clean the inbox while editing a list.');
for I := Inbox.Items.Count downto 1 do begin
MailMsg := Inbox.Items(I);
SenderAddress := GetSendersAddress(MailMsg);
SenderDomain := GetSenderDomain(SenderAddress);
SenderDomain := '*@' + SenderDomain;
// If sender is in exception list, move this message
// to the new message folder.
if (ExceptionList.IndexOf(SenderAddress) <> -1) or
(ExceptionList.IndexOf(SenderDomain) <> -1) then
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end; // if
// If sender is in junk list, delete this message.
if (JunkList.IndexOf(SenderAddress) <> -1) or
(JunkList.IndexOf(SenderDomain) <> -1) then
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end; // if
end; // for

Figure 8: The CleanInbox method.

object. If you have questions about working with any of the

Office applications via automation, you can post them in
the borland.public.delphi.oleautomation newsgroup at
Returning to the main forms OnCreate event handler in
Figure 4, youll see that the last statement is a call to the
CleanJunkSenders method shown in Figure 7. This method
checks every entry in the exceptions list to see if it exists in the
junk senders list, and, if it does, it deletes the entry from junk
senders. This protects you in case you accidentally enter into
the junk senders list an address that is in your exceptions list.
How It Works
The Clean Inbox button calls the CleanInbox method shown
in Figure 8. This method starts by ensuring that none of
the lists are being edited by checking the value of the
forms private member variable named EditingList. Next,
a for loop scans the messages in the Outlook Inbox,



Junk Mail Solution

function TMainForm.GetSendersAddress(MailMsg: Variant):

PrSenderEmailAddress = $0C1F001E;
SafeMsg: Variant;
SafeMsg := CreateOleObject('Redemption.SafeMailItem');
SafeMsg.Item := MailMsg;
Result := SafeMsg.Fields[PrSenderEmailAddress];

Figure 9: The GetSendersAddress method.

starting with the last message and ending with the first
message. The messages are scanned from last to first
because this method will move messages to other folders.
The first statement in the for loop gets a reference to
the current message and passes it as a parameter to
GetSendersAddress, shown in Figure 9.
As I mentioned earlier, Outlook doesnt make the
senders e-mail address available as a property of the
message object. To get it, you have to use one of the
methods of the Redemption library. The code begins
by calling CreateOleObject to get a reference to the
Redemption.SafeMailItem object. Next, the code assigns
the mail message passed as a parameter to the Redemption
mail objects Item property. Finally, the senders address is
retrieved by a call to the SafeMsg objects Fields property,
passing the constant PrSenderEmailAddress as an index.
Returning to Figure 8, the code calls GetSenderDomain
to extract the domain from the senders e-mail address.
GetSenderDomain uses the Pos function to find the position
of the @ in the e-mail address, then calls the Copy function
to extract all of the characters after the @. The following
statement prepends *@ to the beginning of the domain,
because domain entries in the exceptions list and junk
senders list begin with *@ followed by the domain name.
The if statement checks if either the senders e-mail address,
or the senders domain, is in the exception list. If it is, a
call to the message items Move method moves the message
from the Inbox to the new message folder. The continue
statement causes a return to the top of the for loop without
any further processing, if the message was moved.
The next if statement checks if the senders address or
domain are in the junk senders list. If so, the message
is moved to the deleted items folder. The method ends
with a call to CheckMessageContent, which is shown in
Listing One (on page 8). CheckMessageContent gets the
mail message object as a parameter. It begins by setting
the MoveMsg Boolean variable to False, and saving
the messages subject and body in upper case. Next it
creates a string list, StrList, that will be used to hold
the strings from the junk content and exception content
lists. StrLists Delimiter property is set to the delimiter
character read from the INI file at program startup.
The for loop scans the ContentExceptList StringList, and
assigns each string to the StrList.DelimitedText property.


Figure 10: Editing the exception content list.

This parses the string from the exception content list

on the delimiter character, and stores the parsed strings
in StrList. Next, MoveMsg is set to True, and an inner
for loop scans StrList checking if each string in StrList
appears in either the subject or body of the message. If
one of the strings isnt found, MoveMsg is set to False and
the loop ends. If MoveMsg is True after the loop ends,
the message is moved to the new message folder. If the
message was not moved, the same process is repeated
to see if the message contains any of the strings in the
junk content list. If it does, the message is moved to the
filtered message folder.
The Inbox to Junk button on the toolbar scans the messages
in your inbox, adds their senders to the junk senders list,
and deletes the messages. The Filtered to Junk button does
the same thing for messages in your filtered messages
folder. The New to Except button scans your new messages
folder and adds all of the senders to your exceptions list.
The Contacts to Except. button scans all of your contacts and
adds all of their e-mail addresses to your exceptions list.
Since Outlook does not allow access to the e-mail address
fields for contacts, Outlook Redemption is used to retrieve
the e-mail addresses.
The buttons in the Edit Lists group box let you load any of
the lists into a memo component, edit them manually,
and save the changes. Figure 10 shows the exception
content list being edited. The Clear List button lets you
close the list without saving any changes. The edit box
and Find button at the bottom of the group box let you
search for any string in the list youre editing. If the
string is found, it will be highlighted.
When you pop up Outlook and find 30 or 40 messages
in your inbox start OLBYPASS and click the Clean Inbox
button. Now messages from junk senders have been
deleted and messages from senders in your exceptions



Junk Mail Solution

list, or that contain exception content, have been moved

to your new message folder. Any messages that contain
junk content have been moved to your filtered messages
folder. If there are messages left in your inbox that you
want, drag them to your new messages folder. If there are
junk messages left in your inbox, click Inbox to Junk and
they will be deleted.
Next, select your filtered messages folder. Scan the
messages to make sure that a message that you really
want did not get filtered out because it contained a
phrase in your junk content list in a context that you did
not anticipate. If you find any messages that you want to
keep, drag them to your new messages box then click the
Filtered to Junk button to delete the junk messages and add
their senders to your junk senders list. Finally, go to your
new messages folder. If there are messages there from
senders you want to add to your exceptions list, click the
New to Except button. You have now dealt with all of your
junk mail in a few seconds with just five mouse clicks.
The best thing about Microsoft Office is that all of its
applications are Automation servers. If you need some
functionality that one of the Office apps does not provide
out of the box, you can usually add it quite easily. The
only surprising thing about this program is that all of its
features are not already built into Outlook. I hope this
program saves some of you as much time as it does for me.
The OLBYPASS application referenced in this article is
available for download on the Delphi Informant Magazine
Complete Works CD located in INFORM\2003\NOV\
Bill Todd is president of The Database Group, Inc., a database consulting
and development firm based near Phoenix. He is co-author of four database
programming books, author of more than 100 articles, a contributing editor to
Delphi Informant Magazine, and a member of Team B, which provides technical
support on the Borland Internet newsgroups. Bill is also an internationally
known trainer and frequent speaker at Borland Developer Conferences in the
United States and Europe. Readers may reach him at

Begin Listing One The

CheckMessageContent method
procedure TMainForm.CheckMessageContent(MailMsg: Variant);
{ Deletes message if subject or body contains one of the
strings in ContentList. Strings in ContentList can
consist of multiple strings delimited by ContentDelimiter
character. If there are multiple strings, the subject or
body must contain all of them. }
I: Integer;
J: Integer;
StrList: TStringList;
MoveMsg: Boolean;
Subject: string;
Body: string;
S: string;
MoveMsg := False;
// Make everything case-insensitive.
Subject := UpperCase(MailMsg.Subject);


Body := UpperCase(MailMsg.Body);
StrList := TStringList.Create;
StrList.Delimiter := ContentDelimiter[1];
// Move messages with desired content.
for I := 0 to ContentExceptList.Count - 1 do begin
// Parse strings in a content string into StrList.
StrList.DelimitedText := ContentExceptList[I];
MoveMsg := True;
// If one of the strings in StrList isn't contained
// in either subject or body, don't delete message.
for J := 0 to StrList.Count - 1 do begin
S := UpperCase(StrList[J]);
if (Pos(S, Subject) = 0) and
(Pos(S, Body) = 0) then begin
MoveMsg := False;
end; // if
end; // for
// If all of the strings in StrList were in the
// subject or body, delete the message.
if (MoveMsg) then begin
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end; // try
end; // if
end; // for
// If the message wasn't moved because it had desirable
// content, check if it has undesirable content.
if (not MoveMsg) then begin
// Delete messages with undesired content.
for I := 0 to ContentList.Count - 1 do begin
// Parse strings in a content string into StrList.
StrList.DelimitedText := ContentList[I];
MoveMsg := True;
// If one of the strings in StrList isn't contained
// in subject or body, don't delete the message.
for J := 0 to StrList.Count - 1 do begin
S := UpperCase(StrList[J]);
if (Pos(S, Subject) = 0) and
(Pos(S, Body) = 0) then begin
MoveMsg := False;
end; // if
end; // for
// If all of the strings in StrList were in the
// subject or body, delete the message.
if (MoveMsg) then
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end; // try
end; // for
end; // if
end; // try

End Listing One






By Matthew Hess

Creating Multi-threaded
Windows Services
Or, Pooling ADO Connections without Sleeping

elphi provides some amazing built-in support for creating Windows Services and
working with multi-threaded applications.

Using these techniques together allows a developer to build powerful service applications that can
spawn multiple worker threads to do the actual
work of the service, while leaving the main service thread available for incoming requests.

The next step in getting set up is to create a worker thread.

Select File | New | Unit and create a simple class based on the
TThread object. Override its Execute method and provide
three private methods: Init, Cleanup, and DoWork. Once
youve created the event stubs, the interface for your service
and your worker thread should look something like Figure 1.
The last thing to do by way of setup is to go back to the first
unit, reference the second unit in its uses clause, and create
a private instance variable for our worker thread, like this:
uses MyWorkerThread;

The Delphi help files provide a starting place with

examples of how to create TThread objects from within a
TServiceApplication, but in practice there are a few tricks of
the trade you should know to really unleash the power of a
multi-threaded service application.
This article will examine a few of these techniques, including how to suspend threads without using the Sleep function
so that waiting threads can be terminated immediately, how
to handle multiple pooled ADO connections from within a
multi-threaded service, and how to use a TCriticalSection
object to make global functions thread-safe.
Getting Set Up
Delphi makes it incredibly easy to create a Windows
Service application. Simply select File | New | Service
Application. At this point, you could simply create an
OnExecute event handler and program the work of the
service there. But we want to delegate the work to worker
threads, so instead of creating an OnExecute handler, look
at the Events tab for the service in the Object Inspector
and create event stubs for ServiceContinue, ServicePause,
ServiceStart, and ServiceStop. Were going to use these
events to create and control our worker threads.


FMyWorkerThread: TMyWorkerThread;

TMyService = class(TService)
procedure ServiceContinue(Sender: TService;
var Continued: Boolean);
procedure ServicePause(Sender: TService;
var Paused: Boolean);
procedure ServiceStart(Sender: TService;
var Started: Boolean);
procedure ServiceStop(Sender: TService;
var Stopped: Boolean);
function GetServiceController: TServiceController;
TMyWorkerThread = class(TThread)
procedure Init;
procedure Cleanup;
procedure DoWork;
procedure Execute; override;

Figure 1: The interfaces for a TService application and a TThread worker thread.


Te c h

Creating Multi-threaded Windows Services

procedure TMyService.ServiceStart(Sender: TService;

var Started: Boolean);
// Initialize the "stop" event.
hStopEvent:= CreateEvent(nil, True, False, nil);
// Create the worker threads.
FMyWorkerThread := TMyWorkerThread.Create(False);
Started := True;
procedure TMyService.ServiceStop(Sender: TService;
var Stopped: Boolean);
// Tell waiting worker threads to stop immediately.
// Terminate the threads.
// Close handle and free thread objects.
Stopped := True;
procedure TMyService.ServicePause(Sender: TService;
var Paused: Boolean);
Paused := True;
procedure TMyService.ServiceContinue(Sender: TService;
var Continued: Boolean);
Continued := True;

Figure 2: Using a Windows event to control the execute loop of the worker thread.

Now we have the basic framework from which to work.

Throughout the rest of this article well be adding to
these two units, implementing their methods, and making
our multi-threaded service come alive.
Control the Worker Thread
The simplest technique for starting and stopping a worker
thread is to call TThread.Create when you want to start it,
and TThread.Free when you want to stop it. This is what
is shown in the Delphi help files and it works fine. In
this case, the Execute method of the worker thread is a
loop based on the Terminated property of the thread.
The main limitation of this approach is that it is difficult
to sleep the thread without making the service unresponsive to pause and stop requests. For example, lets say
your thread needed to poll for a file every five seconds.
Your Execute method would look something like this:
while not Terminated do begin
DoWork; // Look for file; do work if found.

What happens when someone goes into the service

manager and stops your service? The service takes up to
five seconds to stop, because your worker thread doesnt
know its been terminated until the Sleep is done. This is
not good.


So whats the alternative? The Windows API call

WaitForSingleObject provides a nice way for us to suspend a
thread and have the loop terminate immediately at the same
time. To make this work, we first must go back to our main
unit and create a global unit-level variable (in the interface
section) for a Windows event:
hStopEvent : DWord;

We can use this variable as a handle for an unnamed Windows event. When the Service thread starts the service, it
creates the worker threads and creates this event. When it
needs to stop the service, it sets the event and all the worker
threads immediately to stop, because they are waiting for a
single object rather than sleeping. The full implementation
for the service control events is shown in Figure 2.
A few items in this code deserve a little explanation. To
initialize the stop event, we call CreateEvent. The parameters for the CreateEvent method tell us that the event will
be created in the non-signaled state, and that it must be
manually reset. This allows us to use the same stop event
to control multiple worker threads if needed. Notice that we
dont need to supply a name for the event because the handle is global. This is nice because, at least in theory, named
objects expose a security risk. Then, when we want to stop
our thread, the ServiceStop event simply sets the event and
terminates the thread. After that, we must be sure to release
the event handle using CloseHandle.
So what does the Execute method of our worker thread look
like when its controlled not by the Terminated property, but
by a Windows event? Its pretty simple:
iWaitResult: DWORD;
iWaitResult := WaitForSingleObject(hStopEvent, 5000);
until iWaitResult = WAIT_OBJECT_0;

Now we have the same polling-style behavior with the worker

thread. Every five seconds it wakes up and does some work,
but if the main service thread needs to stop, pause, or resume
this thread, it can do so immediately, without waiting for a
five-second sleep to elapse. This is a much better architecture
than looping based on the Terminated property. The only thing
to be aware of is that, as written, the ServiceStop event doesnt
know whether the worker threads actually terminated. If this is
a problem, you can add some code to ServiceStop to confirm the
Terminated property of the worker threads before saying:
Stopped := True;

ADO Connections from the

Worker Thread
If your multi-threaded service application works with a SQL
database, you may want to take advantage of ADO connection pooling to share a limited number of actual connections
among a larger number of worker threads. For this to work,


Te c h

Creating Multi-threaded Windows Services

procedure TMyWorkerThread.Execute;
iWaitResult: DWORD;
Init; // When thread starts.
DoWork; // Where we open and use the connection.
iWaitResult := WaitForSingleObject(hStopEvent, 2000);
until iWaitResult = WAIT_OBJECT_0;
Cleanup; // When thread stops.
procedure TMyWorkerThread.Init;
// Call this once per thread before doing COM.
CoInitialize(nil); stuff
FCon := CoConnection.Create;
// Client side cursors are nice when
// pooling connnections.
FCon.CursorLocation := adUseClient;
procedure TMyWorkerThread.Cleanup;
// Ensure connection is closed and pointer is nil.
if FCon <> nil then begin
if FCon.State <> adStateClosed then
FCon := nil;
// For each CoInitialize, you need one of these.
procedure TMyWorkerThread.DoWork;
// Acquire a connection from the pool.
FCon.Open(GetConnectString, '', '',
// Do something with the connection.
// Close the connection, but don't nil the pointer.
if FCon.State <> adStateClosed then

Figure 3: Init, Cleanup, Execute, and DoWork implementations for our worker
thread showing how to set up COM support and ADO connection pooling.

each worker thread will need its own connection pointer,

each connection must use the same connection string, and
well need to do some manual COM initialization and finalization in the worker thread.
Before we begin writing the code, lets access the ADO type
library and reference it in our worker thread. In Delphi
5, select Project | Import Type Library, then select the Microsoft
ActiveX Data Object (Version 6.0 or 7.0) dll. Add the resulting unit,
ADODB_TLB.pas, to the implementation uses clause of
your worker thread. Then add a private field variable to
your worker thread for the ADO Connection object:

open (or rather, acquire) a connection from the pool. In Init we

need to create a connection object using the CoClass constructor defined in ADODB_TLB. Before we can do that, we must
initialize COM support by calling CoInitialize (youll need to
include the ActiveX unit in the uses clause). Delphi calls this
for you when youre writing a regular Delphi application, but
when youre writing your own worker threads you must do
this. Each call to CoInitialize should be paired with a call to
CoUnitialize, which well take care of in our Cleanup function
(where well also make sure that our connection is closed and
the pointer is set to nil so the COM object will be freed).
Finally, we must modify our Execute method slightly so that
Init and Cleanup are called when the worker thread starts
and stops. Notice that Init and Cleanup are only called when
the thread starts and stops (see Figure 3), not when it is
paused or resumed.
With our initialization in place, we can start to implement
the worker threads DoWork method to include some database work. In DoWork, we will call Connection.Open. With
ADO connection pooling, open really means acquire a
connection from the pool. Then, when DoWork is done, it
calls Connection.Close, which really means release the connection back to the pool. As long as all threads use the same
connection string (and connection pooling is enabled in your
ODBC settings), the threads will pool connections with this
architecture. The full code for Execute, Init, Cleanup, and
DoWork is shown in Figure 3.
Thread-safe Global Functions
Youll notice in Figure 3 that we use the function
GetConnectString to enforce the fact that all worker threads
will use the identical connection string. This could be a global,
thread-safe function in your main Service Application unit,
which brings us to the next topic: thread-safe functions.
Theres a lot of great material out there on concurrent
programming, and this really isnt the place to delve into all
the theoretical issues. Let me recommend that you start with
Ray Lischners Delphi in a Nutshell (OReilly, 2000), which
has an excellent section on concurrent programming. But the
core issue is that if you need to use global functions such
as GetConnectString in a multi-threaded service, you
should make sure they are thread safe, and this boils down to
serializing access to the functions so that only one thread uses
each function at a time.
Delphi provides a class named TMultiReadExclusiveWriteSynchronizer, which is supposed to protect data for multiple
threads, but there is quite a bit of discussion on the Web about
how this object doesnt work. Because of that, Ive chosen to
go with the more expensive but very simple TCriticalSection
object in my code. To do this, add the SyncObjs unit to the
implementation section of your service application, and create
a unit-level variable for a critical section:

TMyWorkerThread = class(TThread)
FCon: Connection;

FMyCriticalSection: TCriticalSection;

Next, well need to implement the Init procedure. This is where

we initialize the connection object, but not where we actually

Then add initialization and finalization sections to the unit to

create and free the critical section:




Te c h

Creating Multi-threaded Windows Services

FMyCriticalSection := TCriticalSection.Create;

procedure TForm1.StartBtnClick(Sender: TObject);

hStopEvent:= CreateEvent(nil, True, False, nil);
FMyWorkerThread := TMyWorkerThread.Create(False);
State := Started;

Finally, protect each function (or section of code as the case

may be) by entering and leaving the critical section like this:

Running your worker threads this way allows you the full
ability to set breakpoints and watches, and to trace and
debug your code line-by-line. The only thing to be aware
of is that your worker threads must have access to global
variables and functions defined in the TService application,
e.g. the hStopEvent event handle. Youll need to copy the
shared code into your test harness, and then you can easily
tell your worker threads to use one or the other version with
some simple conditional compilation. Heres the enhanced
uses clause from TWorkerThread using the conditional
define Testing to compile either as a service or as a regular application for debugging:

procedure SomeThreadSafeProcedure(var s: string);

// Do something here that needs protecting.

In my experience I can protect multiple functions with a single

TCriticalSection just fine. If thread 1 is in function 1, it wont
block thread 2 from entering function 2, even if both function
1 and function 2 are protected with the same TCriticalSection.
You can use this same technique to serialize access to a database connection, if you dont want to use connection pooling.
One of the first questions that comes up when dealing
with Service Applications is how to run them in the Delphi debugger so you can step through your code. The sad
answer is that, at least with Delphi 5 and Windows 2000,
there is no easy way to do this. Happily, however, delegating
the real work of your Service Application to worker threads
makes it simple to set up a test harness using a regular
Win32 application.
There are two projects that accompany this article (see end
of article for download details). The Service application is
named MultiThreadService.dpr. The other application,
MultiThreadServiceTest.dpr, is a normal Win32 application set up to host the worker threads. The main form of
this application has a series of buttons to Start, Stop, Pause,
and Resume the threads; these are implemented just like the
ServiceStart, ServiceStop, ServicePause, and ServiceContinue
methods of our TServiceApplication. For example:



Windows, ActiveX, SysUtils,
{$IFDEF Testing}
MyServiceTest // Run as foreground app.
MyServiceMain // Run as Windows service.

Despite .NET, Yukon, and all the other new technology
were facing, Windows Service applications remain an indispensable tool and an important core competency for any
Delphi programmer. And when youre writing a Windows
Service, multi-threaded is the way to go for control, scalability, and ease of development. Hopefully this article has
given you some helpful pointers on how to get the most out
of your multi-threaded Windows Services.
The projects referenced in this article are available for download on the Delphi Informant Magazine Complete Works
CD located in INFORM\2003\NOV\DI200311MH.
Matthew Hess is a Delphi developer working for ProLaw Software, the
legal industrys leading practice management provider. At ProLaw, Matthew
specializes in COM programming and cross-product integrations. Readers may
contact him at



By Bill Todd

Trading Up
Building Applications with Advantage Database Server 7.0

ne of the most popular databases for

embedded applications and as a replacement
for the BDE is Advantage Database Server

(ADS). ADS offers both navigational and SQL

access, transactions, stored procedures, triggers,
full text indexing and searching, declarative
referential integrity, database and over-the-wire
encryption, replication, a rich SQL function library,
zero maintenance, and all the other features you
expect in a modern database management system.
One of its most important features is that ADS
has been around since 1984 and is a product of
a publicly traded company, so its likely to be
around for the long haul. You can use ADS from
any development environment using its TDataSet
components, ODBC driver, OLE DB provider, and
JDBC driver. An ADO.NET data provider will be
available soon after you read this article.
The Advantage component suite for Delphi and C++Builder
includes the AdsConnection, AdsTable, AdsQuery, and
AdsStoredProc components. These components are almost
drop-in replacements for the BDE dataset components, which
makes converting existing applications from the BDE to ADS
very easy. There is no performance difference between the
AdsTable navigational interface and the SQL interface, so
you can use whichever style of application development with
which you are most comfortable. The Advantage components
are also fully compatible with DataSnap.
Advantage comes in two versions: The Advantage Database
Server, which is a true database server for client/server


application development; and the Advantage Local Server

(ALS), which is free and is ideal for single-user applications.
Unlike ADS, ALS does not provide transaction support,
centralized data management, and other client/serverspecific functionality.
This article will take you through building an application
with ADS. Along the way well also look at some of the
new features in ADS 7.0. This article assumes youre
familiar with building database applications in Delphi,
so the focus will be on the database, not the user interface.
If you dont have ADS you can download everything you
need to build and run the sample application from
Creating the Database
Advantage Data Architect (ARC for short) is the interactive
design tool for ADS. To create a new database, start ARC
and choose Database | New Database from the menu to
display the Database dialog box shown in Figure 1. Enter
a Name and Alias for your database. Enter the path to the
folder that will contain your database. You can leave the
Default and Temp paths blank if theyre the same as the
Database path. We are going to take full advantage of ADSs
security features, so check the Logins Required, Check User
Rights, and Enable Data Dictionary Encryption check boxes, then
click OK. When asked to set the password for ADSSYS
enter ADS. Right-click the DIOrders database in the ARC tree
view and choose Properties. Check the Table Encryption check
box, click the Change Encryption Password button, enter ads for
the password, then click OK.
There are two ways to create your tables. You can import
tables and data from another database, or you can create
new tables. For the sample application, I created a set of
tables very similar to the Customers, Orders, Items, Parts,
and Vendors tables from the DBDemos database that comes
with Delphi. The main differences are that the tables I
created use the AutoInc field type for primary keys, and all
of the currency fields use the new Money data type. Money
is a new data type in version 7.0 that provides four digits
to the right of the decimal and is stored as a scaled 64-bit



Trading Up

Figure 3: Creating a full text search index.

Figure 1: Creating a new database.

do not specify another index when you open the table with
an AdsTable component. Repeat this process to create the
primary and foreign key indexes for all the tables as shown
in the ReadMe file for the sample application.
Next, add a full text index to the Company field in the
Customer table. Full text indexing is a new feature of ADS 7.0
that makes searching memo and text fields for specific words
or combinations of words very fast. Search expressions can
include the AND, OR, NOT, and NEAR operators, as well as
parentheses for grouping. To create the index, right-click the
Customer.adi node and choose Add Index to display the Index
Management dialog box shown in Figure 3. Click the Create
New FTS Index tab, choose the Company field, and change the
index name to CompanyFTS. Using this dialog box you can also
set the minimum and maximum length of words that will be
indexed, the delimiter characters, whether the index will be
case sensitive, words to ignore, and other options. By default,
full text indexes are updated automatically as you change
data in the indexed field.

Figure 2: Creating a new table.

integer to avoid the fractional imprecision that occurs with

floating point data types.
To create a new table, right-click Tables in the ARC tree
view and choose Create New Table. Choose Advantage Database
Table for the table type. Use the Creating Advantage
Database Table dialog box shown in Figure 2 to enter the
fields in your new table. The ReadMe file that accompanies
the sample application contains the exact structure for all
the tables. It also shows all the primary and foreign key
fields that must be indexed.
The next step is to create the indexes for the tables. Expand
the Tables node in the ARC tree view, then expand Customer.
Right-click Index Files and choose Create New Index File. Enter
Primary for the index name then select CustNo and click the
arrow button to add the field to the index expression. Click
Create Index to create the new index. Create another index
on the Company field and name it Company. Now, right-click
the Primary index in the tree view and choose Make Primary
Index. Right-click the Company index and choose Make Default
Index. The default index determines the table order if you


To define the referential integrity relationships among

tables, drag the tables from the tree view to the ARC design
surface. Next, drag a line from the foreign key index of the
child table to the primary key of the parent table, as shown
in Figure 4. As you create each link you will be asked to
choose the update and delete rule. The choices are Cascade,
Restrict, Set Null, and Set Default.
Next well add two views to the database to let users view
domestic or foreign customers. Views provide an easy
way to limit the data a user can view and update. Rightclick Views in the ARC tree view and choose Create. Enter
USCustomers for the name and enter:
SELECT * FROM Customer WHERE Country = 'US'

in the SQL box, then click OK. Create another view named
ForeignCustomers, and change the WHERE clause to:
Country <> 'US'

To control access to the database, right-click Users in the tree

view and add as many users as you need. Next, right-click



Trading Up

and add as many groups as you need. Set the access

rights for the group to tables, stored procedures, and views,
then add the appropriate members to the group.


Creating a Trigger
The Orders table contains a field named ItemsTotal that
contains the total amount for the items on the order.
The ideal way to keep this field up to date as items are
added, deleted, or changed is with a trigger, another new
feature in ADS 7.0. Although most databases have their
own internal language for writing stored procedures and
triggers, ADS stored procedures and triggers are written as
DLLs on Windows and shared object libraries on Linux,
so you can write your stored procedures and triggers in
Delphi. Triggers can also be written as SQL scripts, COM
objects, or .NET assemblies.
To create a trigger, start Delphi and choose File | New
| Other from the menu and click the Projects tab in the
Object Repository. You will see three Advantage projects
that were added when you installed the Advantage
dataset components. Double-click the Advantage Trigger
icon to create a new DLL project. Save the project as
DIOrdersTriggers. The new project contains a prototype
function named MyFunction. Change the functions name
to UpdateOrderTotal, then scroll down and change the
name in the exports list.
The finished function is shown in Listing One (on page
17). The trigger function begins by defining four SQL
statements. The first selects the sum of the extended price
for the items for the current order. The second updates
the order record with the new total. The third and fourth
are used to get the order number from the items record.
ADS provides two in-memory tables named __OLD and __
NEW to provide the old and new values of the record that
fired the trigger. Both old and new values are available
for an UPDATE. Only the new values are available for
an INSERT, and only the old values are available for a
DELETE. The third SQL statement gets OrderNo from
the __NEW table when an INSERT or UPDATE occurs.
The fourth statement gets OrderNo from the __OLD table
when a DELETE occurs.
The remaining code in the trigger creates an AdsQuery
component, then uses it to get the order number, compute
the sum of the extended price for the items, and update
the order record. In this example the trigger function
is in the project file. If you have more than one trigger
function you can add units to the project and move the
trigger functions into multiple units. Compile the trigger
DLL project and put the DLL in the directory that contains
your database.
The final step is to add the trigger to the database.
Expand the Items table in the ARC tree view and click
Triggers to display the Triggers dialog box shown in Figure
5. Enter a name for the trigger, then use the drop-down
lists to choose the trigger and event types. The event
types are INSERT, UPDATE, and DELETE. The trigger
AFTER determine whether the trigger fires before or after


Figure 4: Defining referential integrity.

the event. An INSTEAD OF trigger fires instead of the

event. For example, if you define an INSTEAD OF trigger
for the INSERT event of a table, the new row will not be
inserted into the table, but instead, the trigger will be
executed. The INSTEAD OF trigger will have access to the
__NEW table, so it can make modifications or additions to
these values and then insert the new record.
Click the Windows DLL or Linux Shared Object tab. Enter the
name of the DLL that contains the trigger in the Container
Path Name and Filename edit box. In the Function Name in
Container edit box enter the name of the function in the
DLL, then click Save. For the sample application, add
an after insert, after update, and after delete trigger,
all of which call the UpdateOrderTotal function in the
DIOrdersTriggers DLL.
Creating a Stored Procedure
The sample application needs to periodically examine
the cost and list price for all parts, and ensure that the
list price represents a minimum specified markup. The
ideal way to do this is with a stored procedure. Stored
procedure support in previous versions of ADS was
of limited use, because the changes made by a stored
procedure occurred outside the context of the current
transaction. Version 7.0 corrects that shortcoming. All
changes made by stored procedures and triggers occur
within the context of the current transaction. If you roll
back the transaction, all the changes made by stored
procedures and triggers will be undone.
Creating a stored procedure is similar to creating a trigger.
Open the Object Repository, click the Projects tab, then
double-click the Advantage Extended Procedure icon to
start a new DLL project. Save the project as DIOrdersProcs.
The project file contains a template stored procedure
function. Change its name to SetMinimumMarkup in
both the procedure declaration and the exports directive.
The finished procedure is shown in Listing Two (on page
18). The template project also contains a data module
named dm1. The data module contains an AdsConnection
component named DataConn and two AdsTable
components named tblInput and tblOutput. The tblInput
component provides access to a single-row table that
contains the values of the input parameters for the stored
procedure. You use the tblOutput component to return the
values of output parameters or a multi-row result set. Add
another AdsTable to the data module, set its Connection



Trading Up

Figure 6: The sample applications data module.

SetMinMarkup method, which is shown in Listing Three (on

page 18). This method validates the minimum markup to
ensure that it is a positive integer, then locks the Parts table to
ensure that all the records can be updated. Next, the method
assigns the MarkupPercent to the stored procedures Markup
input parameter, calls ExecProc to execute the stored procedure,
then retrieves and displays the values of the output parameters.

Figure 5: Adding a trigger to the database.

property to DataConn, its name to

TableName to Parts.


and its

The code opens the input parameter table and reads

the markup percentage dividing by 100 to convert the
percentage to a decimal fraction. Next the function locks
the Parts table to ensure that it can update all records,
loops through the table, calculates the markup for each
part, compares that to the minimum markup, and adjusts
the ListPrice field value if necessary. Finally, the number
of rows checked and the number of rows changed
are assigned to the corresponding fields in the output
parameter table. As with the trigger, compile the DLL and
move it to the directory that contains the database.
To add the stored procedure to the database, right-click
the Stored Procs node in the ARC tree view and choose Add
to display the Advantage Extended Procedure dialog box.
Enter SetMinimumMarkup for the Object Name, DIOrdersProcs
for the Program Id, and SetMinimumMarkup for the Stored
Procedure Name. Next, add one input parameter named
Markup whose type is Money. Enter two output parameters
named RecordsChecked and RecordsChanged of type Integer
and click the OK button.

Clicking the Filter button displays a form that prompts

the user for a full text search expression to apply to the
Customers tables Company field. Clicking the Search button
on this form calls the data modules FilterCustomerByName
method shown here:
procedure TMainDm.FilterCustomersByName(
SearchString: string);
CustomerTbl.Filter :=
'Contains(Company, ' + QuotedStr(SearchString) + ')';
CustomerTbl.Filtered := True;

This method sets a filter on the Customers table using

the Contains function to perform a full text search.
Contains takes two parameters. The first is the field to
be searched. You can use an asterisk to indicate that
all fields with full text indices should be searched. The
second parameter is the search expression, which consists
of words; the AND, OR, NOT, and NEAR operators;
and parentheses to group the operators as desired. For
example, entering the expression:
"(divers and corfu) or underwater"

The Sample Application

Figure 6 shows the data module for the sample application.
The data module contains an AdsConnection component
and AdsTable components for the Customer, Orders,
and Items tables. The PartLkUpTbl AdsTable component
supplies data to the Description and ListPrice lookup fields
in ItemsTbl. The PartsTbl component supplies data to the
Parts form. The last two AdsTable components, USCustTbl
and ForeignCustTbl, are used to access data from the two
views defined earlier in this article.
The MinMarkupProc AdsStoredProc component is used to
call the SetMinimumMarkup stored procedure. The Parts
form contains an edit box for entering the minimum markup
percentage, and a button that calls the data modules


without the quotation marks displays the customers

whose name contains both diver and corfu or
contains underwater. Note that the search is caseinsensitive by default. You can specify case-sensitive
searching when you create the full text index.
To see some of the new features of ADS 7.0 in action, try
the following with the sample application. Click the Parts
button on the main form, then enter a markup percentage
of 300. Click the Set Min Markup button to run the stored
procedure, and watch the values in the ListPrice column
change. Now click the Rollback button and see all the changes
made by the stored procedure rollback. On the Orders tab



Trading Up

of the main form, change the quantity for one of the items
records. Watch the ItemsTotal value in the order record
when you post the change to the item, and youll see the
value change as the trigger fires. Dont forget to try the fulltext search feature described earlier. Again, see the ReadMe
file for more information about the sample application.
Whether you are looking for an embedded database for
a new application or a replacement for the BDE and
local tables, ADS is a great choice for all but the largest
enterprise projects. It provides local and server engines,
low maintenance, and high performance. With the
addition of triggers, stored procedures within transactions,
a fixed point currency data type, full text indexing, a class
4 JDBC driver, and over-the-wire compression, ADS will
meet the needs of anyone looking for a workgroup or
embedded database. Look for Advantage Database Server:
The Official Guide by Cary Jensen and Loy Anderson
(McGraw-Hill/Osborne Media, 2003, ISBN: 0-07-223084-3)
if youre new to Advantage and want to learn more. The
book includes a CD with code samples and a single-user
license of Advantage Database Server version 7.0.
The application referenced in this article is available for
download on the Delphi Informant Magazine Complete
Works CD located in INFORM\2003\NOV\DI200311TB.
Bill Todd is president of The Database Group, Inc., a database consulting
and development firm based near Phoenix. He is co-author of four database
programming books, author of more than 100 articles, a contributing editor
to Delphi Informant Magazine, and a member of Team B, which provides
technical support on the Borland Internet newsgroups. Bill is also an
internationally known trainer and frequent speaker at Borland Developer
Conferences in the United States and Europe. Readers may reach him at

Begin Listing One UpdateOrderTotal

{ Trigger called after INSERT, UPDATE or DELETE in Items
table to update ItemsTotal field in Orders table. }
function UpdateOrderTotal(
// (I) Unique ID identifying user causing this trigger.
ulConnectionID: UNSIGNED32;
// (I) Active ACE connection handle user can perform
// operations on.
hConnection: ADSHANDLE;
// (I) Name of the trigger object in the dictionary.
pcTriggerName: PChar;
// (I) Name of the base table that caused the trigger.
pcTableName: PChar;
// (I) Flag with event type (insert, update, etc.)
ulEventType: UNSIGNED32;
// (I) Flag with trigger type (before, after, etc.)
ulTriggerType: UNSIGNED32;
// (I) Record number of the record being modified.
// Do not change the prototype.
{$IFDEF WIN32}stdcall;{$ENDIF}{$IFDEF LINUX}cdecl;{$ENDIF}
oConn: TAdsConnection;
Qry: TAdsQuery;



SelectOrderNoNew: string;
SelectOrderNoOld: string;
// Result is reserved but not used. Always return zero.
Result := 0;
// SQL statements used later in the trigger.
SelectTotal :=
'SELECT SUM(I.Qty * P.ListPrice) as ItemTotal ' +
'FROM Items I INNER JOIN Parts P ' +
'ON I.PartNo = P.PartNo ' +
'WHERE I.OrderNo = :OrderNo';
UpdateTotal :=
'UPDATE Orders SET ItemsTotal = :ItemsTotal ' +
'WHERE OrderNo = :OrderNo';
SelectOrderNoNew := 'SELECT OrderNo FROM __New';
SelectOrderNoOld := 'SELECT OrderNo FROM __Old';
// Allocate connection object using active connection;
// no need to open it after this.
oConn :=
TAdsConnection.CreateWithHandle(nil, hConnection);
oConn.Name := 'conn';
// Create AdsQuery component to use in trigger.
Qry := TAdsQuery.Create(nil);
Qry.DatabaseName := 'conn';
// If trigger was called after DELETE, get OrderNo
// from __OLD table. If trigger was called after
// INSERT or UPDATE, get OrderNo from __NEW.
if (ulEventType = ADS_TRIGEVENT_DELETE) then
Qry.SQL.Text := SelectOrderNoOld
Qry.SQL.Text := SelectOrderNoNew;
OrderNo := Qry.FieldByName('OrderNo').AsInteger;
// Get total amount for all items for this order.
Qry.SQL.Text := SelectTotal;
Qry.ParamByName('OrderNo').AsInteger := OrderNo;
OrderTotal :=
// Update the total in the Order record.
Qry.SQL.Text := UpdateTotal;
Qry.ParamByName('ItemsTotal').AsCurrency :=
Qry.ParamByName('OrderNo').AsInteger := OrderNo;
on E : EADSDatabaseError do
SetError(oConn, E.ACEErrorCode, E.message);
on E : Exception do
SetError(oConn, 0, E.message);

End Listing One



Trading Up

Begin Listing Two

SetMinimumMarkup stored procedure
function SetMinimumMarkup(ulConnectionID: UNSIGNED32;
hConnection: ADSHANDLE; pulNumRowsAffected: PUNSIGNED32):
// Do not change prototype.
{$IFDEF WIN32}stdcall;{$ENDIF}{$IFDEF LINUX}cdecl;{$ENDIF}
DM1: TDM1;
MinMarkup: Currency;
Markup: Currency;
NewPrice: Currency;
CheckedCount: Integer;
ChangedCount: Integer;
Result := AE_SUCCESS;
CheckedCount := 0;
ChangedCount := 0;
// Get connection's data module from session manager.
DM1 := TDM1(AEPSessionMgr.GetDM(ulConnectionID));
with DM1 do begin
// Get the input parameter value.
MinMarkup :=
tblInput.FieldByName('Markup').AsFloat / 100.0;
// Loop through Parts table and increase ListPrice of
// any part whose markup is below the minimum.
with PartsTbl do begin
if (AdsLockTable) then
while (not EOF) do begin
Markup :=
(FieldByName('ListPrice').AsCurrency FieldByName('Cost').AsCurrency) /
if (Markup < MinMarkup) then begin
NewPrice := (FieldByName(
'Cost').AsCurrency * MinMarkup) +
NewPrice := Round(NewPrice * 100)/100;
FieldByName('ListPrice').AsCurrency :=
end; // if (Markup < MinMarkup)
end; // while (not EOF).
end // if (AdsLockTable).
// If Parts table cannot be locked, return an
// error message.
'INSERT INTO __error VALUES (2, ' +
'Someone is editing the Parts table. ' +
'Try again later.)');
end; // else.
// Close table so it will be unlocked when
// transaction ends.



end; // try
end; // with PartsTbl
// Assign values to the output parameters.
with tblOutput do begin
FieldByName('RecordsChecked').AsInteger :=
FieldByName('RecordsChanged').AsInteger :=
end; // with DM1
on E : EADSDatabaseError do
// ADS-specific error, use ACE error code.
DM1.DataConn.Execute('INSERT INTO __error VALUES (' +
IntToStr(E.ACEErrorCode) + ', ' +
QuotedStr(E.Message) + ')');
on E : Exception do
// Other error.
'INSERT INTO __error VALUES (1, ' +
QuotedStr(E.Message) + ')');

End Listing Two

Begin Listing Three SetMinMarkup

procedure TMainDm.SetMinMarkup(MarkupString: string);
MarkupPercent: Currency;
Checked: string;
Changed: string;
// Ensure markup percentage is positive number.
MarkupPercent := StrToCurr(MarkupString);
if (MarkupPercent <= 0) then begin
ShowMessage('Markup % must be greater than zero.');
ShowMessage('The markup % must be a number.');
with MinMarkupProc do begin
ParamByName('Markup').AsCurrency := MarkupPercent;
Checked := ParamByName('RecordsChecked').AsString;
Changed := ParamByName('RecordsChanged').AsString;
ShowMessage(Checked + ' records checked. ' +
Changed + ' records changed.');

End Listing Three

. N E T




By Xavier Pacheco

Using the Framework

Part I: .NET Collections Queue, Stack, and ArrayList

he .NET Framework is a rich class library that

encompasses almost every possible class that
developers would need to create .NET applica-

tions. For a new developer, this can be a bit daunting.

However, if youve managed to grasp the VCL, youll
find the .NET Framework familiar.

Of course, its unlikely that youll use the entire .NET Framework, but there are some key classes that youll want to know
quite well. This series of articles will delve into the specifics
of these classes to help you understand the important features
of .NET. The first classes to be explored are collections, which
youll find strewn throughout the .NET Framework classes.
This series assumes: 1) you understand the Delphi programming language; 2) you have read the current articles on Delphi for .NET in this magazine, on the Borland Community
Site (, and on Dr. Bobs site
(; and 3) you have a basic understanding
of .NET and its core components.
The System.Collections Namespace
The System.Collections namespace contains classes and interfaces that provide lists of objects called collections. As previously mentioned, collections are contained throughout the
.NET Framework classes. For instance, you will find many
collections in Windows forms components and in ADO.NET
components. By understanding how to use collections, youll
know how to manipulate them when you encounter them in
other classes you use. The classes in the System.Collections
namespace are defined in Figure 1. The interfaces in
System.Collections are defined in Figure 2.
The Stack Collection
The Stack collection stores objects according to a first-in, lastout (FILO) basis. To add objects, one pushes them onto the
Stack. To remove them, one pops them from the Stack. The
common analogy is that of a dish tray in a restaurant. The first
dish placed on the tray stays at the bottom as additional dishes
are added. When dishes are removed, it is from the top of the
stack. The first dish added is thus the last dish to be removed.


The Stack collection implements the ICollection, IEnumerable,

and ICloneable interfaces. When using Stack, you will mostly
be using the Pop, Push, and Peek methods.
Constructing the Stack. There are three ways to construct a
Stack. The first is to create a Stack with a default capacity of 10
objects using the following constructor:
MyStack := Stack.Create();




Provides a collection of objects in an array whose size

can dynamically be adjusted.


Provides an array of bit values represented as Booleans.


Provides a collection of key-value pairs whereas the

value is retrievable by the hash code of a specified key.


Provides a first-in-first-out collection.


Provides a collection of key-value pairs that are

sorted by the key.


Provides a last-in-first-out collection.

Figure 1: Classes in the System.Collections namespace.




Exposes an enumerator interface whose

implementer iterates uni-directionally over
a collection.


Defines size and synchronization methods for

collections. Inherits the IEnumerable methods.


Defines a collection whose items are

accessible via an index.


Defines method for uni-directional iteration over a collection.


Defines a key-value pair collection.


Defines enumeration over a Dictionary.


Defines a method for comparing two



Defines the method for returning the hash

code for a specified object.

Figure 2: Interfaces in the System.Collections namespace.



Using the Framework

program d4dnStack;

program d4dnQueue;

System, System.Collections;

System, System.Collections;

stMyStack: Stack;
i: integer;

qMyQueue: Queue;
i: integer;

procedure ShowStackItems;
enumMyStack: IEnumerator;
enumMyStack := stMyStack.GetEnumerator;
while enumMyStack.MoveNext() do
Console.Write(enumMyStack.Current.ToString() + '

procedure ShowQueueItems;
enumMyQueue: IEnumerator;
enumMyQueue := qMyQueue.GetEnumerator;
while enumMyQueue.MoveNext() do
Console.Write(enumMyQueue.Current.ToString() + '

// Initialize and populate the Stack.
stMyStack := Stack.Create();
for i := 1 to 5 do
stMyStack.Push('Item ' + i.ToString());
// Pop the topmost item.
Console.Write('Pop: ' + stMyStack.Pop.ToString());
// Peek at the topmost item.
Console.Write('Peek: ' + stMyStack.Peek.ToString());
// Pop the two topmost items.
Console.Write('Pop: ' + stMyStack.Pop.ToString());
Console.Write('Pop: ' + stMyStack.Pop.ToString());
// Push another item.
Console.Write('Push: Item 6');
stMyStack.Push('Item 6');
// Clear the stack.
Console.WriteLine('Stack cleared');
// Push another item.
Console.Write('Push: Item 7');
stMyStack.Push('Item 7');

Figure 3: Stack example.



// Initialize and Enqueueulate the Queue.
qMyQueue := Queue.Create();
for i := 1 to 5 do
qMyQueue.Enqueue('Item ' + i.ToString());
'Dequeue: ' + qMyQueue.Dequeue.ToString());
Console.Write('Peek: ' + qMyQueue.Peek.ToString());
'Dequeue: ' + qMyQueue.Dequeue.ToString());
'Dequeue: ' + qMyQueue.Dequeue.ToString());
Console.Write('Enqueue: Item 6');
qMyQueue.Enqueue('Item 6');
Console.WriteLine('Queue cleared');
Console.Write('Enqueue: Item 7');
qMyQueue.Enqueue('Item 7');

Figure 5: Using a Queue.

Finally, you can create a Stack that is populated with elements

from another collection:
MyStack := Stack.Create(AnotherCollection);

Figure 4: Stack example output.

You can also create an empty Stack with a specific

capacity (e.g. 50):
MyStack := Stack.Create(50);



Using and Enumerating the Stack.

Figure 3 is an example from my book,
Delphi for .NET Developers Guide, which
is scheduled to be published, well,
when Im done writing it. Note that the
System.Collections namespace appears in
the uses clause. This gives access to the
classes contained within that namespace.
The Stack variable is stMyStack. When
the program runs, it populates the Stack
with five elements and displays the Stack
contents to the console after each operation is performed.
Note how the Pop operation removes an item from the
Stack, while the Peak operation leaves the element. The
ShowStackItems procedure prints the contents of the Stack.
This programs output is shown in Figure 4.



Using the Framework

MyQueue := Queue.Create(20, 3);

Otherwise, construct the Queue just as

you would the Stack.

Figure 6: Queue example output.

Using and Enumerating the Queue.

Figure 5 is an example of Queue usage
similar to the Stack example. Figure 6
shows the output from this program. This
example is similar to that of the Stack.
The difference is the use of the Queues
specific methods. The output in Figure 6
also illustrates how the Queue and Stack
differ. Both the Stack and the Queue provide read only access to their elements. If
having the ability to modify the elements
is required, then you would use the
ArrayList collection.
The ArrayList Collection
The ArrayList collection is more versatile
than the Queue or Stack. It enables you
to modify its elements and has support
for other useful operations such as sorting, reverse ordering, range operations,
element locating, and binary searching.
ArrayList implements the IList, ICollection,
IEnumerable, and ICloneable interfaces.

Figure 7: Example ArrayList output.

I already mentioned that Stack implements the IEnumerable

interface. IEnumerable defines the GetEnumerator method,
which returns an instance of an IEnumerator interface. Youll
find that collections in general will implement IEnumerable.
Using IEnumerator. The ShowStackItems procedure uses an
instance of the IEnumerator interface to iterate through the
Stacks items and prints them to the console. IEnumerator
defines a Current property, which represents the current item in
the collection. IEnumerator also defines two methods, MoveNext
and Reset. MoveNext advances the current item in the collection.
Reset repositions the current element to the initial position,
which is just before the first element on the Stack.
The Queue Collection
A Queue is similar to the Stack; it differs only in how items are
removed. A Queue collection stores objects on a first-in, first-out
(FIFO) basis. To add objects, one performs an Enqueue operation. To remove them, one performs a Dequeue operation. The
common analogy for the Queue is that of the carwash. Cars
get into line to enter the carwash. The first car in is the first
car out. Like the Stack, the Queue implements the ICollection,
IEnumerable, and ICloneable interfaces. When using the Queue,
youll mostly be using the Enqueue and Dequeue methods.
Constructing the Queue. The Queue has the same constructors as the Stack and includes an additional constructor. The additional constructor creates an empty Queue
with a specified capacity and the specified growth factor by
passing both the initial capacity and growth factor to the
Queues constructor as parameters:


Constructing the ArrayList. There are

three ways to construct an ArrayList. To
create an empty ArrayList with a default initial capacity of 16,
use the following construction:
MyArrayList := ArrayList.Create();

When the ArrayList needs to be expanded, its capacity will be

double of the initialized capacity. To create an empty ArrayList
with a specified capacity, pass the desired capacity to the
ArrayLists constructor as a parameter:
MyArrayList := ArrayList.Create(20);

To create an ArrayList that contains elements from another collection whose capacity is that of the collection or the default
initial capacity (whichever is greater), use the following construction:
MyArrayList := ArrayList.Create(AnotherCollection);

where AnotherCollection is an object that implements the

ICollection interface.
Using and Enumerating the ArrayList. Listing One (on page
22) shows how to use the ArrayList collection. It illustrates
various operations against the ArrayList collection, including
working with ranges (sub-arrays), which can be added to, and
removed from, another ArrayList. First, the array variables are
declared, with alClassicCars as the primary array. The additional arrays will be used to illustrate ranges. The procedure
ShowArrayListItems displays the contents of ArrayList to the
console, as shown in Figure 7.



Using the Framework

// Initialize and populate the stack.
stMyStack := Stack.Create();
for i := 1 to 5 do
stMyStack.Push('Item ' + i.ToString());
// Copy the stack contents to a one-dimensional array.
arMyArray := stMyStack.ToArray();
// Change the contents.
for i := 0 to arMyArray.Length-1 do
arMyArray.SetValue('Delphi ' + IntToStr(i + 1), i);
// Create another stack instance with the new data.
stMyStack := Stack.Create(arMyArray);

Figure 8: Copying elements to an array.

After the ShowArrayListItems procedure is described, the

array is initialized with various makes of classic cars. Then
sort and reverse sort operations are performed. Next, the
ArrayList.GetRange method is used to retrieve a sub-array
of the items contained in alClassicCars. This method takes
as a parameter the starting point index and the number of
items to retrieve. The result is another ArrayList object. The
ArrayList.AddRange method is then used to add a separate
array to an existing ArrayList.
Next up are examples of how to use the ArrayList.Capacity and
ArrayList.Count properties. The Capacity property refers to the
space thats allocated for the ArrayList. This is allocated when
the ArrayList is created as a default, or as a parameter passed
to the constructor. The Count property refers the number of elements that exists in the ArrayList. ArrayList has a Trim method
that reduces the Capacity to that of the Count.
Moving Elements
Figure 8 illustrates how you would move elements from a Stack
to a one-dimensional Array. The Array type used here is from
the System namespace and is declared as:
arMyArray: System.Array;

Not all of the code is shown in Figure 8, because its similar

to that of previous listings. What is shown is how the contents
of a Stack are copied to a one-dimensional array, how array
elements may be modified, and how to create a new Stack
instance with the modified array elements.
This article introduces classes and interfaces contained in the
System.Collections namespace. It also illustrates three of the
classes: Stack, Queue, and ArrayList. There are other methods in the Stack, Queue, and ArrayList classes with which
you should experiment. In the next installment Ill cover the
HashTable class, and demonstrate how you can create your
own collection classes. Visit for tips/
techniques on .NET using Delphi. If you have tips youd like
me to include, send them my way.
The files referenced in this article are available for download
on the Delphi Informant Magazine Complete Works CD
located in INFORM\2003\NOV\DI200311XP.


Begin Listing One Using ArrayList

program d4dnArrayList;
System, System.Collections;
alClassicCars: ArrayList;
alSubList: ArrayList;
alSubList2: ArrayList;
procedure ShowArrayListItems(aName: string;
aArrayList: ArrayList);
enumWrite: IEnumerator;
enumWrite := aArrayList.GetEnumerator;
Console.Write(aName + ': ');
while enumWrite.MoveNext() do
Console.Write(enumWrite.Current.ToString() + ' ');
// Initialize and populate the arraylist.
alClassicCars := ArrayList.Create();
ShowArrayListItems(valClassicCars', alClassicCars);
// Sort the arraylist.
ShowArrayListItems('alClassicCars', alClassicCars);
// Reverse the sort order.
ShowArrayListItems('alClassicCars', alClassicCars);
// Retrieve a range of item, but leave them in the
// original arraylist.
alSubList := alClassicCars.GetRange(2,3);
ShowArrayListItems('alSubList', alSubList);
ShowArrayListItems('alClassicCars', alClassicCars);
// Create a new arraylist and add it to alClassicCars.
alSubList2 := ArrayList.Create(3);
ShowArrayListItems('alClassicCars', alClassicCars);
// Re-sort the arraylist.
ShowArrayListItems('alClassicCars', alClassicCars);
// Output the count and capacity.
Console.WriteLine('alClassicCars count: ' +
Console.WriteLine('alClassicCars capacity: ' +
// Trim to size.
// Output the count and capacity.
Console.WriteLine('After trimmed---');
Console.WriteLine('alClassicCars count: ' +
Console.WriteLine('alClassicCars capacity: ' +

End Listing One

Xavier Pacheco is the president and chief consultant of Xapware Technologies
Inc., provider of Active! Focus a practical solution for managing software
projects and requirements management. Xavier is the co-author of Delphi 6
Developers Guide and is currently writing Delphi for .NET Developers Guide.
Xavier is available for consulting and training engagements. You may contact him
at or visit his company Web site at





By Bill Todd

InterBase 7.1
Whats New in Borlands Cross-platform Embeddable Database

nterBase 7.1 is Borlands most recent update

of their cross-platform, self-maintaining,
embeddable database. This free upgrade

boasts an impressive list of new features.

Among the most important new features are savepoints.

With savepoints you can start a transaction, make some
changes, create a savepoint, make some changes, create
another savepoint, then make more changes. Then you can
roll back to either savepoint, or you can roll back the entire
transaction. Better still, you can create, release, and roll
back to savepoints inside stored procedures and triggers. Use
the following syntax to create a savepoint:

As you can see, a savepoint is given a name when you create it. A savepoint name can be any legal SQL identifier.
Once youve created a savepoint, you can release it or roll
back to it. The following command releases a savepoint:

Additionally, InterBase Express has been updated to

support InterBase 7.1 and savepoints with three new
methods of the TIBTransaction component: StartSavePoint
creates a savepoint, ReleaseSavePoint releases a savepoint,
and RollbackSavePoint rolls the transaction back to the
savepoint. Each of these methods takes the name of a
savepoint as its only parameter.
ADO.NET Data Provider
InterBase 7.1 includes the Borland Data Provider (BDP) for
InterBase. The BDP works with C#Builder, Microsoft Visual
Studio 2003, and as a standalone library to let you connect
to InterBase 7.0 or 7.1 databases. All BDP packages reside in
the Borland.Data namespace.
The BDP ships with C#Builder. The InterBase 7.1 release
notes contain detailed instructions for installing the BDP
in Visual Studio 2003 and as a standalone library. The
release notes also include instructions for deploying the
BDP with your applications.


SMP and Hyperthreading

InterBase 7.1 provides improved support for multi-processor
systems. In addition, the default value for the MAX_THREADS
parameter in the ibconfig file has been changed. If a single
processor is present, MAX_THREADS defaults to one.
MAX_THREADS defaults to 1,000,000 if two or more
processors are present. This ensures that threads will never
wait in a multi-processor environment. At the same time, the
default of one thread ensures best performance in a singleprocessor, single-user environment.

Rolling back to a savepoint rolls back all changes made

in your transaction after the savepoint was created, and
releases the savepoint and any savepoints created after
the savepoint you rolled back to. Note that implementing
savepoints has added two new keywords to InterBase 7.1:

You can adjust the maximum number of threads that can

be active at one time by changing the MAX_THREADS
parameter in the ibconfig file. In a single-processor, multi-user
environment you may find that performance is better if you
set MAX_THREADS to a value higher than one. Try 100 as a
starting point, and adjust from there based on your experience.


Releasing a savepoint removes the savepoint without affecting any changes made to the database after the savepoint
was created. Use the following statement to roll back to a






InterBase 7.1
a generator from a database. InterBase 7.1 uses new
algorithms to optimize garbage collection of duplicate
index nodes. The improved garbage collector provides a
significant performance improvement.
New properties have been added to InterClient 4.0 to provide more complete access to InterBase properties. InterClient also supports Container Managed Persistence (CMP) 2.0.
This allows JDBC DataSource 2.x connections to InterBase
databases. InterBase now uses a new all-Java installer to
provide identical installation support on all platforms.

Figure 1: The IBConsole performance monitor with the Memory tab selected.

InterBase 7.1 supports hyperthreading on Intel Xeon processors. Set the ENABLE_HYPERTHREADING parameter in the
ibconfig file to 1 to enable hyperthreading support. Hyperthreading support is disabled by default.
Restore Performance Improved
The gbak utility and the InterBase services API restore function no longer perform constraint validation during a database restore. This is a welcome change not only because it
improves restore performance dramatically, but also because
it ensures you can restore any database you can back up.
Before version 7.1, it was possible to create a database
backup that could not be restored. Suppose you have a
table that contains data, and some of the rows have the
null state for a certain column. If you add a NOT NULL
constraint to that column, the nulls will still be there.
Although you could back up the database, you could not
restore it, because some of the rows would not satisfy the
NOT NULL constraint.
Version 7.1 also adds a new command-line switch to gbak,
-VA[LIDATE], so you can enable constraint checking during
a restore if you wish.
Other Changes
Several new character sets and collation orders have
been added to provide better support for Eastern
European languages. InterBase SQL now includes a
DROP GENERATOR command so you can easily remove



IBConsole Performance Monitor

IBConsole now has an integrated performance monitor.
To use it, right-click on any InterBase 7.1 database to
which you are currently connected, and choose Performance
Monitor from the context menu. The performance monitor gives you a visual display of the information in the
performance monitoring tables that made their debut in
InterBase 7.0. Figure 1 shows the performance monitor
with the Memory tab selected.
You can set the refresh rate to control how often the information displayed in the performance monitor is updated.
The performance monitor also includes a logging feature
that lets you log information so you can analyze the behavior of your server over any time period.
If you are a registered InterBase 7.0 user, you should have
received the InterBase 7.1 update automatically. If you
havent, contact Borland Customer Service. If youre still
using an earlier version, InterBase 7.0 added a remarkable
list of new features that were described in the February 2003
issue of Delphi Informant. InterBase 7.1 simply adds to the
already impressive list of reasons to upgrade.

Bill Todd is president of The Database Group, Inc., a database consulting

and development firm based near Phoenix. He is co-author of four database
programming books, author of more than 100 articles, a contributing editor to
Delphi Informant Magazine, and a member of Team B, which provides technical
support on the Borland Internet newsgroups. Bill is also an internationally
known trainer and frequent speaker at Borland Developer Conferences in the
United States and Europe. Readers may reach him at

Query Client-Side Data


Solutions & Strategies for .NET Developers


November 2003 Special Preview Edition

C#Builder vs. VS .NET

Cover Story

By Joe Mayo

C#Builder vs. VS .NET:

Choose Wisely

With Borlands new C#Builder, VS .NET now has a real competitor.

Both are great tools, but which will work best for you?


Integrated Development Environment that lets you build
.NET applications in C#. This new product debuted just
a few months after Microsoft released Visual Studio .NET
2003. Although both products target the same audience
the C# developer and have many of the same capabilities, each certainly is unique. Now that youve got a
choice, youre bound to wonder: What does C#Builder
offer? How does C#Builder compare to VS .NET? And of
course, why would you choose one over the other? Well,
lets see what we can do about answering these questions. (For a quick side-by-side comparison, see the sidebar, Making the Grade.)
Out of the box, C#Builder and VS .NET look similar.
They sport a single document interface for code editing
and visual design surfaces, property editors, tool palette,
project managers, and docking/sliding windows. The first
impression many developers get is that C#Builder looks
just like VS .NET (see Figure 1). Then again, first impressions often are misleading. In fact, C#Builder innovates
in pleasingly surprising ways that are definitely worth a
second look.

Project management in both

C#Builder and VS .NET is a case of
being able to accomplish the same
goal but in different ways.
The VS .NET desktop opens with a tabbed view on
the editor/design surface, and it lets you split the editing
window and move to MDI-style editing (see Figure 2).
C#Builder takes a different approach, however, by having
different desktop layout styles. The default style is simi-

lar to VS .NET, but you can change it to a floating window style, similar to JBuilder and Visual Basic. Another
C#Builder desktop layout is used for specifying window
layout while debugging.
From a C# developers perspective, there is no difference between the two IDEs when it comes to the ability
to build any type of .NET application. Because Borland
licensed the .NET Framework SDK from Microsoft, both
C#Builder and VS .NET use the same C# compiler and
library. You can develop any .NET technology with
C#Builder, including Windows Forms, ASP.NET, ADO.NET,
and Web services.
The major advantage of VS .NET is it allows development in multiple languages such as C#, Visual Basic
.NET (VB .NET), Managed C++, and J#. C#Builder, as its
name suggests, limits your language choice to C#. It does
let you work with VB .NET source code, but it is only a
supplementary capability intended for the convenience
of people who need to incorporate existing VB .NET code
into their project.

Go for the Code

Sure, the fancy designers and wizards offered by both
IDEs help me be more productive; I use them quickly
and Im done. But the code editor is where I spend most
of my time. Both IDEs have configurable syntax-colored
code and code folding, but the differences in code formatting, commenting, navigation, and Code Completion/
IntelliSense are significant.
When testing an IDE, the first thing I do is start typing
and banging out code. The VS .NET code editor has intelligent indentation, where pressing Enter after beginning
a block auto-indents to the next tab. If I am typing in an
indented state and type an end-of-block, VS .NET will line

Cover Story

C#Builder vs. VS .NET: Choose Wisely

Figure 1. This shows the C#Builder Default Layout, which is quite similar
to VS .NET. Besides multiple layouts, another difference with C#Builder
is its Tool Palette, shown in the lower-right corner, as well as its ability to
show live design-time data. Also notice the close buttons on the tabs in
the designer and the tab at the bottom of the designer, which lets you
navigate quickly between code and a graphic view.

a type or class member and VS .NET automatically generates a template shell for documentation comments. For a
method with multiple parameters and a return type, this
is a big gain. VS .NET also offers a documentation-generation facility that applies a transform to the XML documentation to produce HTML pages. Another capability of VS
.NET is to highlight a block of text and press Ctrl+K+C;
this adds comments to every highlighted line. Conversely,
pressing Ctrl+K+U removes comments from a highlighted block. In the C#Builder code editor, you must type
in all comments manually and it doesnt have automated
commenting or a comment/uncomment capability.
I was pleased to see that C#Builder lets you jump to a
declaration, the same as VS .NET. There are, however, differences in how to navigate to specific portions of code.
VS .NET has a Class View window that lets you locate
types and their members. C#Builder provides a similar
capability, but it does so through a Model View window.
One feature C#Builder doesnt have, which is something
I use frequently in VS .NET, is the Members ComboBox
(located above the editor), which lets you navigate directly to a type member.
Both C#Builder and VS .NET include context-sensitive
help in the editor, but there are nuances that make them
different. In VS .NET, context-sensitive help is named
IntelliSense, and in C#Builder it is named Code Completion. IntelliSense includes descriptions when a method is
selected from a list, but C#Builder simply lists the member. Additionally, IntelliSense is smart enough to remember your most frequently selected item (or the item youll
most likely choose) and highlight it first. Although Code
Completion isnt that smart, it will filter results so only
relevant items appear in the list. Additional IntelliSense
capabilities, not found in Code Completion, include support for the new keyword, automatic implementation of
interfaces, automatic delegate implementation, and automatic override implementation.

Figure 2. VS .NET has a consistent look and feel, but it is not quite as
colorful as C#Builder. Code and GUI have separate tabs, and notice
that, unlike C#Builder, VS .NET does not have live design-time data.

Nuts and Bolts

up the curly brace in the proper column. C#Builder will

indent to the same column under the begin block, but I
have to press the Tab key to indent. Furthermore, typing
an end-of-block in the C#Builder editor simply types the
character where it is. Another formatting feature in VS
.NET is the ability to highlight a block of code and press
Ctrl+K+F to auto-format the entire block. Perhaps Im
spoiled by VS .NET formatting, but I like it and it makes
me more productive.
One of the great things about C# is its XML documentation-commenting facilities, and both IDEs have options
for extracting documentation comments to file. VS .NET
provides automated support for XML documentation comments. All you have to do is type /// on the line above

Project management in both C#Builder and VS .NET is a

case of being able to accomplish the same goal but in different ways. The C#Builder Project Manager window lets
you control build order in the Project Manager directly,
but the VS .NET Solution Explorer has a separate dialog
box where you can specify build order. The primary difference in philosophy is VS .NET determines build order
and dependencies automatically if you create project
references between projects, but C#Builder makes you
maintain your own build order. I know for a fact that
casual tinkering with the build order and dependencies
in VS .NET can totally spike your project, which is why
you instead should use project references and let VS .NET
sort these things out. Because C#Builder is so new, it will
take some time to see if people have any

Cover Story

Figure 3. C#Builder has a feature named Model View that shows static
UML diagrams of a projects code. This is useful for familiarizing yourself
with code that someone else has written or even reacquainting yourself
with your own code after not seeing it for a while.

ment problems. Ive given some consideration in this area

to C#Builder and have done some experimentation, but I
have yet to reproduce such problems.
Both IDEs also have integrated Help; you simply press
F1 to get it. C#Builder Help pops up in a separate window, and VS .NET Help appears in an IDE window with
its search options displayed as dockable windows (including Dynamic Help). Fortunately, you can turn off Dynamic
Help or hide it behind another docked window in a tab
group if you dont want to view it.
As for designers, C#Builder uses the same design surface as VS .NET, so you get a very similar look-and-feel
to visual user-interface design. Similarly, the C#Builder
Object Inspector is a modified Property Grid from the
.NET Frameworks Base Class Library.
The difference in design interface lies with the C#Builder
Tool Palette, which is analogous to the VS .NET Toolbox.
The Tool Palette sports a configurable list of categories
where you can change the color, order, and layout of items.
VS .NET holds its controls on a standard sliding ListBox. In
code-editing mode, the Tool Palette displays a list of snippets, which are blocks of commonly used code you can
drag to the editor. VS .NET has a clipboard ring containing
the most commonly copied code. The difference is that
C#Builders snippets are persistent between invocations of
C#Builder; in VS .NET, contents of the clipboard ring disappear when it shuts down. Also, the C#Builder Tool Palette
includes a search box that filters the components you seek
as you type in each character of the component name.
When opening files, VS .NET opens a new window for
every file, which is cumbersome because when you are working with multiple files the work area soon becomes full. Also,
a form and its code are separated, prompting you to hunt and

C#Builder vs. VS .NET: Choose Wisely

peck to find the correct code. In C#Builder, form and code

are grouped together and you can switch between each easily using tabs at the bottom of the work area. Another nice
C#Builder feature is the Close button on each workspace
window tab, where a single click closes the window. VS .NET
requires you to find the tab, select it, and click on the close
button on the top right-corner of the workspace.
In addition to supporting the SQL Server, Oracle, OleDb,
and ODBC data providers included in the .NET Framework
that VS .NET supports, C#Builder also includes the Borland
Data Provider (BDP). BDP is an extensible data provider
that lets you connect to multiple databases with the same
code. Current databases supported include Borland InterBase, IBM DB2, Microsoft SQL Server, and Oracle 9i. At
press time, Borland is adding even more support for new
data providers and, by every indication Ive seen, it is committed to continue doing so for the foreseeable future.
Both VS .NET and C#Builder have code browsers for looking at the internals of individual assemblies. But C#Builder
Professional and above also include an application-diagramming capability named Model View (see Figure 3). Based on
standard UML diagrams, Model View lets you view the relationships between application objects, drill down into their
contents, and navigate between model and code. Although
this is only a viewing capability, it helps you get a better
idea of how an application is put together. VS .NET doesnt
have this capability.
One feature set that might entice you to make
C#Builder part of your tool suite is its ability to export
and import VS .NET projects. C#Builder has a wizard for
exporting to a VS .NET project and, correspondingly, you
can import a VS .NET project by selecting Add Existing
Project in the C#Builder Project Manager and choosing a VS .NET project. Think of an architect who uses
C#Builder as her preferred modeling tool. When complete,
she would export the C#Builder project, which would be
added easily to any VS .NET project.
Finally, C#Builder Architect edition has integrated
Together UML modeling technology into the IDE. This
provides a design-time capability where developers and
architects may build UML models and the underlying code
is updated. Similarly, code changes update the UML model
immediately. There is no intermediate translation or representation of the model, which means there is direct interaction between code and model. VS .NET Architect has
Visio, but the capability doesnt even compare.
Another exciting offering in C#Builder Architect is Enterprise Core Objects, or ECO (pronounced eeko). ECO is a
runtime-modeling capability that implements the vision
of the Object Management Group (OMG) Model Driven
Architecture (MDA). With ECO, you can build new models
or import existing XMI format models created from other
modeling tools. ECOs primary capabilities include support
for data persistence, transactions, security, and data

Cover Story

C#Builder vs. VS .NET: Choose Wisely

Making the Grade

Heres a report card of how VS .NET and C#Builder stack up in different programming areas. Each product excels in some
areas while lacking in others, so choose the product that most closely maps to your development style and needs.




Appearance and navigation

Both aesthetically appealing and easy to navigate

Language and platform

Both use .NET and C# code

Code editor

VS .NET has more capability and is well suited for the hands-on coder

Project management

Both do the job well, but I wonder if they could be improved for larger or more
complex projects

Help systems


VS .NET Help has more features and is consistent; C#Builders BDP Help is spotty




With BDP, C#Builder offers more options in addition to what VS .NET has with .NET
Framework data providers

Code browsing

C#Builders Model View provides superior code visualization


C#Builder integrates with Together Control Center for UML design and modeling
support; Visio on VS .NET doesnt even compare

Model Driven Architecture (MDA)

Enterprise Core Objects (ECO) integrates with C#Builder, providing model-driven

runtime MDA support; VS .NET offers nothing in this area

Application Lifecycle
Management (ALM)

C#Builder integrates with many tools to support ALM; although VS .NET has tool
integration, its support of an ALM-like process is blurry

Two different ways to accomplish the same tasks

ing. ECO models may also evolve as changes to database

schema are made. VS .NET has no offerings in this area.

Collaborating Lifecycle Management

Borland has begun an initiative named Application Lifecycle
Management (ALM), where people involved in projects can
coordinate and work in a collaborative fashion via tools that
integrate into the environment. ALM is divided into six distinct phases, which include Define, Design, Develop, Test,
Deploy, and Manage. (For more information on ALM, visit
Borlands Web site at The significance
of ALM is the way tools integrate and interoperate in the
C#Builder environment. For example, CaliberRM, which is
a requirements-management tool for the Define phase, integrates directly with C#Builder to put customer requirements
directly into the developer cockpit. ALM doesnt prescribe
a specific methodology, but it is presented in a way that
supports agile processes. Additionally, C#Builder is open to
integration with third-party vendors, avoiding lock-in from
single-vendor solutions. The integration story goes much
deeper than this, but the fact is C#Builder is part of a larger
vision to help development teams that include managers,
testers, and developers build better software faster.
VS .NET integrates well with other Microsoft technologies, and third-party vendors may integrate if they obtain
a license for the Visual Studio .NET Integration Program

(VSIP). Microsoft prescribes the Microsoft Solutions Framework (MSF), but it doesnt have integrated tools and guidance on how to perform an ALM-like process with VS .NET.
I could have touched on many other subjects in this
article, but I chose what I felt to be the most important
issues. Both C#Builder and VS .NET are wonderful tools
that let you develop any type of .NET application you
want. The difference is the way you construct your code.
For coding, I love the VS .NET source-code editor. From a
team-development perspective, though, C#Builder delivers
on tool integration and productivity for the entire application lifecycle. I personally welcome Borland into the
.NET arena and expect more innovative technology to be
built into C#Builder in upcoming versions. With the extra
competition in the .NET arena, I have no doubt future
versions of both C#Builder and VS .NET will be released
with many more wonderful features that make developers
more productive in shipping their code. #

Joe Mayo is an author, independent consultant, and trainer

specializing in .NET technologies. He operates the C#
Station Web site (, and he is the
author of C# Unleashed (Sams) and C#Builder Kick Start
(Sams). E-mail him at


By Brian Noyes

Query Client-Side Data

Size up your data-access options.

ATA ACCESS IN .NET IS BUILT AROUND A DISCONnected data model. You go to the source, you get your
data, and you work with it on the client side. Youre in,
youre out, you free up those precious server resources as
quickly as possible so you have a truly scalable system.
One side effect of this model is you now need to do
certain things on the client side that in the past you might
have done on the server. Searching and selecting data is
one of those things, and its a common requirement for
many applications. In this article, Ill explore the range
of options you have at your disposal in .NET for querying client-side data to locate and extract specific pieces of
information whether you are doing so for display, computation, or modification.

The sample code in this article is available for download on the Delphi Informant
Magazine Complete Works CD located in


DataRow[] sales =
myDataSet.Tables["sales"].Select("qty >= 15");
DataRow[] 94sales =
"ord_date > #12/31/1993# AND ord_date < #1/1/1995#");

Choose Your Weapon

The first factor that will impact how
you query client-side data is the form
it takes on the client side. Ill discuss
two primary forms of client-side data:
ADO.NET DataSets and XML. (Custom
business-object collections certainly
hold equal importance, but they are
a separate topic because in that case
you would write most of the code
instead of using framework classes.)
When your client-side data is in the
form of an ADO.NET DataSet, you have

several options for querying subsets of the data contained in

that DataSet (see Figure 1). In most cases, the first and most
straightforward way to query data is to use the DataTable.Select
method. This method lets you provide a query expression
that resembles the kind of expression you would provide in
the WHERE clause of a SQL query on the server side. See the
DataColumn.Expression property documentation in the MSDN
Library for full documentation of the things you can put in a
Select expression; here are a couple of examples:

Data Form

Query Approach



Select method

Array of DataRow objects containing the rows that met the

query criteria


RowFilter property

Bindable collection of
DataRowView objects (indexed
off the DataView itself)


XmlNode.SelectNodes method

XmlNodeList containing XmlNode object reference to the

nodes that met the XPath query

or XPathDocument

XPathNavigator.Select method

XPathNodeIterator you can use

to loop through the node that
met the XPath query criteria

Figure 1. You can choose from a wide range of options for querying client-side data based
on the form the data takes on the client side. What you get as a result of the query also helps
determine which approach would work best for your application.


Query Client-Side Data

Be aware of some
key aspects of the Select
expression. For example,
you use single quotes
for string literals and #
delimiters for date literals
(as shown in the preceding code). Also, you can
perform multicolumn comparisons using AND, OR,
Figure 2. When you work with the XPathNavigator class, you call CreateNavigator on a document to get the
and NOT keywords, and
navigator, then you call Select on the navigator with either an XPath expression as a string or a compiled
you can match wildcard
XPathExpression returned from the Compile method. This action provides an XPathNodeIterator you can use to
expressions using the LIKE rip through the results.
keyword combined with
wildcards (% or *) at the beginning and/or end of an
DataRow. The DataRowViews exposed through the Dataexpression. What you get back from a Select call is an
View indexer are only those that meet the filter criteria:
array of DataRows through which you then can iterate to
process the selected rows or populate controls manually.
DataView dv = new DataView(myDataSet.Tables["Files"]);
The DataRow class exposes the column values through
dv.RowFilter = "Name LIKE '*.dll'";
an overloaded indexer that lets you retrieve the value of a
foreach (DataRowView rowview in dv)
field using the column name or ordinal of the column in
the indexer against the DataRow.
string name = (string)rowview["Name"];
Note that if you are querying relational data for dis}
play purposes, you are better off using a DataView and
its RowFilter property. This property takes the same kinds
The problem with ADO.NETs query capabilities is
of expressions allowable for the DataTable.Select method,
they are limited to searching one table at a time. Even
and as a result
if you have multiple tables in a DataSet with relations
the DataView
the tables that enable you to perform multitable
The problem with ADO.NETs effectively hides between
JOIN-like queries, ADO.NET provides no direct way to
query capabilities is they the rows that
do so using the DataSets object model or its underlying
are limited to searching one dont match the objects.
filter condition.
Luckily there is a powerful and easy way to get this
table at a time.
If you have
done, but it does require you to step into the world of
the DataView
XML and XPath queries. You must set the Nested property
bound to a DataGrid or other control for display, seton relations for child rows to true, then you get the data
ting the RowFilter will change rows of data exposed by
into an XmlDataDocument or XmlDocument. You then can
the DataView immediately and your presentation of the
use XPath queries to query the hierarchical representation
data will update accordingly. Moreover, if you are workof that same data.
ing with a Windows Forms DataGrid or other data-bound
controls, the display update will be automatic and immeXML Data Options
diate upon setting the RowFilter. If you are working with
You have several options for managing your XML data on
an ASP.NET DataGrid, you still will need to call DataBind
the client. You can choose between a Document Object
to get the control to render new content based on the
Model (DOM) representation using the XmlDocument
changed data.
class; you can bridge the DOM and the relations worlds
The DataView class also has Find and FindRows meth- using an XmlDataDocument; or you can use a new and
ods that let you search for a particular value within the
powerful .NET model that uses the XPathDocument.
view. But these methods let you match the parameter you
Which form you choose will be determined partly by
pass to the method against values in the sort columns
where the underlying data originates and partly by how
for the view only, and they do not allow for wildcards
you want to work with it for querying and manipulating
or other complex queries, so their use is somewhat limthe data.
ited. You also can use the DataViews indexer to iterate
If you need to be able to modify nodes found from
through the individual rows exposed by a filtered view.
a query, you will be limited either to the XmlDocument
The indexer returns a DataRowView item, which lets you
or XmlDataDocument classes. But if read-only data is
index into it with column names or numbers similar to a
acceptable and you want the lightest weight and fastest


// create the document object

XPathDocument xpdoc =
new XPathDocument("C:\\temp\\files.xml");
// Get the navigator
xpnav = xpdoc.CreateNavigator();
// Precompile the XPath query expression
xpexp = xpnav.Compile("descendant::Files");
// Get an iterator to be able to visit the nodes
XPathNodeIterator iter = xpnav.Select(xpexp);
// Use MoveNext() to visit each node and access its
// Properties
while (iter.MoveNext())
string nodename = iter.Current.Name;

Figure 3. Use the XPathNavigator as the best path for querying XML data.
By calling the Compile method before calling Select, you can reuse the
compiled expression for much faster queries if a query expression will be
used more than once.

means of querying and manipulating your XML data, the

XPathDocument is the way to go. All these document
classes can provide you with an XPathNavigator instance
by calling their CreateNavigator method (see Figure 2).
The XPathNavigator lets you query your XML data more
efficiently than simply using the mechanisms on the XmlDocument class itself.
To query with an XmlDocument or XmlDataDocument
instance, you can call the SelectNodes and SelectSingleNode
methods. Selectcreates an
You might be tempted to Nodes
always put your data in an under the covers
XPathDocument because and calls Select
it for you.
in most situations it offers on
the highest performance.
simply calls
SelectNodes and
returns the first node found. With any of the document types,
though, you can call Select directly on the XPathNavigator
you get from its CreateNavigator method, which is the most
flexible and high-performance approach (see Figure 3). You
either can pass an XPath expression as a string to Select, or
you can precompile the query using the Compile method and
pass the resulting XPathExpression object to Select instead.
This is the way to go if you will be repeating the query more
than once because it makes the Select call fast and efficient.

Query Client-Side Data

DataReader is a snap, but parsing it into an XML document takes considerable effort. You can try different
combinations, such as returning XML data directly from
SQL and reading it in with an XmlReader. Regardless of
which path you choose, in most cases this I/O step will
dominate the cost of querying the data on the client by
a factor of 10 or more. The only way to know for sure is
to profile and experiment with data and hardware that is
representative of your deployment architecture.
The other thing you must factor into your design is
whether you really need all that data on the client side
instead of simply hopping back to the database each time
you need to perform a query. In many cases, the impact
of some extra round trips to the data source to get the
data you need when you need it is minimal, and it might
make a lot more sense than holding a large dataset in
memory on the client side simply to avoid those round
trips. Holding large datasets in memory on the client
could drive client machine hardware requirements, impact
the performance of other applications running, and greatly increase the probability of concurrency issues with the
data you are using on the client.
From a capabilities standpoint, though, the
choice is somewhat of a draw. DataTable.Select and
DataView.RowFilter have some powerful summary function capability and can work with child rows to achieve
GROUP BY-like functionality. They also have a syntax
thats easy to write and understand for anyone familiar
with SQL. XPath-based query options on the XML document classes in .NET can, however, do queries across
multiple tables in memory (see this articles download
code for an example; see the Download box for details).
You also can precompile the XPath queries with the
XPathNavigator to get much higher-performance queries
than those which the client-side relational options are
capable. And XPath as a whole is more expressive than
syntax allows for the relational methods, but it is much
more obtuse to write and learn. The code download
contains an app that demonstrates all the methods Ive
described, and it does some crude profiling measurements
to give you a sense of the cost of each step and to help
you make the right choice for your needs. #

Performance and Capability Trade-Offs

With so many options available to query client data, you
probably are wondering which one you should choose.
You might be tempted to always put your data in an
XPathDocument because in most situations it offers the
highest performance of all the options Ive listed. But
you first must examine the cost of getting the data into
this format. Getting data out of SQL into a DataSet or

Brian Noyes is a consultant, trainer, and writer with IDesign Inc.

( Brian specializes in architecture, design, and coding
of .NET distributed data-driven windows and Web applications and Office
automation and desktop productivity applications. Hes an MCSD with more
than 12 years of programming, design, and engineering experience, and he is a
contributing editor for C#PRO, asp.netPRO, and other publications. E-mail him




By Alan C. Moore, Ph.D.

RoboHelp Office X4
A Suite Support Solution

ocumentation is essential for any type of

software, from a Delphi-built component to a


Microsofts original, RTF-based output. RoboHelp

provides the tools to create several variations,
including WinHelp 3 (16-bit systems), WinHelp
4 (32-bit systems), and WinHelp 2000 (this gives
WinHelp 4 systems the look and feel of HTML
Help with the tri-pane window).

WebHelp and
WebHelp Pro

Un-compiled HTML-based output that supports

standard help features (table of contents, index,
and full-text search), a customizable user interface, support for pure HTML, and fast download performance.


A Microsoft help system that can be used as application help, or standalone under Microsoft Windows. Based on HTML files, it allows developers to
distribute a single compressed file (.CHM) to end
users for viewing in a browser. You can create HTML
Help using RoboHelp HTML or RoboHelp for Word.


This Sun Microsystems-developed system features

a compressed HTML-based output that can work
with applications written in Java. This output runs
on a variety of platforms, as can Java applications
themselves. It does require JavaHelp components
and the Java Runtime Environment to run.

Oracle Help

Oracle Corp.s HTML-based implementation of

Sun Microsystems JavaHelp standard. Although
the output is designed for Java applications, it
can work with those written in any language. Its
output can run on a variety of platforms, including Windows, UNIX, or Macintosh. You can create
Oracle Help using RoboHelp HTML.

large, full-featured application. And documen-

tation will vary greatly depending on the nature of

the software it supports and its target platform. Thats
where RoboHelp Office X4 comes in; it provides a
feature-rich environment in which to create help files
and other documents, including tutorials.

Opening the RoboHelp Starter, you discover a plethora of

formats from which to choose. The major formats are outlined
in Figure 1. Well begin by examining output formats. Then
well examine the two help development environments that
RoboHelp Office provides to produce these output types.
RoboHelp Output Types
Each of the various types of output is created in one
of two environments, RoboHelp HTML or RoboHelp
for Word. The latter creates WinHelp systems, taking
advantage of the features in Microsofts popular word
processor; the former is a standalone application that
produces HTML-based help that can be displayed in a
browser. In some cases, such as JavaHelp and WebHelp,
you have a choice of which tool to use.
WebHelp and WebHelp Pro produce output that can
run on any platform or browser. These formats are
appropriate for both Web-based and desktop applications.
The main difference is WebHelp Pro provides a feedback
mechanism that WebHelp does not. With the latter, Web



Figure 1: Some of the main help systems supported by RoboHelp.




RoboHelp Office X4
for creating this kind of help, as shown in
Figure 2; output from this page is shown
in Figure 3. On the left is a tree view that
provides access to most, if not all, of the
functionality. On the right is a work space
that can display various kinds of information,
but is generally the home of the WYSIWYG
editor. The environment supports drag-anddrop functionality, internal folder creation,
and similar RAD features. In fact, one of the
most attractive features is the variety of ways
in which you can accomplish the same task
with different workspaces, wizards, toolbars,
or context menus.

Figure 2: RoboHelp HTMLs development environment.

In creating a help file, the appearance of the

material is nearly as important as the content.
RoboHelp HTML uses style sheets to control
the formatting, including font styles and sizes,
line spacing, and paragraph indentation. You
can attach a single style sheet (*.CSS) to any
number of topics. You can also create specific
styles for individual topics using embedded
styles. Finally, you can format text directly in
the WYSIWYG editor using toolbars, shortcut
keys, or the Format menu without using a style
sheet. Although this approach doesnt depend
on style sheets or embedded styles, it does
provide power and flexibility.
One of the most attractive features of HTML
and WinHelp is the ability to access the
information sequentially or in more random
fashion. This places a certain burden on
the help writer to anticipate the various
approaches a user might take. Indexes,
tables of contents, and glossaries can all
help to make the text come alive, making
the document navigable. All versions provide
these essential elements.

Figure 3: Output from RoboHelp HTML.

administrators can learn how visitors to their sites are

using the Help system and make appropriate adjustments.
By the time you read this there will be another update
to RoboHelp Office version X4. It will include the ability
to generate FlashHelp, a new Flash-bas ed Help format
that eHelp describes as bandwidth-efficient and firewallfriendly. Based on XML and Flash technology from
Macromedia Inc., FlashHelp offers additional rich-media
capabilities and a consistent look and feel across various
platforms and browsers. In particular, it gives developers
the ability to enhance help systems with Macromedia
Flash interactivity and sound.
RoboHelp HTML
Since it was first introduced, HTML Help has become
increasingly popular with both users and developers.
RoboHelp HTML provides a feature-rich environment


RoboHelp provides the tools that make it easy

to create outlines, indexes, tables of contents,
and browse sequences, each of which helps
users get the most out of your help system.
A good online index incorporates words and phrases that
capture essential features of the application or subject
matter your help file supports. RoboHelp includes a
drag-and-drop Index Designer that makes this task easy.
Similarly, it provides the same RAD approach to building
tables of contents. Browse sequences allow users to move
forward and backward through a series of topics arranged
in a particular order. RoboHelp gives you the ability to
create browse sequences automatically based on the
books and pages in the table of contents.
Its rare for any document to include only simple text.
RoboHelp is no different, as it provides the ability to
create bulleted and numbered lists, as well as tables to
enhance the communications potential of text. RoboHelp
allows you to change the formatting and appearance of
tables. With HTML-based projects, you need hyperlinks to




RoboHelp Office X4
that allows you to test a page you are working
on to make sure the latest additions are
working properly. If you decide to use the
HTML version of RoboHelp, be sure to visit
Robert Chandlers kit of free Delphi tools to
add context-sensitive HTML Help and more (at

Figure 4: RoboHelp for Words development environment.

Figure 5: Output from RoboHelp for Word.

provide navigation between topics (you can also link to

URLs, e-mail addresses, multimedia, and more). You can
also create bookmarks that allow the user to navigate the
contents of lengthy topics. Finally, you can create textonly popups in a small window.
There is an import tool for using existing information
from various sources, including Word files, FrameMaker
files, and HTML files. You can also spell check the main
help text, as well as the content in the Table of Contents,
Index, and Glossary panes.
These days you can hardly find online help that does
not include images, including screen-shots of your
application. Images can add interest, color, and further
explanation to your topics. You can also insert image
maps that feature mouse-clickable regions that link to
topics, URLs, popups, multimedia resources, or similar
targets. And RoboHelp HTML includes a view function


RoboHelp for Word,

New Developments, &
Most of RoboHelp HTMLs capabilities apply
to RoboHelp for Word (see Figures 4 and 5 for
the output). The main differences are defined
simply by the differences between HTML Help
and WinHelp. The environment is similar,
with one important difference. Compare
Figure 4 with Figure 2. While the former is
a single application, the latter consists of
a controlling application, RoboHelp, and
the working environment, Microsoft Word.
Although RoboHelp HTML provides all the
capabilities previously described, RoboHelp for
Word makes all the capabilities of this word
processor available, and actually automates
many tasks, such as automatically creating
an .RTF file for you and inserting the proper
formatting characters to create jumps (and
RoboHelp for Word includes many new features
and enhancements. Many developers like to
use their help files to generate manuals or
other printed documentation, something I have
suggested to developer friends on more than
one occasion. RoboHelp has increased support
for this by giving developers considerable
control over the structure, content, and
appearance of printed output. The documents
produced take full advantage of the capabilities
of Microsoft Word and can include indexes,
tables of contents, headers and footers,
sophisticated formatting, images, and more.
The current version of RoboHelp for Word includes
the introduction of skins, plus additional ways to
customize output by manipulating WebHelps navigation
panes, toolbar, icons, fonts, buttons, and colors. In
the realm of multimedia, there is now support for
integrating RoboDemo tutorials into a help system.
Be sure to visit
versioncomparison.asp to see an outline of the differences
between versions X4, X3, and 2002.
You can also customize output using conditional text.
This makes it possible to deliver multiple versions of
output from a single project using the updated conditional
text feature. This makes it possible to provide customized
output for different versions of software or different user
levels. Another new feature is the ability to produce an
online glossary to increase the effectiveness of WebHelp,
HTML Help, and printed documentation outputs. This is




RoboHelp Office X4

Just the Facts

RoboHelp Office X4 is a suite of tools for producing a variety
of help file formats, especially those related to WinHelp and
HTML Help. Its two main development environments, RoboHelp HTML and RoboHelp for Word, provide many RAD tools
that make the process of producing documentation completely straightforward.
eHelp Corporation
10590 W. Ocean Air Drive
San Diego, CA 92130
Phone: (800) 358-9370;
outside the US/Canada, +1 858-847-7900
Web Site:
Price: US$999; see Web site for upgrade pricing and
maintenance package options.
particularly appropriate for applications specific to fields
that have specialized vocabularies.
In the area of documentation, RoboHelp comes with
excellent online help (as you would certainly expect)
and two short manuals (an installation guide and a
users manual that includes an excellent tutorial). The
users manual tutorial introduces RoboHelp HTML. Its
well designed, with many of the basic tasks prepared
in advance. The first part of the tutorial provides the
foundation, introducing basic tasks such as creating and
organizing new topics, using style sheets, and creating
links. The second part introduces more advanced tasks,
including building a table of contents and index, adding
images, creating hotspots, and using conditional text.
Although theres no tutorial as such for RoboHelp for
Word, there are many excellent examples included.
Furthermore, the user interface and features of the two
environments are close enough that I found I was able to



quickly begin working effectively with RoboHelp for Word

after completing the tutorial on its HTML cousin.
RoboHelp also includes some unexpected features.
Conditional text allows you to generate multiple sets of
content from a single project. Instead of maintaining
separate topics or projects, or creating complex
workarounds for different deliverables, you can select the
content you want to include or exclude from one project.
For example, you can generate a registered version
of your project that includes all the topics and a trial
version that includes only the basics.
There are other features (including new ones) I have not
covered in this review. Be sure to check eHelps Web
site for complete information on RoboHelp Office X4,
especially to learn more about the latest developments.
Although RoboHelp Office X4 may not be appropriate for
every Delphi developer, it will certainly meet the needs
of those looking for a feature-rich tool that can produce
the most modern output. There are other tools that are
more Delphi-friendly, some of which provide the ability
to parse component units to generate help topics and
other integration into Delphi. In fact, I tend to use one
as a pre-processor, importing its output into RoboHelp.
But for producing outstanding documentation with a
minimum of work, Im very pleased with RoboHelp and
expect to use it a great deal.

Alan Moore is a professor at Kentucky State University, where he teaches

music theory and humanities. He was named Distinguished Professor for
2001-2002. He has been named the Project JEDI Director for 2002-2004. He has
developed education-related applications with the Borland languages for more
than 15 years. Hes the author of The Tomes of Delphi: Win32 Multimedia
API (Wordware Publishing, 2000) and co-author (with John C. Penman) of
The Tomes of Delphi: Basic 32-Bit Communications Programming (Wordware
Publishing, 2003). He also has published a number of articles in various
technical journals. Using Delphi, he specializes in writing custom components
and implementing multimedia capabilities in applications, particularly sound
and music. You can reach Alan at



Extremely Basic

By Alan C. Moore, Ph.D.

his month we begin a three-part

series on Extreme Programming
(XP). In this first installment
well examine some of XPs basic
principles. Next month well take a
look at many of the available books.
In the third installment well examine
Internet resources, coming full circle
to answer the fundamental question:
Is it really that extreme?
A little history, a little philosophy.
To understand XP, we must understand the environment in which it
developed the state of computing
in the mid-1990s. That environment
contained many seemingly positive
factors for developers, in particular an
ever increasing importance of technology and technological resources. But
at the same time, the discipline of
computer programming was still very
young compared to some of the fields
of engineering from which it sought
to learn viable approaches and techniques. That lack of maturity took its
toll on many software projects.
The end of the last century saw
important developments in the
maturation of computer programming
(Ive discussed some of these aspects,
like UML and Quality First, in these
pages). Naturally, object-oriented
languages remain at the top of my
list of useful innovations. Modeling
languages and design patterns must
also be included.

But examining critically how we work

and how we code can also make a
difference in our effectiveness. In
the seminal works Code Complete
and Rapid Development, Steve
McConnell demonstrates how to
recognize and eliminate bad coding
habits that make us less productive.
In another seminal work that
well discuss in detail next month,
Refactoring: Improving the Design
of Existing Code, Martin Fowler
picks up the torch and proceeds
further down the path toward an
intelligent programming discipline.
Not surprisingly, refactoring is a
key element of XP. But were getting
ahead of ourselves. Having taken a
brief look at the history of XP, lets
examine the XP philosophy.
In Extreme Programming Explained:
Embrace Change, Kent Beck presents
four basic values, or areas of focus:
communication, simplicity, feedback,
and courage. A careful examination of
the twelve XP practices in the table
shown in Figure 1 should illustrate
the application of one or more of
these principles to recommended
approaches to programming.
In the following statement quoted by
Kent Auer and Roy Miller in Extreme
Programming Applied (p. 29), Beck
makes a strong case for focusing
on communication: Problems with
projects often can invariably be


traced back to somebody not talking

to somebody else about something
important. In discussing these
values, Auer and Miller state that
XP makes it almost impossible not
to communicate. How? By carefully
examining the role of customers,
developers, and managers; by exposing
bad assumptions generally made by
project participants (often leading
to bad practices), and; by defining
precisely the type of communication
needed as the project develops.
As a core value, simplicity relates
directly to how we write code. Most
XP authors recommend constant
refactoring, constant simplification
of code. But we also need to watch
out for certain traps. Sometimes we
become so fixated on future use of
code that we make it overly complex.
XP recommends always using the
simplest workable approach to writing
code. In an ideal XP environment,
you first write your series of
compliance tests, then you write the
code and make certain it passes all
the tests. Then you integrate it into
the system and test some more. And
the code itself is just enough to fulfill
its purpose no more.
Feedback is related to communication,
of course. Because planning is a continuous, dynamic activity in XP, gathering feedback regularly from developers and users is essential. Finally,





The Planning

Managers, users, and developers create a rough

plan quickly and refine it as the project progresses.


There are two types of testing in XP: Unit testing

and acceptance testing; both take place regularly.


The norm in XP is for programmers to work

in pairs (that could change) to write all code.
Proponents claim this approach is efficient, and
encourages better design decisions and permits
greater familiarity with the whole system.


Improving or simplifying code while preserving

existing functionality. It involves cutting unnecessary code, improving code readability, breaking
up large routines, and similar techniques. It
takes place continuously both during and after
implementing new functionality. The aim is to
write the simplest code that gets the job done.

Simple Design

The goal of refactoring to produce a system

that passes all the tests, contains no duplicate
code, contains the bare minimum of classes and
methods, and performs its objective(s).


Code is owned by everyone on the team. Anyone can modify any code. But everybody also
shares responsibility for the code. And if someone
breaks the code, that person is expected to fix it.


Because code integration occurs regularly, several times a day after unit tests have run successfully, there are fewer surprises when everything
is put together. Diagnosing reasons for failure
becomes easier. So does bug squashing.


A customer is needed to drive business decisions,

to write the stories that guide the functionality
developers implement. The customer helps developers set priorities for the current and future iterations. The customer can flesh out the implications of a particular story card, giving developers
the information they need to produce the code.

Small Releases

A key XP strategy. The aim is to release a system

as soon as it contains sufficient business value.
Besides increasing profitability, frequent small
releases provide more immediate feedback to
developers, who can better plan the next release
based on customer needs and desires.

40-hour Week

The aim here is simply to increase effectiveness.

As Auer and Miller put it: Tired developers
make more mistakes, which will slow them down
more in the long run ...


Having agreed upon coding standards (a) keeps

the team focused and not sidetracked by silly
style disputes, and (b) supports the other practices by making it easier for people to read each
others code.


A developer should have some knowledge of a

projects general architecture, the system components, and their functional relationship. Developers will then understand how new components
will fit into the overall design.

Figure 1: XPs Twelve Practices.



courage is needed to put XP into practice; I think the XP

authors would agree that there is no room for hesitation.
As Auer and Miller put it, If you arent moving at top
speed, youll fail. One example they give of courage is
being willing to throw code away or make a change late
in the game. These values may catch our attention. But
how do we actually apply them? Thats where XPs Twelve
Practices come in.
XPs Twelve Practices. So whats so extreme about XP?
Auer and Miller state that, although many of the practices
relate to everyday activities, and many are not that new,
XP takes proven best practices and turns the knob up to
ten. It produces something greater than the sum of its
parts. XPs Twelve Practices are briefly described in the
table in Figure 1.
We can get some sense of the flavor of XP by examining
its first practice, The Planning Game. Users write
stories on a stack of index cards describing certain
tasks or functionalities. These desired results define an
XP projects iterations, which in turn become elements of
a release. An important element is letting the customer/
user make business decisions (desired functionality) and
letting the development team make technical decisions
(how to provide that functionality).
Once functionality is defined, testing begins immediately.
Developers write the unit tests even before they write the code
to be tested. Code is thoroughly tested before it is integrated
into the system, then it is integrated at the earliest possible
point. The system itself then undergoes an acceptance test to
ensure it provides the functionality users want.
Its important to acknowledge that many authors
insist that the XP practices must work together. They
recommend doing XP religiously at first. Once a
developer understands the individual practices and their
interactions, one can become more experimental in
applying them.
Next month well examine several excellent books that
cover various perspectives of the theory and practice of
XP. Well also explore these practices in more detail.
To take Kent Becks focus on feedback one step further, I
want to hear from you! Have you had any experience with
XP? To what extent do you think a single programmer
working alone (my current situation) can practice XP?
Until next time.
Alan Moore is a professor at Kentucky State University, where he teaches
music theory and humanities. He was named Distinguished Professor for
2001-2002. He has been named the Project JEDI Director for 2002-2004. He
has developed education-related applications with the Borland languages
for more than 15 years. Hes the author of The Tomes of Delphi: Win32
Multimedia API (Wordware Publishing, 2000) and co-author (with John
C. Penman) of The Tomes of Delphi: Basic 32-Bit Communications
Programming (Wordware Publishing, 2003). He also has published
a number of articles in various technical journals. Using Delphi, he
specializes in writing custom components and implementing multimedia
capabilities in applications, particularly sound and music. You can reach
Alan at