Sei sulla pagina 1di 26

July

2004

Volume 10 Number 7

INSIDE
ON THE COVER
Delphi at Work

Product Reviews: Delphi Spell Checker and Shell+

Excel to PDF

Mark Meyer shows you how to create a component, Excel2PDF, that


can seamlessly print an Excel document to PDF. Along the way, he
provides a brief tutorial on how to set up a PDF printer, and covers
a few gotchas and caveats that he discovered while creating this
highly useful conversion tool.

The Complete Monthly Guide to Delphi Development

July 2004 vol.10, no. 7

Excel PDF
to

Conversion through Automation

FEATURES

What Is

ASP.NET Really?

Columns & Rows

Briefcase Apps

ADO.NET Data Access Components: Part VI

Delphi Informant
www.DelphiZine.com

Made Easy
Smarter

Bill Todd continues his exploration of DataSets , DataTables , and


DataRows . Topics include making changes permanent by calling
AcceptChanges , merging data in a DataTable into a DataSet ,
computing summary information, using a DataView object to filter
and sort the data in a DataTable, and more.

ADO Connections
More Everyday Tasks
with ADO.NET

Cover Art by Arthur A. Dugoni Jr.

Everyday Delphi

12

Briefcase Apps Made Easy

Rick Spence demonstrates how to use TClientDataSet to


implement briefcase-style applications, allowing them to disconnect
from the server, take the data on the road, and eventually reconnect
to the server and apply the changes they made while offline.

In Development

16

Smarter ADO Connections

Employing an engaging narrative style, Bob Fleischman explains


how to tame the beast that is ADO through use of the decorator
design pattern. The goal is to derive classes from TADOConnection
that can deal with TCP/IP or named pipes, trusted logins, or user/
passwords, and other such choices.

Active Delphi

19

ASP.NET: Part I

Embarking on an important new series, Nick Hodges provides us


with a 10,000-foot view of the ASP.NET architecture, explaining
once and for all What is ASP.NET really? when it comes to a Delphi
project and the .NET Framework itself especially the System.Web
namespace.

REVIEWS
22 Shell+

Product Review by Mike Riley

24
1

Delphi Spell Checker PRO


Product Review by Mike Riley

DELPHI INFORMANT MAGAZINE | July 2004

D E PA R T M E N T S
26 File | New by Alan C. Moore, Ph.D.

D E L P H I
AUTOMATION

A T

W O R K

CONVERSION MICROSOFT EXCEL ADOBE ACROBAT DELPHI 7

By Mark Meyer

Excel to PDF
Conversion Made Easy through Automation

live to write tools! And as luck would have


it, Delphi provides a wonderful environment
to build special purpose utilities to make

programming life easy even downright fun.


In a recent project we came up against the need to convert
hundreds of Excel documents to PDF. When I was given the
project the first thing I did was research the Web to see if
there were products, or readymade alternatives to building
a tool from the ground up. After weighing the pros and
cons of several options in several languages, I decided that
a special purpose Delphi component was the right choice.
In this article Ill show you how to create a component,
TExcel2PDF, to seamlessly print an Excel document to PDF.
Along the way Ill give you a brief tutorial on how to set up a
PDF printer, a necessary prerequisite for this component. Ill
also cover a few gotchas and caveats that I discovered during my learning process. So what are we waiting for! Lets get
to the fun, because anything related to Delphi is never work.
Under the Hood
TExcel2PDF is built to leverage the Automation API exposed by
TExcelApplication. Although TExcelApplication is a wonderful
addition to the Delphi component tab, the Excel Automation
API is quite broad in its application and is somewhat confusing. TExcel2PDF narrows the focus of TExcelApplication to
the specific task of printing an Excel document to PDF. It also
makes the selection of the various attributes associated with
this task as simple as setting a Delphi property. The declaration
for the TExcel2PDF class is shown in Figure 1. Figure 2 shows
its appearance through the Object Inspector.
2

DELPHI INFORMANT MAGAZINE | July 2004

Easy to Use
TExcel2PDF is a wrapper around the TExcelApplication
component. In Delphi 7 Enterprise Edition, the
TExcelApplication component is located on the Servers tab.
TExcel2PDF makes short work of printing Excel documents
to PDF by allowing the user to set a few simple properties
and then fire one small public method: Print. Generating
PDF documents from Excel spreadsheets is as simple as
executing the following code snippet:
Excel2PDF1.InputDirectory :=
Extractfilepath(PTShellList1.ShListData[i].PathName);
Excel2PDF1.InputFilename :=
PTShellList1.ShListData[i].FileName;
Excel2PDF1.Print;

When you review the code for TExcel2PDF youll find that
the workhorse method of TExcel2PDF is a private method
named _Print_Special_ (shown in Figure 3). Like most wrapper components, TExcel2PDF provides a simple, easy-to-use
interface to a few Windows Automation APIs exposed by the
underlying TExcelApplication component.
(More Than) A Few Words about PDF
Printer Setup
As I stated before, using TExcel2PDF is simple. Setting up
your PDF printer is slightly more involved, but necessary
in order to use our tool. Dont worry though; well go
through the process step-by-step.
The first thing you need to do is install Adobe Acrobat
on your system. Adobes marketing nomenclature and
product array is somewhat daunting to the uninitiated.
Essentially you will need access to the Adobe Acrobat
5 or 6 client-based Full Install. When the install first

Delphi

at

Work

Excel to PDF

TExcel2PDF = class(TComponent)
private
FExcelApp : TExcelApplication;
FLCID, FCopies : Integer;
FOutputDirectory, FOutputFilename, FInputFilename,
FInputDirectory, FPDFPrinter : string;
FPrinttofile, FUpdateLinks, FAutoConnect, FAutoQuit,
FExcelVisible, FOpenReadOnly, FEditable,
FPromptForFilename : Boolean;
protected
function GetAutoConnect: Boolean;
function GetAutoQuit: Boolean;
function GetExcelVisible: Boolean;
procedure SetAutoConnect(Value: Boolean);
procedure SetAutoQuit(Value: Boolean);
procedure SetExcelVisible(Value: Boolean);
procedure _Print_Special_;
procedure _Open_;
procedure _Close_;
public
procedure Print;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property LCID: Integer read FLCID;
property PDFPrinter: string
read FPDFPrinter write FPDFPrinter;
property OutputFilename: string
read FOutputFilename write FOutputFilename;
property OutputDirectory: string
read FOutputDirectory write FOutputDirectory;
property InputFilename: string
read FInputFilename write FInputFilename;
property InputDirectory: string
read FInputDirectory write FInputDirectory;
property PrintToFile: Boolean
read FPrintToFile write FPrintToFile;
property Copies: Integer read FCopies write FCopies;
property UpdateLinks: Boolean
read FUpdateLinks write FUpdateLinks;
property AutoConnect: Boolean
read GetAutoConnect write SetAutoConnect;
property AutoQuit: Boolean
read GetAutoQuit write SetAutoQuit;
property ExcelVisible: Boolean
read GetExcelVisible write SetExcelVisible;
property OpenReadOnly: Boolean
read FOpenReadOnly write FOpenReadOnly;
property Editable: Boolean
read FEditable write FEditable;
property PromptForFilename: Boolean
read FPromptForFilename write FPromptForFilename;
end;

Figure 1: The TExcel2PDF class definition.

comes up on your client


machine, youll see the
screen shown in Figure 4.
Select Typical for the setup
type, then follow and accept
the default options for all
the prompts. Acrobat should
be good to go.

Figure 2: Object Inspector view of


TExcel2PDF.
3

After youve installed Adobe


Acrobat 5 or 6 on your
machine, check the Printers
folder in Control Panel and
note the default name that
Acrobat gives the printer,
Acrobat Distiller. You dont
have to keep this default

DELPHI INFORMANT MAGAZINE | July 2004

procedure TExcel2PDF._Print_Special_;
var
i : Integer;
begin
try
try
Self._Open_;
if PromptForFilename then
FExcelApp.ActiveWorkbook.PrintOut(EmptyParam,
EmptyParam, EmptyParam, EmptyParam, PDFPrinter,
True, EmptyParam, EmptyParam, LCID);
else if (Copies > 1) then
for i := 1 to Copies do
FExcelApp.ActiveWorkbook.PrintOut(
EmptyParam, EmptyParam, EmptyParam, EmptyParam,
PDFPrinter, True, EmptyParam, OutputDirectory +
'\' + OutputFilename + '_' + inttostr(i) +
'_' + POST_SCRIPT_EXT, LCID);
else
FExcelApp.ActiveWorkbook.PrintOut(EmptyParam,
EmptyParam, EmptyParam, EmptyParam, PDFPrinter,
PrintToFile, EmptyParam, OutputDirectory + '\' +
OutputFilename + POST_SCRIPT_EXT, LCID);
finally
Self._Close_;
end;
except
on EOleException do
// Handle if needed.
end;
end;

Figure 3: The _Print_Special_ method.

name; you can rename the printer to something more


meaningful in your environment. However, this name will
be important later when we discuss the demo application.
With the Adobe Acrobat installation complete, the next
step is to configure some important attributes of your
PDF printer. Youll see in a moment that configuring your
PDF Printer requires more effort than actually using the
Excel2PDF component.
The first thing you need to do is configure a new PDF
printer port. As with the printer name, Adobe sets up a
default in this case a port thats buried under the
C:\Documents and Settings directory. For the purposes
of this article I am going to set up a port that will map to
C:\PDFPrinterOutput. You can do this by right-clicking on
the Acrobat Distiller printer in the Windows \Printers
folder, then selecting the Properties menu option.
From the Acrobat Distiller Properties dialog box, select
the Ports tab. From here you will select the Add Port
button. At the Printer Ports dialog box, select PDF Port from
the list of Available port types, and then select New Port. From
the Browse for Folder dialog box you will navigate to a
directory in the file system. The selected directory will be
the location where all PDF output will be sent.
One more thing is necessary for you to start generating
PDF output with TExcel2PDF. Adobe offers several printing
preferences with which you may wish to experiment. All
of them are accessible via the General tab of the Acrobat
Distiller Properties dialog box. From its General tab, select
the Printing Preferences button. Youll be presented with the

Delphi

at

Work

Excel to PDF

Figure 4: Installation screen for Adobe Acrobat 5.0.

Figure 6: Demo application for TExcel2PDF in action.

have made your selection, hit the Print to PDF button, and
your PDF files will be generated. As evidence that the
process is working you can check your Windows toolbar
for the distinctive Adobe trademark.
Okay; time for a pop quiz: Where do your PDF files end
up on your hard drive? If you answered, They go to
the location that I defined when I set up my PDF printer
port, you are correct! In the example above, I chose to set
up a new PDF Port associated with C:\PDFPrinterOutput
(see Figure 7).
The demo application is straightforward, but a few of its
options merit additional explanation. These are Print To File,
Prompt For Filename, and Copies. Ill cover each of them in
turn below.

Figure 5: Acrobat Distiller Printing Preferences dialog box.

Acrobat Distiller Printing Preferences dialog box. Select the


Adobe PDF Settings tab (see Figure 5) and uncheck the
Do not send fonts to Distiller option. You will receive an error
if this checkbox is left in a checked state when you start
working with the demo application.
Running the Demo
Wow! I dont know about you, but I thought that was a
lot of work. Fortunately, running the demo application
(shown in Figure 6) that accompanies this article is dead
simple. The demo is self explanatory and will help you
explore and experiment with TExcel2PDF.
Each of the options under Test Out Options map to a specific
property of the TExcel2PDF component. The Edit control
labeled PDF Printer Name must be filled in with the name of
the PDF printer that we discussed earlier.
To print several Excel documents to PDF, simply navigate
to a directory that contains Excel files, and select one or
more files that you would like to print to PDF. After you
4

