Académique Documents
Professionnel Documents
Culture Documents
2003
Volume 9 Number 11
INSIDE
ON THE COVER
Greater Delphi
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.
www.DelphiZine.com
FEATURES
Bill Todd on
InterBase 7.1
Delphi Tech
Multi-threaded
Windows Services
Delphi Informant
Trading Up to Advantage
Database Server 7.0
Using the Framework:
Queues, Stacks, & ArrayLists
Special C#PRO
Preview Section,
Including:
C#Builder
vs. VS .NET
Dynamic Delphi
13
.NET Developer
19
InterBase 7.1
REVIEWS
33 RoboHelp Office X4
23
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.
D E PA R T M E N T S
2 Toolbox
37 File | New by Alan C. Moore, Ph.D.
T O O L B O X
Create Visual Product Walkthroughs with Expo Walkthrough
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.
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.
Contact: info@gnostice.com
Web Site: www.gnostice.com
T O O L B O X
Actual Transparent Windows 2.0 Available
Nevron LLC
Price: One developer license, US$399.
Contact: email@nevron.com
Web Site: www.nevron.com
G R E A T E R
OUTLOOK 2000
MAPI
D E L P H I
DELPHI 4-7
By Bill Todd
Junk
Mail
Solution
Using Outlook as an Automation Server to Manage
Your Unwanted E-mail
Description
Junk Senders
Junk Content
Exceptions
Exception Content
Greater
Delphi
[Files]
ExceptionListFile=exception list.txt
JunkListFile=junk senders.txt
ContentListFile=Contents.txt
ContentExceptListFile=ContentExceptions.txt
[Folders]
FilteredFolder=*_Filtered
NewFolder=*_New
[Settings]
ContentDelimiter=|
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 www.dimastr.com/redemption.
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.LoadLists;
begin
ExceptionList := TStringList.Create;
ExceptionList.Duplicates := dupIgnore;
ExceptionList.Sorted := True;
ExceptionList.LoadFromFile(ExceptionListFileName);
JunkList := TStringList.Create;
JunkList.Duplicates := dupIgnore;
JunkList.Sorted := True;
JunkList.LoadFromFile(JunkListFileName);
ContentList := TStringList.Create;
ContentList.Duplicates := dupIgnore;
ContentList.Sorted := True;
ContentList.LoadFromFile(ContentListFileName);
ContentExceptList := TStringList.Create;
ContentExceptList.Duplicates := dupIgnore;
ContentExceptList.Sorted := True;
ContentExceptList.LoadFromFile(
ContentExceptListFileName);
end;
Greater
Delphi
procedure TMainForm.OpenOutlook;
begin
// 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');
end;
procedure TMainForm.CleanJunkSenders;
{ Deletes any entry from the junk senders list
that's in the exceptions list. }
var
I: Integer;
P: Integer;
begin
for I := 0 to ExceptionList.Count - 1 do begin
P := JunkList.IndexOf(ExceptionList[I]);
if (P <> -1) then
JunkList.Delete(P);
end;
JunkList.SaveToFile(JunkListFileName);
end;
procedure TMainForm.CleanInbox;
{ Move messages in the Inbox to appropriate folder. }
var
I: Integer;
MailMsg: Variant;
SenderAddress: string;
SenderDomain: string;
begin
if (EditingList <> elNone) then begin
ShowMessage(
'You cannot clean the inbox while editing a list.');
Exit;
end;
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
begin
try
MailMsg.Move(NewFolder);
except
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end;
Continue;
end; // if
// If sender is in junk list, delete this message.
if (JunkList.IndexOf(SenderAddress) <> -1) or
(JunkList.IndexOf(SenderDomain) <> -1) then
begin
try
MailMsg.Move(DeletedItems);
except
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end;
Continue;
end; // if
CheckMessageContent(MailMsg);
end; // for
end;
Greater
Delphi
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.
7
Greater
Delphi
Body := UpperCase(MailMsg.Body);
StrList := TStringList.Create;
try
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.Clear;
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;
Break;
end; // if
end; // for
// If all of the strings in StrList were in the
// subject or body, delete the message.
if (MoveMsg) then begin
try
MailMsg.Move(NewFolder);
Break;
except
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.Clear;
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;
Break;
end; // if
end; // for
// If all of the strings in StrList were in the
// subject or body, delete the message.
if (MoveMsg) then
try
MailMsg.Move(FilteredFolder);
Break;
except
on E: Exception do
ShowMessage('Could not move subject "' +
MailMsg.Subject + '". ' + E.Message);
end; // try
end; // for
end; // if
finally
StrList.Free;
end; // try
end;
D E L P H I
THREADING
T E C H
WINDOWS SERVICES
DELPHI 5-7
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.
var
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);
public
function GetServiceController: TServiceController;
override;
end;
TMyWorkerThread = class(TThread)
private
procedure Init;
procedure Cleanup;
procedure DoWork;
public
procedure Execute; override;
end;
Figure 1: The interfaces for a TService application and a TThread worker thread.
Delphi
Te c h
Figure 2: Using a Windows event to control the execute loop of the worker thread.
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:
var
iWaitResult: DWORD;
begin
repeat
DoWork;
iWaitResult := WaitForSingleObject(hStopEvent, 5000);
until iWaitResult = WAIT_OBJECT_0;
end;
Delphi
Te c h
procedure TMyWorkerThread.Execute;
var
iWaitResult: DWORD;
begin
Init; // When thread starts.
repeat
DoWork; // Where we open and use the connection.
iWaitResult := WaitForSingleObject(hStopEvent, 2000);
until iWaitResult = WAIT_OBJECT_0;
Cleanup; // When thread stops.
end;
procedure TMyWorkerThread.Init;
begin
// 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;
end;
procedure TMyWorkerThread.Cleanup;
begin
// Ensure connection is closed and pointer is nil.
if FCon <> nil then begin
if FCon.State <> adStateClosed then
FCon.Close;
FCon := nil;
end;
// For each CoInitialize, you need one of these.
CoUninitialize;
end;
procedure TMyWorkerThread.DoWork;
begin
try
// Acquire a connection from the pool.
FCon.Open(GetConnectString, '', '',
LongInt(adConnectUnspecified));
// Do something with the connection.
finally
// Close the connection, but don't nil the pointer.
if FCon.State <> adStateClosed then
FCon.Close;
end;
end;
Figure 3: Init, Cleanup, Execute, and DoWork implementations for our worker
thread showing how to set up COM support and ADO connection pooling.
TMyWorkerThread = class(TThread)
...
private
FCon: Connection;
var
FMyCriticalSection: TCriticalSection;
11
Delphi
Te c h
initialization
FMyCriticalSection := TCriticalSection.Create;
...
finalization
FMyCriticalSection.Free;
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:
12
uses
Windows, ActiveX, SysUtils,
{$IFDEF Testing}
MyServiceTest // Run as foreground app.
{$ELSE}
MyServiceMain // Run as Windows service.
{$ENDIF}
;
Conclusion
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 matthew@mlhess.com.
D Y N A M I C
ADVANTAGE DATABASE SERVER
D E L P H I
DELPHI 7
By Bill Todd
Trading Up
Building Applications with Advantage Database Server 7.0
Dynamic
Delphi
Trading Up
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.
in the SQL box, then click OK. Create another view named
ForeignCustomers, and change the WHERE clause to:
Country <> 'US'
Dynamic
Delphi
Trading Up
Groups
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
types are BEFORE, AFTER, and INSTEAD OF. BEFORE and
AFTER determine whether the trigger fires before or after
15
Dynamic
Delphi
Trading Up
PartsTbl,
and its
Dynamic
Delphi
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
bill@dbginc.com.
17
SelectTotal:
string;
UpdateTotal:
string;
SelectOrderNoNew: string;
SelectOrderNoOld: string;
OrderNo:
Integer;
OrderTotal:
Currency;
begin
// 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);
try
try
oConn.Name := 'conn';
// Create AdsQuery component to use in trigger.
Qry := TAdsQuery.Create(nil);
try
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
else
Qry.SQL.Text := SelectOrderNoNew;
Qry.Open;
OrderNo := Qry.FieldByName('OrderNo').AsInteger;
Qry.Close;
// Get total amount for all items for this order.
Qry.SQL.Text := SelectTotal;
Qry.ParamByName('OrderNo').AsInteger := OrderNo;
Qry.Open;
OrderTotal :=
Qry.FieldByName('ItemTotal').AsCurrency;
Qry.Close;
// Update the total in the Order record.
Qry.SQL.Text := UpdateTotal;
Qry.ParamByName('ItemsTotal').AsCurrency :=
OrderTotal;
Qry.ParamByName('OrderNo').AsInteger := OrderNo;
Qry.ExecSQL;
finally
Qry.Free;
end;
except
on E : EADSDatabaseError do
SetError(oConn, E.ACEErrorCode, E.message);
on E : Exception do
SetError(oConn, 0, E.message);
end;
finally
oConn.Free;
end;
end;
Dynamic
Delphi
Trading Up
18
end; // try
end; // with PartsTbl
// Assign values to the output parameters.
with tblOutput do begin
Open;
Edit;
FieldByName('RecordsChecked').AsInteger :=
CheckedCount;
FieldByName('RecordsChanged').AsInteger :=
ChangedCount;
Post;
Close;
end;
end; // with DM1
except
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.
DM1.DataConn.Execute(
'INSERT INTO __error VALUES (1, ' +
QuotedStr(E.Message) + ')');
end;
end;
. N E T
.NET FRAMEWORK
D E V E L O P E R
.NET COLLECTIONS
By Xavier Pacheco
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 (community.borland.com/delphi), and on Dr. Bobs site
(www.drbob42.com); 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.
19
Class
Description
ArrayList
BitArray
HashTable
Queue
SortedList
Stack
Interface
Description
IEnumerable
ICollection
IList
IEnumerator
IDictionary
IDictionaryEnumerator
IComparer
IHashCodeProvider
.NET
Developer
program d4dnStack;
{$APPTYPE CONSOLE}
program d4dnQueue;
{$APPTYPE CONSOLE}
uses
System, System.Collections;
uses
System, System.Collections;
var
stMyStack: Stack;
i: integer;
var
qMyQueue: Queue;
i: integer;
procedure ShowStackItems;
var
enumMyStack: IEnumerator;
begin
enumMyStack := stMyStack.GetEnumerator;
Console.WriteLine();
while enumMyStack.MoveNext() do
Console.Write(enumMyStack.Current.ToString() + '
Console.WriteLine();
end;
procedure ShowQueueItems;
var
enumMyQueue: IEnumerator;
begin
enumMyQueue := qMyQueue.GetEnumerator;
Console.WriteLine();
while enumMyQueue.MoveNext() do
Console.Write(enumMyQueue.Current.ToString() + '
Console.WriteLine();
end;
begin
// Initialize and populate the Stack.
stMyStack := Stack.Create();
for i := 1 to 5 do
stMyStack.Push('Item ' + i.ToString());
ShowStackItems();
// Pop the topmost item.
Console.Write('Pop: ' + stMyStack.Pop.ToString());
ShowStackItems();
// Peek at the topmost item.
Console.Write('Peek: ' + stMyStack.Peek.ToString());
ShowStackItems();
// Pop the two topmost items.
Console.Write('Pop: ' + stMyStack.Pop.ToString());
Console.Write('Pop: ' + stMyStack.Pop.ToString());
ShowStackItems();
// Push another item.
Console.Write('Push: Item 6');
stMyStack.Push('Item 6');
ShowStackItems();
// Clear the stack.
stMyStack.Clear();
Console.WriteLine('Stack cleared');
// Push another item.
Console.Write('Push: Item 7');
stMyStack.Push('Item 7');
ShowStackItems();
end.
');
');
begin
// Initialize and Enqueueulate the Queue.
qMyQueue := Queue.Create();
for i := 1 to 5 do
qMyQueue.Enqueue('Item ' + i.ToString());
ShowQueueItems();
Console.Write(
'Dequeue: ' + qMyQueue.Dequeue.ToString());
ShowQueueItems();
Console.Write('Peek: ' + qMyQueue.Peek.ToString());
ShowQueueItems();
Console.WriteLine(
'Dequeue: ' + qMyQueue.Dequeue.ToString());
Console.Write(
'Dequeue: ' + qMyQueue.Dequeue.ToString());
ShowQueueItems();
Console.Write('Enqueue: Item 6');
qMyQueue.Enqueue('Item 6');
ShowQueueItems();
qMyQueue.Clear();
Console.WriteLine('Queue cleared');
Console.Write('Enqueue: Item 7');
qMyQueue.Enqueue('Item 7');
ShowQueueItems();
end.
20
.NET
Developer
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);
.NET
Developer
begin
// Initialize and populate the stack.
stMyStack := Stack.Create();
for i := 1 to 5 do
stMyStack.Push('Item ' + i.ToString());
ShowStackItems();
// Copy the stack contents to a one-dimensional array.
arMyArray := stMyStack.ToArray();
PrintArray(arMyArray);
// Change the contents.
for i := 0 to arMyArray.Length-1 do
arMyArray.SetValue('Delphi ' + IntToStr(i + 1), i);
PrintArray(arMyArray);
// Create another stack instance with the new data.
stMyStack := Stack.Create(arMyArray);
ShowStackItems();
end.
C O L U M N S
INTERBASE 7.1
&
R O W S
DATABASE
By Bill Todd
InterBase 7.1
Whats New in Borlands Cross-platform Embeddable Database
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:
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
savepoint:
23
Columns
&
Rows
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
24
www.C-SharpPRO.com
C#
TOE TO TOE
Cover Story
By Joe Mayo
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.
Cover Story
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.
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.
Cover Story
C#Builder
VS .NET
Comments
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
A
A
VS .NET Help has more features and is consistent; C#Builders BDP Help is spotty
Designers
C
A
Database
With BDP, C#Builder offers more options in addition to what VS .NET has with .NET
Framework data providers
Code browsing
Design
C#Builder integrates with Together Control Center for UML design and modeling
support; Visio on VS .NET doesnt even compare
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
(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. #
www.C-SharpPRO.com
Feature
By Brian Noyes
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
INFORM\2003\NOV\CS200311BN.
Download
6
DataRow[] sales =
myDataSet.Tables["sales"].Select("qty >= 15");
DataRow[] 94sales =
myDataSet.Tables["sales"].Select(
"ord_date > #12/31/1993# AND ord_date < #1/1/1995#");
Data Form
Query Approach
Result
DataTable
Select method
DataView
RowFilter property
Bindable collection of
DataRowView objects (indexed
off the DataView itself)
XmlDocument
XmlNode.SelectNodes method
XmlDocument,
XmlDataDocument,
or XPathDocument
XPathNavigator.Select method
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.
www.C-SharpPRO.com
Feature
Be aware of some
XmlDocument
key aspects of the Select
CreateNavigator
expression. For example,
you use single quotes
MoveNext
Select
for string literals and #
XPathNavigator
XmlDataDocument
XPathNodelterator
delimiters for date literals
(as shown in the preceding code). Also, you can
XPathDocument
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
www.C-SharpPRO.com
Feature
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.
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. #
www.C-SharpPRO.com
N E W
&
U S E D
RoboHelp Office X4
A Suite Support Solution
Output
WinHelp
WebHelp and
WebHelp Pro
(formerly
WebHelp
Enterprise)
HTML Help
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.
JavaHelp
Oracle Help
Description
New
&
Used
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.
New
&
Used
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
www.helpware.net/delphi).
New
&
Used
RoboHelp Office X4
36
F I L E
N E W
Extremely Basic
File
New
Practice
Description
The Planning
Game
Testing
Pair
Programming
Refactoring
Simple Design
Collective
Code
Ownership
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.
Continuous
Integration
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.
On-site
Customer
Small Releases
40-hour Week
Coding
Standards
System
Metaphor
38