DELPHI INFORMANT MAGAZINE | July 2004

Print To File. To exercise this option, simply navigate


to the location of the Excel files that you wish to print.
Select one or more files, then check the box for Print To File,
and hit the Print to PDF button. In the example shown in
Figure 8, I used the Print To File option to create file1.ps
and file2.ps.
Okay; right about now you are probably telling yourself:
Hey! This isnt very helpful. I want PDF output not
PostScript. The fact is that any functionality that
requires the TExcel2PDF.PrintToFile property to be True
will generate a PostScript file. The PrintToFile property
will be True when printing to a filename, prompting for
a filename, or printing to a file. Dont worry; in a minute
Ill show you how to get around this little glitch and
automatically have your PostScript files generated as
PDF documents.
Prompt For Filename. This option works almost the same
as Print To File, except that the Automation API will prompt
you with a small dialog box similar to that generated by
the Delphi InputBox function. There is one caveat here.
As discussed above, its implied that the format of the
file being generated is PostScript. When specifying your
filename in the Print To File dialog box, you should add the
PostScript file extension of .ps.

Delphi

at

Work

Excel to PDF
watched folder and change them to
PDF documents.

Figure 7: Windows directory associated with PDF port containing generated PDF files.

I need to issue another caveat here.


When you define a watched folder,
Acrobat will create two new directories
under the directory that you specified.
The two new directories will be named
\in and \out. In our example, I defined
C:\PDFPrinterOutput to be a watched
folder, because earlier I defined a new
PDF port to be associated with
C:\PDFPrinterOutput.
After I set up the watched folder,
Acrobat Distiller created
C:\PDFPrinterOutput\in and
C:\PDFPrinterOutput\out. The
implication here is that if you
wish your PostScript output to
be processed automatically in the
watched folder, youll need to
change your PDF port to point to
C:\PDFPrinterOutput\in.
Conclusion
The demo application uses the
Plasmatech Shell Control Pack VCL for
Delphi 7. I have included an evaluation
copy of the Plasmatech Delphi 7
components with the demo application.
To run the demo you will need to
install the Plasmatech components first.
The installation is fast and painless. For
more information on these components
see the references section of this article.

Figure 8: Files generated with the Print To File option.

Figure 9: Files generated with the Copies option showing numeric identifier appended to each filename.

Copies. I must admit this part of the Automation API


isnt very intuitive. When the Copies attribute is True, the
Automation API attempts to create multiple copies of
the same file all with the same name! This effectively
overwrites each previous copy. I decided to work around
this by appending a numeric identifier to each generated
file. The results are shown in Figure 9.
In through the Out Directory
Earlier I mentioned that any functionality that requires
the TExcel2PDF.PrintToFile property to be True will
generate a PostScript file. Although this isnt ideal
behavior in all cases, we can automatically have each
PostScript file generated as a PDF document. You do
this by telling Acrobat Distiller to watch a folder for
incoming PostScript documents. This is done by selecting
Settings | Watched Folders in the Acrobat Distiller. It will
then take any PostScript documents that appear in the
5

DELPHI INFORMANT MAGAZINE | July 2004

So there you have it a fully


functional and easy-to-use conversion
utility. The complete source for the
TExcel2PDF class and demonstration
application are available for
download. Enjoy!

References
Debra Pates Web site: www.djpate.freeserve.co.uk/
Automation.htm
Adobes Web site: www.adobe.com/products/acrobat/
main.html
Plasmatech Web site: www.plasmatech.com/shellctl.htm
The source and demo app referenced in this article are available
for download on the Delphi Informant Magazine Complete
Works CD located in INFORM\2004\JUL\DI200407MM.

Mark Meyer is a software engineer with TravelClick Inc.


(www.TravelClick.net). Mark works in Delphi and Java, but still finds time
to evangelize RUP, UML, and patterns to anyone who will listen. When
Mark isnt coding he mourns after the series finale of The X-Files. He is
often seen on his roof, pointing at the sky, with his two teen-age daughters
Paige and Kirsten (the truth is out there). Mark can be reached at home at
Geeky2@gte.net.

C O L U M N S
ADO.NET

&

R O W S

DELPHI 8 FOR THE MICROSOFT .NET FRAMEWORK

By Bill Todd

ADO.NET Data Access


Components

Part VI: DataSets, DataTables, and DataRows Continued

his article continues the exploration of


DataSets, DataTables, and DataRows that
we started last month. Topics include

making changes a permanent part of the DataSet,


DataTable, or DataRow by calling AcceptChanges;
merging the data in a DataTable into a DataSet;
computing summary information for a subset
of rows in a DataTable; and several ways to add
rows to a DataTable.

Well also look at using a DataView object to filter and sort


the data in a DataTable, search the data, and provide multiple views of the same data. Youll also see how binding a
DataGrid to a DataTable and a DataView are different.
Accepting Changes
In Part V, I looked at saving the data contained in a DataSet
to a local file in XML format. This is handy for writing
briefcase model applications where a user may download
some data from a database, save it locally, and disconnect
from the network. While disconnected, the user can load
the data, modify it, and save it again many times. Ultimately
the user will reconnect to the network and apply the
accumulated changes to the database. This works because
the DataSet tracks all the inserts, updates, and deletes made
to all the rows in all the tables that are part of the DataSet.
Each DataRow has a property called RowState that is set
to one of the members of the DataRowState enumeration. The DataRowState enumeration members are Added,
Deleted, Detached, Modified, and Unchanged. The only
member whose purpose is not obvious is Detached. A
DataRow is in the Detached state after the DataRow object
has been created, but before it has been added to the
DataRows collection of a DataTable.
6

DELPHI INFORMANT MAGAZINE | July 2004

If you need a single-user relational database that can


hold modest amounts of data, you can use the DataSet
component as your database. Simply save the data to an
XML file before closing your application, and reload the
data when the application starts. In this case, there is no
reason to preserve the change information. You can merge
the change information and set the RowState property of
every row to Unchanged by calling the AcceptChanges
method of the DataRow, DataTable, or DataSet. When
you call the DataSet.AcceptChanges method, it calls the
AcceptChanges method of each DataTable in the DataSet. The
DataTable.AcceptChanges method calls AcceptChanges for
each DataRow in the DataTable. If you dont need to preserve
the change information, call DataSet.AcceptChanges before
you call DataSet.WriteXML, which saves the data to disk.
If you dont want to keep the changes that have been
made to a DataSet, DataTable, or DataRow object, call its
RejectChanges method. RejectChanges returns the object to
its original state, and sets the RowState property of all affected rows to Unchanged.
Working with DataRow Versions
When you work with a DataTable you can access individual
rows through the DataTable.Rows collection. Each item in
the Rows collection is a DataRow object. Four different versions of a DataRow can exist: Current, Default, Original, and
Proposed. These versions are defined by the members of the
DataRowVersion enumeration. You can access any version of
any column using the following syntax:
Dr := EmpDataSet.Tables['Country'].Rows[2];
S := Dr['Country', DataRowVersion.Original];

This example assumes that Dr is a variable of type DataRow,


and S is a variable of type string. The code assigns the original version of the value of the Country column to the variable S. Use the DataRow.HasVersion method to determine if
a row has a particular version.

Columns

&

Rows

ADO.NET Data Access Components

procedure TCountryForm.OpenCountry;
var
CountryPrimaryKey: array of DataColumn;
begin
// Set name of Table that Fill will create to Country.
CountryAdapter.TableMappings.Add('Table1', 'Country');
CountryAdapter.Fill(EmpDataSet, 'Country');
// Create dynamic array of DataColumn objects, add
// Country column to the array, and set the PrimaryKey
// property of the DataTable.
SetLength(CountryPrimaryKey, 1);
CountryPrimaryKey[0] :=
EmpDataSet.Tables['Country'].Columns['Country'];
EmpDataSet.Tables['Country'].PrimaryKey :=
CountryPrimaryKey;
// Bind the DataGrid to the DataTable.
CountryGrid.DataSource :=
EmpDataSet.Tables['Country'].DefaultView;
end;

Figure 2: The OpenCountry method.

Figure 1: The sample applications main form.

Merging Data into a DataSet


The DataSet.Merge method provides a powerful way to
merge the data in one DataTable into another DataTable.
Figure 1 shows the main form of the sample application
for this article displaying data from the Country table
in the InterBase Employee database. Start the sample
application and choose File | Open Country to fill the
DataTable and show the data in the DataGrid. The menu
items Click event handler calls the OpenCountry method,
as shown in Figure 2.

procedure TCountryForm.MergeItem_Click(
Sender: System.Object; e: System.EventArgs);
var
TempTable: DataTable;
R: DataRow;
begin
// If the Country table does not exist in EmpDataSet,
// create and fill it.
if (EmpDataSet.Tables['Country'] = nil) then
OpenCountry;
// Create a new table by cloning the Country table.
TempTable := EmpDataSet.Tables['Country'].Clone;
// Add a new column.
TempTable.Columns.Add('Continent', TypeOf(string));
// Add two rows.
R := TempTable.NewRow;
R['Country'] := 'China';
R['Currency'] := 'Yuan';
R['Continent'] := 'Asia';
TempTable.Rows.Add(R);
R := TempTable.NewRow;
R['Country'] := 'USA';
R['Currency'] := 'Dollar';
R['Continent'] := 'North America';
TempTable.Rows.Add(R);
// Merge TempTable into the Country table in EmpDataSet.
EmpDataSet.Merge(TempTable, False,
MissingSchemaAction.Add);
end;

In Part V, I populated the DataSets Tables collection at design


time. I also assigned the properties of the DataTables and
DataColumns at design time. The code in Figure 2 takes a
different approach. This code lets the Fill method create the
DataTable dynamically, and assigns the property values at
run time. The EmpDataSet is empty when the application
starts. The call to CountryAdapter.TableMappings.Add adds
a TableMapping to the DataAdapter that tells it to create a
DataTable named Country instead of the default value of
Table1. The call to the Fill method executes the DataAdapters
SelectCommand, creates a DataTable named Country in the
DataSet, and fills it with the rows returned by the SELECT.

Figure 3: The Merge menu items Click event handler.

The next step is to set the PrimaryKey property of the Country


DataTable. The PrimaryKey property is a dynamic array of
DataColumn objects. The next three lines of code set the size
of the dynamic array to one, assigns the Country DataColumn
to the first array element, then assigns the array to the
PrimaryKey property. The last line of code binds the DataGrid
to the Country table in EmpDataSet. The DefaultView property
is described in detail later in this article.

the Clone method. The call to TempTable.Columns.Add


adds a new column to the Columns collection whose
name is Continent and whose type is string. The call to
TempTable.NewRow creates a new DataRow object and
the following lines assign values to the columns in the
new row. The call to TempTable.Rows.Add adds the new
DataRow to TempTables DataRows collection. The next five
lines add another row to TempTable.

Figure 3 shows the code from the Click event handler of the
Demo | Merge menu choice. This method starts by checking
to see if the Country table exists in the DataSet named
EmpDataSet. If the DataTable doesnt exist, OpenCountry
is called to fill the DataSet and bind the DataGrid to the
table. Next, a new DataTable, named TempTable, with the
same structure as the Country table, is created by calling

The last line in Figure 3 calls the EmpDataSet.Merge method


to merge the rows in TempTable into the Country table in
EmpDataSet. Notice that the target table isnt specified as
a parameter in the call to Merge, so how does Merge know
which table in EmpDataSet to merge the rows into? Remember
that the DataTable referenced by the TempTable variable was
created by cloning the Country table in EmpDataSet. That

DELPHI INFORMANT MAGAZINE | July 2004

Columns

&

Rows

ADO.NET Data Access Components

procedure TCountryForm.CountDollarItem_Click(
Sender: System.Object; e: System.EventArgs);
var
DollarCount: Integer;
CountryTbl: DataTable;
begin
// If the Country table does not exist in EmpDataSet,
// create and fill it.
if (EmpDataSet.Tables['Country'] = nil) then
OpenCountry;
CountryTbl := EmpDataSet.Tables['Country'];
DollarCount := Integer(CountryTbl.Compute(
'Count(Country)', 'Currency LIKE ''%Dollar'''));
MessageBox.Show('There are ' + DollarCount.ToString +
' countries using dollars.');
end;

Figure 4: The Click event handler for the Count Countries Using Dollars
menu choice.

means that the Name property of the DataTable that TempTable


points to is also Country. Merge merges records into the table
with the same name.
The first parameter in the call to Merge is the source DataTable.
If the second parameter is False, any existing changes to
the source DataRow wont be preserved. Effectively, if the
second parameter is False, AcceptChanges is called for each
source DataRow before its merged into the target table. The
third parameter is a member of the MissingSchemaAction
enumeration. This parameter determines what happens
when the schema of the source table is different from
the schema of the target table. The code in Figure 3 uses
MissingSchemaAction.Add, which will add any columns in
the source table that arent in the target table. In this case, the
source table contains one column, Continent, that isnt in the
target table.
When the Merge method executes it finds the primary key
field(s) of the target table. If a record being merged has the
same primary key value(s) as an existing record, the incoming record updates the existing record. If the incoming record
doesnt match an existing record, the incoming record is added
to the table. Therefore, the code in Figure 3 makes the following three changes to the Country table in EmpDataSet:
The Continent column is added to the table.
The record for USA is updated so the Continent column
shows North America.
The record for China is added to the table.
Computing Values from a DataTable
The DataTable.Compute method lets you compute the value
of an expression over all the rows in a DataTable that pass the
filter criteria you specify. The sample application has a choice
on the Demo menu that counts the number of countries whose
currency is named Dollar (the code is shown in Figure 4).
This method begins by calling the OpenTable method to create the Country DataTable, if it doesnt exist. Next, the code
saves a reference to the Country table in EmpDataSet in the
variable CountryTbl. I did this to simplify the next statement
and make it easier to read. The next statement calls the
Compute method of CountryTbl and passes two parameters.
The first parameter is the expression you want to evaluate,
in this case Count(Country). The expression must consist of
8

DELPHI INFORMANT MAGAZINE | July 2004

procedure TCountryForm.LoadDataRowItem_Click(
Sender: System.Object; e: System.EventArgs);
var
TempTable: DataTable;
R: DataRow;
begin
// If the Country table does not exist in EmpDataSet,
// create and fill it.
if (EmpDataSet.Tables['Country'] = nil) then
OpenCountry;
// Create a new table by cloning the Country table.
TempTable := EmpDataSet.Tables['Country'].Clone;
// Add Two rows.
TempTable.LoadDataRow(['China', 'Yuan'], False);
TempTable.LoadDataRow(['Russia', 'Ruble'], False);
// Merge TempTable into the Country table in EmpDataSet.
EmpDataSet.Merge(TempTable, False,
MissingSchemaAction.Add);
end;

Figure 5: The LoadDataRow menu items Click event handler.

a single aggregate function that operates on one column of


the DataTable.
The second parameter contains the filter thats applied to the
DataTable to determine which rows are included in the computation. To include all rows, pass a null string for the filter
parameter. In this example the filter is Currency LIKE '%Dollar'
to include all rows whose Currency column value ends with
the word Dollar. Because the string literal '%Dollar' must be
enclosed in single quotes, the single quotes must be doubled
inside the parameter, which is itself a string literal. This causes
the single quotes around '%Dollar' to be treated as literal
characters within the parameter string. See Delphis online
help for detailed information on filter expressions. Finally, the
MessageBox.Show call displays the number of countries whose
currency is named Dollar.
Adding Rows to a DataTable
the Easy Way
Figure 4 shows how to create a new DataRow with
the structure of an existing DataTable by calling the
DataTable.NewRow method. Next, values are assigned to
each field in the new DataRow, then the DataRow is added
to the DataTables Rows collection.
All this can be done in a single statement using the
LoadDataRow method, as shown in Figure 5. This code
is similar to that shown in Figure 4, except LoadDataRow
is used to add a new row to the table, and assign values
to its fields, in a single step. LoadDataRow determines if
the DataTable has a primary key. If it does, LoadDataRow
checks to see if the primary key of the new record matches
the primary key of an existing record. If they match, the
new record is used to update the existing record. If there is
no match, the new record is added to the table.
LoadDataRow takes two parameters. The first is an array of
values. This array provides the values that will be assigned
to the new rows fields. Pass nil as the value if one of the
columns in the DataTable has a default value defined, and
if you want the new row to have the default value in that
column. The second parameter determines if the DataTables
AcceptChanges method will be called after the row is added. If

Columns

&

Rows

ADO.NET Data Access Components

this parameter is False, the RowState of inserted rows is Added,


and the RowState of updated rows is Modified. If this parameter
is True, the RowState of all rows will be Unchanged.
Using ImportRow to Preserve
RowState
As shown in Figure 6, the ImportRow method is what you
want if you have a row in one DataTable, and you need to
add that row to another DataTable, and you want to preserve
the RowState of the row. Again, this code is almost identical
to that shown in Figure 4. It creates a clone of the Country
table and adds two new rows. The new part starts with the call
to TempTable.AcceptChanges, which changes from Added to
Unchanged the RowState of both new rows. The for loop adds
the new rows to the Country table by calling ImportRow.
Next, the call to the Find method of the Country tables
Rows collection gets a reference to the new row for China.
The following if statement displays a message if RowState
is Unchanged. The message is displayed even though this
is a newly added row because ImportRow preserves the
DataRows existing RowState. Because ImportRow takes a
DataRow as its parameter, you should be able to shorten
the code in Figure 5 by eliminating the DataTable. Simply
create the DataRow objects and import them as shown in
Figure 7. However, this code doesnt work. When you create
a DataRow, its RowState is Detached until its added to a
DataTable. Because ImportRow preserves the DataRows
existing RowState, the RowState is still Detached after the
record has been imported into the Country table. I have no
procedure TCountryForm.ImportRowItem_Click(
Sender: System.Object; e: System.EventArgs);
var
TempTable: DataTable;
R: DataRow;
I: Integer;
begin
// If the Country table does not exist in EmpDataSet,
// create and fill it.
if (EmpDataSet.Tables['Country'] = nil) then
OpenCountry;
// Create a new table by cloning the Country table.
TempTable := EmpDataSet.Tables['Country'].Clone;
// Add two rows
R := TempTable.NewRow;
R['Country] := 'China';
R['Currency] := 'Yuan';
TempTable.Rows.Add(R);
R := TempTable.NewRow;
R['Country'] := 'Russia';
R['Currency'] := 'Ruble';
TempTable.Rows.Add(R);
// Change the RowState to Unchanged.
TempTable.AcceptChanges;
// Import the rows into the Country table.
for I := 0 to TempTable.Rows.Count-1 do
EmpDataSet.Tables['Country'].ImportRow(
TempTable.Rows[I]);
// Find the row for China. If its RowState is Unchanged,
// show a message.
R := EmpDataSet.Tables['Country'].Rows.Find('China');
if (R <> nil) and
(R.RowState = DataRowState.Unchanged) then
MessageBox.Show('RowState is Unchanged');
end;

Figure 6: The Click event handler for the ImportRow menu item.
9

DELPHI INFORMANT MAGAZINE | July 2004

idea what happens to the imported DataRow, but you cant


find it by its primary key value, and it doesnt appear in the
DataGrid. So it appears that it isnt added to the DataTable
even though no exception is raised.
The solution is obvious: Change the new rows RowState
to something other than Detached. However, this doesnt
work because RowState is a read-only property, and
theres no method you can call to change the RowState
of a Detached DataRow. ImportRow is only useful for
copying a DataRow from one DataTable to another while
preserving its RowState.
Using the DataView Object
Each DataTable instance includes a default DataView object
that you can access using the DataTable.DefaultView property.
The DataView doesnt store any data, but it does let you
control how the data is displayed:
You can sort by any combination of columns, in any
combination of ascending and descending order.
You can filter using any combination of column values.
You can filter by RowState.
Not only can you sort and filter the data, you can also add
DataView objects to your application at design time, which lets
you show multiple views of the same data.
Figure 8 illustrates sorting the Country table data, descending
by Currency, and ascending by Country. This code is from the
Click event handler of the DataView | Sort | By Currency Descending
menu item. This method assigns a comma-separated list of
column names to the DataView.Sort property (include DESC
after the column name to sort a column in descending order).
procedure TCountryForm.ImportRowFailsItem_Click(
Sender: System.Object; e: System.EventArgs);
var
TempTable: DataTable;
R: DataRow;
CountryTbl: DataTable;
begin
// If the Country table does not exist in EmpDataSet,
// create and fill it.
if (EmpDataSet.Tables['Country'] = nil) then
OpenCountry;
CountryTbl := EmpDataSet.Tables['Country'];
// Add two rows
R := CountryTbl.NewRow;
R['Country'] := 'China';
R['Currency'] := 'Yuan';
EmpDataSet.Tables['Country'].ImportRow(R);
R := EmpDataSet.Tables['Country'].Rows.Find('China');
if (R <> nil) then
MessageBox.Show('Row found');
end;

Figure 7: The Click event handler of the ImportRow Fails menu item.

procedure TCountryForm.ByCurrencyItem_Click(
Sender: System.Object; e: System.EventArgs);
begin
EmpDataSet.Tables['Country'].DefaultView.Sort :=
'Currency DESC, Country';
end;

Figure 8: The By Currency Descending menu items Click event handler.

Columns

&

Rows

ADO.NET Data Access Components

DataViewRowState Member

Includes

Added

Newly inserted rows.

CurrentRows

This is the default. All rows


that havent been deleted are
included. The current values
are shown for modified rows.

Deleted

Deleted rows.

ModifiedCurrent

Modified rows are shown with


their current values.

ModifiedOriginal

Modified rows are included


with their original values.

None

No rows are included

OriginalRows

Deleted, modified, and unmodified rows are included. The


original values are shown for
modified rows.

Unchanged

Unmodified rows.

Figure 9: Members of the DataViewRowState enumeration.

procedure TCountryForm.ChangeRowsItem_Click(
Sender: System.Object; e: System.EventArgs);
begin
EmpDataSet.Tables['Country'].DefaultView.RowStateFilter:=
DataViewRowState.Added or
DataViewRowState.ModifiedCurrent;
end;

Figure 10: The Changed Rows menu items Click event handler.

Ive read at least one article claiming that binding a


DataGrid to a DataTable actually binds the DataGrid to
the default DataView. If that were true, the following two
statements would produce the same result:
CountryGrid.DataSource := EmpDataSet.Tables['Country'];
CountryGrid.DataSource :=
EmpDataSet.Tables['Country'].DefaultDataView;

You can see that these statements are not the same, by using
the first statement to bind a DataGrid directly to a DataTable,
then setting the default DataViews Sort property. The order
of the rows in the DataGrid will not change unless you rebind
the DataGrid to the DataTable by reassigning the DataTable
to the DataGrid.DataSource property. However, as the sample
application demonstrates, if you bind the DataGrid to the
default DataView and set the DefaultView.Sort property, the
order of the data in the DataGrid changes.
You can filter the DataView using any combination of the
members of the DataViewRowState enumeration by setting
the DataView.RowStateFilter property. Figure 9 shows
the members of the DataViewRowState enumeration, and
explains which rows will be shown if that member is
included in the filter.
You can or members of the enumeration together to display any combination of rows. Figure 10 shows the code
from the DataView | Row State Filter | Changed Rows menu item.
This code displays only the new or modified rows, and the
modified rows are shown with their current values.
10

DELPHI INFORMANT MAGAZINE | July 2004

procedure TCountryForm.FilterItem_Click(
Sender: System.Object; e: System.EventArgs);
begin
EmpDataSet.Tables['Country'].DefaultView.RowFilter :=
'Currency LIKE ''%Dollar''';
end;

Figure 11: Filtering a DataView using data values.

procedure TCountryForm.EditRowItem_Click(
Sender: System.Object; e: System.EventArgs);
var
Drv: DataRowView;
begin
Drv := EmpDataSet.Tables['Country'].DefaultView[2];
Drv.BeginEdit;
Drv['Currency'] := 'Unknown;
Drv.EndEdit;
end;
procedure TCountryForm.DeleteRowItem_Click(
Sender: System.Object; e: System.EventArgs);
var
Drv: DataRowView;
begin
Drv := EmpDataSet.Tables['Country'].DefaultView[2];
Drv.Delete;
end;
procedure TCountryForm.AddRowItem_Click(
Sender: System.Object; e: System.EventArgs);
var
Drv: DataRowView;
begin
Drv := EmpDataSet.Tables['Country'].DefaultView.AddNew;
Drv['Country'] := 'China';
Drv['Currency'] := 'Yuan';
Drv.EndEdit;
end;

Figure 12: Editing data with a DataRowView.

Filtering a DataView by data values is equally easy, as


shown in the code from the DataView | Filter | Countries That
Use Dollars menu items Click event handler (see Figure 11).
This example filters the Country data to show only those
countries that use a currency with Dollar in its name.
See the DataColumn.Expression property in the online
help for a detailed description of the filter expression
syntax. Assign a null string to the RowFilter property to
remove the filter.
Using a DataRowView
The Item property of a DataView is a collection of
DataRowView objects (use the DataRowView to access
a single row of data). The Item property is the indexer
for the DataView object, so you dont need to include its
name when you use it. Assuming Drv is a variable of type
DataRowView, the following statements are equivalent:
Drv := MyDataView.Item[3];
Drv := MyDataView[3];

Both statements return a reference to the fourth


DataRowView in the DataView. The index is zero-based, so
Item[3] is the fourth row. The DataView.Count property tells
you how many rows the DataView contains. You can access

Columns

&

Rows

ADO.NET Data Access Components

procedure TCountryForm.FindItem_Click(
Sender: System.Object; e: System.EventArgs);
var
RowIndex: Integer;
Dv: DataView;
OldSort: string;
begin
// Get a reference to default DataView to save typing.
Dv := EmpDataSet.Tables['Country'].DefaultView;
// Save the current sort order.
OldSort := Dv.Sort;
// Sort by Currency and find row with Currency = FFranc.
Dv.Sort := 'Currency';
RowIndex := Dv.Find('FFranc');
// If a matching row was found...
if (RowIndex <> -1) then // Display name of country.
MessageBox.Show(Dv[RowIndex][Country'].ToString +
' uses the FFranc.');
// Return to the original sort order.
Dv.Sort := OldSort;
end;

Figure 13: The Find menu items Click event handler.


procedure TCountryForm.FindRowsItem_Click(
Sender: System.Object; e: System.EventArgs);
var
Dv: DataView;
FoundRows: array of DataRowView;
CountryNames: string;
OldSort: string;
I: Integer;
begin
// Get a reference to default DataView to save typing.
Dv := EmpDataSet.Tables['Country'].DefaultView;
// Save the current sort order.
OldSort := Dv.Sort;
// Sort by Currency and find row with Currency = FFranc.
Dv.Sort := 'Currency';
FoundRows := Dv.FindRows('FFranc');
// If a matching row was found...
if (Length(FoundRows) > 0) then begin
for I := 0 to High(FoundRows) do
CountryNames := CountryNames +
FoundRows[I]['Country'].ToString + ', ';
MessageBox.Show(CountryNames); // Display country name.
end;
// Return to original sort order.
Dv.Sort := OldSort;
end;

Figure 14: The FindRows menu items Click event handler.

the DataRow by using the DataRowView.Row property if you


need to use the properties or methods of the DataRow to
which the DataRowView refers.
You can edit data using a DataRowView, as shown in
Figure 12. Figure 12 shows the three Click event handlers
for the Edit Row, Delete Row, and Add Row menu choices
under DataView | Edit in the sample application. Note that when
you edit a row you must call BeginEdit before you change the
values and call EndEdit after the row has been changed. When
you change a value after calling BeginEdit, the new value
becomes the Proposed version. After you call EndEdit, the new
value becomes the Current version and there is no Proposed
version. Deleting a row is just a matter of finding the row you
want to delete, then calling the Delete method. To insert a new
row, you call the DataView.AddNew method, which adds a new
11

DELPHI INFORMANT MAGAZINE | July 2004

row and returns a reference to its DataRowView. Next, assign


the values to the columns, then call EndEdit.
Searching Using a DataView
In addition to filtering, the DataView offers the Find and
FindRows methods. Find searches the columns assigned to
the DataView.Sort property and returns the index number
of the first matching row. Find returns -1 if it cannot find
a matching row. The number of values passed to Find
must match the number of columns in the Sort property.
Figure 13 shows the Click event handler for the DataView |
Search | Find menu item. This method starts by assigning a
reference to the default DataView to the variable Dv. This
saves having to type EmpDataSet.Tables['Country'].DefaultV
iew over and over. The method saves the current value of
the Sort property, then sets Sort to the Currency column,
calls Find, and saves the returned value in the RowIndex
variable. Pass a dynamic array of Object as the parameter
to Find, if youre searching on more than one column. If
RowIndex is not -1, the method displays the country name
and returns the Sort property to its original value.
The FindRows method is almost identical to the Find
method except that, instead of returning the index of a
single row, it returns a dynamic array of DataRowView
objects (as shown in Figure 14). This code is identical
to the code shown in Figure 13, right down to the call
to FindRows. This method determines if any rows were
found by checking the length of the FoundRows dynamic
array. If rows are found, the for loop iterates through
the array of DataRowView objects, and concatenates the
country names. Finally, the method displays the names in
a dialog box.
Conclusion
By now I hope you have a good understanding of how to
get data from your database into the ADO.NET connected
and disconnected data access objects. The DataSet,
DataTable, and DataRow objects let you maintain a
complete relational database in memory, and provide
methods to manipulate the data in any way you might
need. The DataView and DataRowView let you control the
users view of the data in a DataTable. You can filter by
RowState or column values to limit the rows to which the
user or your code have access, and you can sort the data
to present it in any order. The DataView and DataRowView
also give you the means to search for rows and edit the
data by inserting, deleting, and changing rows.
The example project referenced in this article is available
for download on the Delphi Informant Magazine Complete
Works CD located in INFORM\2004\JUL\DI200407BT.

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 btarticle@dbginc.com.

E V E R Y D A Y
BRIEFCASE APPS

SQL SERVER

D E L P H I

DELPHI 5-8

By Rick Spence

Briefcase Apps Made Easy


Tips and Techniques for Using ClientDataSet

ts easy to use ClientDataSet components to


implement single-user, single-file database
applications. If youre unfamiliar with

using ClientDataSets in this manner, check


out the online tips I write each month for
the DelphiZineNOW e-newsletter (articles
that appear in the newsletter are available at
www.DelphiZine.com/features).

Another common use of ClientDataSet is to implement


briefcase applications. A briefcase application is one in
which users connect to a server database to retrieve data,
then save it and work on it locally. This allows them to
disconnect from the server and take the data on the road,
home, or wherever. Eventually they will reconnect to the
server and apply the changes they made while offline.
In this article Ill show you how to implement a briefcase
application using Microsoft SQL Server as the database
server. Well store the data locally in XML format, then
apply the changes to the server. The only awkward part
about implementing briefcase applications is determining whether other users changed the same data while the
one user was working with it offline. Ill demonstrate how
to detect these conflicts, as well as how to present these
conflicts to users so they can decide how to proceed.

Figure 1: Component usage for ClientDataSet with a provider.

12

DELPHI INFORMANT MAGAZINE | July 2004

Populating the ClientDataSet


from the Server
The first thing we need to do is load the ClientDataSet with
data from an existing database. To do this you must use a
DataSetProvider component (youll find it right next to the
ClientDataSet on the data access tab). The DataSetProvider
acts as a conduit between the ClientDataSet and any TDataSet
descendant. TDataSet is the class upon which all datasets
are based; components youve probably already used, such
as Table, Query, ADOCommand, etc. are all examples of
TDataSets. The DataProvider, then, retrieves data from some
type of TDataSet and provides it to the ClientDataSet.
You use the ClientDataSets Provider property to link a provider
to the ClientDataSet. When you activate the ClientDataSet, it
requests data from the provider, which in turn requests data
from its dataset. You dont need to be concerned about activating the provider or the dataset. All you need to do is activate
the ClientDataSet itself. As youll see a little later, the provider
is also responsible for applying changes back to the actual
dataset. Figure 1 shows how these components are linked.
The most difficult part about implementing briefcase applications
is knowing which components to use and how to link them. As
youll see, you dont have to write very much code.
Its important to note that after you activate the ClientDataSet to
retrieve the data from the server, the connection is immediately
dropped; your application does not remain connected to the
remote database.

Everyday

Delphi

Briefcase Apps Made Easy

Working with Local Data


If you make changes to the data and do not explicitly
save those changes, they are lost. This is because
ClientDataSets are entirely memory-based. You can either
apply the changes back to the original database, or you
can save the entire ClientDataSet locally, including the
updates. This is the nature of a briefcase application. Of
course, youll eventually need to apply those changes
back to the server, and Ill get to that later in this article.
To save a ClientDataSet to a local file, simply use the
SaveToFile method. This method requires the file name
where you will save the data, and optionally permits a
second parameter, which allows you to save the data in
XML format. Youll find that the XML format requires
more space, but many consider it more convenient as you
can look at the data with any application that can read
text files. In addition, Internet Explorer understands
XML and will display it hierarchically.

apply these changes back to the real database, and this is


where things start to get a little tricky.
Actually applying the changes is easy. You call the
ClientDataSets ApplyUpdates method, which, as the
name implies, applies all the changes you made locally
back to the actual database. You must pass it a parameter
(MaxErrors) indicating the maximum number of errors
that the provider should allow before prematurely
stopping the update operation. You can set MaxErrors to
-1 to indicate there is no limit to the number of errors.
When you call the ClientDataSets ApplyUpdates method,
it uses the associated DataSetProvider to apply the actual
updates. The DataSetProviders UpdateMode property
determines what criteria it uses to locate the records
it needs to update and delete (it has no meaning for
inserts). You may have used the UpdateMode property of
the BDE Query component, where it performs a similar
purpose. To see why this is
required, consider the folapplication is one
lowing scenario.

Use the LoadFromFile methA briefcase


od to load the ClientDataSet
in
which
users connect to a server
with data you previously
saved locally, passing the
Say you have downloaded
database to retrieve data, then
file name as a parameter.
records from the real
save it and work on it locally.
Theres a minor complicadatabase into your
tion we need to address,
ClientDataSet. You have
however, with regard to local data when implementing
made local changes to those records, and now you
briefcase applications; youll want to ensure the data you
want to apply those changes back to the server. The
load from a local file is in the same format as the data on
DataSetProvider must be able to match the local records
the server. For example, it doesnt make sense to apply
with the records on the server, and the UpdateMode
updates from a locally loaded customer file to a remote
property tells it how to do this. As you will see, another
invoices table.
consequence of setting this property is that it determines
the circumstances in which youll detect update and
This isnt a problem if you hard code the local filename
delete errors (because of other users changing the same
youre using; this is usually the best solution, i.e. dont
data). You must set it to one of the following values:
give the user any choices. If you do allow the user to
upWhereAll, upWhereKeyOnly, or upWhereChanged.
choose the file to load (as my sample application does),
then you need to be more concerned. The best solution
upWhereAll
is to create persistent fields in the ClientDataSet, based
The provider uses all the fields in the dataset to locate
upon the fields used in the dataset to which the provider
the record on the server. Note that it uses the original valis connected. This way, if you open a local file whose
ues of the fields that were returned from the server. So, if
structure doesnt match the remote table, Delphi will raise any other user has changed any field on the servers, your
an exception, which you can trap.
update (or delete) will fail because it cannot find the record
on the server. A simple example will make this clear.
You create persistent fields for ClientDataSets the same
way you do for normal datasets. Note that unless you
Assume you retrieved records containing three fields from
have attached a provider to your ClientDataSet, which in
a server: CustomerID, CompanyName, and ContactName.
turn is attached to an actual dataset, you wont have any
You update one of the records, which had the following
persistent fields with which to work (also note that if you
values when you initially retrieved it:
arent using a provider component, you use the persistent
fields to define the structure of your table).
CustomerId:ALFKI
Saving Changes to the Server
As stated earlier, ClientDataSets work entirely in memory
which means that if you make changes and close the
application, the changes are lost. If you are implementing a briefcase application, youll save the dataset locally
of course, and if you made changes, those changes are
stored locally as well. The ClientDataSet maintains a
change log, recording all the edits, deletions, and insertions the user made. At some stage, you will need to
13

DELPHI INFORMANT MAGAZINE | July 2004

CompanyName:Alfreds Futterkiste
ContactName:Maria Anders

Because you set the providers UpdateMode property to


upWhereAll, to update this record the provider generates
a SQL statement with the following WHERE clause:
WHERE CustomerID = "ALFKI"
AND CompanyName = "Alfreds Futterkiste"
AND ContactName = "Maria Anders"

Everyday

Delphi

Briefcase Apps Made Easy


upWhereChanged
With this setting, the provider uses the fields you have
changed and the key fields (note that it does indeed
use the key fields; this is not entirely obvious from the
name). Using the same example, if you indicated that
CustomerID is the primary key and you updated the
CompanyName field, the provider would generate the
following WHERE clause:
WHERE CustomerID = "ALFKI"
AND CompanyName = "Alfreds Futterkiste"

Figure 2: This dialog box shows update conflicts.

This will report an error if another user changed the


CompanyName field of the same record (preventing you from
overwriting his/her changes), but not if they changed the
ContactName field of the same record. This may or may not
be a problem, depending on the specifics of your application.

These are the original values of the fields, which the


ClientDataSet saved, and its ensuring that these fields
on the server still have these values. Remember that this
Handling Update Conflicts
update could take place days later, and its possible that
Now that youve seen how to use the providers Updateother users have changed these fields. Using upWhereAll,
Mode property to detect when other users have changed
then, guarantees that your
the same data as your
updates (or deletes) will
user, we need to look at
At some stage, you will need to
fail if any other user has
apply these changes back to the real how to handle the errors.
changed any other field.
Note that this is the clasdatabase, and this is where things
sic multi-user optimistic
By default, then, using
locking scenario, but
start to get a little tricky.
upWhereAll uses all fields
because ClientDataSets
to locate the records, but you can override this on a
dont remain connected to the server, its a little more
field-by-field basis by setting some properties of the
complicated to handle:
persistent fields. Note that we are talking about the
1) The scope for conflict is much greater. Typical online
persistent fields in the dataset itself, not the persistent
transactions (i.e. when the user remains connected
fields of the ClientDataSet.
to the database) usually take place over a matter of
minutes at the most. These briefcase transactions can
Persistent fields have a property named ProviderFlags. This
be over a period of days, making it much more likely
allows you to control, on a field-by-field basis, whether this
that youll encounter a conflict.
field should form part of the WHERE clause used to locate
2) ClientDataSets batch their updates, so if you performed,
a record on the server. By default, each fields ProviderFlags
for example, three deletes, two edits, and four inserts,
property includes pfInWhere (meaning they do contribute
theyre all applied within the same transaction. You must
to the WHERE clause), but you can change this, thus
decide how to handle the case where one or more of the
removing that field from the WHERE clause.
updates failed, while others worked. Typically, you dont
have this issue with connected datasets, because you
upWhereKeyOnly
submit one update at a time.
The provider only uses the key field(s) of the record to
locate the record on the server. You must tell the provider
Delphi makes it relatively painless to handle the
which are the key fields, and you do this by setting the
conflicts, although theres one important point you must
ProviderFlags properties of the dataset. To indicate that
understand (it isnt documented very well, but Ill get to
a field is part of the primary key for a record, set its
that in a moment). Theres a built-in dialog box you can
ProviderFlags property to include pfInKey.
use to present to the user the new data, the changed data,
and the original data, and let them decide how to handle
Using the previous example, CustomerID would be
the problems. Figure 2 shows an example of this dialog
the primary key. So if we set that fields ProviderFlags
box in use; youll see how to call it in a moment.
to include pfInKey, and use upWhereKeyOnly as the
providers UpdateMode property, the provider would
You already know that when you call the ClientDataSets
generate the following WHERE clause:
ApplyUpdates method it uses the DataSetProvider
to apply the actual updates. You also know that the
WHERE CustomerID = "ALFKI"
DataSetProviders UpdateMode property determines which
criteria it uses to locate the records it needs to update,
Note that this WHERE clause will not report an error if
and the records it needs to delete.
some other user changed any field other than the primary
key field in the server. Using upWhereKeyOnly can result
If the update or delete fails, it reports this error to you
in one user overwriting another users changes.
by calling the ClientDataSets OnReconcileError event

14

DELPHI INFORMANT MAGAZINE | July 2004

Everyday

Delphi

Briefcase Apps Made Easy

on a record-by-record basis. If you make 10 updates


to the server, and three of them fail, it will call the
OnReconcileError event handler three times. The
OnReconcileError event handler expects four parameters:
DataSet: TCustomClientDataSet
E: EReconcileError
UpdateKind: TUpdateKind
var Action: TReconcileAction
The easiest way to write this event handler is to use
Delphis predefined Reconcile Error dialog box (again, see
Figure 2). Call it using:

Conclusion
In this article I demonstrated how to use ClientDataSets
to implement briefcase applications. You must use
the DataSetProvider property to initially load the
ClientDataSet from an actual database table, and to
apply the changes back to the server. Make sure you
set the providers UpdateMode property to tell it how
to locate records on the server (and therefore in what
circumstances to report errors), and dont forget to set
the ProviderFlags properties of the persistent fields in the
dataset to indicate which fields form the primary key.
Finally, youll find it easiest to use the Borland-supplied
Reconcile Error dialog box to report errors to the user.

Action := HandleReconcileError(DataSet, UpdateKind, e)

Youll need to add RecError to your uses clause (the unit


containing this function), and add the \Objrepos directory
to your projects search path (thats where the Pascal file
is located).
The most common problem with using this dialog box
(and this is the part that isnt documented very well) is
that sometimes the dialog box doesnt display the original
data to the user. This happens when Delphi cannot locate
the original record, and is caused by not including the
pfInKey value in the ProviderFlags property of the key
fields in the actual data set. (Again, note that you do not
set this value in the ClientDataSets persistent fields, but
in the persistent fields of the actual dataset.)

15

DELPHI INFORMANT MAGAZINE | July 2004

The examples referenced in this article are available for


download on the Delphi Informant Magazine Complete
Works CD located in INFORM\2004\JUL\DI200407RS.

Rick Spence (RSpence@WebTechCorp.com) runs WebTech Training and


Development (www.WebTechCorp.com). WebTech has offices in Florida and
in the UK, and specializes in Delphi and .NET training and development.

I N
ADO

D E V E L O P M E N T
DATABASE CONNECTIVITY

DELPHI 4-8

By Bob Fleischman

Smarter ADO Connections


Popis First Lesson: Learning to Connect

hat a waste of time, I shouted as

ADO doesnt work, I told him.

I blasted into the lab. Esther D.

And aside from the obvious, what exactly was your problem? Popi again inquired.

looked up from her online cross-

word puzzle and gave me that condescending glare


she gave all the grad students who disturbed the
serenity of the place. The secretary had been at Spinach Solutions, Inc. since the days of Z80 chips, and no
one was about to change her ideas on how a software
development lab should be run.
Undeterred, I continued my rant. I cant believe how much
time I spent getting that Time and Attendance app to run
at General Amalgamated, and all because of that *#&%ing
ADO. And with that, I threw my folder of notes across my
desk and into the pile of empty Styrofoam cups that were
stacked, not so neatly, in the corner right next to the pile
of Internet articles I had sworn Id read.
My little outburst brought unexpected company. And what,
exactly, is your problem today, Billy? came the authoritative
voice from behind.
My given name is Gates McWilliam, but most everyone calls
me Billy. Im a senior CompSci major at HyTech Tech and I
earn tuition money by working at Spinach Solutions for the
indefatigable Professor Otto Pistiger, but everyone just calls
him Popi. The school lets him carry on his little consulting
business on the side to further student education and as
payback for all the lucrative grants hes generated for the
university over the years.
16

DELPHI INFORMANT MAGAZINE | July 2004

I tested the app, just like you instructed, I said, a bit on


the defensive. It worked fine when I left here. When I got
to GA it wouldnt run. I compiled the app connected to our
development server, with the application user and password
hard coded into my data module. When I got there I found
out they use TCP/IP connectivity, not named pipes, and
trusted security, not application user names. I couldnt get
the whining tone out of my voice. It took me all afternoon
to get the connection string working.
Popi continued his interrogation. Did you try the ADO Connection Wizard?
Yeah, I answered with a bit of resentment. And it felt like
it was written by a co-op. It had a lot of options, but very
little telling you how to use any of them.
And so, Popi asked, his right index finger crooked up to
match his left eye brow, in the future, what would you do
differently? Ever the professor, he was always interested in
teaching us a better way.
Well, for one thing, I replied, Id communicate with the
client more often and find out their server name and how
they connect to their servers.
A good first approach, Popi said; always useful, but
not really the best approach overall. And even if they did
give you this information, you would still be writing your
program to work in a particular situation instead of being
adaptable to many situations.

In

Development

Smarter ADO Connections

He made a good point. So what would you do? I asked,


again trying to determine how my mentor would handle the
situation. Would you have the users type in the connection
string at run time? I suppose that would then allow the software to work under any possible scenario ...

toolbox. This tool will allow us to quickly switch between


development servers or authentication methodologies when
we go to a client location. If we design the rest of our program well, we can dance in and out of servers or databases
without recompiling a line of code.

Popi thought for a moment. Well, that is another approach,


I suppose. However, not a truly practical one. Remember one
of the basic tenets of object-oriented programming: Do not
depend on smart users when you can write smart software
smart objects make for smart programs.

Then he sauntered over to the whiteboard, the one next to


the coffee machine, took up a marker, and drew this:

Well, yeah, I laughed, but were talking about ADO here,


not objects and components.
Actually, he countered, were talking about all three.
ADO and the TADOConnection component of Delphi are
objects, just not very intelligent ones. We have here a
great example of a decorator pattern. We want to derive
classes from TADOConnection that can deal with TCP/IP
or named pipes, trusted logins, or user/passwords and
other such choices. Again, Popi challenged me: And
how can we accomplish this?
Confident, I answered, Create a variety of derived components; one for TCP/IP, another for named pipes, and then a
variant of each for trusted logins or with user/passwords settings. It seemed so obvious I was still not even sure why Popi
had asked the question.
No, Popi snapped. Sloppy.
Sloppy? I dont understand, I sputtered. I suppose we could
then create a factory, which would create the correct component as needed. Then we could expand the factory as things
change in the future.
Not a bad approach if we were writing Java, Popi said derisively. Java loves lots of little classes, each doing their own
thing. But youve got Delphi in front of you. The oracle, the
font of knowledge. Use the force, Luke. Popi loved tossing
out movie lines just when it seemed our discussions were getting most heated.
But before we can design a good solution, Popi advised, we
must state a good problem. Heres what he came up with.
ADO uses a connection string to decide where to obtain data,
how to connect to the data source, and how to authenticate to
the data source. Connection strings have a particular format
based on the nature of the authentication and communication.
Our problem is that we need a component that can create a
connection string that can self configure. Well limit our problem by assuming TCP/IP connectivity (by default ADO wants to
use named pipes) and limit our choices:
1) to what are we connecting (server and database); and
2) how are we authenticating to the Server (user/password or
trusted connection)
By stating the problem thus, Popi counseled, we can
see our way clear to designing another powerful tool in our
17

DELPHI INFORMANT MAGAZINE | July 2004

OK, look at this, he said with that tone all teachers


use when they know something you dont. You see,
we simply subclass TADOComponent, well call it
TPOPIADOComponent. Never one to suffer false modesty,
Popi always prefixed our objects and components with
POPI so theyd be recognizable. Then well create some
new properties that well need. Lets start with the basics.
He opened Delphi, closed everything, and selected
Component | New Component from the top menu. For an
ancestor class he selected TADOConnection, and he
named the class TPOPIADOConnection and put it on the
POPI palette. After the basic code was created, he pressed
CJ and called up the code wizard, selecting the class
with create/destroy. Then he moved to the published
section of the declaration and typed:
property Server: string;
property Database: string;

We also needed to decide how to authenticate to the server


and database, so he typed:
property User: string;
property Password: string;

Popi typed in this property, in case we were going to be


using trusted connections:
property UseTrusted: Boolean;

But Popi, I asked like an eager schoolboy, when or where


do we create the actual connection string? I can see all the
parts, but not the whole.

In

Development

Smarter ADO Connections

You tell me, Popi challenged. When do you need the connection string?
Well, I suppose when I want to connect, I told him matter-of-factly. But ADO has a Connected property a Boolean, I believe. So we can just hook into the BeforeConnect
event and build our string then.
That we could, Popi responded with a wry smile. But
why not go closer to the source. Lets just do this:
property Connected: Boolean
read GetConnected write SetConnected;

We simply conceal the original Connected property and


expose our own, he explained. Then, when we set it in
the SetConnected procedure, we can build our string. And, of
course, we need to overwrite the constructor, so we can set
some defaults.
constructor Create(AOwner: TComponent); override;

With all the declarations complete, and with his trademark


flourish, Popi raised his two index fingers in an exaggerated high arc imitating a dive-bomber and simultaneously
brought the two fingers down on the keyboard, one on the
left C key and one on the C. Delphi did its magic and
created some 70-odd lines of code.
Popi then scrolled the cursor down to the SetConnected procedure and wrote the following code:
FConnected := Value;
// Build the connection string and then connect.
inherited Connected := False;
if not UseTrusted then
ConnectionString :=
Format(CONNSTR, [Password, User, Database, Server]);
else
ConnectionString :=
Format(TRUSTEDCONNSTR, [Database, Server]);
inherited Connected := Value;

The inherited directive allows us to set the property in the


ancestor class, which actually begins the process of connecting to the server. Quickly, Popi jumped his cursor to the top
of the unit, and, right after the uses clause, added:
const
CONNSTR = 'Provider=SQLOLEDB.1;Password=%s;' +
'Persist Security Info=True;User ID=%s; ' +
'Initial Catalog=%s;Data Source=%s; ' +
'Network Library=DBMSSOCN;
TRUSTEDCONNSTR = 'Provider=SQLOLEDB.1; ' +
'Integrated Security=SSPI; ' +
'Persist Security Info=FALSE;Initial Catalog=%s; ' +
'Data Source=%s;Network Library=DBMSSOCN';

And just how did you know the connection string variants
to write out? I asked, impressed as always by Popis vast
store of knowledge.
Well, actually, it was quite easy, he said. I simply used
the Connection String wizard to create a few variants
18

DELPHI INFORMANT MAGAZINE | July 2004

that I use most often. I replaced the variable parts with


%variables and pasted them into the code. It seemed
so simple when he explained it like that; I was left
wondering why I hadnt thought of it before.
And thats about it, he proclaimed as he crossed his
arms over his cardigan and looked me straight in the
eye. Add a Register statement to put it on your palette,
and you can substitute it anyplace you would have used
TADOConnection with the advantage that youll never
have to create a Connection String again.
Now Billy, Popi started in again with his challenging tone,
Ive got a final question for you. As built, this component
isnt fully dynamic. Once connected, if you change some
setting, its not reflected in the Connection String until you
set the connection False and then True again. So we are
again relying on a smart user, or in this case programmer,
rather than smart code. How can we smarten this code?
With the gauntlet flung to the ground in this fashion, I was
in a spot of trouble. But Popi had taught me well, and I had
seen this situation before. I strode to the whiteboard, picked
up the marker, and in the private declaration section wrote:
procedure ResetConnected;

and promptly filled it in with:


begin
if Connected then begin
SetConnected(False);
SetConnected(True);
end;
end;

And at each Set[property] procedure I inserted the following


line:
ResetConnected;

right after the appropriate:


F[PropertyName] := Value;

Replacing the cap on the marker, and the marker on the


tray, I turned and took a bow to the sound of one pair of
clapping hands. Well done, Master Billy, Popi said approvingly, almost exactly as I would have done it. My complete
notes and the code for the entire unit is here in this file.
And with that, Popi left the lab to teach his 3:30 Algorithms
seminar.
The source referenced in this article is available for download
on the Delphi Informant Magazine Complete Works CD
located in INFORM\2004\JUL\DI200407BF.

Bob Fleischman is the president of Automated Document Systems, Inc., a


Delphi and Java consulting firm in Philadelphia, PA that has provided services
to corporations and law firms since in 1989. He has been writing Pascal code
since getting his first copy of Turbo Pascal for CP/M in 1983.

A C T I V E
ASP.NET

D E L P H I

DELPHI 8

By Nick Hodges

ASP.NET

Part I: The View from 10,000 Feet

ow! My very own column. Hard to


believe. And I get to write about the
latest and greatest Web application

development technology, ASP.NET, and how to use


it with the greatest programming language out
there, Delphi. It doesnt get any better than this!

ASP.NET is Microsofts Web application development framework that is a core part of the .NET Framework, delivering
a robust architecture for developing Web server-based applications that provide their user interface via HTML and a
Web browser. The architecture provides literally hundreds of

classes and types for you to use to build and manage your
Web sites and Web applications. In addition, the architecture
is quite extensible, allowing you to build or buy components
to enhance your ability to develop cool, dynamic, and powerful Web applications.
Before I go much further, I have a confession to make.
I didnt really like ASP.NET when it first came out. It
seemed so unwieldy with all those pages and files. Each
page appeared to be sort of its own little program. The
script mixed in with the HTML made me cringe, because
the user interface elements were all mashed in with the
code logic. Plus, WebSnap was so cool and powerful that
my bigoted brain couldnt imagine why anyone would
want to use ASP.NET.

Element

Description

Example

The binary DLL file

The code-behind binary file. It contains all the code from your *.pas
files thats associated with all the pages, as well as the global object
for your application. Your *.aspx pages will reference this file to find the
code they need to make your application work.

MyWebApp.dll

*.aspx, *.ascx, *.asmx, etc. pages

Pages containing the HTML and ASP.NET control tags that will make up
the user interface of your application. These pages are normally created in the WebForms designer, although they can be hand coded and
edited using any appropriate tool.

MainPage.aspx,
LoginPage.aspx,
MenuColumn.ascx,
etc.

web.config file

An XML file containing all basic configuration information for your


application. It resides in the root directory of your application and is read
when the application is loaded into memory. The ConfigurationSettings
class in the System.Configuration namespace can be used to access this
information programmatically.

web.config

The IIS virtual directory

To be deployed, your application must reside in an IIS virtual directory. When you create a new ASP.NET application in the IDE, you can
choose to have it create the directory for you, or you can always create it later yourself.

C:\Inetpub\wwwroot\
WebApplication1

Figure 1: The basic elements of a compiled, ready-for-deployment, Delphi-based ASP.NET application.

19

DELPHI INFORMANT MAGAZINE | July 2004

Active

Delphi

ASP.NET

Filename

Description

Filename

Description

Global.asax

A simple text file that contains global tags


that define application-level attributes.
By default, it defines the file that contains
the System.Web.HttpApplication class
implementation that will be used by your
application. By default, this is a TGlobal
class defined in global.pas.

System.Web

Holds classes to manage the HTTP


request and response process, as
well as the basic classes for handling
and creating a Web page to send
back to your user.

Global.pas

The code-behind file for the application as a


whole. It contains the System.Web.HttpApplication
descendant. This class provides application-level
events such as Application_Start, Session_Start,
etc., to which you can add code.

Web.config

Same as discussed above. Note that since this is


an XML file, the IDE will color highlight it for you.

Webform1.aspx

This is the HTML file that contains the


ASP.NET tags for the controls on the given
page. You can add components using the
designer, or manually edit the HTML directly
in the code editor.

Webform1.pas

This is the code-behind file for the associated


ASPX file. It contains a descendant of
System.Web.UI.Page that defines the behavior
for the page. By default, this page will also
act as the default page for the application.

Figure 2: The files found in a default ASP.NET application.

System.Web.UI,
Bread-and-butter namespaces that
SystemWeb.UI.HTMLControls, hold the classes that will make up
System.Web.UI.Controls
the majority of the functionality in
your Web applications. They hold
the classes for managing pages, the
HTML, and controls on those pages.
Put together, these three namespaces
make up what is commonly called
WebForms.
System.Web.SessionState

Provides the classes that implement


the Session object for your application.
The Session object maintains information on the server about specific users.
Since HTTP is a stateless protocol,
the Session object allows your user to
identify himself to the server with each
subsequent request, allowing you to
provide consistent, custom content to
users. Future columns will be covering
this important feature in detail.

Figure 3: Descriptions of the important namespaces in the ASP.NET Framework.

But then I found out about code-behind (which Ill get to


in a minute) and user controls (which Ill get to in later
columns) and I started to see how ASP.NET might not be
so bad after all. Code-behind let me decouple business
logic from the presentation layer (well, sort of anyway),
and user controls made it really easy to structure Web
sites so that each page had common elements.

Thats right; for now, ASP.NET officially only deploys to IIS.


This might be a limitation for some folks. You can use IIS
to develop and debug your application, but it might not be
the best choice for doing so. Delphi includes a testing server,
named Cassini, that you can use to test and debug your applications. I recommend that you do all your testing with the
Cassini server, as it isnt nearly as finicky as IIS can be, and
is much easier to deal with in the development cycle. You can
I started to come around to the ASP.NET way of thinking,
read about how to set up the Cassini server on my blog at
when I discovered that ASP.NET doesnt really have data
http://www.lemanix.com/nick/archive/2004/02/15/153.aspx.
modules. That set me back a few steps, but I got over it
(Unofficially, ASP.NET applications will deploy to Linux via
(another future column).
the Mono implementation of
.NET. Well look at the option
I have a confession to make.
Dont get me wrong; I still
in a later column as well.)
love WebSnap. I think its a I didnt really like ASP.NET when
cool technology and grossly
One interesting thing is that
it first came out.
under-appreciated by the
an ASP.NET application runs
development world. The
in the context of a regular
marketplace appears to have spoken, however, so what is
old Win32 ISAPI DLL named aspnet_isapi.dll. If you open the
one to do? (I hope WebSnap finds its niche on the Kylix
properties page for the IIS default Web site, select the Home
side of things. It does totally rock for building Apache
Directory tab, and then press the Configuration button, youll see a
DSO modules.) But in the end, Ive come around to see
listview of items. The first column in the listview contains file
the innate coolness of ASP.NET and am a whole-hearted
extensions with associated ISAPI DLLs that will process pages
believer.
with that extension that are requested from ISS. In the case
of the *.aspx extension, you should see the aspnet_isapi.dll
What the Heck Is ASP.NET Really?
associated with it, along with a number of others.
I wrote the obligatory Introduction-to-ASP.NET paragraph
above. But what the heck is it really? Well, in Delphi,
If you dont see such an association, navigate to the direconce compiled, an ASP.NET application is a DLL, a
tory where your framework is installed (mine is C:\WINNT\
collection of *.as?x pages, a web.config file, and a virtual
Microsoft.NET\Framework\v1.1.4322), and run:
directory on Microsofts Internet Information Server (IIS).
The table in Figure 1 describes these basic parts.
aspnet_regiis i

20

DELPHI INFORMANT MAGAZINE | July 2004

Active

Delphi

ASP.NET

from the command line. That will register the ASP.NET


engine with IIS.

will do most of that work for you, but you can if you need
or want to. The HTTPRequest class is filled out with the
information from each request that comes in, and either
Aspnet_isapi.dll is really just a scripting engine that hosts
you or the framework will fill out the HTTPResponse
the .NET Framework, and knows how to take aspx pages
class with the response you want sent to the client. The
and use the pages associated DLL to render HTML pages
HTTPContext class also provides access to application
for IIS. Cool, eh? I bet most people didnt know that
services like the Session object, the Application object, and
their ASP.NET application was really just another Win32
information about the current user. The Page object will
scripting language as far as IIS is concerned.
do almost all of the HTTP handling in a typical ASP.NET
application, but the framework provides a robust set
So, what is ASP.NET on the project side? Have a look
of classes that allow you to provide any type of HTTP
for yourself. Fire up
handling for a request such
Delphi 8, select File | New
as image handling or other
Any Web application development types of binary responses.
| ASP.NET Web Application,
framework is really an elaborate way
and have a look at
Ill cover this type of
what the resulting
specialized HTTP handling
to accept HTTP requests, process
project includes. First,
those requests, and provide an HTTP in subsequent columns.
youll see the WebForm
response back to the requesting
Designer, where you can
Conclusion
client. ASP.NET is no different.
drop components just
Thats the 10,000 feet view
like youre used to for
of the ASP.NET architecture,
Windows applications.
and a rather short one at that,
The Project Manager shows that you have five files to
given the vastness that is ASP.NET. The entire framework
start out with, two of which are closely associated with
is designed to make things quite easy on you via the IDE,
each other. They are discussed in Figure 2.
while at the same time giving you the power and flexibility
to control the entire process. In addition, the architecture
From this humble beginning, you can add more pages and is designed to be extensible and customizable, just like the
code to build your dream Web site. Exactly how that gets
venerable VCL. Building custom ASP.NET controls is straightdone will obviously be the subject of future columns.
forward, and if youve done any component building in
Delphi before, ASP.NET control development should be quite
As far as the .NET Framework itself is concerned, all of
familiar.
the functionality of ASP.NET resides in the System.Web
namespace and the sub-namespaces within it. The table
And this column, in the coming issues, will show you how to
in Figure 3 lists some, but not all, of the namespaces
do all that, and more. That is, if the fame and glory of havwithin System.Web.
ing my own column doesnt go to my head and I start missing deadlines like all the famous writers do. (Note to Editor: I
Any Web application development framework is really
made that last part up. Really.)
an elaborate way to accept HTTP requests, process those
requests, and provide an HTTP response back to the
requesting client. ASP.NET is no different. Pursuant to
that, the System.Web namespace provides a class named
HTTPContext that holds all the information about the
HTTP request, and its subsequent response, in the aptly
named HTTPRequest and HTTPResponse properties. These
properties are classes that give you total control of the
Nick Hodges is the Chief Technology Officer for Lemanix Corporation
whole HTTP request/response process.
(www.lemanix.com), a Borland Solutions Partner in the Twin Cities of

Most of the time you wont need to bother with


manipulating these classes, as the ASP.NET Framework

21

DELPHI INFORMANT MAGAZINE | July 2004

Minnesota. Nick is a member of TeamB (www.teamb.com) and a frequent


speaker at the annual Borland Conference. Nick lives in St. Paul with his wife
and three children. He can be reached at nickhodges@yahoo.com.

N E W

&

U S E D

By Mike Riley

Shell+
Extend the Power of Delphi to the Windows Desktop Shell

s Delphi experts know, the advantage


to using Delphi over other Windows
compiled languages is the speed at which

sophisticated functionality can be implemented.


What might take C++ developers a day, Delphi
developers can do in minutes.
Another edge for Delphi is its expansive selection of components that accelerate productivity by encapsulating some of the
more difficult aspects of Windows programming into a simple
method call. Such is the case with Shell+, a collection that
corrals the complex world of Windows shell manipulation into
a set of incredibly-easy-to-use components.
Professional Features without the
Professional Headaches
I have often wanted to polish my Delphi applications with
the slick shell extensions Ive seen other commercial products exhibit. For example, one utility created binary files
with an .MR2 extension that was only useful within the
program that produced it. I wanted Windows to not only
recognize the file type (it was an easy task to register it),
but also to interact with it via context-sensitive right-click
menus (which was considerably more challenging).
After researching what it would take to add this functionality, I opted to leave it out due to time constraints. Had
Shell+ been available, it would have been a no-brainer.
In fact, thats what repeatedly struck me while tinkering
with the Shell+ demos: what was once a laborious programming task had now become an uninhibited field of
fresh ideas to easily implement. I had to curb my enthusiasm for the product, since I began to view everything I
wrote with a Shell+ hook in mind.
22

DELPHI INFORMANT MAGAZINE | July 2004

The Shell+ components encapsulate nearly every facet of


Windows shell manipulation thats visible to the end user.
These include:
custom copy, drag and drop, and change notify shell hooks
folder columns
icon overlay and InfoTip handlers
property sheets and Control Panel tabs
namespace extensions
shortcut (.lnk) file handlers and shortcut menus
thumbnail viewing
balloon tips and animations for system tray icons
Each of these hooks can spawn full applications via the
registered DLLs they are compiled into. My personal
favorites from the collection are the namespace
extension (with assistance from the TSxSysShellView
and TsxVirtualFolder units), Change notification
(TSxChangeNotifier) and Shortcut menu (TSxContextMenu)
components. They can really add a level of custom
application awareness within the Windows environment.
Using these hooks helped me redefine my desktop by leveraging a slew of applications I had written in the past that could
now interact with Windows on a much deeper level than ever
before. The least interesting was the TrayIcon, since this has
been done many times over, with some freeware implementations (CoolTray comes to mind) considerably more powerful
and full-featured than the one attempted by the Shell+ creator.

Figure 1: Over 30 components populate the two Shell+ tabs installed in the Delphi IDE.

New

&

Used

Shell+

Figure 2: Creating context-sensitive shell menu


additions is as straightforward as dropping the
TsxContextMenu on the design palette, setting the
menu properties, and creating and registering the
compiled DLL.

Figure 4: The ShellReset program is an invaluable


utility for registered users that helps accelerate the
Windows shell extension debugging process.

Figure 3: Create custom property sheets for file types

As most Delphi DLL developers know,


that really add a professional polish to your custom
Delphi applications.
DLLs are fickle and often troublesome to debug. Recognizing this, the
Whats Missing?
Shell+ staff provides a helpful Debugging Shell ExtenAnother questionable omission from the installation was the
sions using Delphi page on their Web site to optimize
lack of demos in the registered package. While a healthy
the Windows shell for non-Delphi 7 user debugging. How- supply of demos exists on the companys Web site, I had to
ever, even though I tested the components under Delphi
download, unzip, and categorize each independently.
7, I still encountered occasional access violations when
executing compiled extensions for the first time. These
More egregious was the omission of documentaalways disappeared upon successive executions.
tion, even though the installer deposited a faux link to
the ShellPlus.chm help file. Again, I had to revisit the
Another invaluable debugging tool created specifically for
shellplus.com Web site to download, unzip, and configure
registered Shell+ users is the ShellReset program, designed
the products online help. The site noted a pre-release
to quickly initialize, load and/or unload the Windows
disclaimer on the documentation files, and rightly so the
Shell. This is an incredible timesaver when tweaking shell
documentation is downright sparse and incomplete. While
extensions; while there still exists the regsvr32 install/
most of the features are advertised effectively with the
uninstall cycle, seeing a registered shell-modifying DLL in
numerous demos, the current quality of the documentation
action requires a reset of the desktop. Without the ShellReset
needs to be considerably improved before paying customers
program, developers would need to log out and log back
will feel truly satisfied while working with the components.
into their desktops to see the impact of the registered DLL.
While its apparent that the company used this tool as a
At least the company maintains public support
registered user incentive, it should have been bundled with
via an active message board on their Web site
the installation. I had already started using the components
(www.shellplus.com/forum/) to facilitate any users queswithout it and it wasnt until I combed the shellplus.com
tions or problems with the product. Registered users also
Web site that I stumbled across this timesaving utility in the
receive unlimited e-mail support and user-requested code
registered members area of the site.
snippets to help overcome deficiencies in the current
product documentation.

Just the Facts


Shell+ is a component set that encapsulates nearly every
facet of Windows shell manipulation thats visible to the
end user, including: custom copy, drag and drop, and
change notify shell hooks; folder columns; icon overlay
and InfoTip handlers; property sheets and Control Panel
tabs; and more. Despite poor documentation, this is a
product to get excited about.
Shell Plus Development Group
Phone: (450) 928-9842
E-Mail: info@shellplus.com
Web Site: www.shellplus.com
Price: US$49.95 to US$599 depending on license
configuration.

23

DELPHI INFORMANT MAGAZINE | July 2004

Conclusion
Documentation quality issues notwithstanding, Shell+ is a
welcome addition to a Delphi developers component library.
While a few freeware and shareware components have
attempted some of the features advertised by the product, no
commercial offering has approached the breadth of Windows
shell manipulation that Shell+ provides. Interested developers are encouraged to check out an animated demonstration
of the product at www.shellplus.com/flash-demo.html to
experience the depth of capabilities this useful set of components can provide.

Mike Riley is an advanced computing professional specializing in emerging


technologies and new development trends. Readers may contact him at
mike@mikeriley.com.

N E W

&

U S E D

By Mike Riley

Delphi Spell Checker PRO


Banish Spelling Errors from any Delphi 5, 6, or 7 Form

have had the pleasure of working with some


amazing coders throughout my professional
application development career. These

talented individuals could speak in binary,


dream in assembly, write in a sea of languages,
and execute in a number of environments.
Unfortunately, although they could manage to
keep track of the obscure variable identifiers,
they goofed on correctly spelling labels and
option lists that, in the eyes of their user base,
instantly deflated their programming prowess to
that of an elementary school student.

Just the Facts


Delphi Spell Checker is a well-meaning attempt to minimize
embarrassing spelling errors, but its execution stumbles. If
Spline redesigned the utility to address its shortcomings, as
well as exceed expectations of what a spell checking utility targeting the Delphi coding community really needed, it
may be an add-on worth considering. Until then, peer review
remains the better, more economical alternative.
Spline Technologies Corp.
801-110 rue de La Barre
Longueuil PQ J4K 1A3
Canada
Phone: (450) 928 9842
E-Mail: support@remotedebugger.com
Web Site: www.remotedebugger.com/
delphispell.asp?redir=tradwww
Price: US$48.88

24

DELPHI INFORMANT MAGAZINE | July 2004

Often these misspellings were simply errant key presses


that were obvious if only they had a proofreader check
over their work. Spline Technologies decided it was time
to deliver that proofreader to Delphi developers via a
form-centric spell checking utility, aptly called Delphi
Spell Checker. Spline also offers Delphi Spell Checker
PRO, which essentially adds the ability to automatically
spell check multiple controls on a form instead of manually selecting individual controls and instantiating the
spell check function. The tool also supports 18 languages,
making it a much more global tool than other English-oriented add-ons.
Delphi Spell Checker PRO works by iterating through each
control on a Delphi form, determining if the control has a caption, text, or any other TStrings descendant. Once it identifies
a controls string property, it executes an internal spell check
engine against the string, identifies any misspelled or unrecognized words, and offers suggested replacements if available.
It then moves on to the next control on the form until all controls have been processed.
Although it certainly delivers what it advertises, I was
unimpressed with Delphi Spell Checker PROs execution. First,
I had false expectations that the utility could identify and
spell check all text strings in both code and resources, not
just those contained in a form control. I have a tendency to
misspell words more often in MessageBox strings than in form
controls; its much easier to visually identify a misspelling in a
form than the text strings quoted inside a function, because I
stare more often at the forms during the design process.
Unfortunately, Delphi Spell Checker currently cannot scan
through compiled resource files, or even .pas text files, and
parse the file for strings (which is a relatively straightforward
utility to create; simply check the words between quotation marks). Second, the Spell Check Current Form function
misleadingly reports that the Spell Check is complete after
spell checking each control on the form. Although one might
argue that this alert box is correct for that particular control,

New

&

Used

Delphi Spell Checker PRO

it nonetheless requires
unnecessary mouse clicks
that could have easily been
avoided with a loop flag
checking if the function
were really analyzing the
last control on the form.
This seemingly minor logic
omission becomes a major
annoyance on forms with
a dozen controls.
Finally, the utility leverages Addictive Softwares
Addict Spell Check 3 as the
spelling engine. Frankly,
the English dictionaries
that are included are practically useless because of
the limited range of words
they contain and thus the
inability to suggest spelling
replacements. For example,
the misspelling of maintainance returned no
suggestions (see Figure 1).
Instead of being forced to
Figure 1: Spell checking an entire form is as simple as pressing the hard-coded CAQ. However, the spelling engine has
use the embedded Addict
trouble providing suggestions for many commonly misspelled words.
spell checking engine,
I wouldve preferred to
have the choice of selecting which spelling engines the utility
Conclusion
should employ. Ive spent years adding industry-specific terms
Borland fans need to recognize the efforts of fellow Delphi
to Microsoft Words spelling dictionary; the thought of repopu- developers seeking to solve problems of their peers. Howlating a different, less powerful spelling file is something for
ever, commercializing those efforts only succeeds when
which I have neither the time nor interest.
the solution elegantly delivers on its promise. Delphi Spell
Checker is a well-meaning attempt to minimize spelling
Although I applaud Spline for their recognition of the need errors, but its execution stumbles in embarrassing ways. If
to provide multi-language dictionary support, this feature
Spline redesigned the utility to address the shortcomings
doesnt provide any type of language translation. Basically, identified in this review, as well as exceed expectations of
the design allows Spline to market the utility internationwhat a spell checking utility targeting the Delphi coding
ally, as it does not provide language translation services
community really needed, it may be an add-on worth conof any kind. In this evolving age of Web services, Spline
sidering. Until then, peer review remains the better, more
could have simply integrated an Xmethods.net Babelfish
economical alternative.
translator to provide additional value to their product, if
only as an unsupported feature.
And because the utility doesnt spell check resource files, the
multiple language support is inadequate for those developers seeking efficient localization capabilities; the only way
to leverage Delphi Spell Checker is if the text in question is
embedded in the control. As anyone who has developed a
localized Delphi application knows, this type of practice is
greatly discouraged when designing multi-language support in
an application because of the inability to contain all resources
into a single manageable location.

25

DELPHI INFORMANT MAGAZINE | July 2004

Mike Riley is an advanced computing professional specializing in emerging


technologies and new development trends. Readers may contact him at
mike@mikeriley.com.

F I L E

N E W

The Last Delphi Book Column

By Alan C. Moore, Ph.D.

he title of this column may seem polemic, but theres


unfortunately a strong possibility that its true. New Delphi books are becoming so rare that even a handful of new
ones in a given year exceeds our expectations. Recently however, I was able to supplement my own collection with some
titles I had not examined before. Some are quite old; others
more recent. Several are by well-known authors. Ill discuss my
observations about these books, taking them in chronological
order. Ill end with some comments and questions on the larger
context surrounding the lack of new Delphi books.
Back to Delphi 3 and 4. Two books from Delphi 3 days I had
never seen until recently are Warren Kovachs Delphi 3: User
Interface Design (Prentice Hall, 1998) and Neil J. Rubenkings
Delphi 3 for Dummies (IDG Books, 1997). User interface design
is a topic covered briefly in some large Delphi works, but
Kovachs book is the only one Ive seen devoted exclusively to
this topic. It covers this important subject in some depth, but
goes further by introducing such advanced matters as system
information, assembly language, and working at the Windows
API level. Although most of the material will be familiar to
experienced Delphi programmers, there is much here that
could benefit those new to Delphi development, especially anyone whose main focus is developing user interfaces.
Neil J. Rubenking has been writing for PC Magazine for many
years. I remember talking with him at one of the Borland conferences in the mid 1990s and his complaining to me that he
was the only Delphi advocate at his magazine. Over the years
he has used Delphi to produce some nice freeware utilities.
His Delphi 3 for Dummies is a real treat. The writing style is
clear and concise, with more than a dash of humor. Similar to
Kovach, Rubenkings book includes information on Delphi 1
as well as the environment for which it was written, Windows
3.x. I especially appreciated the tips and technical information
throughout the entire volume. With its extensive coverage of
Delphi components and essential programming topics, its a
wonderful introduction for the novice Delphi developer.
Tom Swan is another well-known writer I had the pleasure
of meeting at a Borland conference. His Delphi 4 Bible (IDG
Books, 1998) is a comprehensive introduction to our favorite
development tool. While Rubenkings book is component-oriented, Swans is more task-oriented, with detailed chapters on
working with lists, strings, graphics, and more. Much time is
spent on user interface components such as menus, toolbars,
and buttons, but there is much more emphasis on code, especially compared with Rubenkings book.
Advanced/specialized titles. Warren Racheles The Tomes of
Delphi: Win32 Database Developers Guide (Wordware, 1999) is
an excellent resource, even if it is a bit dated. I especially recommend this book for any developer who has some experience
in Delphi, but is just starting database development. Racheles
introductions to database design and SQL are especially beneficial for such a developer. This book will also be a useful refer26

DELPHI INFORMANT MAGAZINE | July 2004

ence for developers using older versions of Delphi that depend


on the Borland Database Engine (BDE).
Jon Q. Jacobs Delphi Developers Guide to OpenGL (Wordware, 1999) addresses the popular graphics library of the title.
Starting with basic issues such as working with (and freeing)
resources and handling errors, this book is task-oriented, covering all the main topics. There are chapters explaining OpenGLs
three-dimensional capabilities, perspective and lighting, textures, animations, and special effects, including antialiasing
techniques, transparency, and more. This book is a must-have
for anyone planning to write games or other graphics-intensive
applications with Delphi and OpenGL.
What does it all mean? Ive had several readers write to
me asking for advice on Delphi books; in some cases, to help
friends new to Delphi programming. Clearly, the lack of new
Delphi titles is discouraging to those just getting started. Further, when veteran developers visit their local bookstore and
fail to see new Delphi resources, they often jump to the conclusion that Delphi must be in decline. I have seen this view
expressed on some of the lists to which I subscribe.
As one who has written or helped to write Delphi books, I
caution you against jumping to such conclusions. The current
popularity of Delphi is not as important as larger economic
challenges that are effecting all of us. Publishers are becoming
increasingly selective in choosing topics for new books. And
Delphi is simply not a hot topic these days. John Penman and
I had intended to write a follow-up to our communications
book, but shortly after publication a former executive at Wordware told me they were not going to publish any more Delphi
books. That was bad news, but it got worse. I had completed
a tech edit of another authors advanced Delphi book, expecting to be compensated. The book was canceled and I received
nothing. Although I took it as a business loss on my taxes, I
was left with considerable resentment and disappointment.
Can Borland do anything to turn the situation around? For
me, that is the question that keeps coming up. All of us are
wishing for the greatest success for Delphi for .NET, but where
are the new books? To ensure Delphi 8s success, I think that
Borland needs to become more proactive and ensure that such
titles become available, even if it means helping to underwrite
the endeavor. I really want to hear from readers about this, and
may share some of your views in future columns.
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 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). Using Delphi, he specializes in writing custom components
and implementing multimedia capabilities in applications, particularly sound
and music. You can reach Alan at acmdoc@aol.com.

Potrebbero piacerti anche