0% found this document useful (0 votes)
305 views

Delphi - Kylix Database Development

Delphi_Kylix Database Development

Uploaded by

Shenay Caliskan
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
305 views

Delphi - Kylix Database Development

Delphi_Kylix Database Development

Uploaded by

Shenay Caliskan
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 427

Delphi/Kylix Database

Development
Eric Harmon

800 East 96th Street, Indianapolis, Indiana, 46240 USA


ASSOCIATE PUBLISHER
Delphi/Kylix Database Development Linda Engelman
Copyright © 2002 by Sams Publishing
ACQUISITIONS EDITOR
All rights reserved. No part of this book shall be reproduced, stored in a
Karen Wachs
retrieval system, or transmitted by any means, electronic, mechanical, photo-
copying, recording, or otherwise, without written permission from the pub- DEVELOPMENT EDITOR
lisher. No patent liability is assumed with respect to the use of the information Laurie McGuire
contained herein. Although every precaution has been taken in the preparation
MANAGING EDITOR
of this book, the publisher and author assume no responsibility for errors or
Charlotte Clapp
omissions. Nor is any liability assumed for damages resulting from the use of
the information contained herein. PROJECT EDITOR
International Standard Book Number: 067232265x Heather McNeill

Library of Congress Catalog Card Number: 2001093571 COPY EDITOR


Katie Robinson
Printed in the United States of America
First Printing: November 2001 INDEXER
Mary SeRine
03 02 01 00 4 3 2 1
PROOFREADER
Trademarks Bob LaRoche
Plan-it-Publishing
All terms mentioned in this book that are known to be trademarks or service
marks have been appropriately capitalized. Sams Publishing cannot attest to TECHNICAL EDITORS
the accuracy of this information. Use of a term in this book should not be Dan Miser
regarded as affecting the validity of any trademark or service mark. Ramesh Theivendran
Philippe Bruno
Warning and Disclaimer TEAM COORDINATOR
Every effort has been made to make this book as complete and as accurate as Lynne Williams
possible, but no warranty or fitness is implied. The information provided is on
MEDIA DEVELOPER
an “as is” basis. The author and the publisher shall have neither liability nor
Dan Scherf
responsibility to any person or entity with respect to any loss or damages aris-
ing from the information contained in this book or from the use of the pro- INTERIOR DESIGNER
grams accompanying it. Gary Adair

COVER DESIGNER
Gary Adair

PAGE LAYOUT
Ayanna Lacey
Contents at a Glance
Introduction 1
1 Establishing and Using Database Connections 7
2 dbExpress Datasets 57
3 Client Dataset Basics 93
4 Advanced Client Dataset Operations 147
5 Data-Aware Components 201
6 Data-Aware Grids 239
7 Dataset Providers 273
8 DataSnap 317
9 The ConMan Application 347
Appendixes
A Redistributing dbExpress Applications 375
B dbExpress Plus 379
Index 385
Contents
Introduction 1
Who This Book Is For ............................................................................1
How This Book Is Organized ..................................................................1
VCL or CLX? ....................................................................................2
Components Developed in This Book................................................3
Sample Applications ..........................................................................3
dbExpress............................................................................................5
Databases Used in This Book ............................................................6
Conventions Used in This Book ..............................................................6
Contacting the Author..............................................................................6

1 Establishing and Using Database Connections 7


Connecting to and Disconnecting from a Database ................................8
Establishing the Connection ..............................................................9
Disconnecting from the Database ....................................................13
Connect and Disconnect Events ......................................................14
Retrieving Database Metadata ..............................................................18
GetTableNames ..................................................................................18
GetFieldNames ..................................................................................18
GetIndexNames ..................................................................................19
GetProcedureNames ..........................................................................19
GetProcedureParams ........................................................................19
Executing DDL and DML Statements ..................................................27
DDL Commands ..............................................................................27
DML Commands ..............................................................................29
Transaction Support ..............................................................................37
Checking for Transaction Support....................................................38
Starting a Transaction ......................................................................39
Committing a Transaction ................................................................40
Rolling Back a Transaction ..............................................................40
Multiple Transactions ......................................................................40
Providing Feedback During SQL Operations........................................46
Changing the Cursor While Executing SQL Statements ................47
Creating a Callback Event to Monitor SQL Commands ................47
TSQLMonitor......................................................................................49
Using Multiple Feedback Mechanisms ............................................50
Summary ................................................................................................55
2 dbExpress Datasets 57
What Are dbExpress Datasets?..............................................................58
dbExpress Datasets Are Unidirectional............................................58
dbExpress Datasets Are Read-Only ................................................59
dbExpress Datasets Are Lightweight ..............................................59
Types of Datasets ..................................................................................59
Tables................................................................................................59
Queries..............................................................................................60
Stored Procedures ............................................................................60
General-Purpose Datasets ................................................................60
Data Manipulation ................................................................................63
Opening a Dataset ............................................................................63
Closing a Dataset..............................................................................64
Retrieving Field Contents from a Dataset........................................64
Navigating a Dataset ........................................................................65
BLOB Support ......................................................................................69
Parameterized Queries ..........................................................................71
Ordering Data Returned from the Server ..............................................73
Ordering Data from a Table..............................................................73
Ordering Data from a Query ............................................................74
Master/Detail Relationships ..................................................................74
Retrieving Schema Information ............................................................79
Summary ................................................................................................91

3 Client Dataset Basics 93


What Is a Client Dataset? ......................................................................94
Advantages and Disadvantages of Client Datasets................................94
Creating Client Datasets ........................................................................95
Creating a Client Dataset at Design-Time ......................................96
Creating a Client Dataset at Runtime ............................................101
Accessing Fields ............................................................................103
Populating and Manipulating Client Datasets ....................................105
Populating Manually ......................................................................105
Populating from Another Dataset ..................................................106
Populating from a File or Stream: Persisting Client Datasets ......106
Example: Creating, Populating, and Manipulating a
Client Dataset ..............................................................................108
Navigating Client Datasets ..................................................................113
Sequential Navigation ....................................................................113
Random-Access Navigation ..........................................................114
Client Dataset Indexes ........................................................................118
Creating Indexes ............................................................................119
Using Indexes ................................................................................121
Retrieving Index Information ........................................................122
vi
DELPHI/KYLIX DATABASE DEVELOPMENT

Filters and Ranges................................................................................126


Ranges ............................................................................................126
Filters ..............................................................................................127
Searching..............................................................................................136
Nonindexed Search Techniques......................................................136
Indexed Search Techniques ............................................................138
Summary ..............................................................................................145

4 Advanced Client Dataset Operations 147


Dataset Events......................................................................................148
Disabling Data-Aware Components ....................................................158
BLOBs ................................................................................................162
Notes ..............................................................................................162
Images ............................................................................................162
Streamed Data ................................................................................165
Streamed Components....................................................................167
File BLOBs ....................................................................................168
Limitations of BLOB Fields ..........................................................168
Nested Datasets....................................................................................172
Undo Support ......................................................................................176
Cancel ............................................................................................177
The Change Log ............................................................................177
Viewing the Change Log................................................................182
Cloning Data from Another Client Dataset ........................................186
Maintained Aggregates ........................................................................192
Creating a Maintained Aggregate at Design Time ........................193
Creating a Maintained Aggregate at Runtime................................195
Aggregate Expressions ..................................................................195
Aggregates Across a Group of Records ........................................196
Enabling and Disabling Aggregates ..............................................197
GetGroupState ................................................................................197
Miscellaneous Properties ....................................................................197
Constraints ......................................................................................197
DisableStringTrim ........................................................................198
ReadOnly ........................................................................................199
Summary ..............................................................................................199

5 Data-Aware Components 201


What Are Data-Aware Components? ..................................................202
TDataSource ........................................................................................204
Common Data-Aware Component Characteristics..............................205
Modifying Component Data from Code ........................................205
Controlling When the User Is Allowed to Edit Data ....................206
Formatting and Editing Field Values..............................................206
vii
CONTENTS

Simple Data-Aware Components ........................................................211


TDBText ..........................................................................................211
TDBEdit ..........................................................................................212
TDBMemo ..........................................................................................212
TDBCheckBox....................................................................................212
TDBRadioGroup ................................................................................213
TDBComboBox....................................................................................213
TDBListBox ....................................................................................218
TDBImage ........................................................................................221
VCL-Only Data-Aware Controls ........................................................222
Lookup Data-Aware Controls ..............................................................222
TDBNavigator ......................................................................................223
Creating Your Own Data-Aware Components ....................................225
TFieldDataLink ..............................................................................225
Setting Up the TFieldDataLink......................................................226
Setting Up a Connection to the Data Source ................................227
Responding to Changes in the Dataset ..........................................227
Updating the Dataset ......................................................................227
Message Handlers ..........................................................................228
Action Handlers..............................................................................228
Data-Aware TDateTimePicker ........................................................228
Sample Application..............................................................................232
Summary ..............................................................................................236

6 Data-Aware Grids 239


TDBGrid ................................................................................................240
TDBGrid Basic Operation ................................................................240
Customizing Columns ....................................................................241
Grid Options ..................................................................................244
Events ............................................................................................245
Custom Drawing ............................................................................252
Solutions to Common Grid Questions ..........................................257
Limitations......................................................................................263
TClientDataSetGrid ............................................................................263
Automatic Sorting ..........................................................................264
Column Customization ..................................................................265
TDBCtrlGrid ........................................................................................266
Properties ........................................................................................267
Events ............................................................................................267
Third-Party Data-Aware Grids ............................................................271
Summary ..............................................................................................272
viii
DELPHI/KYLIX DATABASE DEVELOPMENT

7 Dataset Providers 273


What Is a Dataset Provider? ................................................................274
Connecting to a Dataset ......................................................................275
Resolving Changes to Data..................................................................276
Applying Updates ..........................................................................276
Resolving to a Dataset....................................................................278
Reconciliation Errors......................................................................278
Resolving Changes to BLOB Fields ..............................................290
Refreshing Data from the Server....................................................290
Update Modes ................................................................................291
Provider Options ..................................................................................293
Provider Events....................................................................................295
Changing Field Values on the Server ..................................................297
Intercepting Data..................................................................................298
Optional Parameters ............................................................................300
Master/Detail Relationships ................................................................301
Providing and Resolving Data from Stored Procedures and Joins ....302
Providing and Resolving Data from a Stored Procedure ..............302
Providing and Resolving Data from a Join ....................................302
Connecting to a Local Database ..........................................................308
Using Providers Located on a Different Form ..............................308
One-Stop Shopping: TSQLClientDataSet ......................................309
Limiting the Amount of Data Returned by the Server ..................309
Summary ..............................................................................................315

8 DataSnap 317
What Is DataSnap? ..............................................................................318
Creating the Application Server ..........................................................318
Remote Data Modules ....................................................................318
Creating the Application Server’s User Interface ..........................326
Preparing the Application Server for Testing ................................328
Creating the Client Application ..........................................................329
Connecting to a Local Database Connection ................................329
Connecting to a Remote Database Connection..............................330
A Complete Example ..........................................................................336
The Briefcase Model............................................................................340
Stateless Servers ..................................................................................341
Sharing a Connection Between Multiple Client DataSets ..................343
Brokering Connections Between Multiple Servers ............................344
Summary ..............................................................................................345
ix
CONTENTS

9 The ConMan Application 347


What Is ConMan? ................................................................................348
Database Structure ..............................................................................349
Overview of the Code ..........................................................................352
The Server Application ........................................................................352
The Client Application ........................................................................358
Room for Improvement ......................................................................373
Summary ..............................................................................................373

Appendixes
A Redistributing dbExpress Applications 375
Redistributable Files ............................................................................376
Redistributing a Windows Application ..........................................376
Redistributing a Linux Application ................................................377
Licensing Issues ..................................................................................378
CD-ROM-Based Applications ............................................................378

B dbExpress Plus 379


What Is dbExpress Plus? ....................................................................380
Scripting..........................................................................................380
Enhanced Metadata ........................................................................381
Data Pumping ................................................................................383
For More Information ..........................................................................384

Index 385
About the Author
Eric Harmon is Director of Software Development at Advanced Estimating Systems, Inc.,
located in Delray Beach, Florida. Advanced Estimating Systems is the developer of The
EDGE, the industry standard in construction-estimating software. Eric is also a member of
TPX (TurboPower experts), a volunteer group of programmers that assists the TurboPower
Software company in providing support for its newsgroups. TurboPower is one of the premier
providers of tools coded in Delphi for Delphi programmers. Eric was recruited by TurboPower
as the original member of TPX in 1997. He has contributed Delphi- and COM-related
articles to Visual Developer Magazine and is the author of the highly regarded book
Delphi COM Programming (MTP/New Riders, 2000). Eric can be reached at
[email protected].

Dan Miser is a Research and Development Project Manager for the DSP group at Borland,
where he spends most of his time researching emerging technologies. Dan also worked on the
Delphi R&D team where his responsibilities included DataSnap development. Dan’s major
focus is finding ways to allow information to be shared across boundaries, and this has
allowed him to work with a variety of distributed computing technologies, including MIDAS,
SOAP, DCOM, RMI, J2EE, EJB, Struts, and RDS. He has also been involved with promoting
Delphi by contributing to the “Delphi x Developer’s Guide,” acting as a technical editor, writ-
ing magazine articles, participating on the Borland newsgroups as a member of TeamB, and
speaking at BorCon on topics such as COM and MIDAS.
Ramesh Theivendran has been a member of the SQL Links research and development team
since October 1995. Prior to joining Borland, Ramesh was employed as a Programmer at the
Indian Institute of Technology, Bombay (IITB) and as a Systems Analyst in Ramco Systems,
Madras, INDIA. He has over 10 years of experience in client/server tools development.
Currently, he leads the database connectivity efforts at Borland in its RAD products group and
serves as an architect for dbExpress. Ramesh lives in Santa Cruz, California with his wife,
Aruna, and their little one, Vineha.
Philippe Bruno is the Director of Research and Development at Scanpak Inc., a firm head-
quartered in Montreal, Quebec, specializing in radio frequency identification (RFID) systems.
Scanpak is the creator of GETS (Galley Equipment Tracking System), an asset tracking sys-
tem specifically targeted to the airline industry. He is also a part-time teacher for computer-
related courses in various universities and colleges in the Montreal area. Philippe has
programmed in several computer languages since 1987, but Pascal and Delphi have always
been his favorites. He is also a member of TPX (TurboPower experts), where he volunteers his
expertise in serial communications, networks, and protocols to the service of fellow program-
mers in the TurboPower newsgroups.
Dedication
For my wife, Tina.

Acknowledgments
Writing a book isn’t a one-man (or woman) operation, and I would like to thank the people
who helped take this book from the concept stage to reality.
Once again, Karen Wachs at Sams worked with me on this book from beginning to end. She
patiently led me through the process of writing my first book and was back to assist on this
one, also. She’s a pleasure to work with. Thanks, Karen! I also want to thank Katie Robinson
and Chip Gardner, who copyedited the text and fixed up my typos and grammatical errors.
Thanks to Heather McNeill, who oversaw this book through all its stages of production and
helped to make sure that things ran smoothly; and to Laurie McGuire, who suggested ways to
improve the flow of the text and otherwise ensured that the overall quality of the book was up
to par.
I’d like to say a special thanks to my technical reviewers, Dan Miser and Ramesh Theivendran,
both Borland employees, who provided large quantities of extremely helpful feedback and
pointed out where I made technical mistakes. Ramesh is one of the key dbExpress engineers,
and Dan is well known for his MIDAS expertise. In addition, Phillipe Bruno provided valuable
and timely technical review of the final chapter and appendixes. I couldn’t have asked for bet-
ter tech reviewers.
With all these people assisting me, I have made every attempt to fix all errors, both technical
and typographical, that may have originally appeared in the manuscript. Writing a book is a
very complex process, and inevitably, some errors will have survived. Any errors that remain
are, of course, my own fault.
My apologies to anyone who I may have inadvertently omitted. A number of people worked on
this book that I never had direct contact with, so I don’t know them individually. Thanks to all
those whose names I didn’t specifically mention.
Tell Us What You Think!
As the reader of this book, you are our most important critic and commentator. We value your
opinion and want to know what we’re doing right, what we could do better, what areas you’d
like to see us publish in, and any other words of wisdom you’re willing to pass our way.
As an associate publisher for Sams, I welcome your comments. You can e-mail or write
me directly to let me know what you did or didn’t like about this book—as well as what we
can do to make our books stronger.
Please note that I cannot help you with technical problems related to the topic of this book,
and that because of the high volume of mail I receive, I might not be able to reply to every
message.
When you write, please be sure to include this book’s title and author as well as your name
and phone or fax number. I will carefully review your comments and share them with the
author and editors who worked on the book.

E-mail: [email protected]

Mail: Sams Publishing


800 East 96th Street Street
Indianapolis, IN 46240 USA
Introduction
This book is about database programming in Delphi 6 and Kylix. Most of the code in the book
(with the exception of the dbExpress chapters) should also work with Delphi 5, but I have
made no effort to test it.

Who This Book Is For


This book targets Delphi 6 and Kylix database programmers. I assume that you already have
an understanding of the Object Pascal language and that you know how to create a Delphi or
Kylix application, drop components on a form, create and connect event handlers, and perform
the various and sundry tasks required to produce a working application.
I further assume that you have some basic knowledge of databases and their terminology. For
that reason, I won’t explain what table, view, column, and other database-related terms mean in
this book.
This book also uses some of the standard components in its sample applications. Apart from
data-aware components, I don’t explain how to use the standard components used in these
samples, such as action lists, buttons, list boxes, and the like. If you need additional information
on those components, please refer to the Delphi or Kylix help or to a general-purpose,
third-party Delphi book.

How This Book Is Organized


If you are new to Delphi/Kylix database programming, it is best to read the chapters in order.
If you have some experience in database programming, and you want to learn only about
dbExpress (for example), you can jump directly to the appropriate chapter(s) and read those.
Whether you read sequentially or not, the following is a quick overview of what you’ll find in
each of the chapters.
• Chapter 1, “Establishing and Using Database Connections,” introduces dbExpress, the
new data-access technology provided with Delphi 6 and Kylix. It shows you how to
connect to a database using dbExpress.
• Chapter 2, “dbExpress Datasets,” continues with the dbExpress overview and discusses
the dataset components specific to dbExpress.
• Chapter 3, “Client Dataset Basics,” introduces client datasets and the TClientDataSet
component, which provides for high-speed, in-memory datasets.
• Chapter 4, “Advanced Client Dataset Operations,” continues with the discussion of client
datasets and goes into detail about a number of more advanced client dataset operations.
2
DELPHI/KYLIX DATABASE DEVELOPMENT

• Chapter 5, “Data-Aware Components,” introduces data-aware components, which provide


a bridge between the data and user interface of an application, automatically displaying
information from a dataset and allowing the user to enter new data.
• Chapter 6, “Data-Aware Grids,” continues with the data-aware component discussion to
show you how to display and edit data in a grid format.
• Chapter 7, “Dataset Providers,” provides the foundation for multitier database development
by introducing the concept of a provider.
• Chapter 8, “DataSnap,” shows how to create a true multitier database application by
creating separate client and server applications that connect over a network.
• Chapter 9, “The ConMan Application,” develops a simple contact manager to illustrate
multitier database development techniques in a real-world (albeit simplistic) application.

VCL or CLX?
Because the technology discussed in this book applies equally well to Delphi 6 and Kylix
(with the exception of Chapter 8, “DataSnap”), all the code listings in this book are CLX list-
ings. The downloadable source code is provided in both CLX and VCL form, so if you don’t
write cross-platform applications, you may want to experiment with the VCL code instead.
In case you aren’t familiar with these terms, VCL stands for Visual Component Library; it is
the original, Windows-specific class library supported by Delphi. CLX stands for Component
Library Cross-Platform (the X stands for Cross-Platform) and is the new, cross-platform class
library supported both by Delphi and Kylix.
CLX is broken down into four categories:
• BaseCLX This includes the “behind-the-scenes” utility classes and functions, such as
TStringList, TObjectList, and so on.
• DataCLX This wraps the CLX database functionality, such as dbExpress and data-
aware components.
• VisualCLX This includes visual components such as menu bars, toolbars, buttons, list
boxes, and so on.
• NetCLX This includes Internet-related components.
The only part of CLX specifically discussed in this book is DataCLX (although bits and pieces
of BaseCLX and VisualCLX are used to create the sample CLX applications).
You’ll see from the code listings that apart from the uses clause at the top of each unit, there is
almost no difference between the VCL code and CLX code, so you shouldn’t have any trouble
following along with the CLX code.
3
INTRODUCTION

To avoid clumsy constructs such as “Delphi 6 or Kylix” or “Delphi/Kylix” throughout the


book, I use the generic term “Delphi,” which will serve to mean either Delphi 6 or Kylix. In
the few cases where a statement applies only to Delphi 6 (VCL), I specifically point that out.

Components Developed in This Book


Although this isn’t a book about component development, I have included four VCL-specific
descendents of data-aware components that I think you will find useful. These components are
discussed in Chapters 5 and 6. The source code for this book includes the Delphi package
ETH, which includes the following components:
• TETHDBComboBox A descendent of the data-aware component TDBComboBox that allows
you to select an item from a combo box and store its index in an integer field.
• TETHDBListBox A descendent of the data-aware component TDBListBox that allows
you to select an item from a list box and store its index in an integer field.
• TETHDBDateTimePicker A data-aware version of the Win32 component
TDateTimePicker.

• TETHDBGrid A descendent of TDBGrid that fires an event when the user resizes a column.

Sample Applications
Each chapter in this book includes a number of sample applications to help you understand the
concepts being discussed. The samples were all compiled and tested using Delphi 6—both the
VCL and CLX versions.
The source code for the sample programs can be downloaded from http://
www.samspublishing.com/detail_sams.cfm?item=067232265x6 or from my own Web site,
located at http://www.tpx.turbopower.com/~Eric.Harmon. In the latter case, click the Books
and Articles link, and then click the download link near the top of the page.
The following list provides a road map, by chapter, of the sample applications developed in
this book.

Chapter 1
• Events Illustrates the different connection events fired by the TSQLConnection component.
• MetaData Shows how to retrieve simple metadata information from a dbExpress
connection.
• DDLSQL Shows how to send DDL and SQL commands directly to a TSQLConnection
component.
• Trans Illustrates how transaction support works in dbExpress.
• Feedback Shows how to provide feedback about what’s happening in a dbExpress
connection.
4
DELPHI/KYLIX DATABASE DEVELOPMENT

Chapter 2
• Basic Illustrates basic TSQLDataSet operation.
• Advanced Shows more advanced TSQLDataSet methods and operations.
• Schema Shows how to retrieve more advanced metadata information from a dbExpress
connection using TSQLDataSet.

Chapter 3
• CDS Shows the basics of client dataset support.
• Navigate Shows how to navigate through a TClientDataSet.
• CDSIndex Illustrates how to create and use indexes on a TClientDataSet.
• RangeFilter Shows how to limit the amount of data in a TClientDataSet by applying
ranges and filters.
• Search Shows a variety of ways to quickly locate a given record in a client dataset.

Chapter 4
• EventLog Illustrates the events fired by TClientDataSet.
• Updates Shows how to disable and enable updates to data-aware controls to speed
dataset operations.
• BLOBs Shows how to store pictures and notes in a client dataset.
• Nested Shows how client datasets implement master/detail relationships.
• ChangeLog Shows how to implement undo support using a client dataset.
• Clone Illustrates cloning, which is a way to create a duplicate copy of a
TClientDataSet.

Chapter 5
• DataAware Illustrates a variety of data-aware components discussed in the chapter.

Chapter 6
• Options Shows how the various options for a TDBGrid work.
• CustomDraw Illustrates the correct way to override the TDBGrid’s default drawing to
provide visually exciting grids.
• CtrlGrid VCL-only sample that shows how to use the TDBCtrlGrid component.

Chapter 7
• Updates Shows the basic operation of dataset providers.
• Joins Shows how to correctly resolve data that was retrieved through an SQL JOIN.
• DataFetch Illustrates how to limit the amount of BLOB and detail data returned from a
dataset to speed application performance.
5
INTRODUCTION

Chapter 8
• Methods Shows how to add callable methods to an application server.
• LocalConn Shows how to implement a single-EXE database application using multitier
techniques.
• Stateless Shows how to create a stateless application server for use with MTS or
COM+.

Chapter 9
• ConMan A complete sample application that draws on many of the techniques discussed
throughout the book to create a simple contact manager.
With respect to the source code, each chapter has its own subdirectory, with VCL and CLX
subdirectories under it. In turn, the VCL and CLX subdirectories have a separate subdirectory
for each sample application.
In addition to a subdirectory for each chapter, there is a separate subdirectory named
Components, which contains the data-aware component descendents mentioned earlier.
The Data subdirectory contains the conman.gdb data file used in a number of the sample
applications and the SQL script file (conman.sql) used to create the database.
If you maintain this directory structure on your own drive, the sample programs should all run
fine out of the box. They are set up to access the CONMAN database using the relative path
..\..\..\Data\conman.gdb. If you have trouble running the sample programs, you might
want to modify them to provide a complete path to the data, such as D:\Data\conman.gdb.

dbExpress
dbExpress is Borland’s newest database-access technology, supported both by Delphi and
Kylix. Several database access technologies are supported by Delphi in previous releases,
including BDE, ADO, and IBX. With these three technologies, you may wonder why we need
a new one. dbExpress has a number of exciting characteristics, including
• Cross-platform Whereas BDE and ADO are specific to the Windows platform,
dbExpress currently operates under Windows and Linux (the two platforms that
Delphi/Kylix support). If Borland ever decides to support another platform, such as Mac,
BE, or what have you, dbExpress will be there also.
• Low overhead dbExpress is a thin layer over the underlying database engine’s API. For
this reason, it adds very little overhead to database operations.
• High-performance Largely because of its low overhead, dbExpress is extremely high-
performance. It is designed to work in conjunction with Delphi’s client dataset technology.
6
DELPHI/KYLIX DATABASE DEVELOPMENT

• Easy to distribute Again largely because of its low overhead, dbExpress applications
are easy to redistribute. A typical multitier application needs to deploy MIDAS.DLL and
a dbExpress driver for the back-end database, which commonly weighs in at around
150KB. Contrast this to the BDE’s 10MB footprint.

Databases Used in This Book


dbExpress can connect to a number of database backends, including InterBase, Oracle, DB2,
and MySQL. I had to pick a single database engine to use for the examples presented in this
book. I chose InterBase, for four reasons:
• It’s free. Anyone can download a free copy of InterBase from Borland’s Web site and
work with any of the examples in this book.
• It’s universally accessible. InterBase ships on the Delphi and Kylix CDs, so if you have a
copy of either Delphi 6 or Kylix, you should already have a copy of InterBase.
• It’s manageable. I can easily provide a small InterBase database for download. You can
copy the database onto your local machine and be off and running. I don’t need to worry
about how I’m going to redistribute a 30MB Oracle database to my readers.
• It’s the only one of the four database engines that I have.

Conventions Used in This Book


Several typographic conventions are used through Delphi/Kylix Database Development. These
have been kept to a minimum in an attempt to make the text as concise and clean as possible,
but the ones that have been used should help clarify certain types of text. Specifically,
monospace font is used for Web addresses, code listings, and Object Pascal syntax, such as
TClientDataSet. Filenames are written with lowercase letters.

Contacting the Author


If you would like to contact me regarding any questions, comments, praise, or criticism you
might have concerning this book, please feel free to email me at
[email protected]. I will do my best to respond to you as quickly as possible.
Please understand, though, that I receive a large amount of e-mail on a daily basis, so it can
sometimes take a little while.
Establishing and Using CHAPTER

1
Database Connections

IN THIS CHAPTER
• Connecting to and Disconnecting from a
Database 8

• Retrieving Database Metadata 18

• Executing DDL and DML Statements 27

• Transaction Support 37

• Providing Feedback During SQL


Operations 46
Chapter 1
8

In this chapter, I’ll show you how to connect to and disconnect from a database using
dbExpress, as well as how to manage the connection after you’ve established it.

Connecting to and Disconnecting from a Database


Before you can perform any operation on a database, you must connect to it. Connecting to a
database ensures that the database is physically accessible from your location and that you
have the necessary rights to connect.

NOTE
Just because you have the rights to connect to a database doesn’t mean you have the
right to actually do anything with the database. You might be prevented from creating
new tables, modifying data, or even viewing existing data.

To connect to a database using dbExpress, you use the TSQLConnection component.


TSQLConnection publishes a short list of properties that you can use to specify connection
parameters and attributes. These properties are listed in Table 1.1.

TABLE 1.1 TSQLConnection Properties

Property Description
Connected A Boolean property that you can set to True to try connecting to
a database, or set to False to disconnect.
ConnectionName Used to establish a named connection. Setting ConnectionName
automatically sets DriverName, GetDriverFunc, LibraryName,
Params, and VendorLib. See the section titled “Named
Connections” for more information on setting up a named con-
nection.
DriverName Indicates the type of database you are connecting to, such as
InterBase, Oracle, and so on.
GetDriverFunc Specifies the name of the function exported by the dbExpress dri-
ver that provides access to the driver.
KeepConnection When True, keeps the connection to the database, even when
there are no active datasets for the connection. When False,
drops the connection as soon as all active datasets are closed.
LibraryName Specifies the name of the dbExpress database driver, such as
dbexpint.dll.
Establishing and Using Database Connections
9

TABLE 1.1 Continued 1


Property Description

ESTABLISHING AND
USING DATABSE
CONNECTIONS
LoadParamsOnConnect When True, dbExpress loads the DriverName, GetDriverFunc,
LibraryName, Params, and VendorLib from the
dbxconnections.ini configuration file at runtime. When False, all
properties must be set at design time.
LoginPrompt When True, dbExpress prompts the user for username and
password information when connecting to the database. When
False, the application must supply username and password
information directly, through the Params property.
Params Enables you to set database-specific parameters at design time or
at runtime. See the section “Setting Database Parameters” for
more information.
TableScope Specifies the types of information returned by GetTableNames.
This property is discussed in more detail in the section
“Retrieving Database Metadata.”
VendorLib Refers to the database vendor client library used for connecting
to the database server. For example, InterBase supplies the
gds32.dll client-side library.

Establishing the Connection


To connect to a database, set the appropriate connection properties and issue one of the following
two statements:
SQLConnection1.Connected := True;

or
SQLConnection1.Open;

Both of these statements do the same thing, and it is up to you to decide whether you prefer to
set a property or call a method to connect to the database.
The properties you set depend on whether you elect to use a named connection or an unnamed
connection. Both connection types are discussed in the following two sections.

Named Connections
Named connections refer to the fact that you set the properties for a connection in the file
dbxconnections.ini, and give the properties a name. For example, the following example
illustrates how you might set up a named connection for an accounting package:
Chapter 1
10

[Accounting]
BlobSize=-1
CommitRetain=False
Database=D:\InterbaseData\Accounting.gdb
DriverName=Interbase
LocaleCode=0
Password=masterkey
RoleName=Admin
ServerCharSet=ASCII
SQLDialect=1
Interbase TransIsolation=ReadCommited
User_Name=sysdba
WaitOnLocks=True

There are actually two places that you can establish a named connection: on your development
machine, or on the end user’s machine. To create a named connection on your development
machine, drop a TSQLConnection component onto a form or data module. Either double-click
the component, or right-click the component and select Edit Connection Properties from
the pop-up menu. The dbExpress Connection Editor, shown in Figure 1.1, appears.

FIGURE 1.1
The Connection Editor helps you to easily create named connections.

On the left side of the Connection Editor, you see a Driver Name combo box and a
Connection Name list box. By default, the list box shows all existing named connections on
your development machine. If you only want to see named connections for InterBase, Oracle,
or another database server, select the appropriate server from the Driver Name combo box.
Click a connection name in the list box to view the settings for that named connection, or click
the Add Connection button (which looks like a plus sign) on the toolbar to create a new named
connection.
Establishing and Using Database Connections
11

The dbExpress Connection Editor displays the settings for the selected, named connection on 1
the right side in the Connection Settings section. You should set the appropriate settings for

ESTABLISHING AND
USING DATABSE
your database engine. For example, with an InterBase connection, you want to at least set the

CONNECTIONS
Database setting. You might also want to enter values for RoleName, SQLDialect, and User_Name.

For an Oracle connection, you probably want to set the Database, and you might also want to
set values for User_Name.

CAUTION
Although you can set the Password in a named connection, I would generally advise
you not to. If you’re working with a production database, anyone could open the
Connection Editor (or simply view dbxconnections.ini) on your computer to determine
the password for a given database. For this reason, you usually want to provide the
database password at runtime.

If you want your end users to be able to create their own named connections, you should
redistribute dbxconnections.ini along with your application. Your users can either edit
dbxconnections manually, or you can write and redistribute a utility program similar to the
dbExpress Connection Editor that assists your end users in creating named connections.

Unnamed Connections
Named connections are useful in applications that support a number of different database
servers. However, many database applications are written to work with a single database backend.
For example, you might distribute an application on CD that ships with a precreated InterBase
database, such as a parts listing or customer list. In these cases, your end users have no need to
create their own databases. They only need to access the database that you provide on the CD.
For these situations, a named connection is unnecessary. Instead, you set the connection
properties at design time (excluding User_Name and Password, if your application requires
the user to log in at runtime).

Setting Database Parameters


TSQLConnection’s Params property contains settings specific to the database server you are
connecting to. Several properties, listed in Table 1.2, are common to all databases. You should
refer to your database server documentation for information on other database-specific properties.
Chapter 1
12

TABLE 1.2 Common Database Parameters


Property Description
Database Specifies the database to connect to. For an InterBase database, this
refers to the actual filename, such as \\SERVER\D:\RemoteData.gdb.
For an Oracle database, this refers to the entry in TNSNames.ora that
uniquely identifies the database.
Password Password corresponds to User_Name. You usually do not want to set
this at design time, as it bypasses database security.
User_Name The username employed while establishing the connection. You
typically want the user to supply this information at runtime.

You can also access the Params property at runtime to set database parameters, like this:
SQLConnection1.Params.Values[‘Database’] := ‘D:\Interbase\LocalDatabase.gdb’;

For an InterBase connection, you can specify the database in one of three ways: as a local
path, as a UNC path, or as a TCP path. The following are examples of these three constructs,
respectively:
D:\Interbase\LocalDatabase.gdb
\\SERVER\D:\Data\RemoteDatabase.gdb
192.168.0.1:D:\Data\RemoteDatabase.gdb

It is generally advisable to use either a UNC path or a TCP path when connecting to the database
because InterBase has difficulties connecting to a database using a local path under certain
conditions (such as connecting to a database from within a service application). You can
connect to a local database with a TCP path by using 127.0.0.1 as the IP address, like this:
127.0.0.1:D:\Interbase\LocalDatabase.gdb

Controlling Login
As indicated previously, the connection’s LoginPrompt property determines whether
VCL/DataCLX automatically prompts the user for a username and password at runtime. If you
set LoginPrompt to True, the default connection dialog is displayed at runtime, as shown in
Figure 1.2.
You can override the default login dialog and provide your own means of retrieving the
username and password by handling the connection’s OnLogin event, and setting the appropriate
parameters there. The following code snippet shows how:
Establishing and Using Database Connections
13

procedure TForm1.SQLConnection1Login(Database: TSQLConnection;


LoginParams: TStrings);
1

ESTABLISHING AND
var

USING DATABSE
CONNECTIONS
TheUserName: string;
ThePassword: string;
begin
// Display a custom dialog to retrieve TheUserName and ThePassword

LoginParams.Values[szUSERNAME] := TheUserName;
LoginParams.Values[szPASSWORD] := ThePassword;
end;

FIGURE 1.2
Delphi’s default Database Login dialog.

Disconnecting from the Database


When you are finished accessing the database in your application, you should disconnect from
it. This releases resources on both the client and the server. You can disconnect from the
database manually or automatically.

Manually Disconnecting from a Database


Manually disconnecting from a database is straightforward. Simply issue one of the following
two commands:
SQLConnection1.Connected := False;

or
SQLConnection1.Close;

The method you use depends on whether you prefer to set a property or call a method to
disconnect from the database.

Automatically Disconnecting from a Database


If you want to enable dbExpress to automatically disconnect from the database when there are
no open datasets, you should set TSQLConnection.KeepConnection := False. VCL/DataCLX
monitors the number of open datasets on the connection, and when the last dataset is closed,
the connection is automatically dropped.
Chapter 1
14

CAUTION
You should not set KeepConnection to False in cases where the connection takes a
long time to establish, or in applications where you frequently open and close datasets.
Repeated connecting to and disconnecting from a database (especially in situations
where it takes a long time to connect) can severely impact program performance.

Connect and Disconnect Events


TSQLConnection surfaces a handful of events that fire at opportune times during the
connect/disconnect process. The events and their usage are listed in Table 1.3.

TABLE 1.3 TSQLConnection Events

Event Description
AfterConnect Fires after the database connection has been successfully established.
AfterDisconnect Fires after the database connection has been dropped.
BeforeConnect Fires immediately before the connection to the database is
attempted. You can raise an exception in this event handler to prevent
the connection from being established.
BeforeDisconnect Fires immediately before the database connection is dropped. You
can raise an exception in this event handler to prevent the connection
from being dropped.
OnLogin Fires before the connection is made so that you can provide a
username and password at runtime.

Listing 1.1 contains the source code for an application that demonstrates when, and under what
circumstances, these events fire. It also shows how you can prevent the user from connecting to
or disconnecting from the database.

LISTING 1.1 Events—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DBXpress,
ExtCtrls, DB, SqlExpr, QStdCtrls, QExtCtrls;
Establishing and Using Database Connections
15

LISTING 1.1 Continued 1

ESTABLISHING AND
type

USING DATABSE
CONNECTIONS
TfrmMain = class(TForm)
conn: TSQLConnection;
pnlClient: TPanel;
pnlBottom: TPanel;
btnConnect: TButton;
btnDisconnect: TButton;
Label1: TLabel;
lbEvents: TListBox;
grpOptions: TGroupBox;
cbAllowConnect: TCheckBox;
cbAllowDisconnect: TCheckBox;
procedure connAfterConnect(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure connBeforeConnect(Sender: TObject);
procedure connBeforeDisconnect(Sender: TObject);
procedure connLogin(Database: TSQLConnection;
LoginParams: TStrings);
procedure btnConnectClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
lbEvents.Items.Add(‘AfterConnect’);
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
lbEvents.Items.Add(‘AfterDisconnect’);
end;
Chapter 1
16

LISTING 1.1 Continued

procedure TfrmMain.connBeforeConnect(Sender: TObject);


begin
lbEvents.Items.Add(‘BeforeConnect’);
if not cbAllowConnect.Checked then
Abort;
end;

procedure TfrmMain.connBeforeDisconnect(Sender: TObject);


begin
lbEvents.Items.Add(‘BeforeDisconnect’);
if not cbAllowDisconnect.Checked then
Abort;
end;

procedure TfrmMain.connLogin(Database: TSQLConnection;


LoginParams: TStrings);
begin
lbEvents.Items.Add(‘OnLogin’);
end;

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
lbEvents.Items.Add(‘---Begin Open---’);
try
conn.Open;
except
on EAbort do
lbEvents.Items.Add(‘Connect aborted’);

on E: Exception do
lbEvents.Items.Add(E.Message);
end;
lbEvents.Items.Add(‘---End Open---’);
lbEvents.Items.Add(‘’);
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
lbEvents.Items.Add(‘---Begin Close---’);
try
conn.Close;
except
on EAbort do
lbEvents.Items.Add(‘Disconnect aborted’);

on E: Exception do
Establishing and Using Database Connections
17

LISTING 1.1 Continued 1

ESTABLISHING AND
USING DATABSE
lbEvents.Items.Add(E.Message);

CONNECTIONS
end;
lbEvents.Items.Add(‘---End Close---’);
lbEvents.Items.Add(‘’);
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
conn.Close;
end;

end.

Notice in the code that the BeforeConnect and BeforeDisconnect event handlers call Abort if
the appropriate check box is not selected. btnConnectClick and btnDisconnectClick check
for an EAbort (or other exception), and display an appropriate message in the list box if the
connect or disconnect attempt fails for any reason.
Figure 1.3 shows what the application looks like at runtime. The list box is filled with informative
text that illustrates exactly when, and in what order, the connection events fire.

FIGURE 1.3
The Events application makes it easy to understand connection events.
Chapter 1
18

Retrieving Database Metadata


TSQLConnection surfaces a handful of properties that enable you to retrieve basic schema
information from the database, including table, field, index, stored procedure, and stored
procedure parameter attributes. The following sections describe how to obtain each of these
types of information from the database connection.

GetTableNames
You can call GetTableNames to retrieve a list of the tables in the database, including user
tables, system tables, views, and synonyms (Oracle databases only).
TSQLConnection.GetTableNames is defined in sqlexpr.pas like this:
procedure GetTableNames(List: TStrings; SystemTables: Boolean = False);

The first parameter specifies the string list in which the table names are returned. Any existing
strings in the list are cleared. The second parameter indicates whether to return only system
tables.
If the SystemTables parameter is set to True, only system tables are added to the list, regardless
of the current setting for the TableScope property (shown in Table 1.4). If SystemTables is
False, TableScope controls the types of tables that are added to the list.

TABLE 1.4 TableScope Settings

Property Description
TsSynonym Synonyms
TsSysTable System tables
TsTable Normal, user-defined tables
TsView Views

The following code snippet shows a typical call to GetTableNames:


GetTableNames(ListBox1.Items, False);

GetFieldNames
GetFieldNames is used to retrieve the names of all fields defined for a given table or view.
GetFieldNames takes two parameters, and is defined like this:

procedure GetFieldNames(const TableName: string; List: TStrings);


Establishing and Using Database Connections
19

Pass the table name for which you want to retrieve field names in the first parameter. The 1
second parameter indicates the list in which the resulting field names are to be loaded. Any

ESTABLISHING AND
USING DATABSE
existing strings in the list are deleted.

CONNECTIONS
GetFieldNames(‘CONTACTS’, ListBox1.Items);

GetIndexNames
Similar to GetFieldNames, GetIndexNames is used to retrieve the names of all indexes defined
on a given table. GetIndexNames is defined as follows:
procedure GetIndexNames(const TableName: string; List: TStrings);

As with GetFieldNames, TableName represents the table that you want to get index names for.
List indicates the list in which the resulting index names are to be loaded. Any existing strings
in the list are deleted.
GetIndexNames(‘CONTACTS’, ListBox1.Items);

GetProcedureNames
To retrieve a list of stored procedures in a database, call the GetProcedureNames method.
GetProcedureNames is defined like this:

procedure GetProcedureNames(List: TStrings);

Upon return from the procedure, List contains the list of stored procedure names. Any
information previously stored in the list is deleted.
GetProcedureNames(ListBox1.Items);

GetProcedureParams
GetProcedureParams returns a list of parameters for a given stored procedure. It is defined
like this:
procedure GetProcedureParams(ProcedureName:string; List: TList);

The first parameter, ProcedureName, specifies the name of the stored procedure that you want
to retrieve parameter names for. The second parameter refers to a precreated TList in which
the procedure parameters are returned. Upon return from the procedure, List contains a list of
parameters for the stored procedure.
List itself is not directly usable. You should call the helper function, LoadParamListItems, to
convert the TList to a TParams object, which is a much easier structure to inspect.
The following code snippet shows how to correctly retrieve parameters for the stored procedure
named CONTACTSBYSTATE.
Chapter 1
20

var
listParams: TList;
Params: TParams;
begin
listParams := TList.Create;
try
SQLConnection1.GetProcedureParams(‘CONTACTSBYSTATE’, listParams);
Params := TParams.Create;
try
LoadParamListItems(Params, listParams);

// Do something with Params here...


finally
Params.Free;
end;
finally
FreeProcParams(listParams);
end;

Note the call to FreeProcParams at the end of this code snippet. FreeProcParams frees and
nils the listParams TList, so you don’t need to explicitly free the list.
Listing 1.2 shows the complete source code for a sample application that uses TSQLConnection
to retrieve table, field, index, procedure, and parameter names from an InterBase database.

LISTING 1.2 MetaData—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,
DBXpress, QComCtrls, QStdCtrls, DB, SqlExpr;

type
TfrmMain = class(TForm)
conn: TSQLConnection;
pnlBottom: TPanel;
lblConnection: TLabel;
pnlClient: TPanel;
grpTableScope: TGroupBox;
btnTable: TCheckBox;
btnView: TCheckBox;
btnSynonym: TCheckBox;
btnSystemTable: TCheckBox;
Establishing and Using Database Connections
21

LISTING 1.2 Continued 1

ESTABLISHING AND
btnConnect: TButton;

USING DATABSE
CONNECTIONS
btnDisconnect: TButton;
Label4: TLabel;
Label5: TLabel;
OpenDialog1: TOpenDialog;
PageControl1: TPageControl;
tabTables: TTabSheet;
tabProcedures: TTabSheet;
cbProcedure: TComboBox;
Label2: TLabel;
Label3: TLabel;
cbTable: TComboBox;
lbFields: TListBox;
lbIndexes: TListBox;
Label1: TLabel;
Label6: TLabel;
Label7: TLabel;
lvParameters: TListView;
procedure btnConnectClick(Sender: TObject);
procedure cbTableClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure cbProcedureClick(Sender: TObject);
procedure connAfterConnect(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);

procedure CheckScope(Value: Boolean; TableScope: TTableScope);


begin
if Value then
conn.TableScope := conn.TableScope + [TableScope]
Chapter 1
22

LISTING 1.2 Continued

else
conn.TableScope := conn.TableScope - [TableScope];
end;

begin
if OpenDialog1.Execute then begin
conn.Params.Values[‘Database’] := OpenDialog1.FileName;
CheckScope(btnTable.Checked, tsTable);
CheckScope(btnView.Checked, tsView);
CheckScope(btnSynonym.Checked, tsSynonym);
CheckScope(btnSystemTable.Checked, tsSysTable);
conn.Open;
end;
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
conn.Close;
end;

procedure TfrmMain.cbTableClick(Sender: TObject);


begin
conn.GetFieldNames(cbTable.Items[cbTable.ItemIndex], lbFields.Items);
conn.GetIndexNames(cbTable.Items[cbTable.ItemIndex], lbIndexes.Items);
end;

procedure TfrmMain.cbProcedureClick(Sender: TObject);


var
listParams: TList;
Params: TParams;
Index: Integer;
Param: TParam;
ListItem: TListItem;
begin
listParams := TList.Create;
try
conn.GetProcedureParams(cbProcedure.Items[cbProcedure.ItemIndex],
listParams);
Params := TParams.Create;
try
LoadParamListItems(Params, listParams);

lvParameters.Items.BeginUpdate;
try
Establishing and Using Database Connections
23

LISTING 1.2 Continued 1

ESTABLISHING AND
lvParameters.Items.Clear;

USING DATABSE
CONNECTIONS
for Index := 0 to Params.Count - 1 do begin
Param := Params[Index];
ListItem := lvParameters.Items.Add;
ListItem.Caption := Param.Name;
case Param.DataType of
ftUnknown: ListItem.SubItems.Add(‘Unknown’);
ftString: ListItem.SubItems.Add(‘String’);
ftSmallint: ListItem.SubItems.Add(‘Smallint’);
ftInteger: ListItem.SubItems.Add(‘Integer’);
ftWord: ListItem.SubItems.Add(‘Word’);
ftBoolean: ListItem.SubItems.Add(‘Boolean’);
ftFloat: ListItem.SubItems.Add(‘Float’);
ftCurrency: ListItem.SubItems.Add(‘Currency’);
ftBCD: ListItem.SubItems.Add(‘BCD’);
ftDate: ListItem.SubItems.Add(‘Date’);
ftTime: ListItem.SubItems.Add(‘Time’);
ftDateTime: ListItem.SubItems.Add(‘DateTime’);
ftBytes: ListItem.SubItems.Add(‘Bytes’);
ftVarBytes: ListItem.SubItems.Add(‘VarBytes’);
ftAutoInc: ListItem.SubItems.Add(‘AutoInc’);
ftBlob: ListItem.SubItems.Add(‘Blob’);
ftMemo: ListItem.SubItems.Add(‘Memo’);
ftGraphic: ListItem.SubItems.Add(‘Graphic’);
ftFmtMemo: ListItem.SubItems.Add(‘FmtMemo’);
ftParadoxOle: ListItem.SubItems.Add(‘ParadoxOle’);
ftDBaseOle: ListItem.SubItems.Add(‘DBaseOle’);
ftTypedBinary: ListItem.SubItems.Add(‘TypedBinary’);
ftCursor: ListItem.SubItems.Add(‘Cursor’);
ftFixedChar: ListItem.SubItems.Add(‘FixedChar’);
ftWideString: ListItem.SubItems.Add(‘WideString’);
ftLargeint: ListItem.SubItems.Add(‘Largeint’);
ftADT: ListItem.SubItems.Add(‘ADT’);
ftArray: ListItem.SubItems.Add(‘Array’);
ftReference: ListItem.SubItems.Add(‘Reference’);
ftDataSet: ListItem.SubItems.Add(‘DataSet’);
ftOraBlob: ListItem.SubItems.Add(‘OraBlob’);
ftOraClob: ListItem.SubItems.Add(‘OraClob’);
ftVariant: ListItem.SubItems.Add(‘Variant’);
ftInterface: ListItem.SubItems.Add(‘Interface’);
ftIDispatch: ListItem.SubItems.Add(‘IDispatch’);
ftGuid: ListItem.SubItems.Add(‘Guid’);
ftTimeStamp: ListItem.SubItems.Add(‘TimeStamp’);
Chapter 1
24

LISTING 1.2 Continued


ftFMTBcd: ListItem.SubItems.Add(‘FMTBcd’);
end;
end;
finally
lvParameters.Items.EndUpdate;
end;
finally
Params.Free;
end;
finally
FreeProcParams(listParams);
end;
end;

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
btnTable.Enabled := False;
btnView.Enabled := False;
btnSynonym.Enabled := False;
btnSystemTable.Enabled := False;

lblConnection.Font.Color := clGreen;
lblConnection.Caption := conn.Params.Values[‘Database’];

conn.GetTableNames(cbTable.Items, btnSystemTable.Checked);
conn.GetProcedureNames(cbProcedure.Items);

cbTable.ItemIndex := 0;
cbTableClick(cbTable);
PageControl1.ActivePage := tabTables;
ActiveControl := cbTable;

cbProcedure.ItemIndex := 0;
cbProcedureClick(cbProcedure);
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
btnTable.Enabled := True;
btnView.Enabled := True;
btnSynonym.Enabled := True;
btnSystemTable.Enabled := True;

lblConnection.Font.Color := clRed;
Establishing and Using Database Connections
25

LISTING 1.2 Continued 1

ESTABLISHING AND
USING DATABSE
lblConnection.Caption := ‘Not connected’;

CONNECTIONS
cbTable.Items.Clear;
cbProcedure.Items.Clear;
lbFields.Items.Clear;
lbIndexes.Items.Clear;
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
conn.Close;
end;

end.

Figure 1.4 shows table, field, and index names returned from the CONMAN database.

FIGURE 1.4
Column and field lists for the CONTACTS table.

Figure 1.5 shows a list of procedure parameters for the CONTACTSBYSTATE stored procedure.
Most of the code in Listing 1.2 is fairly straightforward. However, there are two items of
interest that I would like to point out.
First, the cbProcedureClick method illustrates how you can loop through the parameters for a
stored procedure to determine their name, type, and other attributes.
Chapter 1
26

FIGURE 1.5
Parameter names and types for the long list of parameters output from the CONTACTSBYSTATE procedure.

Second, there is a bug in VCL/DataCLX that effectively prevents you from retrieving table,
view, and system table metadata together. If you check Tables, Views, and System Tables at the
same time, GetTableNames (in the connAfterConnect method) does not return any information.
Be aware of this in your own applications. If you need to retrieve all three types of information,
you can do something like the following:
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SQLConnection1.TableScope := [tsTable, tsView];
SQLConnection1.GetTableNames(ListBox1.Items, False);
SQLConnection1.GetTableNames(SL, True);
ListBox1.Items.AddStrings(SL);
finally
SL.Free;
end;
end;

This code retrieves only table and view information first, putting the results into a list box.
Next, it retrieves only system tables, putting the results into a temporary string list. Finally, it
adds the strings from the temporary string list into the list box. So, the list box contains tables,
views, and system tables.
As you can see from this discussion, the schema information returned from TSQLConnection is
extremely basic. Other than for stored procedure parameters, the only data that
Establishing and Using Database Connections
27

TSQLConnection returns for tables, fields, indexes, and stored procedures is their names. In the 1
following chapter, I’ll show you how to retrieve much more detailed schema information from

ESTABLISHING AND
USING DATABSE
a database.

CONNECTIONS
Executing DDL and DML Statements
The most common operations that you will perform on a database are DDL (Data Definition
Language) and DML (Data Manipulation Language) statements. You can execute DDL and
DML statements directly through a TSQLConnection. DML statements that return a cursor (that
is, SQL SELECT statements) require a dataset component in addition to the TSQLConnection, as
you’ll see in Chapter 3, “Client Dataset Basics.”

DDL Commands
DDL commands are statements that operate on the database schema, rather than on the data
itself. In the previous section, I showed you how to retrieve information about the database
schema. In this section, I’ll show you how to change the database schema.
TSQLConnection provides a method named ExecuteDirect, which you use to execute DDL
commands. ExecuteDirect takes a single parameter, which is the SQL command to execute.
It returns 0 on success, or a dbExpress error code on failure. dbExpress error codes can be
found in the file DBXpress.pas, which is included with Delphi.
The Direct part of the name ExecuteDirect comes from the fact that the statement is sent
directly to the database. The statement is not prepared before it is executed, and it cannot
contain any parameters. (Parameterized SQL statements are discussed in the section titled
“Parameterized SQL Statements” later in this chapter.)

NOTE
Some databases don’t support direct SQL execution. On those databases, dbExpress
will internally prepare the SQL statement, and then execute it.

Creating a Table
One of the simplest and most useful DDL commands you can issue is the command to create a
new table in the database. Assume that you want to create a table named EMPLOYEE with the
structure shown in Table 1.5. You would issue the following statement:
SQLConnection1.ExecuteDirect(‘CREATE TABLE EMPLOYEE (EMPNO INTEGER, ‘ +
‘NAME VARCHAR(30), ‘HIREDATE DATE, SALARY DOUBLE PRECISION’);
Chapter 1
28

TABLE 1.5 Sample EMPLOYEE Table Definition


Column Definition
EMPNO INTEGER
NAME VARCHAR(30)
HIREDATE DATE
SALARY DOUBLE PRECISION

Creating a Database
You cannot use ExecuteDirect to create a new InterBase database. The following line of code
does not work:
SQLConnection1.ExecuteDirect(‘CREATE DATABASE ‘’C:\NewData.gdb’’’);

If you try to execute this code, you receive the following exception:
Cannot prepare a CREATE DATABASE/SCHEMA statement.

The InterBase client doesn’t allow direct execution of DDL or DML, so the dbExpress driver
attempts to prepare, and then to execute the CREATE DATABASE statement. Because InterBase
doesn’t allow a CREATE DATABASE statement to be prepared, an exception is raised.
If you want your applications to be capable of creating new InterBase databases, you need to
find another way to do it. One way is to keep an empty copy of the database in your program
directory and copy it when the user creates a new database.
I have found the following to be useful in my own experience: Save a copy of the empty data-
base as a resource in your application. When the user creates a new database, save the resource
to disk under the filename that the user selects. The following explanation shows how this can
be done in a Windows environment. Note that this is Windows specific and can’t be used for a
Linux or cross-platform application.
First, you want to create a resource script file that turns your empty database into a resource.
Assuming that you have an empty copy of your database in the D:\Interbase directory, the
following script file creates a resource named EMPTYDB:
EMPTYDB RCDATA DISCARDABLE “D:\Interbase\Empty.GDB”

Save this script as EmptyDB.RC. To create a .RES file from the resource script, execute the
following command:
BRCC32 EMPTYDB.RC
Establishing and Using Database Connections
29

This creates the file EMPTYDB.RES, which can be included in your application. Somewhere 1
in your program code, include the code from Listing 1.3.

ESTABLISHING AND
USING DATABSE
CONNECTIONS
LISTING 1.3 Code to Create an Empty Database from a Resource
{$R EmptyDB.RES} // Add the empty database to the program executable

procedure CreateDatabase(const DatabaseName: string);


var
HRsrc: THandle;
Stream: TResourceStream;
begin
HRsrc := FindResource(HInstance, PChar(‘EMPTYDB’), RT_RCDATA);
if HRsrc <> 0 then begin
Stream := TResourceStream.Create(HInstance, ‘EMPTYDB’, RT_RCDATA);
try
Stream.SaveToFile(DatabaseName);
finally
Stream.Free;
end;
end else
raise Exception.Create(‘Internal error: unable to create database.’);
end;

Now when you want to create a new database in your application, you simply call
CreateDatabase, passing the complete pathname of the database, like this:

CreateDatabase(‘C:\NewDatabase.gdb’);

As you’re working on your application, if you change the database schema, you must remem-
ber to reissue the BRCC32 command (shown previously) to re-create the EMPTYDB.RES file.
Otherwise, you wind up with an incorrect, empty database inside your application.
Another way that you could handle this situation is to make direct, low-level calls to the data-
base API from within your application. This approach has the benefit of being cross-platform,
but you must learn the appropriate API commands for the database backend in question.

DML Commands
Whereas DDL commands are used to define the database schema, DML commands are used to
manipulate (read, write, and update) the data in the database.
Chapter 1
30

Simple SQL Statements


The simplest DML statement is one that takes no parameters and returns no rows. For example,
you might want to delete all employees whose Active status is N. To do so, you would issue
the following statement:
SQLConnection1.ExecuteDirect(‘DELETE FROM EMPLOYEE WHERE Active = ‘’N’’’);

Note in the preceding code snippet that the ‘N’ is surrounded by quotes. A better approach to
quoting string constants manually is to use the RTL function, QuotedStr. QuotedStr takes a
string parameter and returns the string within quotes. One of the main reasons for using
QuotedStr is that it correctly handles string constants that contain quotes. For example, given
the name O’Toole, would you know how to quote it manually? The correct way is
‘O’’Toole’

Using QuotedStr, you don’t have to worry about how to correctly quote a string. The preceding
ExecuteDirect call becomes

SQLConnection1.ExecuteDirect(‘DELETE FROM EMPLOYEE WHERE Active = ‘ +


QuotedStr(‘N’));

Parameterized SQL Statements


Many times, you want to execute the same basic SQL statement more than once, changing
only the values that are passed to the parameters in the statement. For example, say you want
to insert a number of records into the previously created EMPLOYEE table. To insert an
employee, John Doe, you would execute the following SQL statement:
SQLConnection1.ExecuteDirect(‘INSERT INTO EMPLOYEE VALUES (123,
‘’John Doe’’, ‘’5/15/1994’’, 35000)’);

Again, QuotedStr could be used here instead of quoting the strings manually.
For each employee that you want to add, you would create and execute a similar SQL statement.
Parameterized SQL statements enable you to create a sort of statement template, in which you
can easily enter the appropriate values for each statement before executing. For the EMPLOYEE
table, the SQL INSERT statement becomes the following:
INSERT INTO EMPLOYEE VALUES (:EmpNumber, :Name, :Hired, :Salary)

Each value to be inserted into the database is replaced with a parameter. Parameters are easily
detectable because they start with a colon. Internally, the dbExpress components parse the SQL
statement, and convert the parameters into question marks that the core dbExpress code
supports. In turn, the dbExpress driver replaces the question marks with the parameter markers,
which are supported by the backend database engine.
Establishing and Using Database Connections
31

For example, given the previous INSERT statement, TSQLConnection changes it to 1

ESTABLISHING AND
INSERT INTO EMPLOYEE VALUES (?, ?, ?, ?)

USING DATABSE
CONNECTIONS
The dbExpress driver may replace the question marks with some other construct that is specific
to the database server.

NOTE
Parameters do not need to have the same name as the underlying column in the
table. Notice in the preceding code snippet that I named the parameter EmpNumber
instead of EmpNo, and Hired instead of DateHired.

After you create the SQL statement, you need to fill in the value of each parameter before
executing. The following code snippet shows how this is done:
Params := TParams.Create(nil);
try
// Create the parameters
Params.CreateParam(ftInteger, ‘EmpNumber’, ptInput);
Params.CreateParam(ftString, ‘Name’, ptInput);
Params.CreateParam(ftSQLDateTime, ‘Hired’, ptInput);
Params.CreateParam(ftFloat, ‘Salary’, ptInput);

// Assign values to the parameters


Params.ParamByName(‘EmpNumber’).Value := 123;
Params.ParamByName(‘Name’).Value := ‘John Doe’;
Params.ParamByName(‘Hired’).Value := ‘5/15/1994’;
Params.ParamByName(‘Salary’).Value := 35000;

// Execute the statement


SQLConnection1.Execute(‘INSERT INTO EMPLOYEE VALUES (:EmpNumber, :Name, ‘ +
‘:Hired, :Salary)’, Params);
finally
Params.Free;
end;

You might be wondering why you would ever want to do this rather than simply executing
each statement directly. After all, creating the parameters and assigning them takes a lot more
effort. In addition, time tests on my development machine show that the second method takes
about twice as long as the first.
In cases where I need to repeatedly execute the same statement, I still prefer using a
parameterized query for the following reasons:
Chapter 1
32

• Simplicity. Given a long, complicated SQL statement, it’s often difficult to form the SQL
statement manually. Getting the quotes lined up correctly can be prone to errors, considering
that you must double up on single quotes in the Pascal language.
• Robustness. Say for the sake of argument that one of the employee names has a quote in
it, such as Frank O’Donnell. If you don’t use parameters, the quote looks as if it were the
terminating quote on the name. The parameterless SQL statement would look like this:
INSERT INTO EMPLOYEE VALUES (123, ‘Frank O’Donnell’, ‘5/15/1994’, 35000)

In this case, the name appears to be Frank O, and the following character (D) in the
statement is in error. You can solve this problem by double quoting the name, like this:
INSERT INTO EMPLOYEE VALUES (123, ‘Frank O’’Donnell’, ‘5/15/1994’, 35000)

• When using parameters, a true relational database can compile the prepared statement,
and then use that compiled version for all subsequent calls to the SQL statement. When
repeatedly issuing the same SQL statement, this more than compensates for the additional
time required to prepare the SQL statement.
The use of QuotedStr to automatically quote strings helps to alleviate the first two “problems.”

SQL Statements That Return a Cursor


In the previous two sections, I discussed simple and parameterized SQL statements, but so far,
no statements have returned any data from the database server. The SELECT statement is the
most commonly used DML statement. So, why am I shying away from it?
It’s because SELECT statements return data, and you need to have a place to put the resulting
data. For dbExpress, that place is a TCustomSQLDataSet, which I don’t discuss in detail until
the next chapter. For now, I’ll just give a quick overview of how to retrieve data from a
TSQLConnection.

The Execute method, which I discussed in the previous section, actually takes a third parameter:
a pointer to a result set.
function Execute(const SQL: string; Params: TParams;
ResultSet: Pointer = nil): Integer;

If you don’t pass a third parameter to Execute, it defaults to nil, which means that the statement
doesn’t return a result set.

NOTE
If you pass a nil value for the ResultSet and the SQL command actually does return
a result set, the result set is simply discarded. No exception is raised, and no error is
returned.
Establishing and Using Database Connections
33

The following code snippet shows one way to execute an SQL statement that returns a result 1
set. In the following chapter, you’ll see easier ways to do this.

ESTABLISHING AND
USING DATABSE
CONNECTIONS
SQLDataSet1 := TSQLDataSet.Create(nil);
try
SQLConnection1.Execute(‘SELECT * FROM EMPLOYEE’, nil, SQLDataSet1);

// Do something with SQLDataSet1 here...


finally
SQLDataSet1.Free;
end;

The preceding code also shows the correct way to execute a nonparameterized SELECT statement:
Simply pass nil for Params.
The following example program, DDLSQL, shows how to create a test table, fill it with data,
and destroy the table programmatically. It also demonstrates how to retrieve data from the table
using the Execute method. Listing 1.4 shows the complete code for DDLSQL.

LISTING 1.4 DDLSQL—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
QStdCtrls, QExtCtrls, DBXpress, DB, SqlExpr;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
conn: TSQLConnection;
btnCreate: TButton;
btnPopulate: TButton;
btnConnect: TButton;
btnDelete: TButton;
btnDisconnect: TButton;
btnParameters: TButton;
btnDrop: TButton;
lbOutput: TListBox;
procedure btnCreateClick(Sender: TObject);
procedure btnConnectClick(Sender: TObject);
procedure btnPopulateClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure btnDeleteClick(Sender: TObject);
Chapter 1
34

LISTING 1.4 Continued

procedure btnParametersClick(Sender: TObject);


procedure btnDropClick(Sender: TObject);
procedure connAfterConnect(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
conn.Open;
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
conn.Close;
end;

procedure TfrmMain.btnCreateClick(Sender: TObject);


begin
conn.ExecuteDirect(‘CREATE TABLE TESTING (NAME VARCHAR(20) NOT NULL, ‘ +
‘AGE INTEGER, PRIMARY KEY (NAME))’);

lbOutput.Items.Add(‘Created TESTING table’);


end;

procedure TfrmMain.btnPopulateClick(Sender: TObject);


begin
conn.ExecuteDirect(‘INSERT INTO TESTING VALUES (“Eric”, 34)’);
conn.ExecuteDirect(‘INSERT INTO TESTING VALUES (“Tina”, 33)’);

lbOutput.Items.Add(‘Added Eric and Tina to TESTING table’);


end;
Establishing and Using Database Connections
35

LISTING 1.4 Continued 1

ESTABLISHING AND
USING DATABSE
procedure TfrmMain.btnDeleteClick(Sender: TObject);

CONNECTIONS
begin
conn.ExecuteDirect(‘DELETE FROM TESTING WHERE NAME = “Tina”’);

lbOutput.Items.Add(‘Deleted Tina from TESTING table’);


end;

procedure TfrmMain.btnParametersClick(Sender: TObject);


const
SQL = ‘INSERT INTO TESTING VALUES (:Name, :Age)’;
var
Params: TParams;
begin
Params := TParams.Create(nil);
try
Params.CreateParam(ftString, ‘PName’, ptInput);
Params.CreateParam(ftInteger, ‘PAge’, ptInput);

// Add first name


Params.ParamByName(‘PName’).AsString := ‘Mike’;
Params.ParamByName(‘PAge’).AsInteger := 34;
conn.Execute(SQL, Params);

lbOutput.Items.Add(‘Added Mike to TESTING table’);


finally
Params.Free;
end;
end;

procedure TfrmMain.btnDropClick(Sender: TObject);


begin
conn.ExecuteDirect(‘DROP TABLE TESTING’);

lbOutput.Items.Add(‘Removed TESTING table’);


end;

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
btnConnect.Enabled := False;
btnCreate.Enabled := True;
btnPopulate.Enabled := True;
btnDelete.Enabled := True;
btnParameters.Enabled := True;
btnDrop.Enabled := True;
Chapter 1
36

LISTING 1.4 Continued

btnDisconnect.Enabled := True;

lbOutput.Items.Add(‘Connected’);
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
btnConnect.Enabled := True;
btnCreate.Enabled := False;
btnPopulate.Enabled := False;
btnDelete.Enabled := False;
btnParameters.Enabled := False;
btnDrop.Enabled := False;
btnDisconnect.Enabled := False;

lbOutput.Items.Add(‘Disconnected’);
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
conn.Close;
end;

end.

Figure 1.6 shows the DDLSQL program at runtime.

FIGURE 1.6
DDLSQL after running the gamut of buttons.
Establishing and Using Database Connections
37

Transaction Support 1

ESTABLISHING AND
USING DATABSE
Most SQL databases, with the notable exception of versions of MySQL prior to 3.23, provide

CONNECTIONS
support for transactions. A transaction must satisfy four criteria, which are called the ACID
properties of transactions. Specifically, transactions are
• Atomic. The transaction must either succeed or fail as a whole. It is not acceptable for
part of the transaction to succeed and part of it to fail.
• Consistent. After a transaction finishes, the data must be left in a consistent state. All
data must adhere to current referential integrity constraints.
• Isolated. Changes made in one transaction must not be visible to another transaction until
the transaction is committed.
• Durable. Once a transaction is committed, its changes must be permanent. Nothing,
including a system crash, must alter the effects of the committed transaction.
Transactions are most easily described with an example, so I’ll go through the most often-cited
example of transaction support.
Imagine a bank with a database that contains clients and account information. John Q.
Customer has both a savings account and a checking account at the bank. He makes a trip to
his local ATM, and decides to transfer $100 from his savings account to his checking account.
In SQL terms, this constitutes two operations: one INSERT statement to record a withdrawal
from the savings account, and a second INSERT statement to record the deposit into the checking
account. Assuming the ATM software is written using Delphi and dbExpress, it might contain
some code similar to the following:
procedure TransferFunds(FromAccountID: Integer; ToAccountID: Integer;
Amount: Double);
var
SQL: string;
Params: TParams;
begin
Params := TParams.Create;
try
SQL := ‘INSERT INTO ACCOUNTDETAIL (ACCOUNTID, TRANSDATE, AMOUNT) ‘ +
VALUES (:AccountID, :TransDate, :TransAmount)’;
Params.ParamByName(‘AccountID’).Value := FromAccountID;
Params.ParamByName(‘TransDate’).Value := Date;
Params.ParamByName(‘TransAmount’).Value := -Amount;
SQLConnection1.Execute(SQL, Params);

Params.ParamByName(‘AccountID’).Value := ToAccountID;
Params.ParamByName(‘TransAmount’).Value := Amount;
Chapter 1
38

SQLConnection1.Execute(SQL, Params);
finally
Params.Free;
end;
end;

Conceptually, this code snippet creates a withdrawal for the originating account, and a deposit
for the target account. At first, it might seem like there’s nothing wrong with this code, but let’s
take a look at the possibilities.
What happens if a power outage, connection failure, or some other catastrophic failure occurs
between the time the withdrawal is recorded and the time the deposit is recorded? The money
would be deducted from the savings account, but it would never get added to the checking
account (which makes Mr. Customer a very unhappy, quite possibly former, customer).
If the code is reversed to create the deposit before the withdrawal, the opposite can happen:
The deposit gets recorded, but the withdrawal never occurs (which makes Mr. Customer very
happy and $100 richer, but the bank is shorted).
Clearly, the ATM software needs to have some assurance that either the withdrawal and the
deposit both occur, or that neither of them occur. This is what transactions are designed to handle.
The following few sections describe how to detect whether a given database supports transactions,
how to start (and subsequently end) a transaction, and how to handle multiple (and nested)
transactions.

Checking for Transaction Support


If you’re writing an application that only works with a single database backend (such as Oracle
or InterBase), you know before you start coding that the database supports transactions. If
you’re writing a general-purpose application that can access many different database backends,
you don’t know up front whether the selected database backend supports transactions or not.
TSQLConnection provides a way to detect whether the underlying database engine supports
transactions: the TransactionsSupported property. Before you check
TransactionsSupported, you need to establish a connection to the database, like this:

SQLConnection1.Open;
...
if SQLConnection1.TransactionsSupported then begin
// Go ahead with transaction code
end else begin
// Transactions not supported - proceed with alternate code or bail out
end;
Establishing and Using Database Connections
39

Starting a Transaction 1

ESTABLISHING AND
When you’ve determined that the database supports transactions, you can start a transaction.

USING DATABSE
CONNECTIONS
To begin, call the method TSQLConnection.StartTransaction. StartTransaction is defined
like this:
procedure StartTransaction( TransDesc: TTransactionDesc);

TTransactionDesc is a record that describes the transaction in detail. Its definition is shown
here:
TTransactionDesc = packed record
TransactionID : LongWord; { Transaction id }
GlobalID : LongWord; { Global transaction id }
IsolationLevel : TTransIsolationLevel; {Transaction Isolation level}
CustomIsolation : LongWord; { DB specific custom isolation }
end;

Table 1.6 shows the meaning of the individual fields in the TTransactionDesc record.

TABLE 1.6 TTransactionDesc Fields

Field Definition
TransactionID User-defined, local transaction number that uniquely identifies the
transaction for purposes of this application.
GlobalID Used for Oracle transactions to define a transaction number that
must be unique across the entire Oracle database.
IsolationLevel Used to specify how this transaction reacts to other transactions.
Valid values for this field are listed in Table 1.7.
CustomIsolation Identifies the custom isolation level when IsolationLevel is set to
xilCUSTOM. No dbExpress drivers currently support this.

Table 1.7 shows the valid settings for TTransactionDesc’s IsolationLevel field.

TABLE 1.7 Valid IsolationLevel Values


Field Definition
xilDIRTYREAD The transaction sees all changes made by other transactions, even if
those changes have not yet been committed. Oracle does not support
this level of transaction isolation.
xilREADCOMMITTED The transaction sees only those changes made by other transactions
that have been committed both before this transaction was started,
and after this transaction was started.
Chapter 1
40

TABLE 1.7 Continued


Field Definition
xilREPEATABLEREAD The transaction sees only those changes made by other transactions,
but only if they were committed before this transaction started.
xilCUSTOM Database-specific isolation level. CustomIsolation specifies the
actual isolation level. No dbExpress drivers currently support this.

The following code snippet shows how to start a transaction.


var
TransDesc: TTransactionDesc;
begin
TransDesc.TransactionID := 1;
TransDesc.IsolationLevel := xilREADCOMMITTED;
SQLConnection1.StartTransaction(TransDesc);
end;

Committing a Transaction
After you have issued the appropriate SQL commands inside a transaction, you want to commit
the transaction. This ends the transaction, and saves any changes made during that transaction.
Committing a transaction is as easy as calling TSQLConnection.Commit, which the following
line of code illustrates:
SQLConnection.Commit(TransDesc);

Rolling Back a Transaction


At times, you start a transaction only to find out later that you don’t want to save the changes
made during that transaction. If that should occur, you can roll back the transaction rather than
committing it. Rolling back a transaction ends the transaction, but all changes made in the
context of the transaction are discarded.
To roll back a transaction, call TSQLConnection.Rollback, like this:
SQLConnection1.Rollback(TransDesc);

Multiple Transactions
You are not limited to just one transaction at a time. Transactions can be nested or overlapped.
Figure 1.7 shows what nested and overlapped transactions look like at a conceptual level.
Establishing and Using Database Connections
41

ESTABLISHING AND
USING DATABSE
CONNECTIONS
FIGURE 1.7
Transactions may be nested or overlapped.

Not all databases support multiple transactions. Unfortunately, to determine whether a database
supports multiple transactions, you can’t check a simple property. TSQLConnection contains a
private field named FSupportsMultiTrans, but there is no public access to it. All is not lost,
however, as you can use the following function to retrieve the value of this property:
function SupportsMultiTrans(conn: TSQLConnection): Boolean;
var
Supported: LongBool;
PropSize: SmallInt;
begin
conn.MetaData.GetOption(eMetaSupportsTransactions, @Supported,
SizeOf(Integer), PropSize);
Result := Supported;
end;

If the database does not support multiple transactions, you want to refrain from calling
StartTransaction while a transaction is active. You can test to see whether a transaction is
currently active by checking the InTransaction property, like this:
if not SQLConnection1.InTransaction then
// Safe to start a transaction

If the database supports multiple transactions, you can nest them, as the following code snippet
shows:
var
TransOuter: TTransactionDesc;
TransInner: TTransactionDesc;
begin
TransOuter.TransactionID := 1;
TransOuter.IsolationLevel := xilREADCOMMITTED;
SQLConnection1.StartTransaction(TransOuter);
try
Chapter 1
42

// Execute some SQL statements here

TransInner.TransactionID := 2;
TransInner.IsolationLevel := xilREADCOMMITTED;
SQLConnection1.StartTransaction(TransInner);
try
// Execute some more SQL statements here

SQLConnection1.Commit(TransInner);
except
SQLConnection1.Rollback(TransInner);
raise;
end;

// Even more SQL statements here

SQLConnection1.Commit(TransOuter);
except
SQLConnection1.Rollback(TransOuter);
raise;
end;
end;

Note in the preceding code that the transactions are enclosed in try/except blocks. If an
exception occurs while executing the code, the transaction is rolled back.
Listing 1.5 contains the complete source code for an application that demonstrates dbExpress
transaction support (including transaction isolation, committing and rolling back transactions,
and nested transactions).

LISTING 1.5 Trans—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,
DBXpress, QStdCtrls, DB, SqlExpr;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
conn: TSQLConnection;
btnConnect: TButton;
btnDisconnect: TButton;
Establishing and Using Database Connections
43

LISTING 1.5 Continued 1

ESTABLISHING AND
lbOutput: TListBox;

USING DATABSE
CONNECTIONS
btnCommit: TButton;
btnRollback: TButton;
btnMultiLevel: TButton;
btnOverlapping: TButton;
procedure btnConnectClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure btnCommitClick(Sender: TObject);
procedure btnRollbackClick(Sender: TObject);
procedure btnMultiLevelClick(Sender: TObject);
procedure btnOverlappingClick(Sender: TObject);
procedure connAfterConnect(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

function SupportsMultiTrans(conn: TSQLConnection): Boolean;


var
Supported: LongBool;
PropSize: SmallInt;
begin
conn.MetaData.GetOption(eMetaSupportsTransactions, @Supported,
SizeOf(Integer), PropSize);
Result := Supported;
end;

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
conn.Open;

if conn.TransactionsSupported then
lbOutput.Items.Add(‘Connection supports transactions’)
else
Chapter 1
44

LISTING 1.5 Continued

lbOutput.Items.Add(‘Connection does not support transactions’);

if SupportsMultiTrans(conn) then
lbOutput.Items.Add(‘Connection supports multiple transactions’)
else
lbOutput.Items.Add(‘Connection does not support multiple transactions’);
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
conn.Close;
end;

procedure TfrmMain.btnCommitClick(Sender: TObject);


var
TransDesc: TTransactionDesc;
begin
TransDesc.TransactionID := 1;
TransDesc.IsolationLevel := xilREADCOMMITTED;
conn.StartTransaction(TransDesc);
conn.ExecuteDirect(‘DELETE FROM TODOS’);
conn.Commit(TransDesc);
lbOutput.Items.Add(‘Transaction committed’);
end;

procedure TfrmMain.btnRollbackClick(Sender: TObject);


var
TransDesc: TTransactionDesc;
begin
TransDesc.TransactionID := 1;
TransDesc.IsolationLevel := xilREADCOMMITTED;
conn.StartTransaction(TransDesc);
conn.ExecuteDirect(‘DELETE FROM CONTACTS’);
conn.Rollback(TransDesc);
lbOutput.Items.Add(‘Transaction rolled back’);
end;

procedure TfrmMain.btnMultiLevelClick(Sender: TObject);


var
TransDesc1: TTransactionDesc;
TransDesc2: TTransactionDesc;
begin
TransDesc1.TransactionID := 1;
TransDesc1.IsolationLevel := xilREADCOMMITTED;
Establishing and Using Database Connections
45

LISTING 1.5 Continued 1

ESTABLISHING AND
conn.StartTransaction(TransDesc1);

USING DATABSE
CONNECTIONS
conn.ExecuteDirect(‘UPDATE CONTACTS SET PHONE = “Trans 1” WHERE ID = 3’);

TransDesc2.TransactionID := 2;
TransDesc2.IsolationLevel := xilREADCOMMITTED;
conn.StartTransaction(TransDesc2);
conn.ExecuteDirect(‘UPDATE CONTACTS SET PHONE = “Trans 2” WHERE ID = 2’);
conn.Rollback(TransDesc2);
lbOutput.Items.Add(‘Second transaction rolled back’);

conn.Commit(TransDesc1);
lbOutput.Items.Add(‘First transaction committed’);
end;

procedure TfrmMain.btnOverlappingClick(Sender: TObject);


var
TransDesc3: TTransactionDesc;
TransDesc4: TTransactionDesc;
begin
TransDesc3.TransactionID := 3;
TransDesc3.IsolationLevel := xilREADCOMMITTED;

conn.StartTransaction(TransDesc3);
conn.ExecuteDirect(‘UPDATE CONTACTS SET PHONE = “Trans 3” WHERE ID = 3’);

TransDesc4.TransactionID := 4;
TransDesc4.IsolationLevel := xilREADCOMMITTED;
conn.StartTransaction(TransDesc4);
conn.ExecuteDirect(‘UPDATE CONTACTS SET PHONE = “Trans 4” WHERE ID = 2’);

conn.Rollback(TransDesc3);
lbOutput.Items.Add(‘Transaction 3 rolled back’);

conn.Commit(TransDesc4);
lbOutput.Items.Add(‘Transaction 4 committed’);
end;

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
btnConnect.Enabled := False;
btnDisconnect.Enabled := True;
btnCommit.Enabled := True;
btnRollback.Enabled := True;
btnMultiLevel.Enabled := True;
Chapter 1
46

LISTING 1.5 Continued

btnOverlapping.Enabled := True;
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
btnConnect.Enabled := True;
btnDisconnect.Enabled := False;
btnCommit.Enabled := False;
btnRollback.Enabled := False;
btnMultiLevel.Enabled := False;
btnOverlapping.Enabled := False;
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
conn.Close;
end;

end.

Figure 1.8 shows the Trans application at runtime.

FIGURE 1.8
Trans shows how to perform nested transactions.

Providing Feedback During SQL Operations


There are several good reasons for providing feedback as SQL operations execute. Consider
the following two:
Establishing and Using Database Connections
47

• Some SQL operations are fast, but some are extremely slow. A very complicated SELECT 1
statement that is performed on a large database might take several minutes (or even

ESTABLISHING AND
USING DATABSE
hours) to execute.

CONNECTIONS
• Especially when using TSQLConnection in conjunction with datasets (discussed in
Chapter 3), the connection might run some SQL commands on your behalf. It can be
useful for both learning and debugging purposes to intercept all SQL commands sent to
the database.
In either of these situations, it’s helpful to be able to provide feedback or logging facilities to
the end user of your application (or to yourself) in the form of a log file, database, CodeSite
(from Raize Software, at www.raize.com), or other debugging tool.

Changing the Cursor While Executing SQL Statements


The simplest form of feedback that you can provide is to change the cursor to an hourglass
when an SQL statement is executing. To provide this functionality in your application, all you
need to do is set the connection’s SQLHourGlass property to True. SQLHourGlass is not a
published property, so you can’t set it at design time.
SQLConnection1.SQLHourGlass := True;

Creating a Callback Event to Monitor SQL Commands


If you want to intercept every SQL command that passes from TSQLConnection to the database,
you can set up what is called a trace callback event.
You set up a trace callback event by calling TSQLConnection.SetTraceCallbackEvent.
SetTraceCallbackEvent takes two parameters: the event to call for SQL activity, and a user-
defined integer value that is passed to the callback event.
The format of the callback event is defined as follows:
TSQLCallbackEvent = function(CallType: TRACECat;
CBInfo: Pointer): CBRType; stdcall;

CallType is always set to cbTRACE on entry to the callback function. CBInfo is a pointer to
an SQLTRACEDesc record, which is defined like this:
SQLTRACEDesc = packed record { trace callback info }
pszTrace : array [0..1023] of Char;
eTraceCat : TRACECat;
ClientData : Integer;
uTotalMsgLen : Word;
end;
Chapter 1
48

The fields and their meanings are shown in Table 1.8.

TABLE 1.8 SQLTraceDesc Field Explanations

Field Definition
pszTrace The NULL-terminated command that was just passed to or from the
database.
eTraceCat The category of the command just sent or received. Table 1.9 lists the
possible values of this field.
ClientData The user-defined value passed as the second parameter to
SetTraceCallbackEvent.
uTotalMsgLen The length, in characters, of the string contained in pszTrace.

Table 1.9 describes the possible values for the eTraceCat field.

TABLE 1.9 eTraceCat Values

Value Definition
traceQPREPARE A query was sent to the server to prepare.
traceQEXECUTE A query was sent to the server to execute.
traceERROR An error message was returned by the server.
traceSTMT An operation for the database to perform was sent to the server.
traceCONNECT A connect-or disconnect-related operation was sent to the server.
traceTRANSACT A transaction-related operation was sent to the server.
traceBLOB A BLOB-related operation was sent to the server.
traceVENDOR A vendor-specific API call was sent to the server.
traceDATAIN Parameter data was sent to the server during an INSERT or UPDATE
command.
traceDATAOUT Data was retrieved from the server.
traceMISC Any other command not falling under one of the previous categories.

NOTE
Not all of these options are currently supported. Many of them are there for future
expansion. Currently, all vendor calls and executed SQL commands are traced.
Establishing and Using Database Connections
49

The following code snippet sets a callback event: 1

ESTABLISHING AND
function SQLCallback(CallType: TRACECat; CBInfo: Pointer): CBRType; stdcall;

USING DATABSE
CONNECTIONS
var
CBI: pSQLTRACEDesc;
begin
CBI := pSQLTRACEDesc(CBInfo);
ShowMessage(CBI.pszTrace);
end;

begin
SQLConnection1.SetTraceCallbackEvent(SQLCallback, 1);
end;

CAUTION
Do not pass 0 as the second parameter to SetTraceCallbackEvent, or your callback
event will never be called and you’ll rack your brains trying to figure out why
callback events are not working.

To remove a callback handler, execute the following line of code:


SQLConnection1.SetTraceCallbackEvent(nil, 0);

TSQLMonitor
TSQLMonitor provides a ready-made, easy-to-use mechanism for capturing database events.
You can log database messages to a list box, a log file, or another destination as they occur.
You can also allow messages to accumulate in the monitor’s internal buffer, and dump them to
a file (or other destination) in one fell swoop.
To use a TSQLMonitor, drop it on a form or data module, along with your TSQLConnection
component. To begin logging events, set the monitor’s SQLConnection property to the
TSQLConnection component and set the Active property to True.

There are two ways to monitor database messages: You can elect to log each message as soon
as TSQLMonitor is notified of it, or you can allow the component to buffer up the messages
and save them to a log file or string list at a later time. The following sections explain these
options.

Logging Messages as They Occur


There are actually two different ways that you can log database messages as they occur. The
first is through the use of TSQLMonitor’s AutoSave and FileName properties. Set FileName to
Chapter 1
50

the name of the log file that you want to create, such as C:\TRACE.LOG. Set AutoSave to
True, and the component automatically logs all database messages to the specified filename. If
the file does not exist, it is automatically created. If the file does exist, it is appended to.
The second method of logging messages on the fly is to write an event handler for the OnTrace
or OnLogTrace event. OnTrace is fired as soon as the component receives an indication that a
message has passed between the application and the database server. The event handler looks
like this:
procedure TForm1.SQLMonitor1Trace(Sender: TObject; CBInfo: pSQLTRACEDesc;
var LogTrace: Boolean);
begin

end;

Inside the event handler, you can set LogTrace to False if, for some reason, you don’t want the
message to be saved to the internal list. By default, the message is logged.
OnLogTrace is fired after the message is added to the internal trace list. Its event handler looks
like this:
procedure TForm1.SQLMonitor1LogTrace(Sender: TObject; CBInfo: pSQLTRACEDesc);
begin

end;

The parameters are the same as the first two parameters passed to the OnTrace event handler.

Buffering Messages
Rather than dealing with each individual message as it arrives, you can allow them to be
buffered in an internal list by the TSQLMonitor component. You can then save them to a file or
to a string list at a later time.
To save the list to a file, call the component’s SaveToFile method, like this:
SQLMonitor1.SaveToFile(‘C:\EventList.LOG’);

Alternately, you can save the messages to a string list by accessing the TraceList property,
which is a TStrings object containing the list of messages. By calling methods and/or properties
on TraceList, you can access the individual lines in the list.

Using Multiple Feedback Mechanisms


TSQLConnection is not designed to easily support sending database trace events to multiple
destinations at the same time. In other words, you can’t use both a TSQLMonitor and a user-
defined feedback event at the same time. So, if you want to assign a callback event, you should
Establishing and Using Database Connections
51

set the TSQLMonitor’s Active property to False (assuming that you also have a TSQLMonitor 1
component in your application).

ESTABLISHING AND
USING DATABSE
CONNECTIONS
Another thing you should avoid is accidentally overwriting an existing callback handler by
assigning a new one. To check for an existing callback event (including the presence of a
TSQLMonitor component), you should check the value of the read-only
TSQLConnection.TraceCallbackEvent property to ensure that it is nil.

If TSQLConnection.TraceCallbackEvent is not nil, a callback handler is already installed.


At this point, you have three options:
• Elect not to install your feedback event.
• Save a pointer to the existing feedback event, call SetTraceCallbackEvent to install
your own event handler, and reinstate the existing callback event when you’re finished.
• Save a pointer to the existing callback event, call SetTraceCallbackEvent to install your
own event handler, and call the existing event handler from within your new event handler.
Listing 1.6 shows the source code for a sample application that traces database operations
either through a TSQLMonitor component, or through a programmer-defined callback event.

LISTING 1.6 Feedback—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DBXpress,
QExtCtrls, SqlExpr, DB, QStdCtrls;

type
TfrmMain = class(TForm)
conn: TSQLConnection;
pnlClient: TPanel;
lbTrace: TListBox;
Label1: TLabel;
btnDump: TButton;
btnConnect: TButton;
btnDisconnect: TButton;
btnExecSQL: TButton;
btnLogTrace: TCheckBox;
monitor: TSQLMonitor;
cbUseCallback: TCheckBox;
procedure btnDumpClick(Sender: TObject);
procedure btnConnectClick(Sender: TObject);
Chapter 1
52

LISTING 1.6 Continued


procedure btnDisconnectClick(Sender: TObject);
procedure btnExecSQLClick(Sender: TObject);
procedure connAfterConnect(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure cbUseCallbackClick(Sender: TObject);
procedure monitorTrace(Sender: TObject; CBInfo: pSQLTRACEDesc;
var LogTrace: Boolean);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

function MySQLCallBack(CallType: TRACECat; CBInfo: Pointer): CBRType; stdcall;


var
CBI: pSQLTRACEDesc;
begin
Result := cbrUSEDEF;
if CBInfo <> nil then begin
CBI := pSQLTRACEDesc(CBInfo);
frmMain.lbTrace.Items.Add(‘Callback: ‘ + CBI.pszTrace);
end;
end;

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
conn.Open;

cbUseCallbackClick(cbUseCallback);
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
conn.Close;
end;
Establishing and Using Database Connections
53

LISTING 1.6 Continued 1

ESTABLISHING AND
procedure TfrmMain.btnExecSQLClick(Sender: TObject);

USING DATABSE
CONNECTIONS
begin
conn.ExecuteDirect(‘SELECT * FROM CONTACTS’);
end;

procedure TfrmMain.btnDumpClick(Sender: TObject);


begin
lbTrace.Items.Assign(monitor.TraceList);
end;

procedure TfrmMain.monitorTrace(Sender: TObject; CBInfo: pSQLTRACEDesc;


var LogTrace: Boolean);
begin
if btnLogTrace.Checked then begin
lbTrace.Items.Add(CBInfo.pszTrace);

// Since we handled the message ourselves, don’t log it.


LogTrace := False;
end;
end;

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
btnConnect.Enabled := False;
btnDisconnect.Enabled := True;
btnExecSQL.Enabled := True;
btnDump.Enabled := True;
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
btnConnect.Enabled := True;
btnDisconnect.Enabled := False;
btnExecSQL.Enabled := False;
btnDump.Enabled := False;
end;

procedure TfrmMain.cbUseCallbackClick(Sender: TObject);


begin
if cbUseCallback.Checked then begin
monitor.Active := False;
conn.SetTraceCallbackEvent(MySQLCallback, 1);
end else begin
conn.SetTraceCallbackEvent(nil, 0);
Chapter 1
54

LISTING 1.6 Continued

monitor.Active := True;
end;
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
conn.Close;
end;

end.

Let’s take a look at the cbUseCallbackClick event handler. If you check the Use Callback
check box, the code disables the monitor. It then calls SetTraceCallbackEvent to set up a
callback procedure to monitor database messages.
When you uncheck the Use Callback check box, the code sets the monitor to active. This
re-establishes the TSQLMonitor component as the feedback mechanism for database messages.
Another point of interest is the MonitorTrace method. If the Log Trace check box is checked,
we capture the message immediately and send it to the list box. Because we’ve handled the
message, there’s no need to buffer it in the monitor’s internal list. For that reason, the code sets
LogTrace to False.

Figure 1.9 shows the Feedback application at runtime.

FIGURE 1.9
Feedback logs messages sent to and from the database server.
Establishing and Using Database Connections
55

Summary 1

ESTABLISHING AND
USING DATABSE
This chapter introduced you to the TSQLConnection component, which is used to establish and

CONNECTIONS
maintain a connection to an SQL database. Specifically, you learned:
• You can create either named or unnamed database connections.
• TSQLConnection surfaces a small number of events that are useful in allowing or
preventing connections to, and disconnections from, a database.
• You can easily retrieve schema information (also called metadata) from a database
connection, including table, field, index, and procedure data.
• TSQLConnections can be used to execute both DDL and DML commands against a
database.
• If a database server supports transactions, you can control those transactions through the
connection component.
• Several mechanisms are available for you to report feedback while performing operations
against a database.
The next chapter introduces unidirectional datasets, which enable you to retrieve result sets
from an SQL connection.
dbExpress Datasets CHAPTER

2
IN THIS CHAPTER
• What Are dbExpress Datasets? 58

• Types of Datasets 59

• Data Manipulation 63

• BLOB Support 69

• Parameterized Queries 71

• Ordering Data Returned from the Server 73

• Master/Detail Relationships 74

• Retrieving Schema Information 79


Chapter 2
58

The preceding chapter presented an overview of dbExpress connections and the


TSQLConnection component. You learned how to connect to and disconnect from a database,
how to set connection parameters, and how to retrieve schema information from a database.
In this chapter, I’ll introduce dbExpress datasets, which enable you to retrieve data from the
database connection.
I’ll make references to a number of dataset methods, such as First, Next, FieldByName, and
so on. These methods are actually defined at the TDataSet level, which means that they are
applicable to all types of datasets (including BDE, ADO, dbExpress, and the like).
These methods are well documented in the Delphi help files and in other general-purpose
Delphi books—I apply them in Chapter 3, “Client Dataset Basics.” For these reasons, I won’t
go into excruciating detail here. Instead, I will provide short code snippets to show how they’re
used in context.

What Are dbExpress Datasets?


Datasets are the means by which dbExpress retrieves data from a database. For example, given
the following SQL SELECT statement:
SELECT * FROM EMPLOYEES

The employee rows are returned in a dataset. You saw in the preceding chapter that
TSQLConnection could directly process SQL statements that did not return a result set. For
queries that return a result set, TSQLDataSet (and its derivatives) should be used.
Let’s take a moment to discuss the characteristics of dbExpress datasets, and then we’ll move
into the concrete dbExpress classes that implement them.

dbExpress Datasets Are Unidirectional


The most important thing to know about dbExpress datasets is that they are unidirectional. At
first this might seem like a huge disadvantage, but in the rest of this book, you’ll see how the
dbExpress architecture provides for an extremely lightweight, flexible, and powerful means of
accessing and updating data.
Because dbExpress datasets are unidirectional, the only navigating that you can do is moving
from the beginning of the dataset to the end—one record at a time. If you’re familiar with
other Delphi datasets (such as TTable, TADODataSet, or TIBDataSet), you might be wondering
how to search for records or move backward through the dataset. Again, the solution to these
issues will become crystal clear in later chapters.
dbExpress Datasets
59

dbExpress Datasets Are Read-Only


dbExpress datasets are a read-only view into the underlying data in a database. No editing
features are directly supported by dbExpress datasets, so any attempt to edit a dbExpress
record results in a Delphi exception, as shown in Figure 2.1.

2
FIGURE 2.1

DBEXPRESS
DATASETS
dbExpress datasets are read-only.

dbExpress Datasets Are Lightweight


Because dbExpress datasets are unidirectional and read-only, they are extremely lightweight.
There is no overhead involved for the fundamental (but expensive) tasks of bidirectional cursor
support, record buffering, and the like.
If you’re familiar with the Borland Database Engine (BDE), you know that the BDE supports
full editing capabilities, forward and backward navigation of datasets, and drivers for multiple
database backends. However, the BDE install is approximately 10MB, and the BDE itself is
memory-intensive. Consider that dbExpress (when coupled with client datasets, which I’ll
discuss in future chapters) consists of a mere two library files totaling some 400K. Consider
further that dbExpress (with client datasets) supports even more functionality than the BDE,
and you begin to appreciate why dbExpress is such a remarkable database technology.

Types of Datasets
As with other Delphi database technologies (such as ADO and IBX), dbExpress supports three
different types of datasets: tables, queries, and stored procedures. These are discussed in the
following sections.

Tables
A table is a direct view of the underlying database table. It consists of all columns for all rows
in the table. You cannot limit the rows returned from the table, and you cannot select a subset
of columns (or join columns from another table).
Chapter 2
60

Queries
A query provides a way to retrieve a subset of the data stored in the underlying database table.
It also enables you to join information from one table to another. In general, a query enables
you to execute any SQL SELECT statement and return the results.

Stored Procedures
Stored procedures are procedures written in the underlying database, and stored in the database
itself. dbExpress stored procedures enable you to retrieve data from a database stored procedure.

General-Purpose Datasets
The dbExpress components that provide table-level, query-level, and stored procedure access
are TSQLTable, TSQLQuery, and TSQLStoredProc, respectively. These components are provided
solely for ease of conversion from BDE applications, and correspond to the BDE data-access
components TTable, TQuery, and TStoredProc.
For all new development, it is strongly recommended that you use the general-purpose component
TSQLDataSet. TSQLDataSet allows access to tables, queries, and stored procedures alike, and is
more flexible than any of the other special-purpose components mentioned previously.
TSQLDataSet implements almost all of the dbExpress dataset functionality. TSQLTable,
TSQLQuery, and TSQLStoredProc descend from TSQLDataSet and add behavior specific to
tables, queries, and stored procedures. In turn, TSQLDataSet descends from TDataSet, which is
the root class for all Delphi datasets.
Table 2.1 lists the relevant properties defined by TSQLDataSet.

TABLE 2.1 TSQLDataSet Properties

Property Description
Active Set to True to open the dataset, or to False to close the dataset.
You can also read this property to determine whether the dataset is
currently open or closed.
CommandText The way in which CommandText is used depends on the value of
CommandType. See the following sections for more information on
CommandText.
CommandType Set CommandType to ctQuery to execute a query, to ctStoredProc to
execute a stored procedure, or to ctTable to open a table. See the
following sections for more information on CommandType.
DataSource Used to establish a master/detail link between two datasets. See the
section titled “Master/Detail Relationships” for more information.
dbExpress Datasets
61

TABLE 2.1 Continued


Property Description

MaxBlobSize Sets the maximum amount of data returned from BLOB fields. See
the section titled “BLOB Support” for more information.
ObjectView When True, the dataset provides additional support for ADT fields,
array fields, and master/detail relationships. See the section titled
“Master/Detail Relationships” for more information.
ParamCheck When True, the dataset automatically generates parameters whenever
CommandText changes. If you want to create parameters manually, 2
set this to False. See the section titled “Parameterized Queries” for
more information.

DBEXPRESS
DATASETS
Params Contains a list of input and output parameters for the current query
or stored procedure. See the section titled “Parameterized Queries”
for more information.
SortFieldNames Only used when ComandType = ctTable. Defines the order in which
data is returned from the server. See the section titled “Ordering
Data Returned from the Server” for more information.
SQLConnection The TSQLConnection component to which this dataset is connected.
You should set this property before setting any other properties in
the dataset.

Table-Level Access
To access an underlying database table, you can use the TSQLTable component. The fundamental
properties are SQLConnection and TableName. You can also set IndexName if you want to select
an index for record-ordering purposes. The following code snippet illustrates how to set table
properties and open a table-based dataset:
SQLTable1.SQLConnection := conn;
SQLTable1.TableName := ‘CONTACTS’;
SQLTable1.IndexName := ‘IX_CONNAME’;
SQLTable1.Open;

Query-Level Access
You can use TSQLQuery to create an ad hoc query for retrieving data from a database. The
following code shows how to do this:
SQLQuery1.SQLConnection := conn;
SQLQuery1.SQL.Text := ‘SELECT * FROM CONTACTS WHERE COUNTRY = “United
➥States”’;
SQLQuery1.Open;
Chapter 2
62

You can also create parameterized queries, as the following code snippet illustrates:
SQLQuery1.SQLConnection := conn;
SQLQuery1.SQL.Text := ‘SELECT * FROM CONTACTS WHERE COUNTRY = :TheCountry;
SQLQuery1.ParamByName(‘TheCountry’).Value := ‘United States’;
SQLQuery1.Open;

Parameterized queries are discussed in more detail later in the section titled “Parameterized
Queries.”

Stored Procedure Access


When you want to execute a stored procedure on the server, you can use the TSQLStoredProc
component, as shown in the following code snippet:
SQLStoredProc1.StoredProcName := ‘ContactsByState’;
SQLStoredProc1.ParamByName(‘ASTATE’).Value := ‘FL’;
SQLStoredProc1.Open;

Again, if the underlying stored procedure accepts one or more parameters, you should use the
ParamByName method to set the parameters before executing the query.

General-Purpose Data Access


As the preceding sections show, there are several different components that you can use for
dbExpress data access (depending on whether you’re accessing a table, query, or stored
procedure). However, there is a single, multipurpose component that provides all the functionality
of the three separate components: TSQLDataSet.
You should use TSQLDataSet in all new code that you write, and in my opinion, you should
also use it when converting existing applications. Borland provides the separate, special-purpose
components to more easily convert a BDE application to a dbExpress application. However,
there is very little additional work required to convert to the general-purpose TSQLDataSet
component.
The following code snippet shows how to use TSQLDataSet to access a table:
SQLDataset1.SQLConnection := conn;
SQLDataset1.CommandType := ctTable;
SQLDataset1.CommandText := ‘CONTACTS’;
SQLDataset1.IndexName := ‘IX_CONNAME’;
SQLDataset1.Open;

As you can see, it’s almost identical to the code required for TSQLTable. There is one
additional line, to tell the dataset that it’s accessing a table, as opposed to a query or stored
procedure.
dbExpress Datasets
63

To execute a query, you write something like the following:


SQLDataset1.SQLConnection := conn;
SQLDataset1.CommandType := ctQuery;
SQLDataset1.CommandText := ‘SELECT * FROM CONTACTS WHERE Country =
➥:ThsCountry’;
SQLDataset1.ParamByName(‘TheCountry’).Value := ‘United States’;
SQLDataset1.Open;

This code is also extremely similar to that used for a TSQLQuery component.
The following code snippet executes a stored procedure using the TSQLDataSet component: 2
SQLDataset1.SQLConnection := conn;
SQLDataset1.CommandType := ctStoredProc;

DBEXPRESS
DATASETS
SQLDataset1.CommandText := ‘ContactsByState’;
SQLDataset1.ParamByName(‘ASTATE’).Value := ‘FL’;
SQLDataset1.Open;

Again, notice the similarities to the TSQLStoredProc component.


In many applications, you set these properties at design time rather than at runtime. I’m showing
the assignments at runtime simply to point out the similarities and differences between the
various components.
In the examples for this book, I use TSQLDataSet for all database access. The only exception is
the Navigate example (shown later in this chapter), which serves as the single example of how
to use the different dbExpress dataset components.

Data Manipulation
Now that we’ve discussed the components necessary for data access, let’s spend a few minutes
discussing the methods provided by those components. Because dbExpress datasets are a
unidirectional read-only technology, you’ll see that there isn’t a lot to cover in this area. The
following sections discuss the most common operations that you will perform on a dbExpress
dataset.

Opening a Dataset
After you’ve set the appropriate properties on the dataset, you need to open the dataset to
retrieve data from the database connection. There are actually two ways to do this, both of
which achieve exactly the same result:
SQLDataSet1.Open;

or
SQLDataSet1.Active := True;
Chapter 2
64

If you look at the VCL/CLX source code, you’ll see that TSQLDataSet.Open resolves to a call
to TDataSet.Open, which looks like this:
procedure TDataSet.Open;
begin
Active := True;
end;

It’s a matter of personal preference whether you make the method call to Open, or set the
Active property yourself. I find that I prefer the method call, but either way is correct.

Closing a Dataset
Closing a dataset is as easy as opening one:
SQLDataSet1.Close;

or
SQLDataSet1.Active := False;

Again, calling the Close method does nothing except set Active := False, so feel free to
adopt whichever method you prefer.

NOTE
Remember that closing the database connection also closes all open datasets. So, if
you have a number of open datasets (and you are finished with the database
connection), you can elect to simply close the connection rather than closing all open
datasets manually.

Retrieving Field Contents from a Dataset


When the dataset is open, you normally want to access the individual columns (or fields) in the
result set. To do this, you typically call the FieldByName method, like this:
ShowMessage(‘The name is ‘ + SQLDataset1.FieldByName(‘Name’).AsString);

Usually, you know the name of the field that you want to access (as shown in the preceding
code snippet). However, if you’re writing a general-purpose database utility application that
works with any dbExpress-supported database or table, you might not know in advance what
columns are in the table that you are accessing. In those cases, you can access the Fields
object directly, like this:
ShowMessage(‘The first field’’s contents are: ‘ +
dbExpress Datasets
65

SQLDataset1.Fields[0].AsString);

You can also use the FieldCount property to determine the number of fields in the result set
and to loop through them, as the following code illustrates:
for Index := 0 to SQLDataset1.FieldCount - 1 do
// Do something with SQLDataset1.Fields[Index]

Navigating a Dataset
Because dbExpress datasets are unidirectional, there isn’t a lot of navigation that is supported.
The only two operations that you can perform are moving to the beginning of the dataset, and 2
moving to the next record in the result set. These operations are illustrated in the following

DBEXPRESS
code snippet:

DATASETS
while not SQLDataset1.EOF do begin
for Index := 0 to SQLDataset1.FieldCount - 1 do begin
// Do something with SQLDatasets.Fields[Index]
SQLDataset1.Next;
end;
end;

The following code listing is an example of all the concepts discussed so far in this chapter. It
illustrates how to use the TSQLTable, TSQLQuery, TSQLStoredProc, and TSQLDataSet components
to retrieve data from the ConMan database. The code listing also shows how to use these
components to loop through the results, and to do something with the data. (In this case, it
simply loads a TListView component with some of the field contents.)
Listing 2.1 shows the complete source code for the main form of the application.

LISTING 2.1 Basic—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, DBXpress, DB, SqlExpr, QStdCtrls, QComCtrls, QExtCtrls, FMTBcd,
QDBCtrls;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
btnConnect: TButton;
Chapter 2
66

LISTING 2.1 Continued

conn: TSQLConnection;
SQLTable1: TSQLTable;
SQLQuery1: TSQLQuery;
SQLStoredProc1: TSQLStoredProc;
SQLDataSet1: TSQLDataSet;
Label1: TLabel;
lvResults: TListView;
procedure btnConnectClick(Sender: TObject);
private
{ Private declarations }
procedure OpenTable;
procedure OpenQuery;
procedure OpenStoredProcedure;
procedure OpenDataset;
procedure LoadResults(DataSet: TDataSet);
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses DatasetTypeForm;

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);


var
frmDatasetType: TfrmDatasetType;
begin
// Ask the user whether to open the table, query, stored procedure,
// or general-purpose dataset
frmDatasetType := TfrmDatasetType.Create(nil);
try
if frmDatasetType.ShowModal = mrOk then begin
case frmDatasetType.DatasetType of
dtTable: OpenTable;
dtQuery: OpenQuery;
dtStoredProc: OpenStoredProcedure;
dtDataset: OpenDataset;
end;
dbExpress Datasets
67

LISTING 2.1 Continued


conn.Close;
end;
finally
frmDatasetType.Free;
end;
end;

procedure TfrmMain.OpenTable;
begin
SQLTable1.TableName := ‘CONTACTS’; 2
SQLTable1.IndexName := ‘IX_CONNAME’;
SQLTable1.Open;

DBEXPRESS
DATASETS
LoadResults(SQLTable1);
end;

procedure TfrmMain.OpenQuery;
begin
SQLQuery1.SQL.Text := ‘SELECT * FROM CONTACTS ‘ +
‘WHERE COUNTRY = “United States”’;
SQLQuery1.Open;

LoadResults(SQLQuery1);
end;

procedure TfrmMain.OpenStoredProcedure;
begin
SQLStoredProc1.StoredProcName := ‘CONTACTSBYSTATE’;
SQLStoredProc1.ParamByName(‘ASTATE’).Value := ‘FL’;
SQLStoredProc1.Open;

LoadResults(SQLStoredProc1);
end;

procedure TfrmMain.OpenDataset;
begin
SQLDataset1.CommandType := ctQuery;
SQLDataset1.CommandText := ‘SELECT FIRST, LAST, PHONE FROM CONTACTS ‘ +
‘ORDER BY LAST, FIRST’;
SQLDataset1.Open;

LoadResults(SQLDataset1);
end;
Chapter 2
68

LISTING 2.1 Continued


procedure TfrmMain.LoadResults(DataSet: TDataSet);
var
ListItem: TListItem;
begin
lvResults.Items.BeginUpdate;
try
lvResults.Items.Clear;

while not DataSet.EOF do begin


ListItem := lvResults.Items.Add;
ListItem.Caption := DataSet.FieldByName(‘FIRST’).AsString;
ListItem.SubItems.Add(DataSet.FieldByName(‘LAST’).AsString);
ListItem.SubItems.Add(DataSet.FieldByName(‘PHONE’).AsString);
DataSet.Next;
end;
finally
lvResults.Items.EndUpdate;
end;
end;

end.

As you can see from the code, when the user clicks the Connect button, the program creates an
instance of TfrmDatasetType (shown in Listing 2.2), which asks the user to select the type of
dataset to open: table, query, stored procedure, or generic dataset. After the user selects the
dataset type, the program calls one of four methods: OpenTable, OpenQuery,
OpenStoredProcedure, or OpenDataset. These methods each set the properties of the selected
dataset and make a call to LoadResults.
Notice that LoadResults takes a TDataSet as a parameter, which I indicated earlier is the root
class of all datasets. What this means is that LoadResults would actually work with any
dataset, whether it is a dbExpress dataset, BDE dataset, or some third-party dataset.
LoadResults loops through the records in the dataset, loading the contact’s first name, last
name, and phone number into a TListView.

LISTING 2.2 Basic—DatasetTypeForm.pas


unit DatasetTypeForm;

interface

uses
dbExpress Datasets
69

LISTING 2.2 Continued

SysUtils, Variants, Classes, QGraphics, QControls, QForms,


QDialogs, QExtCtrls, QStdCtrls;

type
TDatasetType = (dtTable, dtQuery, dtStoredProc, dtDataset);

TfrmDatasetType = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
btnOk: TButton; 2
btnCancel: TButton;
grpDatasetType: TRadioGroup;

DBEXPRESS
DATASETS
procedure btnOkClick(Sender: TObject);
private
{ Private declarations }
FDatasetType: TDatasetType;
public
{ Public declarations }
property DatasetType: TDatasetType read FDatasetType;
end;

implementation

{$R *.xfm}

procedure TfrmDatasetType.btnOkClick(Sender: TObject);


begin
FDatasetType := TDatasetType(grpDatasetType.ItemIndex);
end;

end.

Figure 2.2 shows the Basic demo application at runtime.

BLOB Support
Like most datasets, dbExpress datasets support BLOB data. BLOB stands for Binary Large
Object, and is used to store free-format data (such as images, memos, and the like).
BLOBs can easily become large, so many times you will want to limit the size of the BLOB
data that is retrieved from the database to improve query performance. For example, say that
you have a BLOB field that’s used to store an image of a contact. Executing a query that
returns a large result set (such as SELECT * FROM CONTACTS) could potentially retrieve
Chapter 2
70

thousands of records from the database. If each contact has a picture that averages 100K in
size, the amount of data returned from the server will be huge. This problem compounds when
the database connection is across a local area network, or worse, across the Internet.

FIGURE 2.2
TSQLDataSet is used to retrieve data from the database.

In these cases, you might want to eliminate the BLOB column from the result set. There are
two ways that you can accomplish this. First, if you’re executing a predefined query, simply
omit the BLOB field from the query. For example, if you are executing the query SELECT
FIRST, LAST, PHONE, IMAGE FROM CONTACTS, modify the SQL statement to be SELECT
FIRST, LAST, PHONE FROM CONTACTS.

Second, although the previous solution works fine if you know the exact columns that you are
retrieving from the database, what about a general-purpose database utility? If you don’t know
the column names or field types, you can execute a statement like the following:
SELECT * FROM CONTACTS

In a situation such as this, you can set TSQLDataSet’s BlobSize property to determine the
maximum amount of data to retrieve for each BLOB field. If this parameter is 0 (the default),
the maximum BLOB size is determined by the associated TSQLConnection’s BlobSize parameter.
If this parameter is –1, the dataset retrieves the entire BLOB, regardless of size. Any other
value constitutes the maximum number of bytes to retrieve for a BLOB.

NOTE
Regardless of whether you set BlobSize at the connection level or at the dataset level,
it applies to all BLOB fields returned in the dataset. There is no way to retrieve just the
first 100 bytes of one BLOB field and the entire contents of another BLOB field.
dbExpress Datasets
71

Parameterized Queries
In most of the examples shown so far in this chapter, when querying the database, the entire
query was specified. For example,
SELECT * FROM CONTACTS WHERE COUNTRY = “United States”

Although this works, it is less than efficient if you are going to execute the same general query
multiple times. For example, say that you want to first retrieve all the contacts in the United
States, and then all the contacts in Canada, and finally all the contacts in Mexico. You could
write something like the following: 2
SQLDataSet1.CommandType := ctQuery;

DBEXPRESS
SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS ‘ +

DATASETS
‘WHERE COUNTRY = “United States”’;
SQLDataSet1.Open;
ProcessDataSet;
SQLDataSet1.Close;

SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = “Canada”’;


SQLDataSet1.Open;
ProcessDataSet;
SQLDataSet1.Close;

SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = “Mexico”’;


SQLDataSet1.Open;
ProcessDataSet;

In this example, ProcessDataSet is some fictitious method that would operate on the results of
the query in some manner.
Although the preceding code works correctly, it is far from optimal. Each time
SQLDataSet1.CommandText is set, the backend database engine must parse and prepare the
SQL statement. A better way to accomplish the same result is to parameterize the query, like
this:
SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = :Country’;

This sets up a parameter named Country which acts like a placeholder in the SQL statement.
By setting various values for this parameter, you can issue the same SQL statement for different
countries, like this:
SQLDataSet1.CommandType := ctQuery;
SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = :Country’;
SQLDataSet1.ParamByName(‘Country’).AsString := ‘United States’;
SQLDataSet1.Open;
ProcessDataSet;
Chapter 2
72

SQLDataSet1.Close;

SQLDataSet1.ParamByName(‘Country’).AsString := ‘Canada’;
SQLDataSet1.Open;
ProcessDataSet;
SQLDataSet1.Close;

SQLDataSet1.ParamByName(‘Country’).AsString := ‘Mexico’;
SQLDataSet1.Open;
ProcessDataSet;

In this scenario, the SQL statement is prepared only once—the first time that it is executed.
After that, the statement does not need to be prepared again because the only thing that
changes is the Country parameter.

NOTE
Note that the name of the parameter does not need to be the same as the column
name that it refers to. In the previous example, the parameter could have been
named ACountry, CountryParam, or Fred.

There is one additional property that governs how parameters are treated: the ParamCheck
property. When ParamCheck is set to True, parameters are automatically created by dbExpress
(as the previous example indicates). However, if you set ParamCheck to False, you are
responsible for creating the parameters yourself. The following code snippet shows how
this is done:
SQLDataSet1.CommandType := ctQuery;
SQLDataSet1.ParamCheck := False;
SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = :Country’;
SQLDataSet1.Params.CreateParam(‘Country’, ftString, ptInput);
SQLDataSet1.ParamByName(‘Country’).AsString := ‘United States’;
SQLDataSet1.Open;

Note that, typically, you would only set ParamCheck to False when issuing a DDL statement
that creates a stored procedure which accepts parameters as part of the stored procedure.
Because that sounds confusing, let’s take a look at an example:
SQLDataSet1.CommandType := ctQuery;
SQLDataSet1.ParamCheck := False;
SQLDataSet1.CommandText :=
‘CREATE PROCEDURE CONTACTSBYTITLE(ATITLE VARCHAR(20)) ‘ +
‘RETURNS ( ‘ +
‘ID INTEGER, ‘ +
‘FIRST VARCHAR(20), ‘ +[DM] Fix typo: should be FIRST
dbExpress Datasets
73

‘LAST VARCHAR(30), ‘ +
‘) ‘ +
‘AS ‘ +
‘BEGIN +
‘ FOR SELECT ID, FIRST, LAST ‘ +
‘ FROM CONTACTS ‘ +
‘ WHERE TITLE = :ATITLE ‘ +
‘ INTO :ID, :FIRST, :LAST DO ‘ +
‘ BEGIN ‘ +
‘ SUSPEND; ‘ +
‘ END ‘ +
‘END;’;
2
SQLDataSet1.ExecSQL;

DBEXPRESS
DATASETS
In this case, the parameters ATITLE, ID, FIRST, and LAST used in the body of the stored procedure
are not parameters to the SQL statement at all. They are a part of the stored procedure. To
keep dbExpress from treating them as parameters, you should set ParamCheck to False before
setting CommandText.

Ordering Data Returned from the Server


There are two ways to order the data returned from the server. One way relates to the
TSQLTable component, and the other is used with the TSQLQuery and TSQLDataSet components.
I’ll address the TSQLTable component first.

Ordering Data from a Table


If you are using a TSQLTable, you can set the component’s IndexName or IndexFieldNames
property before opening the dataset. IndexName refers to the name of an index as it is stored in
the underlying database. For example, in the ConMan database, I have defined an index,
named IX_CONNAME, which is composed of the LAST and FIRST columns (in that order).
SQLTable1.IndexName := ‘IX_CONNAME’;

If you don’t know the name of the underlying index, you can set the IndexFieldNames property
instead. IndexFieldNames is a semicolon-delimited list of fields that make up the index.
SQLTable1.IndexFieldNames := ‘LAST;FIRST’;

NOTE
You do not need to ensure that there is an index defined on the fields that you set in
the IndexFieldNames property. Behind the scenes, TSQLTable creates an ORDER BY
clause for the SQL statement that it passes to the database (based on the fields listed
in IndexFieldNames).
Chapter 2
74

Ordering Data from a Query


If you’re using a TSQLQuery or TSQLDataSet component, the method used to order the result
set is more straightforward: You simply add an ORDER BY clause to the SQL statement yourself,
like this:
SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS ORDER BY LAST, FIRST’;

If there is an index defined in the database that supports the ORDER BY clause, the underlying
database engine uses the index for increased speed. If not, the database engine sorts the data in
the requested order before it is returned to the application.

Master/Detail Relationships
Although standalone datasets are useful, datasets are commonly related to other datasets in
master/detail relationships. A master/detail relationship (also known as a one-to-many relationship)
is one in which a single record in one dataset corresponds to one, or more, records in another
dataset.
The most commonly cited example of a master/detail relationship uses customers, orders, and
items as its datasets. A single customer can place more than one order with a given vendor. In
turn, a single order can contain more than one item. Figure 2.3 shows a graphic representation
of typical customer, order, and item datasets.

Customers
ID
More fields… Orders
ID
CustomerID Items
More fields… ID
OrderID
More fields…

FIGURE 2.3
Master/Detail relationships can have multiple levels.

In the ConMan database, the CONTACT and ACTIVITIES tables are joined in a master/detail
relationship on the ContactID field.
To create this master/detail link in an application, you would perform the following steps:
dbExpress Datasets
75

1. Place a TSQLConnection component on a form and connect it to the ConMan database.


2. Drop a TSQLDataSet on the form and set its TSQLConnection property to the
TSQLConnection component that you created in step 1. Set the Name property to
sqlContacts and the CommandText property to SELECT * FROM CONTACTS. This is the
master dataset.
3. Drop a TDataSource on the form, set its Name property to dsContacts, and set its
DataSet property to sqlContacts.

4. Drop another TSQLDataSet on the form. Set the Name property to sqlActivities and the
DataSource property to dsContacts. This is the detail dataset. 2
5. Set sqlActivities.CommandText to SELECT * FROM ACTIVITIES WHERE ContactID =

DBEXPRESS
:ContactID.

DATASETS
That’s all that’s required to establish a master/detail relationship in your program code. You
should note the following points:
• The detail dataset’s CommandText property is always a parameterized query. The parameter
names actually refer to column names in the master dataset.
• Whenever the master dataset changes records, the detail dataset automatically retrieves
the records that are associated with the current master record.
The first point deserves a little more explanation.
You’ll recall from the section titled “Parameterized Queries” that when using parameterized
queries, you typically make calls to TSQLDataSet.ParamByName to set the parameters.
With a detail dataset, parameter substitution is a bit more automated. VCL/CLX notices that
the detail dataset’s DataSource parameter is assigned, so it looks to the data source’s dataset as
the source of parameter values.
For example, in the CONTACT/ACTIVITIES example, sqlActivities.CommandText contains
a single parameter: ContactID. When Delphi assigns the value of the ContactID parameter, it
looks to the master dataset for a column named ContactID. Because the master dataset does,
indeed, contain a column named ContactID, the value of the parameter is taken from that column.

NOTE
You can create a detail dataset that gets only some of its parameter values from the
master dataset. In this case, you must set the values of the other parameters manually.
For example, given the SQL statement SELECT * FROM ACTIVITIES WHERE (ContactID
= :ContactID) AND (SCHEDULED > :Earliest), the detail dataset would get the
value for the ContactID parameter from the master dataset. You would make a call
to sqlActivities.ParamByName to set the Earliest parameter.
Chapter 2
76

As you scroll through the master dataset, the detail dataset automatically updates itself to stay
in sync with the master.
Listing 2.3 shows the complete source code for an application that makes use of parameterized
queries, master/detail relationships, and BLOB fields.

LISTING 2.3 Advanced—MainForm.pas


unit MainForm;

interface

uses
Types, SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, DBXpress, DB, SqlExpr, QStdCtrls, QComCtrls, QExtCtrls, FMTBcd,
QDBCtrls;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
btnConnect: TButton;
btnDisconnect: TButton;
Label1: TLabel;
DBImage1: TDBImage;
DBText1: TDBText;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
Label8: TLabel;
Label9: TLabel;
DBText2: TDBText;
DBText3: TDBText;
DBText4: TDBText;
DBText5: TDBText;
DBText6: TDBText;
DBText7: TDBText;
DBText8: TDBText;
DBText9: TDBText;
DBNavigator1: TDBNavigator;
Bevel1: TBevel;
Label10: TLabel;
lvActivities: TListView;
dbExpress Datasets
77

LISTING 2.3 Continued

Label11: TLabel;
cbCountry: TComboBox;
btnRetrieve: TButton;
conn: TSQLConnection;
sqlContacts: TSQLDataSet;
sqlContactsFIRST: TStringField;
sqlContactsLAST: TStringField;
sqlContactsDEAR: TStringField;
sqlContactsTITLE: TStringField;
sqlContactsCOMPANYNAME: TStringField;
2
sqlContactsADDRESS1: TStringField;

DBEXPRESS
sqlContactsADDRESS2: TStringField;

DATASETS
sqlContactsCITY: TStringField;
sqlContactsSTATE: TStringField;
sqlContactsPOSTALCODE: TStringField;
sqlContactsCOUNTRY: TStringField;
sqlContactsPHONE: TStringField;
sqlContactsFAX: TStringField;
sqlContactsCELLULAR: TStringField;
sqlContactsPAGER: TStringField;
sqlContactsEMAIL: TStringField;
sqlContactsIMAGE: TBlobField;
sqlContactsNOTES: TMemoField;
DataSource1: TDataSource;
sqlActivities: TSQLDataSet;
sqlActivitiesCONTACTID: TIntegerField;
sqlActivitiesDESCRIPTION: TStringField;
sqlActivitiesSCHEDULED: TSQLTimeStampField;
sqlActivitiesCOMPLETED: TSQLTimeStampField;
sqlContactsCONTACTID: TIntegerField;
sqlActivitiesTODOID: TIntegerField;
procedure btnConnectClick(Sender: TObject);
procedure btnNextClick(Sender: TObject);
procedure btnDisconnectClick(Sender: TObject);
procedure btnRetrieveClick(Sender: TObject);
procedure connAfterDisconnect(Sender: TObject);
procedure connAfterConnect(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure sqlContactsAfterScroll(DataSet: TDataSet);
private
{ Private declarations }
public
{ Public declarations }
end;
Chapter 2
78

LISTING 2.3 Continued


var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
conn.Open;
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
sqlContacts.Close;
conn.Close;
end;

procedure TfrmMain.connAfterConnect(Sender: TObject);


begin
btnConnect.Enabled := False;
btnDisconnect.Enabled := True;
end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);


begin
btnConnect.Enabled := True;
btnDisconnect.Enabled := False;
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);


begin
btnDisconnectClick(btnDisconnect);
end;

procedure TfrmMain.btnRetrieveClick(Sender: TObject);


begin
sqlContacts.Close;
sqlContacts.ParamByName(‘CountryName’).Value := cbCountry.Text;
sqlContacts.Open;
end;

procedure TfrmMain.btnNextClick(Sender: TObject);


dbExpress Datasets
79

LISTING 2.3 Continued

begin
sqlContacts.Next;
end;

procedure TfrmMain.sqlContactsAfterScroll(DataSet: TDataSet);


var
ListItem: TListItem;
begin
lvActivities.Items.BeginUpdate;
try
2
lvActivities.Items.Clear;

DBEXPRESS
DATASETS
while not sqlActivities.EOF do begin
ListItem := lvActivities.Items.Add;
ListItem.Caption := DateTimeToStr(sqlActivitiesSCHEDULED.AsDateTime);
ListItem.SubItems.Add(sqlActivitiesDESCRIPTION.AsString);
if not sqlActivitiesCOMPLETED.IsNull then
ListItem.Data := Pointer(1);
sqlActivities.Next;
end;
finally
lvActivities.Items.EndUpdate;
end;
end;

end.

Figure 2.4 shows the Advanced program in action.

Retrieving Schema Information


The preceding chapter showed how to retrieve basic schema information for a database (such
as table names, index names, and stored procedure names). This section explains how to use
TSQLDataSet to retrieve more detailed information about tables and columns.

To retrieve comprehensive schema information from a database, you call


TSQLDataSet.SetSchemaInfo, which specifies the object whose schema you want to retrieve,
and what type of schema data to return. SetSchemaInfo takes three parameters, and is defined
like this:
procedure SetSchemaInfo(SchemaType: TSchemaType; SchemaObjectName,
SchemaPattern: string);
Chapter 2
80

FIGURE 2.4
Advanced displays contact data and related activities.

SchemaType refers to the type of schema data to return, and must be one of the values listed in
Table 2.2.

TABLE 2.2 TSchemaType Values

Value Schema Information Returned


stNoSchema No schema information. The dataset returns the results of the query
or stored procedure rather than schema information for that object.
stTables Information about the tables in the database that match the object
name and pattern.
stSysTables Information about the system tables in the database.
stProcedures Information about the stored procedures in the database.
stColumns Information about the columns for a single table.
stProcedureParams Information about the parameters for a single stored procedure.
stIndexes Information about the indexes for a single table.

SchemaObjectName specifies the name of the table or stored procedure to return data for. It is
only used for schema types of stColumns, stProcedureParams, and stIndexes. If the schema
type is stColumns or stIndexes, SchemaObjectName specifies the table to return column or
index information for. If the schema type is stProcedureParams, SchemaObjectName specifies
the name of the stored procedure to return parameter information for.
dbExpress Datasets
81

SchemaPattern is an SQL pattern that is used to filter the data that’s returned in the result set.
For instance, to return only columns that start with the letter A, you could pass a
SchemaPattern of A% to the call to SetSchemaInfo. If you don’t want to filter the result set, set
SchemaPattern to an empty string.

Within SchemaPattern, use a percent sign (%) to match a string of any length and an under-
score (_) to match a single character. If you want to include a percent sign or underscore in the
pattern, double it up (%% or __).
After you’ve retrieved schema data for a database, you can use the same dataset to run normal
queries against the database by calling SetSchemaInfo with a SchemaType of stNoSchema. 2
Listing 2.4 shows the complete source code for an example program that can extract and dis-

DBEXPRESS
DATASETS
play schema information for a dbExpress database. After the listing, I’ll give just a quick
overview of the code because most of it should be self-explanatory at this point.
Figure 2.5 shows the Schema program as it displays column information from the CONTACTS
table in the ConMan.gdb database.

FIGURE 2.5
The Schema application decodes and displays column information.

LISTING 2.4 Schema—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, DBXpress, FMTBcd, DB, SqlExpr, QExtCtrls, QStdCtrls, QComCtrls;
Chapter 2
82

LISTING 2.4 Continued

type
TfrmMain = class(TForm)
pnlClient: TPanel;
conn: TSQLConnection;
dataset: TSQLDataSet;
Label1: TLabel;
lvColumns: TListView;
grpSchemaType: TRadioGroup;
Label2: TLabel;
ecObjectName: TEdit;
btnRetrieve: TButton;
ecSchemaPattern: TEdit;
Label3: TLabel;
procedure btnRetrieveClick(Sender: TObject);
private
{ Private declarations }
function GetTableTypeString(TableType: Integer): string;
function GetProcTypeString(ProcType: Integer): string;
function GetColTypeString(ColType: Integer): string;
function GetColDataTypeString(ColDataType: Integer): string;
function GetColSubTypeString(ColSubType: Integer): string;
function GetIndexTypeString(IndexType: Integer): string;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnRetrieveClick(Sender: TObject);


var
SchemaType: TSchemaType;
ListColumn: TListColumn;
ListItem: TListItem;
Index: Integer;
begin
SchemaType := TSchemaType(grpSchemaType.ItemIndex + 1);

// Columns, Indexes, and Procedure Params must have a schema object name
case SchemaType of
dbExpress Datasets
83

LISTING 2.4 Continued

stColumns,
stIndexes,
stProcedureParams:
if ecObjectName.Text = ‘’ then
raise Exception.Create(‘You must enter a schema object name ‘ +
‘for this schema type.’);
end;

conn.Open;
try
2
dataset.SetSchemaInfo(SchemaType, ecObjectName.Text, ecSchemaPattern.Text);

DBEXPRESS
dataset.Open;

DATASETS
lvColumns.Items.BeginUpdate;
try
lvColumns.Items.Clear;
lvColumns.Columns.BeginUpdate;
try
lvColumns.Columns.Clear;
for Index := 0 to dataset.FieldCount - 1 do begin
ListColumn := lvColumns.Columns.Add;
ListColumn.Caption := dataset.Fields[Index].FieldName;
end;

while not dataset.EOF do begin


ListItem := lvColumns.Items.Add;

ListItem.Caption := dataset.Fields[0].AsString;
for Index := 1 to dataset.FieldCount - 1 do begin
if dataset.Fields[Index].FieldName = ‘TABLE_TYPE’ then
ListItem.SubItems.Add(GetTableTypeString(
dataset.Fields[Index].AsInteger))
else if dataset.Fields[Index].FieldName = ‘PROC_TYPE’ then
ListItem.SubItems.Add(GetProcTypeString(
dataset.Fields[Index].AsInteger))
else if dataset.Fields[Index].FieldName = ‘COLUMN_TYPE’ then
ListItem.SubItems.Add(GetColTypeString(
dataset.Fields[Index].AsInteger))
else if dataset.Fields[Index].FieldName = ‘COLUMN_DATATYPE’ then
ListItem.SubItems.Add(GetColDataTypeString(
dataset.Fields[Index].AsInteger))
else if dataset.Fields[Index].FieldName = ‘COLUMN_SUBTYPE’ then
ListItem.SubItems.Add(GetColSubTypeString(
dataset.Fields[Index].AsInteger))
Chapter 2
84

LISTING 2.4 Continued

else if dataset.Fields[Index].FieldName = ‘INDEX_TYPE’ then


ListItem.SubItems.Add(GetIndexTypeString(
dataset.Fields[Index].AsInteger))
else
ListItem.SubItems.Add(dataset.Fields[Index].AsString);
end;

dataset.Next;
end;
finally
lvColumns.Columns.EndUpdate;
end;
finally
lvColumns.Items.EndUpdate;
end;
finally
conn.Close;
end;
end;

function TfrmMain.GetTableTypeString(TableType: Integer): string;

procedure Check(SQLTableType: Integer; const Desc: string);


begin
if (TableType and SQLTableType) <> 0 then begin
if Result <> ‘’ then
Result := Result + ‘, ‘;
Result := Result + Desc;
end;
end;

begin
Result := ‘’;

Check(eSQLTable, ‘Table’);
Check(eSQLView, ‘View’);
Check(eSQLSynonym, ‘Synonym’);
Check(eSQLSystemTable, ‘System’);
Check(eSQLTempTable, ‘Temp’);
Check(eSQLLocal, ‘Local’);

if Result = ‘’ then
Result := ‘$’ + IntToHex(TableType, 2);
end;
dbExpress Datasets
85

LISTING 2.4 Continued

function TfrmMain.GetProcTypeString(ProcType: Integer): string;

procedure Check(SQLProcType: Integer; const Desc: string);


begin
if (ProcType and SQLProcType) <> 0 then begin
if Result <> ‘’ then
Result := Result + ‘, ‘;
Result := Result + Desc;
end;
end;
2

DBEXPRESS
begin

DATASETS
Result := ‘’;

Check(eSQLProcedure, ‘Procedure’);
Check(eSQLFunction, ‘Function’);
Check(eSQLPackage, ‘Package’);
Check(eSQLSysProcedure, ‘System’);

if Result = ‘’ then
Result := ‘$’ + IntToHex(ProcType, 2);
end;

function TfrmMain.GetColTypeString(ColType: Integer): string;

procedure Check(SQLColType: Integer; const Desc: string);


begin
if (ColType and SQLColType) <> 0 then begin
if Result <> ‘’ then
Result := Result + ‘, ‘;
Result := Result + Desc;
end;
end;

begin
Result := ‘’;

Check(eSQLRowId, ‘Row Id’);


Check(eSQLRowVersion, ‘Row Version’);
Check(eSQLAutoIncr, ‘Auto Incr’);
Check(eSQLDefault, ‘Default’);

if Result = ‘’ then
Result := ‘$’ + IntToHex(ColType, 2);
Chapter 2
86

LISTING 2.4 Continued

end;

function TfrmMain.GetColDataTypeString(ColDataType: Integer): string;


begin
case ColDataType of
fldUNKNOWN: Result := ‘Unknown’;
fldZSTRING: Result := ‘ZString’;
fldDATE: Result := ‘Date’;
fldBLOB: Result := ‘BLOB’;
fldBOOL: Result := ‘Bool’;
fldINT16: Result := ‘Int16’;
fldINT32: Result := ‘Int32’;
fldFLOAT: Result := ‘Float’;
fldBCD: Result := ‘BCD’;
fldBYTES: Result := ‘Bytes’;
fldTIME: Result := ‘Time’;
fldTIMESTAMP: Result := ‘Timestamp’;
fldUINT16: Result := ‘UInt16’;
fldUINT32: Result := ‘UInt32’;
fldFLOATIEEE: Result := ‘FloatIEEE’;
fldVARBYTES: Result := ‘VarBytes’;
fldLOCKINFO: Result := ‘LockInfo’;
fldCURSOR: Result := ‘Cursor’;
fldINT64: Result := ‘Int64’;
fldUINT64: Result := ‘UInt64’;
fldADT: Result := ‘ADT’;
fldARRAY: Result := ‘Array’;
fldREF: Result := ‘RefADT’;
fldTABLE: Result := ‘Table’;
fldDATETIME: Result := ‘DateTime’;
fldFMTBCD: Result := ‘FmtBCD’;
else Result := ‘$’ + IntToHex(ColDataType, 2);
end;
end;

function TfrmMain.GetColSubTypeString(ColSubType: Integer): string;


begin
case ColSubType of
fldstMONEY: Result := ‘Money’;
fldstMEMO: Result := ‘Memo’;
fldstBINARY: Result := ‘Binary’;
fldstFMTMEMO: Result := ‘Fmt Memo’;
fldstOLEOBJ: Result := ‘Pdox OLE’;
fldstGRAPHIC: Result := ‘Graphic’;
dbExpress Datasets
87

LISTING 2.4 Continued

fldstDBSOLEOBJ: Result := ‘dBase OLE’;


fldstTYPEDBINARY: Result := ‘Typed Binary’;
fldstACCOLEOBJ: Result := ‘Access OLE’;
fldstHMEMO: Result := ‘CLOB’;
fldstHBINARY: Result := ‘BLOB’;
fldstBFILE: Result := ‘BFILE’;
fldstPASSWORD: Result := ‘Pasword’;
fldstFIXED: Result := ‘Char’;
fldstUNICODE: Result := ‘Unicode’;
fldstAUTOINC: Result := ‘AutoInc’;
2
fldstADTNestedTable: Result := ‘ADT Nest’;

DBEXPRESS
fldstADTDATE: Result := ‘ADT Date’;

DATASETS
else Result := ‘$’ + IntToHex(ColSubType, 2);
end;
end;

function TfrmMain.GetIndexTypeString(IndexType: Integer): string;

procedure Check(SQLIndexType: Integer; const Desc: string);


begin
if (IndexType and SQLIndexType) <> 0 then begin
if Result <> ‘’ then
Result := Result + ‘, ‘;
Result := Result + Desc;
end;
end;

begin
Result := ‘’;

Check(eSQLNonUnique, ‘Non-unique’);
Check(eSQLUnique, ‘Unique’);
Check(eSQLPrimaryKey, ‘Primary’);

if Result = ‘’ then
Result := ‘$’ + IntToHex(IndexType, 2);
end;

end.

The guts of the Schema application are contained within a single method: btnRetrieveClick.
btnRetrieveClick determines what schema type the user selected and ensures that the user
enters an object name if the requested schema type is columns, indexes, or procedure parameters.
Chapter 2
88

When the schema type is known, it is a simple matter to make the appropriate call to
SetSchemaInfo, open the dataset, and load the list view with the schema information. Certain
columns (namely TABLE_TYPE, PROC_TYPE, COLUMN_TYPE, COLUMN_DATATYPE, COLUMN_SUBTYPE,
and INDEX_TYPE) are bitmapped numeric columns. The program makes calls to a handful of
helper routines to display a textual representation of these values. The rest of the columns are
displayed as is.
You will notice when you run this application that different schema types return different data
fields. Tables 2.3–2.7 explain the columns that are returned for each of the schema types.
Table 2.3 lists the columns that are returned for a schema type of stTables or
stSystemTables.

TABLE 2.3 stTables and stSystemTables Schema Columns

Column Description
RECNO The absolute record number. It is one for the first record, two for the
second, and so on.
CATALOG_NAME The name of the catalog, or database, that contains the table.
SCHEMA_NAME The owner of the table.
TABLE_NAME The table name.
TABLE_TYPE A bitmapped value that represents the type of table. See Listing 2.4, or
the source code for DBXpress.pas, for an explanation of the
possible values for this field.

Table 2.4 lists the columns that are returned for a schema type of stProcedures.

TABLE 2.4 stProcedures Schema Columns

Column Description
RECNO The absolute record number. It is one for the first record, two for the
second, and so on.
CATALOG_NAME The name of the catalog, or database, that contains the stored procedure.
SCHEMA_NAME The owner of the stored procedure.
PROC_NAME The name of the stored procedure.
PROC_TYPE A bitmapped value that represents the type of stored procedure. See
Listing 2.4, or the source code for DBXpress.pas, for an explanation of
the possible values for this field.
IN_PARAMS The number of input parameters to the stored procedure.
OUT_PARAMS The number of output parameters from the stored procedure.
dbExpress Datasets
89

Table 2.5 lists the columns that are returned for a schema type of stColumns.

TABLE 2.5 stColumns Schema Columns

Column Description
RECNO The absolute record number. It is one for the first record, two for
the second, and so on.
CATALOG_NAME The name of the catalog, or database, that contains the table.
SCHEMA_NAME The owner of the column.
TABLE_NAME The name of the table containing the column. 2
COLUMN_NAME The name of the column.

DBEXPRESS
DATASETS
COLUMN_POSITION The zero-based position of the column in the table definition.
COLUMN_TYPE A bitmapped value that represents the type of column. See Listing
2.4, or the source code for DBXpress.pas, for an explanation of the
possible values for this field.
COLUMN_DATATYPE The logical field type. See Listing 2.4, or the source code for
DBXpress.pas, for an explanation of the possible values for this field.
COLUMN_TYPENAME The SQL column type (VARCHAR, BLOB, and the like).
COLUMN_SUBTYPE The logical field subtype. See Listing 2.4, or the source code for
DBXpress.pas, for an explanation of the possible values for this field.
COLUMN_LENGTH The size of the column in bytes.
COLUMN_PRECISION The precision of the column. It varies by column type. For example,
it is the number of characters for strings, and it is the number of
significant digits for BCD values.
COLUMN_SCALE The numeric scale. It is the number of digits to the right of the
decimal point for BCD columns.
COLUMN_NULLABLE It is one if the column can contain NULL values, and zero if it cannot
contain NULL values.

Table 2.6 lists the columns that are returned for a schema type of stProcedureParams.

TABLE 2.6 stProcedureParams Schema Columns

Column Description
RECNO The absolute record number. It is one for the first record, two for the
second, and so on.
CATALOG_NAME The name of the catalog, or database, that contains the stored
procedure.
Chapter 2
90

TABLE 2.6 Continued


Column Description
SCHEMA_NAME The owner of the procedure parameter.
PROCEDURE_NAME The name of the procedure that contains the parameter.
PARAM_NAME The name of the parameter.
PARAM_POSITION The zero-based position of the parameter. Note that input and output
parameters each have their own list, so the first input parameter is
position zero, and the first output parameter is also position zero.
PARAM_TYPE It is one for an input parameter, two for an output parameter, three
for an input/output parameter, and four for a return value.
PARAM_DATATYPE The logical parameter type. See Listing 2.4, or the source code for
DBXpress.pas, for an explanation of the possible values for this field.
PARAM_SUBTYPE The logical parameter subtype. See Listing 2.4, or the source code
for DBXpress.pas, for an explanation of the possible values for this
field.
PARAM_TYPENAME The SQL parameter type (VARCHAR, BLOB, and the like).
PARAM_LENGTH The size of the parameter in bytes.
PARAM_PRECISION The precision of the parameter. It varies by parameter type. For
example, it is the number of characters for strings, and it is the number
of significant digits for BCD values.
PARAM_SCALE The numeric scale. It is the number of digits to the right of the decimal
point for BCD parameters.
PARAM_NULLABLE It is one if the parameter can contain NULL values, and zero if it can-
not contain NULL values.

Table 2.7 lists the columns that are returned for a schema type of stIndexes.

TABLE 2.7 stIndexes Schema Columns

Column Description
RECNO The absolute record number. It is one for the first record, two for
the second, and so on.
CATALOG_NAME The name of the catalog, or database, that contains the index.
SCHEMA_NAME The owner of the index.
TABLE_NAME The name of the table on which the index is defined.
INDEX_NAME The name of the index.
COLUMN_NAME The name of the column that is part of the index.
dbExpress Datasets
91

TABLE 2.7 Continued


Column Description

COLUMN_POSITION The position of the column within the index.


PKEY_NAME If a primary key, this is the name of the primary key.
INDEX_TYPE A bitmapped value that represents the type of column. See Listing
2.4, or the source code for DBXpress.pas, for an explanation of the
possible values for this field.
SORT_ORDER This is A for an ascending column, and D for a descending column.
FILTER This represents the filter condition on a filtered/range index, or the
2
expression on an expression index. For example, (LAST + FIRST). It

DBEXPRESS
DATASETS
is only supported by certain databases, such as Oracle.

Summary
This chapter discussed the dbExpress dataset component TSQLDataSet, including the following
key concepts:
• TSQLDataSet is a unidirectional, read-only, lightweight data access mechanism. It can be
used to retrieve data from a table, query, or stored procedure in the database.
• dbExpress datasets support two navigation options: moving to the beginning of the
dataset and moving to the next record in the result set.
• To retrieve data from a TSQLDataSet, you typically call the FieldByName method.
• TSQLDataSet supports BLOB information. To improve query performance, you can set
TSQLDataSet’s BlobSize property to specify the maximum amount of data to retrieve for
each BLOB field.
• To make repeated queries more efficient, you can parameterize them by creating a
placeholder in the SQL statement. Then, you can set different values for this parameter.
The SQL statement is prepared only once—the first time that it is executed.
• To order the data that’s returned from the database, you can specify an ORDER BY
clause in the dataset’s CommandText property.
• You can easily set up master/detail relationships between tables and queries.
• It is possible to use dbExpress datasets to retrieve detailed schema information for tables,
queries, and stored procedures in a database.
The following chapter begins a two-chapter exploration of client datasets.
Client Dataset Basics CHAPTER

3
IN THIS CHAPTER
• What Is a Client Dataset? 94

• Advantages and Disadvantages of Client


Datasets 94

• Creating Client Datasets 95

• Populating and Manipulating Client Datasets


105

• Navigating Client Datasets 113

• Client Dataset Indexes 118

• Filters and Ranges 126

• Searching 136
Chapter 3
94

In the preceding two chapters, I discussed dbExpress—a unidirectional database technology. In


the real world, most applications support bidirectional scrolling through a dataset. As noted
previously, Borland has addressed bidirectional datasets through a technology known as client
datasets. This chapter introduces you to the basic operations of client datasets, including how
they are a useful standalone tool. Subsequent chapters focus on more advanced client dataset
capabilities, including how you can hook a client dataset up to a dbExpress (or other) database
connection to create a true multitier application.

What Is a Client Dataset?


A client dataset, as its name suggests, is a dataset that is located in a client application (as
opposed to an application server). The name is a bit of a misnomer, because it seems to indicate
that client datasets have no use outside a client/server or multitier application. However, as
you’ll see in this chapter, client datasets are useful in other types of applications, especially
single-tier database applications.

NOTE
Client datasets were originally introduced in Delphi 3, and they presented a method
for creating multitier applications in Delphi. As their use became more widespread,
they were enhanced to support additional single-tier functionality.

The base class in VCL/CLX for client datasets is TCustomClientDataSet. Typically, you don’t
work with TCustomClientDataSet directly, but with its direct descendent, TClientDataSet.
(In Chapter 7, “Dataset Providers,” I’ll introduce you to other descendents of
TCustomClientDataSet.) For readability and generalization, I’ll refer to client datasets
generically in this book as TClientDataSet.

Advantages and Disadvantages of Client Datasets


Client datasets have a number of advantages, and a couple of perceived disadvantages. The
advantages include
• Memory based. Client datasets reside completely in memory, making them useful for
temporary tables.
• Fast. Because client datasets are RAM based, they are extremely fast.
• Efficient. Client datasets store their data in a very efficient manner, making them
resource friendly.
Client Dataset Basics
95

• On-the-fly indexing. Client datasets enable you to create and use indexes on-the-fly,
making them extremely versatile.
• Automatic undo support. Client datasets provide multilevel undo support, making it easy
to perform what if operations on your data. Undo support is discussed in Chapter 4,
“Advanced Client Dataset Operations.”
• Maintained aggregates. Client datasets can automatically calculate averages, subtotals,
and totals over a group of records. Maintained aggregates are discussed in detail in
Chapter 4.
The perceived disadvantages include
• Memory based. This client dataset advantage can also be a disadvantage. Because client
datasets reside in RAM, their size is limited by the amount of available RAM.
• Single user. Client datasets are inherently single-user datasets because they are kept in
RAM.
When you understand client datasets, you’ll discover that these so-called disadvantages really
aren’t detrimental to your application at all. In particular, basing client datasets entirely in
RAM has both advantages and disadvantages. 3

CLIENT DATASET
Because they are kept entirely in your computer’s RAM, client datasets are extremely useful
for temporary tables, small lookup tables, and other nonpersistent database needs. Client

BASICS
datasets also are fast because they are RAM based. Inserting, deleting, searching, sorting, and
traversing in client datasets are lightening fast.
On the flip side, you need to take steps to ensure that client datasets don’t grow too large
because you waste precious RAM if you attempt to store huge databases in in-memory
datasets. Fortunately, client datasets store their data in a very compact form. (I’ll discuss this
in more detail in the “Undo Support” section of Chapter 7.)
Because they are memory based, client datasets are inherently single user. Remote machines
do not have access to a client dataset on a local machine. In Chapter 8, “DataSnap,” you’ll
learn how to connect a client dataset to an application server in a three-tier configuration that
supports true multiuser operation.

Creating Client Datasets


Using client datasets in your application is similar to using any other type of dataset because
they derive from TDataSet.
You can create client datasets either at design-time or at runtime, as the following sections
explain.
Chapter 3
96

Creating a Client Dataset at Design-Time


Typically, you create client datasets at design-time. To do so, drop a TClientDataSet component
(located on the Data Access tab) on a form or data module. This creates the component, but
doesn’t set up any field or index definitions. Name the component cdsEmployee.
To create the field definitions for the client dataset, double-click the TClientDataSet component
in the form editor. The standard Delphi field editor is displayed. Right-click the field editor
and select New Field… from the pop-up menu to create a new field. The dialog shown in
Figure 3.1 appears.

FIGURE 3.1
Use the New Field dialog to add a field to a dataset.

If you’re familiar with the field editor, you notice a new field type available for client datasets,
called Aggregate fields. I’ll discuss Aggregate fields in detail in the following chapter. For now,
you should understand that you can add data, lookup, calculated, and internally calculated
fields to a client dataset—just as you can for any dataset.
The difference between client datasets and other datasets is that when you create a data field
for a typical dataset, all you are doing is creating a persistent field object that maps to a field in
the underlying database. For a client dataset, you are physically creating the field in the dataset
along with a persistent field object. At design-time, there is no way to create a field in a client
dataset without also creating a persistent field object.

Data Fields
Most of the fields in your client datasets will be data fields. A data field represents a field that
is physically part of the dataset, as opposed to a calculated or lookup field (which are discussed
in the following sections). You can think of calculated and lookup fields as virtual fields
because they appear to exist in the dataset, but their data actually comes from another location.
Client Dataset Basics
97

Let’s add a field named ID to our dataset. In the field editor, enter ID in the Name edit control.
Tab to the Type combo box and type Integer, or select it from the drop-down list. (The
component name has been created for you automatically.) The Size edit control is disabled
because Integer values are a fixed-length field. The Field type is preset to Data, which is
what we want. Figure 3.2 shows the completed dialog.

FIGURE 3.2
The New Field dialog after entering information for a new field.

Click OK to add the field to the client dataset. You’ll see the new ID field listed in the field editor. 3

CLIENT DATASET
Now add a second field, called LastName. Right-click the field editor to display the New Field

BASICS
dialog and enter LastName in the Name edit control. In the Type combo, select String. Then, set
Size to 30—the size represents the maximum number of characters allowed for the field. Click
OK to add the LastName field to the dataset.
Similarly, add a 20-character FirstName field and an Integer Department field.Finally, let’s
add a Salary field. Open the New Field dialog. In the Name edit control, type Salary. Set the
Type to Currency and click OK. (The currency type instructs Delphi to automatically display
it with a dollar sign.)
If you have performed these steps correctly, the field editor looks like Figure 3.3.

FIGURE 3.3
The field editor after adding five fields.
Chapter 3
98

That’s enough fields for this dataset. In the next section, I’ll show you how to create a
calculated field.

Calculated Fields
Calculated fields, as indicated previously, don’t take up any physical space in the dataset.
Instead, they are calculated on-the-fly from other data stored in the dataset. For example, you
might create a calculated field that adds the values of two data fields together. In this section,
we’ll create two calculated fields: one standard and one internal.

NOTE
Actually, internal calculated fields do take up space in the dataset, just like a standard
data field. For that reason, you can create indexes on them like you would on a data
field. Indexes are discussed later in this chapter.

Standard Calculated Fields


In this section, we’ll create a calculated field that computes an annual bonus, which we’ll
assume to be five percent of an employee’s salary.
To create a standard calculated field, open the New Field dialog (as you did in the preceding
section). Enter a Name of Bonus and a Type of Currency.
In the Field Type radio group, select Calculated. This instructs Delphi to create a calculated
field, rather than a data field. Click OK.
That’s all you need to do to create a calculated field. Now, let’s look at internal calculated
fields.

Internal Calculated Fields


Creating an internal calculated field is almost identical to creating a standard calculated field.
The only difference is that you select InternalCalc as the Field Type in the New Field dia-
log, instead of Calculated.
Another difference between the two types of calculated fields is that standard calculated fields
are calculated on-the-fly every time their value is required, but internal calculated fields are
calculated once and their value is stored in RAM. (Of course, internal calculated fields recal-
culate automatically if the underlying fields that they are calculated from change.)
The dataset’s AutoCalcFields property determines exactly when calculated fields are recom-
puted. If AutoCalcFields is True (the default value), calculated fields are computed when the
dataset is opened, when the dataset enters edit mode, and whenever focus in a form moves
Client Dataset Basics
99

from one data-aware control to another and the current record has been modified. If
AutoCalcFields is False, calculated fields are computed when the dataset is opened, when the
dataset enters edit mode, and when a record is retrieved from an underlying database into the
dataset.
There are two reasons that you might want to use an internal calculated field instead of a standard
calculated field. If you want to index the dataset on a calculated field, you must use an internal
calculated field. (Indexes are discussed in detail later in this chapter.) Also, you might elect to
use an internal calculated field if the field value takes a relatively long time to calculate.
Because they are calculated once and stored in RAM, internal calculated fields do not have to
be computed as often as standard calculated fields.
Let’s add an internal calculated field to our dataset. The field will be called Name, and it will
concatenate the FirstName and LastName fields together. We probably will want an index on
this field later, so we need to make it an internal calculated field.
Open the New Field dialog, and enter a Name of Name and a Type of String. Set Size to 52
(which accounts for the maximum length of the last name, plus the maximum length of the
first name, plus a comma and a space to separate the two).
3
In the Field Type radio group, select InternalCalc and click OK.

CLIENT DATASET
Providing Values for Calculated Fields

BASICS
At this point, we’ve created our calculated fields. Now we need to provide the code to
calculate the values. TClientDataSet, like all Delphi datasets, supports a method named
OnCalcFields that we need to provide a body for.

Click the client dataset again, and in the Object Inspector, click the Events tab. Double-click
the OnCalcFields event to create an event handler.
We’ll calculate the value of the Bonus field first. Flesh out the event handler so that it looks
like this:
procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);
begin
cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;
end;

That’s easy—we just take the value of the Salary field, multiply it by five percent (0.05), and
store the value in the Bonus field.
Now, let’s add the Name field calculation. A first (reasonable) attempt looks like this:
procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);
begin
cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;
Chapter 3
100

cdsEmployeeName.AsString := cdsEmployeeLastName.AsString + ‘, ‘ +
cdsEmployeeFirstName.AsString;
end;

This works, but it isn’t efficient. The Name field calculates every time the Bonus field calculates.
However, recall that it isn’t necessary to compute internal calculated fields as often as standard
calculated fields. Fortunately, we can check the dataset’s State property to determine whether
we need to compute internal calculated fields or not, like this:
procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);
begin
cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;

if cdsEmployee.State = dsInternalCalc then


cdsEmployeeName.AsString := cdsEmployeeLastName.AsString + ‘, ‘ +
cdsEmployeeFirstName.AsString;
end;

Notice that the Bonus field is calculated every time, but the Name field is only calculated when
Delphi tells us that it’s time to compute internal calculated fields.

Lookup Fields
Lookup fields are similar, in concept, to calculated fields because they aren’t physically stored
in the dataset. However, instead of requiring you to calculate the value of a lookup field,
Delphi gets the value from another dataset. Let’s look at an example.
Earlier, we created a Department field in our dataset. Let’s create a new Department dataset to
hold department information.
Drop a new TClientDataSet component on your form and name it cdsDepartment. Add two
fields: Dept (an integer) and Description (a 30-character string).
Show the field editor for the cdsEmployee dataset by double-clicking the dataset. Open the
New Field dialog. Name the field DepartmentName, and give it a Type of String and a Size of
30.
In the FieldType radio group, select Lookup. Notice that two of the fields in the Lookup
definition group box are now enabled. In the Key Fields combo, select Department. In the
Dataset combo, select cdsDepartment.

At this point, the other two fields in the Lookup definition group box are accessible. In the
Lookup Keys combo box, select Dept. In the Result Field combo, select Description. The
completed dialog should look like the one shown in Figure 3.4.
Client Dataset Basics
101

FIGURE 3.4
Adding a lookup field to a dataset.

The important thing to remember about lookup fields is that the Key field represents the field
in the base dataset that references the lookup dataset. Dataset refers to the lookup dataset. The
Lookup Keys combo box represents the Key field in the lookup dataset. The Result field is
the field in the lookup dataset from which the lookup field obtains its value.
To create the dataset at design time, you can right-click the TClientDataSet component and
select Create DataSet from the pop-up menu. 3
Now that you’ve seen how to create a client dataset at design-time, let’s see what’s required to

CLIENT DATASET
create a client dataset at runtime.

BASICS
Creating a Client Dataset at Runtime
To create a client dataset at runtime, you start with the following skeletal code:
var
CDS: TClientDataSet;
begin
CDS := TClientDataSet.Create(nil);
try
// Do something with the client dataset here
finally
CDS.Free;
end;
end;

After you create the client dataset, you typically add fields, but you can load the client dataset
from a disk instead (as you’ll see later in this chapter in the section titled “Persisting Client
Datasets”).
Chapter 3
102

Adding Fields to a Client Dataset


To add fields to a client dataset at runtime, you use the client dataset’s FieldDefs property.
FieldDefs supports two methods for adding fields: AddFieldDef and Add.

AddFieldDef
TFieldDefs.AddFieldDef is defined like this:
function AddFieldDef: TFieldDef;

As you can see, AddFieldDef takes no parameters and returns a TFieldDef object. When you
have the TFieldDef object, you can set its properties, as the following code snippet shows.
var
FieldDef: TFieldDef;
begin
FieldDef := ClientDataSet1.FieldDefs.AddFieldDef;
FieldDef.Name := ‘Name’;
FieldDef.DataType := ftString;
FieldDef.Size := 20;
FieldDef.Required := True;
end;

Add
A quicker way to add fields to a client dataset is to use the TFieldDefs.Add method, which is
defined like this:
procedure Add(const Name: string; DataType: TFieldType; Size: Integer = 0;
Required: Boolean = False);

The Add method takes the field name, the data type, the size (for string fields), and a flag
indicating whether the field is required as parameters. By using Add, the preceding code snippet
becomes the following single line of code:
ClientDataSet1.FieldDefs.Add(‘Name’, ftString, 20, True);

Why would you ever want to use AddFieldDef when you could use Add? One reason is that
TFieldDef contains several more-advanced properties (such as field precision, whether or not
it’s read-only, and a few other attributes) in addition to the four supported by Add. If you want
to set these properties for a field, you need to go through the TFieldDef. You should refer to
the Delphi documentation for TFieldDef for more details.

Creating the Dataset


After you create the field definitions, you need to create the empty dataset in memory. To do
this, call TClientDataSet.CreateDataSet, like this:
ClientDataSet1.CreateDataSet;
Client Dataset Basics
103

As you can see, it’s somewhat easier to create your client datasets at design-time than it is at
runtime. However, if you commonly create temporary in-memory datasets, or if you need to
create a client dataset in a formless unit, you can create the dataset at runtime with a minimal
amount of fuss.

Accessing Fields
Regardless of how you create the client dataset, at some point you need to access field infor-
mation—whether it’s for display, to calculate some values, or to add or modify a new record.
There are several ways to access field information in Delphi. The easiest is to use persistent fields.

Persistent Fields
Earlier in this chapter, when we used the field editor to create fields, we were also creating
persistent field objects for those fields. For example, when we added the LastName field,
Delphi created a persistent field object named cdsEmployeeLastName.
When you know the name of the field object, you can easily retrieve the contents of the field
by using the AsXxx family of methods. For example, to access a field as a string, you would
reference the AsString property, like this: 3

CLIENT DATASET
ShowMessage(‘The employee’’s last name is ‘ +
cdsEmployeeLastName.AsString);

BASICS
To retrieve the employee’s salary as a floating-point number, you would reference the AsFloat
property:
Bonus := cdsEmployeeSalary.AsFloat * 0.05;

See the VCL/CLX source code and the Delphi documentation for a list of available access
properties.

NOTE
You are not limited to accessing a field value in its native format. For example, just
because Salary is a currency field doesn’t mean you can’t attempt to access it as a
string. The following code displays an employee’s salary as a formatted currency:
ShowMessage(‘Your salary is ‘ + cdsEmployeeSalary.AsString);
You could access a string field as an integer, for example, if you knew that the field
contained an integer value. However, if you try to access a field as an integer (or
other data type) and the field doesn’t contain a value that’s compatible with that
data type, Delphi raises an exception.
Chapter 3
104

Nonpersistent Fields
If you create a dataset at design-time, you probably won’t have any persistent field objects. In
that case, there are a few methods you can use to access a field’s value.
The first is the FieldByName method. FieldByName takes the field name as a parameter and
returns a temporary field object. The following code snippet displays an employee’s last name
using FieldByName.
ShowMessage(‘The employee’’s last name is ‘ +
ClientDataSet1.FieldByName(‘LastName’).AsString);

CAUTION
If you call FieldByName with a nonexistent field name, Delphi raises an exception.

Another way to access the fields in a dataset is through the FindField method, like this:
if ClientDataSet1.FindField(‘LastName’) <> nil then
ShowMessage(‘Dataset contains a LastName field’);

Using this technique, you can create persistent fields for datasets created at runtime.
var
fldLastName: TField;
fldFirstName: TField;
begin
...
fldLastName := cds.FindField(‘LastName’);
fldFirstName := cds.FindField(‘FirstName’);
...
ShowMessage(‘The last name is ‘ + fldLastName.AsString);
end;

Finally, you can access the dataset’s Fields property. Fields contains a list of TField objects
for the dataset, as the following code illustrates:
var
Index: Integer;
begin
for Index := 0 to ClientDataSet1.Fields.Count - 1 do
ShowMessage(ClientDataSet1.Fields[Index].AsString);
end;

You do not normally access Fields directly. It is generally not safe programming practice to
assume, for example, that a given field is the first field in the Fields list. However, there are
Client Dataset Basics
105

times when the Fields list comes in handy. For example, if you have two client datasets with
the same structure, you could add a record from one dataset to the other using the following code:
var
Index: Integer;
begin
ClientDataSet2.Append;
for Index := 0 to ClientDataSet1.Fields.Count - 1 do
ClientDataSet2.Fields[Index].AsVariant :=
ClientDataSet1.Fields[Index].AsVariant;
ClientDataSet2.Post;
end;

The following section discusses adding records to a dataset in detail.

Populating and Manipulating Client Datasets


After you create a client dataset (either at design-time or at runtime), you want to populate it
with data. There are several ways to populate a client dataset: You can populate it manually
through code, you can load the dataset’s records from another dataset, or you can load the
dataset from a file or a stream. The following sections discuss these methods, as well as how to 3
modify and delete records.

CLIENT DATASET
BASICS
Populating Manually
The most basic way to enter data into a client dataset is through the Append and Insert methods,
which are supported by all datasets. The difference between them is that Append adds the new
record at the end of the dataset, but Insert places the new record immediately before the
current record.
I always use Append to insert new records because it’s slightly faster than Insert. If the dataset
is indexed, the new record is automatically sorted in the correct order anyway.
The following code snippet shows how to add a record to a client dataset:
cdsEmployee.Append; // You could use cdsEmployee.Insert; here as well
cdsEmployee.FieldByName(‘ID’).AsInteger := 5;
cdsEmployee.FieldByName(‘FirstName’).AsString := ‘Eric’;
cdsEmployee.Post;

Modifying Records
Modifying an existing record is almost identical to adding a new record. Rather than calling
Append or Insert to create the new record, you call Edit to put the dataset into edit mode. The
following code changes the first name of the current record to Fred.
Chapter 3
106

cdsEmployee.Edit; // Edit the current record


cdsEmployee.FieldByName(‘FirstName’).AsString := ‘Fred’;
cdsEmployee.Post;

Deleting Records
To delete the current record, simply call the Delete method, like this:
cdsEmployee.Delete;

If you want to delete all records in the dataset, you can use EmptyDataSet instead, like this:
cdsEmployee.EmptyDataSet;

Populating from Another Dataset


dbExpress datasets are unidirectional and you can’t scroll backward through them. This makes
them incompatible with bidirectional, data-aware controls such as TDBGrid. However,
TClientDataSet can load its data from another dataset (including dbExpress datasets, BDE
datasets, or other client datasets) through a provider. Using this feature, you can load a client
dataset from a unidirectional dbExpress dataset, and then connect a TDBGrid to the client
dataset, providing bidirectional support.
Indeed, this capability is so powerful and important that it forms the basis for Delphi’s multitier
database support.

Populating from a File or Stream: Persisting Client


Datasets
Though client datasets are located in RAM, you can save them to a file or a stream and reload
them at a later point in time, making them persistent. This is the third method of populating a
client dataset.
To save the dataset to a file, use the SaveToFile method, which is defined like this:
procedure SaveToFile(const FileName: string = ‘’;
Format: TDataPacketFormat = dfBinary);

Similarly, to save the dataset to a stream, you call SaveToStream, which is defined as follows:
procedure SaveToStream(Stream: TStream; Format: TDataPacketFormat = dfBinary);

SaveToFile accepts the name of the file that you’re saving to. If the filename is blank, the data
is saved using the FileName property of the client dataset.
Both SaveToFile and SaveToStream take a parameter that indicates the format to use when
saving data. Client datasets can be stored in one of three file formats: binary, or either flavor of
XML. Table 3.1 lists the possible formats.
Client Dataset Basics
107

TABLE 3.1 Data Packet Formats for Loading and Saving Client Datasets
Value Description
dfBinary Data is stored using a proprietary, binary format.
dfXML Data is stored in XML format. Extended characters are represented
using an escape sequence.
dfXMLUTF8 Data is stored in XML format. Extended characters are represented
using UTF8.

When client datasets are stored to disk, they are referred to as MyBase files. MyBase stores
one dataset per file, or per stream, unless you use nested datasets.

NOTE
If you’re familiar with Microsoft ADO, you recall that ADO enables you to persist
datasets using XML format. The XML formats used by ADO and MyBase are not
compatible. In other words, you cannot save an ADO dataset to disk in XML format,
and then read it into a client dataset (or vice versa). 3

CLIENT DATASET
Sometimes, you need to determine how many bytes are required to store the data contained in

BASICS
the client dataset. For example, you might want to check to see if there is enough room on a
floppy disk before saving the data there, or you might need to preallocate the memory for a
stream. In these cases, you can check the DataSize property, like this:
if ClientDataSet1.DataSize > AvailableSpace then
ShowMessage(‘Not enough room to store the data’);

DataSize always returns the amount of space necessary to store the data in binary format
(dfBinary). XML format usually requires more space, perhaps twice as much (or even more).

NOTE
One way to determine the amount of space that’s required to save the dataset in
XML format is to save the dataset to a memory stream, and then obtain the size of
the resulting stream.
Chapter 3
108

Example: Creating, Populating, and Manipulating a Client


Dataset
The following example illustrates how to create, populate, and manipulate a client dataset at
runtime. Code is also provided to save the dataset to disk and to load it.
Listing 3.1 shows the complete source code for the CDS (ClientDataset) application.

LISTING 3.1 CDS—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Types, IdGlobal, Classes, QGraphics, QControls, QForms, QDialogs,
QStdCtrls, DB, DBClient, QExtCtrls, QGrids, QDBGrids, QActnList;

const
MAX_RECS = 10000;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnPopulate: TButton;
btnSave: TButton;
btnLoad: TButton;
ActionList1: TActionList;
btnStatistics: TButton;
Populate1: TAction;
Statistics1: TAction;
Load1: TAction;
Save1: TAction;
DBGrid1: TDBGrid;
lblFeedback: TLabel;
procedure FormCreate(Sender: TObject);
procedure Populate1Execute(Sender: TObject);
procedure Statistics1Execute(Sender: TObject);
procedure Save1Execute(Sender: TObject);
procedure Load1Execute(Sender: TObject);
private
{ Private declarations }
FCDS: TClientDataSet;
public
Client Dataset Basics
109

LISTING 3.1 Continued


{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
FCDS := TClientDataSet.Create(Self);
FCDS.FieldDefs.Add(‘ID’, ftInteger, 0, True);
FCDS.FieldDefs.Add(‘Name’, ftString, 20, True);
FCDS.FieldDefs.Add(‘Birthday’, ftDateTime, 0, True);
FCDS.FieldDefs.Add(‘Salary’, ftCurrency, 0, True);
FCDS.CreateDataSet;
DataSource1.DataSet := FCDS;
end;
3

CLIENT DATASET
procedure TfrmMain.Populate1Execute(Sender: TObject);

BASICS
const
FirstNames: array[0 .. 19] of string = (‘John’, ‘Sarah’, ‘Fred’, ‘Beth’,
‘Eric’, ‘Tina’, ‘Thomas’, ‘Judy’, ‘Robert’, ‘Angela’, ‘Tim’, ‘Traci’,
‘David’, ‘Paula’, ‘Bruce’, ‘Jessica’, ‘Richard’, ‘Carla’, ‘James’,
‘Mary’);
LastNames: array[0 .. 11] of string = (‘Parker’, ‘Johnson’, ‘Jones’,
‘Thompson’, ‘Smith’, ‘Baker’, ‘Wallace’, ‘Harper’, ‘Parson’, ‘Edwards’,
‘Mandel’, ‘Stone’);
var
Index: Integer;
t1, t2: DWord;
begin
RandSeed := 0;

t1 := GetTickCount;
FCDS.DisableControls;
try
FCDS.EmptyDataSet;
for Index := 1 to MAX_RECS do begin
FCDS.Append;
FCDS.FieldByName(‘ID’).AsInteger := Index;
FCDS.FieldByName(‘Name’).AsString := FirstNames[Random(20)] + ‘ ‘ +
Chapter 3
110

LISTING 3.1 Continued

LastNames[Random(12)];
FCDS.FieldByName(‘Birthday’).AsDateTime := StrToDate(‘1/1/1950’) +
Random(10000);
FCDS.FieldByName(‘Salary’).AsFloat := 20000.0 + Random(600) * 100;
FCDS.Post;
end;
FCDS.First;
finally
FCDS.EnableControls;
end;
t2 := GetTickCount;
lblFeedback.Caption := Format(‘%d ms to load %.0n records’,
[t2 - t1, MAX_RECS * 1.0]);
end;

procedure TfrmMain.Statistics1Execute(Sender: TObject);


var
t1, t2: DWord;
msLocateID: DWord;
msLocateName: DWord;
begin
FCDS.First;
t1 := GetTickCount;
FCDS.Locate(‘ID’, 9763, []);
t2 := GetTickCount;
msLocateID := t2 - t1;

FCDS.First;
t1 := GetTickCount;
FCDS.Locate(‘Name’, ‘Eric Wallace’, []);
t2 := GetTickCount;
msLocateName := t2 - t1;

ShowMessage(Format(‘%d ms to locate ID 9763’ +


#13’%d ms to locate Eric Wallace’ +
#13’%.0n bytes required to store %.0n records’,
[msLocateID, msLocateName, FCDS.DataSize * 1.0, MAX_RECS * 1.0]));
end;

procedure TfrmMain.Save1Execute(Sender: TObject);


var
t1, t2: DWord;
begin
t1 := GetTickCount;
Client Dataset Basics
111

LISTING 3.1 Continued

FCDS.SaveToFile(‘C:\Employee.cds’);
t2 := GetTickCount;
lblFeedback.Caption := Format(‘%d ms to save data’, [t2 - t1]);
end;

procedure TfrmMain.Load1Execute(Sender: TObject);


var
t1, t2: DWord;
begin
try
t1 := GetTickCount;
FCDS.LoadFromFile(‘C:\Employee.cds’);
t2 := GetTickCount;
lblFeedback.Caption := Format(‘%d ms to load data’, [t2 - t1]);
except
FCDS.Open;
raise;
end;
end; 3

CLIENT DATASET
end.

BASICS
There are five methods in this application and each one is worth investigating:
• FormCreate creates the client dataset and its schema at runtime. It would actually be easier
to create the dataset at design-time, but I wanted to show you the code required to do this
at runtime. The code creates four fields: Employee ID, Name, Birthday, and Salary.
• Populate1Execute loads the client dataset with 10,000 employees made up of random
data. At the beginning of the method, I manually set RandSeed to 0 to ensure that multiple
executions of the application would generate the same data.

NOTE
The Delphi Randomizer normally seeds itself with the current date and time. By
manually seeding the Randomizer with a constant value, we can ensure that the random
numbers generated are consistent every time we run the program.

• The method calculates approximately how long it takes to generate the 10,000 employees,
which on my computer is about half of a second.
Chapter 3
112

• Statistics1Execute simply measures the length of time required to perform a couple of


Locate operations and calculates the amount of space necessary to store the data on disk
(again, in binary format). I’ll be discussing the Locate method later in this chapter.
• Save1Execute saves the data to disk under the filename C:\Employee.cds. The .cds
extension is standard, although not mandatory, for client datasets that are saved in a
binary format. Client datasets stored in XML format generally have the extension .xml.

NOTE
Please make sure that you click the Save button because the file created
(C:\EMPLOYEE.CDS) is used in the rest of the example applications in this chapter, as
well as some of the examples in the following chapter.

• Load1Execute loads the data from a file into the client dataset. If LoadFromFile fails
(presumably because the file doesn’t exist or is not a valid file format), the client dataset
is left in a closed state. For this reason, I reopen the client dataset when an exception is
raised.
Figure 3.5 shows the CDS application running on my computer. Note the impressive times
posted to locate a record. Even when searching through almost the entire dataset to find ID
9763, it only takes approximately 10 ms on my computer.

FIGURE 3.5
The CDS application at runtime.
Client Dataset Basics
113

Navigating Client Datasets


A dataset is worthless without a means of moving forward and/or backward through it.
Delphi’s datasets provide a large number of methods for traversing a dataset. The following
sections discuss Delphi’s support for dataset navigation.

Sequential Navigation
The most basic way to navigate through a dataset is sequentially in either forward or reverse
order. For example, you might want to iterate through a dataset when printing a report, or for
some other reason. Delphi provides four simple methods to accomplish this:
• First moves to the first record in the dataset. First always succeeds, even if the dataset
is empty. If it is empty, First sets the dataset’s EOF (end of file) property to True.
• Next moves to the next record in the dataset (if the EOF property is not already set). If
EOF is True, Next will fail. If the call to Next reaches the end of the file, it sets the EOF
property to True.
• Lastmoves to the last record in the dataset. Last always succeeds, even if the dataset is
empty. If it is empty, Last sets the dataset’s BOF (beginning of file) property to True. 3
• moves to the preceding record in the dataset (if the BOF property is not already

CLIENT DATASET
Prior
set). If BOF is True, Prior will fail. If the call to Prior reaches the beginning of the file,

BASICS
it sets the BOF property to True.
The following code snippet shows how you can use these methods to iterate through a dataset:
if not ClientDataSet1.IsEmpty then begin
ClientDataSet1.First;
while not ClientDataSet1.EOF do begin
// Process the current record

ClientDataSet1.Next;
end;

ClientDataSet1.Last;
while not ClientDataSet1.BOF do begin
// Process the current record

ClientDataSet1.Prior;
end;
end;
Chapter 3
114

Random-Access Navigation
In addition to First, Next, Prior, and Last (which provide for sequential movement through a
dataset), TClientDataSet provides two ways of moving directly to a given record: bookmarks
and record numbers.

Bookmarks
A bookmark used with a client dataset is very similar to a bookmark used with a paper-based
book: It marks a location in a dataset so that you can quickly return to it later.
There are three operations that you can perform with bookmarks: set a bookmark, return to a
bookmark, and free a bookmark. The following code snippet shows how to do all three:
var
Bookmark: TBookmark;
begin
Bookmark := ClientDataSet1.GetBookmark;
try
// Do something with ClientDataSet1 here that changes the current record
...
ClientDataSet1.GotoBookmark(Bookmark);
finally
ClientDataSet1.FreeBookmark(Bookmark);
end;
end;

You can create as many bookmarks as you want for a dataset. However, keep in mind that a
bookmark allocates a small amount of memory, so you should be sure to free all bookmarks
using FreeBookmark or your application will leak memory.
There is a second set of operations that you can use for bookmarks instead of
GetBookmark/GotoBookmark/FreeBookmark. The following code shows this alternate method:
var
BookmarkStr: string;
begin
BookmarkStr := ClientDataSet1.Bookmark;
try
// Do something with ClientDataSet1 here that changes the current record
...
finally
ClientDataSet1.Bookmark := BookmarkStr;
end;
end;
Client Dataset Basics
115

Because the bookmark returned by the property, Bookmark, is a string, you don’t need to concern
yourself with freeing the string when you’re done. Like all strings, Delphi automatically frees
the bookmark when it goes out of scope.

Record Numbers
Client datasets support a second way of moving directly to a given record in the dataset: setting
the RecNo property of the dataset. RecNo is a one-based number indicating the sequential
number of the current record relative to the beginning of the dataset.
You can read the RecNo property to determine the current absolute record number, and write
the RecNo property to set the current record. There are two important things to keep in mind
with respect to RecNo:
• Attempting to set RecNo to a number less than one, or to a number greater than the number
of records in the dataset results in an At beginning of table, or an At end of table
exception, respectively.
• The record number of any given record is not guaranteed to be constant. For instance,
changing the active index on a dataset alters the record number of all records in the
dataset. 3

CLIENT DATASET
NOTE

BASICS
You can determine the number of records in the dataset by inspecting the dataset’s
RecordCount property. When setting RecNo, never attempt to set it to a number
higher than RecordCount.

However, when used discriminately, RecNo has its uses. For example, let’s say the user of your
application wants to delete all records between the John Smith record and the Fred Jones
record. The following code shows how you can accomplish this:
var
RecNoJohn: Integer;
RecNoFred: Integer;
Index: Integer;
begin
if not ClientDataSet1.Locate(‘Name’, ‘John Smith’, []) then
raise Exception.Create(‘Cannot locate John Smith’);
RecNoJohn := ClientDataSet1.RecNo;

if not ClientDataSet1.Locate(‘Name’, ‘Fred Jones’, []) then


raise Exception.Create(‘Cannot locate Fred Jones’);
RecNoFred := ClientDataSet1.RecNo;
Chapter 3
116

if RecNoJohn < RecNoFred then


// Locate John again
ClientDataSet1.RecNo := RecNoJohn;

for Index := 1 to Abs(RecNoJohn - RecNoFred) + 1 do


ClientDataSet1.Delete;
end;

This code snippet first locates the two bounding records and remembers their absolute record
numbers. Then, it positions the dataset to the lower record number. If Fred occurs before John,
the dataset is already positioned at the lower record number.
Because records are sequentially numbered, we can subtract the two record numbers (and add
one) to determine the number of records to delete. Deleting a record makes the next record
current, so a simple for loop handles the deletion of the records.
Keep in mind that RecNo isn’t usually going to be your first line of attack for moving around in
a dataset, but it’s handy to remember that it’s available if you ever need it.
Listing 3.2 contains the complete source code for an application that demonstrates the different
navigational methods of client datasets.

LISTING 3.2 Navigate—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids, QDBCtrls;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnFirst: TButton;
btnLast: TButton;
btnNext: TButton;
btnPrior: TButton;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
btnSetRecNo: TButton;
DBNavigator1: TDBNavigator;
btnGetBookmark: TButton;
Client Dataset Basics
117

LISTING 3.2 Continued

btnGotoBookmark: TButton;
procedure FormCreate(Sender: TObject);
procedure btnNextClick(Sender: TObject);
procedure btnLastClick(Sender: TObject);
procedure btnSetRecNoClick(Sender: TObject);
procedure btnFirstClick(Sender: TObject);
procedure btnPriorClick(Sender: TObject);
procedure btnGetBookmarkClick(Sender: TObject);
procedure btnGotoBookmarkClick(Sender: TObject);
private
{ Private declarations }
FBookmark: TBookmark;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation
3

CLIENT DATASET
{$R *.xfm}

BASICS
procedure TfrmMain.FormCreate(Sender: TObject);
begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
end;

procedure TfrmMain.btnFirstClick(Sender: TObject);


begin
ClientDataSet1.First;
end;

procedure TfrmMain.btnPriorClick(Sender: TObject);


begin
ClientDataSet1.Prior;
end;

procedure TfrmMain.btnNextClick(Sender: TObject);


begin
ClientDataSet1.Next;
end;

procedure TfrmMain.btnLastClick(Sender: TObject);


Chapter 3
118

LISTING 3.2 Continued

begin
ClientDataSet1.Last;
end;

procedure TfrmMain.btnSetRecNoClick(Sender: TObject);


var
Value: string;
begin
Value := ‘1’;
if InputQuery(‘RecNo’, ‘Enter Record Number’, Value) then
ClientDataSet1.RecNo := StrToInt(Value);
end;

procedure TfrmMain.btnGetBookmarkClick(Sender: TObject);


begin
if Assigned(FBookmark) then
ClientDataSet1.FreeBookmark(FBookmark);

FBookmark := ClientDataSet1.GetBookmark;
end;

procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject);


begin
if Assigned(FBookmark) then
ClientDataSet1.GotoBookmark(FBookmark)
else
ShowMessage(‘No bookmark set!’);
end;

end.

Figure 3.6 shows this program at runtime.

Client Dataset Indexes


So far, we haven’t created any indexes on the client dataset and you might be wondering if
(and why) they’re even necessary when sequential searches through the dataset (using Locate)
are so fast.
Indexes are used on client datasets for at least three reasons:
• To provide faster access to data. A single Locate operation executes very quickly, but if
you need to perform thousands of Locate operations, there is a noticeable performance
gain when using indexes.
Client Dataset Basics
119

• To enable the client dataset to be sorted on-the-fly. This is useful when you want to order
the data in a data-aware grid, for example.
• To implement maintained aggregates.

FIGURE 3.6
The Navigate application demonstrates various navigational techniques.

3
Creating Indexes

CLIENT DATASET
Like field definitions, indexes can be created at design-time or at runtime. Unlike field

BASICS
definitions, which are usually created at design-time, you might want to create and destroy
indexes at runtime. For example, some indexes are only used for a short time—say, to create a
report in a certain order. In this case, you might want to create the index, use it, and then
destroy it. If you constantly need an index, it’s better to create it at design-time (or to create it
the first time you need it and not destroy it afterward).

Creating Indexes at Design-Time


To create an index at design-time, click the TClientDataSet component located on the form
or data module. In the Object Inspector, double-click the IndexDefs property. The index editor
appears.
To add an index to the client dataset, right-click the index editor and select Add from the
pop-up menu. Alternately, you can click the Add icon on the toolbar, or simply press Ins.
Next, go back to the Object Inspector and set the appropriate properties for the index. Table
3.2 shows the index properties.
Chapter 3
120

TABLE 3.2 Index Properties


Property Description
Name The name of the index. I recommend prefixing index names with
the letters by (as in byName, byState, and so on).
Fields Semicolon-delimited list of fields that make up the index. Example:
‘ID’ or ‘Name;Salary’.
DescFields A list of the fields contained in the Fields property that should be
indexed in descending order. For example, to sort ascending by
name, and then descending by salary, set Fields to ‘Name;Salary’
and DescFields to ‘Salary’.
CaseInsFields A list of the fields contained in the Fields property that should be
indexed in a manner which is not case sensitive. For example, if the
index is on the last and first name, and neither is case sensitive, set
Fields to ‘Last;First’ and CaseInsFields to ‘Last;First’.
GroupingLevel Used for aggregation.
Options Sets additional options on the index. The options are discussed in
Table 3.3.
Expression Not applicable to client datasets.
Source Not applicable to client datasets.

Table 3.3 shows the various index options that can be set using the Options property.

TABLE 3.3 Index Options


Option Description
IxPrimary The index is the primary index on the dataset.
IxUnique The index is unique.
IxDescending The index is in descending order.
IxCaseInsensitive The index is not case sensitive.
IxExpression Not applicable to client datasets.
IxNonMaintained Not applicable to client datasets.

You can create multiple indexes on a single dataset. So, you can easily have both an ascending
and a descending index on EmployeeName, for example.
Client Dataset Basics
121

Creating and Deleting Indexes at Runtime


In contrast to field definitions (which you usually create at design-time), index definitions are
something that you frequently create at runtime. There are a couple of very good reasons for this:
• Indexes can be quickly and easily created and destroyed. So, if you only need an index
for a short period of time (to print a report in a certain order, for example), creating and
destroying the index on an as-needed basis helps conserve memory.
• Index information is not saved to a file or a stream when you persist a client dataset.
When you load a client database from a file or a stream, you must re-create any indexes
in your code.
To create an index, you use the client dataset’s AddIndex method. AddIndex takes three mandatory
parameters, as well as three optional parameters, and is defined like this:
procedure AddIndex(const Name, Fields: string; Options: TIndexOptions;
const DescFields: string = ‘’; const CaseInsFields: string = ‘’;
const GroupingLevel: Integer = 0);

The parameters correspond to the TIndexDef properties listed in Table 3.2. The following code
snippet shows how to create a unique index by last and first names: 3
ClientDataSet1.AddIndex(‘byName’, ‘Last;First’, [ixUnique]);

CLIENT DATASET
When you decide that you no longer need an index (remember, you can always re-create it if

BASICS
you need it later), you can delete it using DeleteIndex. DeleteIndex takes a single parameter:
the name of the index being deleted. The following line of code shows how to delete the index
created in the preceding code snippet:
ClientDataSet1.DeleteIndex(‘byName’);

Using Indexes
Creating an index doesn’t perform any actual sorting of the dataset. It simply creates an available
index to the data. After you create an index, you make it active by setting the dataset’s
IndexName property, like this:

ClientDataSet1.IndexName := ‘byName’;

If you have two or more indexes defined on a dataset, you can quickly switch back and forth
by changing the value of the IndexName property. If you want to discontinue the use of an
index and revert to the default record order, you can set the IndexName property to an empty
string, as the following code snippet illustrates:
// Do something in name order
ClientDataSet1.IndexName := ‘byName’;
Chapter 3
122

// Do something in salary order


ClientDataSet1.IndexName := ‘bySalary’;

// Switch back to the default ordering


ClientDataSet1.IndexName := ‘’;

There is a second way to specify indexes on-the-fly at runtime. Instead of creating an index
and setting the IndexName property, you can simply set the IndexFieldNames property.
IndexFieldNames accepts a semicolon-delimited list of fields to index on. The following code
shows how to use it:
ClientDataSet1.IndexFieldNames := ‘Last;First’;

Though IndexFieldNames is quicker and easier to use than AddIndex/IndexName, its simplicity
does not come without a price. Specifically,
• You cannot set any index options, such as unique or descending indexes.
• You cannot specify a grouping level or create maintained aggregates.
• When you switch from one index to another (by changing the value of
IndexFieldNames), the old index is automatically dropped. If you switch back at a later
time, the index is re-created. This happens so fast that it’s not likely to be noticeable, but
you should be aware that it’s happening, nonetheless. When you create indexes using
AddIndex, the index is maintained until you specifically delete it using DeleteIndex.

NOTE
Though you can switch back and forth between IndexName and IndexFieldNames in
the same application, you can’t set both properties at the same time. Setting
IndexName clears IndexFieldNames, and setting IndexFieldNames clears IndexName.

Retrieving Index Information


Delphi provides a couple of different methods for retrieving index information from a dataset.
These methods are discussed in the following sections.

GetIndexNames
The simplest method for retrieving index information is GetIndexNames. GetIndexNames takes
a single parameter, a TStrings object, in which to store the resultant index names. The follow-
ing code snippet shows how to load a list box with the names of all indexes defined for a
dataset.
ClientDataSet1.GetIndexNames(ListBox1.Items);
Client Dataset Basics
123

CAUTION
If you execute this code on a dataset for which you haven’t defined any indexes,
you’ll notice that there are two indexes already defined for you: DEFAULT_ORDER and
CHANGEINDEX. DEFAULT_ORDER is used internally to provide records in nonindexed
order. CHANGEINDEX is used internally to provide undo support, which is discussed later
in this chapter. You should not attempt to delete either of these indexes.

TIndexDefs
If you want to obtain more detailed information about an index, you can go directly to the
source: TIndexDefs. TIndexDefs contains a list of all indexes, along with the information
associated with each one (such as the fields that make up the index, which fields are descend-
ing, and so on).
The following code snippet shows how to access index information directly through
TIndexDefs.

var
Index: Integer;
3

CLIENT DATASET
IndexDef: TIndexDef;
begin

BASICS
ClientDataSet1.IndexDefs.Update;

for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do begin


IndexDef := ClientDataSet1.IndexDefs[Index];
ListBox1.Items.Add(IndexDef.Name);
end;
end;

Notice the call to IndexDefs.Update before the code that loops through the index definitions.
This call is required to ensure that the internal IndexDefs list is up-to-date. Without it, it’s pos-
sible that IndexDefs might not contain any information about recently added indexes.
The following application demonstrates how to provide on-the-fly indexing in a TDBGrid. It
also contains code for retrieving detailed information about all the indexes defined on a
dataset.
Figure 3.7 shows the CDSIndex application at runtime, as it displays index information for the
employee client dataset.
Listing 3.3 contains the complete source code for the CDSIndex application.
Chapter 3
124

FIGURE 3.7
CDSIndex shows how to create indexes on-the-fly.

LISTING 3.3 CDSIndex—MainForm.pas

unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
pnlBottom: TPanel;
btnDefaultOrder: TButton;
btnIndexList: TButton;
ListBox1: TListBox;
procedure FormCreate(Sender: TObject);
procedure DBGrid1TitleClick(Column: TColumn);
procedure btnDefaultOrderClick(Sender: TObject);
procedure btnIndexListClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
Client Dataset Basics
125

LISTING 3.3 Continued

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
end;

procedure TfrmMain.DBGrid1TitleClick(Column: TColumn);


begin
try
ClientDataSet1.DeleteIndex(‘byUser’);
except
end;

ClientDataSet1.AddIndex(‘byUser’, Column.FieldName, []);


3

CLIENT DATASET
ClientDataSet1.IndexName := ‘byUser’;
end;

BASICS
procedure TfrmMain.btnDefaultOrderClick(Sender: TObject);
begin
// Deleting the current index will revert to the default order
try
ClientDataSet1.DeleteIndex(‘byUser’);
except
end;

ClientDataSet1.IndexFieldNames := ‘’;
end;

procedure TfrmMain.btnIndexListClick(Sender: TObject);


var
Index: Integer;
IndexDef: TIndexDef;
begin
ClientDataSet1.IndexDefs.Update;

ListBox1.Items.BeginUpdate;
try
ListBox1.Items.Clear;
Chapter 3
126

LISTING 3.3 Continued

for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do begin


IndexDef := ClientDataSet1.IndexDefs[Index];
ListBox1.Items.Add(IndexDef.Name);
end;
finally
ListBox1.Items.EndUpdate;
end;
end;

end.

The code to dynamically sort the grid at runtime is contained in the method
DBGrid1TitleClick. First, it attempts to delete the temporary index named byUser, if it exists.
If it doesn’t exist, an exception is raised, which the code simply eats. A real application should
not mask exceptions willy-nilly. Instead, it should trap for the specific exceptions that might be
thrown by the call to DeleteIndex, and let the others be reported to the user.
The method then creates a new index named byUser, and sets it to be the current index.

NOTE
Though this code works, it is rudimentary at best. There is no support for sorting on
multiple grid columns, and no visual indication of what column(s) the grid is sorted
by. For an elegant solution to these issues, I urge you to take a look at John Kaster’s
TCDSDBGrid (available as ID 15099 on Code Central at
http://codecentral.borland.com).

Filters and Ranges


Filters and ranges provide a means of limiting the amount of data that is visible in the dataset,
similar to a WHERE clause in a SQL statement. The main difference between filters, ranges, and
the WHERE clause is that when you apply a filter or a range, it does not physically change which
data is contained in the dataset. It only limits the amount of data that you can see at any given
time.

Ranges
Ranges are useful when the data that you want to limit yourself to is stored in a consecutive
sequence of records. For example, say a dataset contains the data shown in Table 3.4.
Client Dataset Basics
127

TABLE 3.4 Sample Data for Ranges and Filters


ID Name Birthday Salary
4 Bill Peterson 3/28/1957 $60,000.00
2 Frank Smith 8/25/1963 $48,000.00
3 Sarah Johnson 7/5/1968 $52,000.00
1 John Doe 5/15/1970 $39,000.00
5 Paula Wallace 1/15/1971 $36,500.00

The data in this much-abbreviated table is indexed by birthday. Ranges can only be used when
there is an active index on the dataset.
Assume that you want to see all employees who were born between 1960 and 1970. Because
the data is indexed by birthday, you could apply a range to the dataset, like this:
ClientDataSet1.SetRange([‘1/1/1960’], [‘12/31/1970’]);

Ranges are inclusive, meaning that the endpoints of the range are included within the range. In
the preceding example, employees who were born on either January 1, 1960 or December 31, 3
1970 are included in the range.

CLIENT DATASET
To remove the range, simply call CancelRange, like this:

BASICS
ClientDataSet1.CancelRange;

Filters
Unlike ranges, filters do not require an index to be set before applying them. Client dataset
filters are powerful, offering many SQL-like capabilities, and a few options that are not even
supported by SQL. Tables 3.5–3.10 list the various functions and operators available for use in
a filter.

TABLE 3.5 Filter Comparison Operators


Function Description Example
= Equality test Name = ‘John Smith’
<> Inequality test ID <> 100
< Less than Birthday < ‘1/1/1980’
> Greater than Birthday > ‘12/31/1960’
<= Less than or equal to Salary <= 80000
>= Greater than or equal to Salary >= 40000
Chapter 3
128

TABLE 3.5 Continued


Function Description Example
BLANK Empty string field Name = BLANK
(not used to test for
NULL values)
IS NULL Test for NULL value Birthday IS NULL
IS NOT NULL Test for non-NULL value Birthday IS NOT NULL

TABLE 3.6 Filter Logical Operators


Function Example
And (Name = ‘John Smith’) and (Birthday = ‘5/16/1964’)
Or (Name = ‘John Smith’) or (Name = ‘Julie Mason’)
Not Not (Name = ‘John Smith’)

TABLE 3.7 Filter Arithmetic Operators


Function Description Example
+ Addition. Can be used with Birthday + 30 < ‘1/1/1960’
numbers, strings, or Name + ‘X’ = ‘SmithX’
dates/times. Salary + 10000 = 100000
– Subtraction. Can be used Birthday - 30 > ‘1/1/1960’
with numbers or dates/times. Salary - 10000 > 40000
* Multiplication. Can be used Salary * 0.10 > 5000
with numbers only.
/ Division. Can be used with Salary / 10 > 5000
numbers only.

TABLE 3.8 Filter String Functions


Function Description Example
Upper Uppercase Upper(Name) = ‘JOHN SMITH’
Lower Lowercase Lower(Name) = ‘john smith’
SubString Return a portion of a string SubString(Name,6) = ‘Smith’
SubString(Name,1,4) = ‘John’
Trim Trim leading and trailing Trim(Name)
characters from a string Trim(Name, ‘.’)
Client Dataset Basics
129

TABLE 3.8 Continued


Function Description Example
TrimLeft Trim leading characters TrimLeft(Name)
from a string TrimLeft(Name, ‘.’)
TrimRight Trim trailing characters TrimRight(Name)
from a string TrimRight(Name, ‘.’)

TABLE 3.9 Filter Date/Time Functions


Function Description Example
Year Returns the year portion Year(Birthday) = 1970
of a date value.
Month Returns the month portion Month(Birthday) = 1
of a date value.
Day Returns the day portion Day(Birthday) = 15
of a date value.
Hour Returns the hour portion Hour(Appointment) = 18
3

CLIENT DATASET
of a time value in 24-hour
format.

BASICS
Minute Returns the minute portion Minute(Appointment) = 30
of a time value.
Second Returns the second portion Second(Appointment) = 0
of a time value.
GetDate Returns the current date Appointment < GetDate
and time.
Date Returns the date portion Date(Appointment)
of a date/time value.
Time Returns the time portion Time(Appointment)
of a date/time value.

TABLE 3.10 Other Filter Functions and Operators


Function Description Example
LIKE Partial string comparison. Name LIKE ‘%Smith%’
IN Tests for multiple values. Year(Birthday) IN (1960,
1970, 1980)
* Partial string comparison. Name = ‘John*’
Chapter 3
130

To filter a dataset, set its Filter property to the string used for filtering, and then set the
Filtered property to True. For example, the following code snippet filters out all employees
whose names begin with the letter M.
ClientDataSet1.Filter := ‘Name LIKE ‘ + QuotedStr(‘M%’);
ClientDataSet1.Filtered := True;

To later display only those employees whose names begin with the letter P, simply change the
filter, like this:
ClientDataSet1.Filter := ‘Name LIKE ‘ + QuotedStr(‘P%’);

To remove the filter, set the Filtered property to False. You don’t have to set the Filter
property to an empty string to remove the filter (which means that you can toggle the most
recent filter on and off by switching the value of Filtered from True to False).
You can apply more advanced filter criteria by handling the dataset’s OnFilterRecord event
(instead of setting the Filter property). For example, say that you want to filter out all
employees whose last names sound like Smith. This would include Smith, Smythe, and possibly
others. Assuming that you have a Soundex function available, you could write a filter method
like the following:
procedure TForm1.ClientDataSet1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Accept := Soundex(DataSet.FieldByName(‘LastName’).AsString) =
Soundex(‘Smith’);
end;

If you set the Accept parameter to True, the record is included in the filter. If you set Accept
to False, the record is hidden.
After you set up an OnFilterRecord event handler, you can simply set
TClientDataSet.Filtered to True. You don’t need to set the Filter property at all.

The following example demonstrates different filter and range techniques.


Listing 3.4 contains the source code for the main form.

LISTING 3.4 RangeFilter—MainForm.pas

unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, DBClient, QExtCtrls, QGrids, QDBGrids;
Client Dataset Basics
131

LISTING 3.4 Continued

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnFilter: TButton;
btnRange: TButton;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
btnClearRange: TButton;
btnClearFilter: TButton;
procedure FormCreate(Sender: TObject);
procedure btnFilterClick(Sender: TObject);
procedure btnRangeClick(Sender: TObject);
procedure btnClearRangeClick(Sender: TObject);
procedure btnClearFilterClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
3
end;

CLIENT DATASET
BASICS
var
frmMain: TfrmMain;

implementation

uses FilterForm, RangeForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);

ClientDataSet1.AddIndex(‘bySalary’, ‘Salary’, []);


ClientDataSet1.IndexName := ‘bySalary’;
end;

procedure TfrmMain.btnFilterClick(Sender: TObject);


var
frmFilter: TfrmFilter;
begin
frmFilter := TfrmFilter.Create(nil);
Chapter 3
132

LISTING 3.4 Continued

try
if frmFilter.ShowModal = mrOk then begin
ClientDataSet1.Filter := frmFilter.Filter;
ClientDataSet1.Filtered := True;
end;
finally
frmFilter.Free;
end;
end;

procedure TfrmMain.btnClearFilterClick(Sender: TObject);


begin
ClientDataSet1.Filtered := False;
end;

procedure TfrmMain.btnRangeClick(Sender: TObject);


var
frmRange: TfrmRange;
begin
frmRange := TfrmRange.Create(nil);
try
if frmRange.ShowModal = mrOk then
ClientDataSet1.SetRange([frmRange.LowValue], [frmRange.HighValue]);
finally
frmRange.Free;
end;
end;

procedure TfrmMain.btnClearRangeClick(Sender: TObject);


begin
ClientDataSet1.CancelRange;
end;

end.

As you can see, the main form loads the employee dataset from a disk, creates an index on the
Salary field, and makes the index active. It then enables the user to apply a range, a filter, or
both to the dataset.
Listing 3.5 contains the source code for the filter form. The filter form is a simple form that
enables the user to select the field on which to filter, and to enter a value on which to filter.
Client Dataset Basics
133

LISTING 3.5 RangeFilter—FilterForm.pas

unit FilterForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
QExtCtrls;

type
TfrmFilter = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Label1: TLabel;
cbField: TComboBox;
Label2: TLabel;
cbRelationship: TComboBox;
Label3: TLabel;
ecValue: TEdit;
btnOk: TButton;
btnCancel: TButton; 3
private

CLIENT DATASET
function GetFilter: string;

BASICS
{ Private declarations }
public
{ Public declarations }
property Filter: string read GetFilter;
end;

implementation

{$R *.xfm}

{ TfrmFilter }

function TfrmFilter.GetFilter: string;


begin
Result := Format(‘%s %s ‘’%s’’’,
[cbField.Text, cbRelationship.Text, ecValue.Text]);
end;

end.

The only interesting code in this form is the GetFilter function, which simply bundles the
values of the three input controls into a filter string and returns it to the main application.
Chapter 3
134

Listing 3.6 contains the source code for the range form. The range form prompts the user for a
lower and an upper salary limit.

LISTING 3.6 RangeFilter—RangeForm.pas

unit RangeForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,
QStdCtrls;

type
TfrmRange = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Label1: TLabel;
Label2: TLabel;
ecLower: TEdit;
ecUpper: TEdit;
btnOk: TButton;
btnCancel: TButton;
procedure btnOkClick(Sender: TObject);
private
function GetHighValue: Double;
function GetLowValue: Double;
{ Private declarations }
public
{ Public declarations }
property LowValue: Double read GetLowValue;
property HighValue: Double read GetHighValue;
end;

implementation

{$R *.xfm}

{ TfrmRange }

function TfrmRange.GetHighValue: Double;


begin
Result := StrToFloat(ecUpper.Text);
end;

function TfrmRange.GetLowValue: Double;


Client Dataset Basics
135

LISTING 3.6 Continued


begin
Result := StrToFloat(ecLower.Text);
end;

procedure TfrmRange.btnOkClick(Sender: TObject);


var
LowValue: Double;
HighValue: Double;
begin
try
LowValue := StrToFloat(ecLower.Text);
HighValue := StrToFloat(ecUpper.Text);

if LowValue > HighValue then begin


ModalResult := mrNone;
ShowMessage(‘The upper salary must be >= the lower salary’);
end;
except
ModalResult := mrNone;
ShowMessage(‘Both values must be a valid number’);
3
end;

CLIENT DATASET
end;

BASICS
end.

Figure 3.8 shows the RangeFilter application in operation.

FIGURE 3.8
RangeFilter applies both ranges and filters to a dataset.
Chapter 3
136

Searching
In addition to filtering out uninteresting records from a client dataset, TClientDataSet provides
a number of methods for quickly locating a specific record. Some of these methods require an
index to be active on the dataset, and others do not. The search methods are described in detail
in the following sections.

Nonindexed Search Techniques


In this section, I’ll discuss the search techniques that don’t require an active index on the client
dataset. Rather than using an index, these methods perform a sequential search through the
dataset to find the first matching record.

Locate
Locate is perhaps the most general purpose of the TClientDataSet search methods. You can
use Locate to search for a record based on any given field or combination of fields. Locate can
also search for records based on a partial match, and can find a match without respect to case.
TClientDataSet.Locate is defined like this:
function Locate(const KeyFields: string; const KeyValues: Variant;
Options: TLocateOptions): Boolean; override;

The first parameter, KeyFields, designates the field (or fields) to search. When searching multiple
fields, separate them by semicolons (for example, ‘Name;Birthday’).
The second parameter, KeyValues, represents the values to search for. The number of values
must match the number of key fields exactly. If there is only one search field, you can simply
pass the value to search for here. To search for multiple values, you must pass the values as a
variant array. One way to do this is by calling VarArrayOf, like this:
VarArrayOf([‘John Smith’, ‘4/15/1965’])

The final parameter, Options, is a set that determines how the search is to be executed. Table
3.11 lists the available options.

TABLE 3.11 Locate Options


Value Description
loPartialKey KeyValues do not necessarily represent an exact match. Locate
finds the first record whose field value starts with the value specified
in KeyValues.
loCaseInsensitive Locate ignores case when searching for string fields.
Client Dataset Basics
137

Both options pertain to string fields only. They are ignored if you specify them for a nonstring
search.
Locate returns True if a matching record is found, and False if no match is found. In case
of a match, the record is made current.
The following examples help illustrate the options:
ClientDataSet1.Locate(‘Name’, ‘John Smith’, []);

This searches for a record where the name is ‘John Smith’.

ClientDataSet1.Locate(‘Name’, ‘JOHN’, [loPartialKey, loCaseInsensitive]);

This searches for a record where the name begins with ‘JOHN’. This finds ‘John Smith’,
‘Johnny Jones’, and ‘JOHN ADAMS’, but not ‘Bill Johnson’.

ClientDataSet1.Locate(‘Name;Birthday’, VarArrayOf([‘John’, ‘4/15/1965’]),


[loPartialKey]);

This searches for a record where the name begins with ‘John’ and the birthday is April 15,
1965. In this case, the loPartialKey option applies to the name only. Even though the birthday
is passed as a string, the underlying field is a date field, so the loPartialKey option is ignored 3
for that field only.

CLIENT DATASET
BASICS
Lookup
Lookup is similar in concept to Locate, except that it doesn’t change the current record pointer.
Instead, Lookup returns the values of one or more fields in the record. Also, Lookup does not
accept an Options parameter, so you can’t perform a lookup that is based on a partial key or
that is not case sensitive.
Lookup is defined like this:
function Lookup(const KeyFields: string; const KeyValues: Variant;
const ResultFields: string): Variant; override;

KeyFields and KeyValues specify the fields to search and the values to search for, just as with
the Locate method. ResultFields specifies the fields for which you want to return data. For
example, to return the birthday of the employee named John Doe, you could write the
following code:
var
V: Variant;
begin
V := ClientDataSet1.Lookup(‘Name’, ‘John Doe’, ‘Birthday’);
end;
Chapter 3
138

The following code returns the name and birthday of the employee with ID number 100.
var
V: Variant;
begin
V := ClientDataSet1.Lookup(‘ID’, 100, ‘Name;Birthday’);
end;

If the requested record is not found, V is set to NULL. If ResultFields contains a single field
name, then on return from Lookup, V is a variant containing the value of the field listed in
ResultFields. If ResultFields contains multiple single-field names, then on return from
Lookup, V is a variant array containing the values of the fields listed in ResultFields.

NOTE
For a comprehensive discussion of variant arrays, see my book, Delphi COM
Programming, published by Macmillan Technical Publishing.

The following code snippet shows how you can access the results that are returned from Lookup.
var
V: Variant;
begin
V := ClientDataSet1.Lookup(‘ID’, 100, ‘Name’);
if not VarIsNull(V) then
ShowMessage(‘ID 100 refers to ‘ + V);

V := ClientDataSet1.Lookup(‘ID’, 200, ‘Name;Birthday’);


if not VarIsNull(V) then
ShowMessage(‘ID 200 refers to ‘ + V[0] + ‘, born on ‘ + DateToStr(V[1]));
end;

Indexed Search Techniques


The search techniques mentioned earlier do not require an index to be active (in fact, they
don’t require the dataset to be indexed at all), but TDataSet also supports several indexed
search operations. These include FindKey, FindNearest, and GotoKey, which are discussed in
the following sections.

FindKey
FindKey searches for an exact match on the key fields of the current index. For example, if the
dataset is currently indexed by ID, FindKey searches for an exact match on the ID field. If the
dataset is indexed by last and first name, FindKey searches for an exact match on both the last
and the first name.
Client Dataset Basics
139

FindKey takes a single parameter, which specifies the value(s) to search for. It returns a
Boolean value that indicates whether a matching record was found. If no match was found, the
current record pointer is unchanged. If a matching record is found, it is made current.
The parameter to FindKey is actually an array of values, so you need to put the values in
brackets, as the following examples show:
if ClientDataSet.FindKey([25]) then
ShowMessage(‘Found ID 25’);
...
if ClientDataSet.FindKey([‘Doe’, ‘John’]) then
ShowMessage(‘Found John Doe’);

You need to ensure that the values you search for match the current index. For that reason, you
might want to set the index before making the call to FindKey. The following code snippet
illustrates this:
ClientDataSet1.IndexName := ‘byID’;
if ClientDataSet.FindKey([25]) then
ShowMessage(‘Found ID 25’);
...
ClientDataSet1.IndexName := ‘byName’; 3
if ClientDataSet.FindKey([‘Doe’, ‘John’]) then

CLIENT DATASET
ShowMessage(‘Found John Doe’);

BASICS
FindNearest
FindNearest works similarly to FindKey, except that it finds the first record that is greater
than or equal to the value(s) passed to it. This depends on the current value of the
KeyExclusive property.

If KeyExclusive is False (the default), FindNearest finds the first record that is greater than
or equal to the passed-in values. If KeyExclusive is True, FindNearest finds the first record
that is greater than the passed-in values.
If FindNearest doesn’t find a matching record, it moves the current record pointer to the end
of the dataset.

GotoKey
GotoKey performs the same function as FindKey, except that you set the values of the search
field(s) before calling GotoKey. The following code snippet shows how to do this:
ClientDataSet1.IndexName := ‘byID’;
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(‘ID’).AsInteger := 25;
ClientDataSet1.GotoKey;
Chapter 3
140

If the index is made up of multiple fields, you simply set each field after the call to SetKey,
like this:
ClientDataSet1.IndexName := ‘byName’;
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(‘First’).AsString := ‘John’;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Doe’;
ClientDataSet1.GotoKey;

After calling GotoKey, you can use the EditKey method to edit the key values used for the
search. For example, the following code snippet shows how to search for John Doe, and then
later search for John Smith. Both records have the same first name, so only the last name
portion of the key needs to be specified during the second search.
ClientDataSet1.IndexName := ‘byName’;
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(‘First’).AsString := ‘John’;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Doe’;
ClientDataSet1.GotoKey;
// Do something with the record

// EditKey preserves the values set during the last SetKey


ClientDataSet1.EditKey;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Smith’;
ClientDataSet1.GotoKey;

GotoNearest
GotoNearest works similarly to GotoKey, except that it finds the first record that is greater than
or equal to the value(s) passed to it. This depends on the current value of the KeyExclusive
property.
If KeyExclusive is False (the default), GotoNearest finds the first record that is greater than
or equal to the field values set after a call to either SetKey or EditKey. If KeyExclusive is
True, GotoNearest finds the first record that is greater than the field values set after calling
SetKey or EditKey.

If GotoNearest doesn’t find a matching record, it moves the current record pointer to the end
of the dataset.
The following example shows how to perform indexed and nonindexed searches on a dataset.
Listing 3.7 shows the source code for the Search application, a sample program that illustrates
the various indexed and nonindexed searching techniques supported by TClientDataSet.
Client Dataset Basics
141

LISTING 3.7 Search—MainForm.pas

unit MainForm;

interface

uses
SysUtils, Classes, Variants, QGraphics, QControls, QForms, QDialogs,
QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnSearch: TButton;
btnGotoBookmark: TButton;
btnGetBookmark: TButton;
btnLookup: TButton;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
btnSetRecNo: TButton;
3
procedure FormCreate(Sender: TObject);

CLIENT DATASET
procedure btnGetBookmarkClick(Sender: TObject);

BASICS
procedure btnGotoBookmarkClick(Sender: TObject);
procedure btnSetRecNoClick(Sender: TObject);
procedure btnSearchClick(Sender: TObject);
procedure btnLookupClick(Sender: TObject);
private
{ Private declarations }
FBookmark: TBookmark;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses SearchForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
Chapter 3
142

LISTING 3.7 Continued

ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

ClientDataSet1.AddIndex(‘byName’, ‘Name’, []);


ClientDataSet1.IndexName := ‘byName’;
end;

procedure TfrmMain.btnGetBookmarkClick(Sender: TObject);


begin
if Assigned(FBookmark) then
ClientDataSet1.FreeBookmark(FBookmark);

FBookmark := ClientDataSet1.GetBookmark;
end;

procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject);


begin
if Assigned(FBookmark) then
ClientDataSet1.GotoBookmark(FBookmark)
else
ShowMessage(‘No bookmark assigned’);
end;

procedure TfrmMain.btnSetRecNoClick(Sender: TObject);


var
Value: string;
begin
Value := ‘1’;
if InputQuery(‘RecNo’, ‘Enter Record Number’, Value) then
ClientDataSet1.RecNo := StrToInt(Value);
end;

procedure TfrmMain.btnSearchClick(Sender: TObject);


var
frmSearch: TfrmSearch;
begin
frmSearch := TfrmSearch.Create(nil);
try
if frmSearch.ShowModal = mrOk then begin
case TSearchMethod(frmSearch.grpMethod.ItemIndex) of
smLocate:
ClientDataSet1.Locate(‘Name’, frmSearch.ecName.Text,
[loPartialKey, loCaseInsensitive]);
Client Dataset Basics
143

LISTING 3.7 Continued

smFindKey:
ClientDataSet1.FindKey([frmSearch.ecName.Text]);

smFindNearest:
ClientDataSet1.FindNearest([frmSearch.ecName.Text]);

smGotoKey: begin
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(‘Name’).AsString :=
frmSearch.ecName.Text;
ClientDataSet1.GotoKey;
end;

smGotoNearest: begin
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(‘Name’).AsString :=
frmSearch.ecName.Text;
ClientDataSet1.GotoNearest;
end;
end;
3

CLIENT DATASET
end;
finally

BASICS
frmSearch.Free;
end;
end;

procedure TfrmMain.btnLookupClick(Sender: TObject);


var
Value: string;
V: Variant;
begin
Value := ‘1’;
if InputQuery(‘ID’, ‘Enter ID to Lookup’, Value) then begin
V := ClientDataSet1.Lookup(‘ID’, StrToInt(Value), ‘Name;Salary’);
if not VarIsNull(V) then
ShowMessage(Format(‘ID %s refers to %s, who makes %s’,
[Value, V[0], FloatToStrF(V[1], ffCurrency, 10, 2)]));
end;
end;

end.
Chapter 3
144

Listing 3.8 contains the source code for the search form. The only interesting bit of code in this
listing is the TSearchMethod, defined near the top of the unit, which is used to determine what
method to call for the search.

LISTING 3.8 Search—SearchForm.pas

unit SearchForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,
QStdCtrls;

type
TSearchMethod = (smLocate, smFindKey, smFindNearest, smGotoKey,
smGotoNearest);

TfrmSearch = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Label1: TLabel;
ecName: TEdit;
grpMethod: TRadioGroup;
btnOk: TButton;
btnCancel: TButton;
private
{ Private declarations }
public
{ Public declarations }
end;

implementation

{$R *.xfm}

end.

Figure 3.9 shows the Search application at runtime.


Client Dataset Basics
145

FIGURE 3.9
Search demonstrates indexed and nonindexed searches.

Summary
TClientDataSet is an extremely powerful in-memory dataset that supports a number of
high-performance sorting and searching operations. Following are several key points to take
away from this chapter: 3

CLIENT DATASET
• You can create client datasets both at design-time and at runtime. This chapter showed
how to save them to a disk for use in single-tier database applications.

BASICS
• The three basic ways of populating a client dataset are
Manually with Append or Insert
From another dataset
From a file or stream (that is, via persisting client datasets)
• Datasets in Delphi can be navigated in a variety of ways: sequentially, via bookmarks,
and via record numbers.
• You can create indexes on a dataset enabling you to quickly and easily sort the records in
a given order, and to locate records that match a certain criteria.
• Filters and ranges can be used to limit the amount of data that is visible in the dataset.
Ranges are useful when the relevant data is stored in a consecutive sequence of records.
Unlike ranges, filters do not require an index to be set before applying them.
• Locate and Lookup are nonindexed search techniques for locating a specific record in a
client dataset. FindKey, FindNearest, GotoKey, and GotoNearest are indexed search
techniques.
In the following chapter, I’ll discuss more advanced client dataset functionality.
Advanced Client Dataset CHAPTER

4
Operations

IN THIS CHAPTER
• Dataset Events 148

• Disabling Data-Aware Components 158

• BLOBs 162

• Nested Datasets 172

• Undo Support 176

• Cloning Data from Another Client


Dataset 186

• Maintained Aggregates 192

• Miscellaneous Properties 197


Chapter 4
148

The preceding chapter introduced you to TClientDataSet and discussed much of its basic
functionality in detail. In this chapter, I’ll explore a number of more advanced client dataset
capabilities, including:
• Dataset Events
• Disabling Data-Aware Components
• BLOBs
• Nested Datasets
• Undo Support
• Cloning Data from Another Client Dataset
• Maintained Aggregates
• Miscellaneous Properties

Dataset Events
Client datasets support a large number of events. Some of these events are useful in single-tier
applications (such as the ones we’re developing in this chapter), and some are only useful in
multitier applications (which we’ll be developing in future chapters).
This chapter discusses dataset events that are useful in all applications, including single-tier
and multitier. Broadly speaking, these events fall into three categories: BeforeXxx notification
events, AfterXxx notification events, and other events. BeforeXxx and AfterXxx notification
events are fired by Delphi before and after interesting activities occur. For purposes of this
discussion, interesting refers to normal, everyday activities that the dataset performs—activities
for which you want to receive notification when they occur.
An example will help to clarify that last statement: Say that you want to verify all deletions
from a dataset. One way to do this is to display a confirmation message to the user at every
point in the program where you allow a deletion to take place. This method has three
drawbacks, however:
• It is repetitive.
• It is prone to error. If you change the confirmation message in one location in your code,
you can easily forget to change the message in other locations. You might also forget to
implement the confirmation altogether.
• It doesn’t work in cases where VCL/CLX implicitly deletes a record. If you press
Ctrl+Delete while in a data-aware grid, VCL/CLX handles the deletion for you with no
coding on your part.
Advanced Client Dataset Operations
149

A better way to code for this situation is to handle the BeforeDelete event and display a message
there asking the user if he is sure he wants to delete the record. As with most BeforeXxx event
handlers, raising an exception (usually Abort) inside the event handler prevents the operation
from taking place.
Later in this section, I’ll present a sample application that illustrates this technique.
Table 4.1 lists the client dataset’s BeforeXxx events and their uses.

TABLE 4.1 Client Dataset BeforeXxx Events


Event Description
BeforeCancel Triggered by the Cancel method just before the edits to the current
record are canceled. You might take advantage of this event to confirm
that the user does indeed want to cancel any changes that he has made.
BeforeClose Called immediately before the dataset is closed.
BeforeDelete Called just before the current record in the dataset is deleted. This is
a good place to confirm that the user really wants to delete the record.
BeforeEdit Triggered by the Edit method immediately before the dataset is put
into edit mode. You could use this event as a handy place to restrict
editing (by raising an Abort exception).
BeforeInsert Triggered by the Append and Insert methods immediately before
the dataset is put into insert mode. You could use this event as a way
to restrict editing (by raising an Abort exception).
BeforeOpen Called just before the dataset is opened.
BeforePost Occurs just before the data in a newly inserted or edited record is
posted to the dataset. This is a good place to perform validation on
the data. 4

ADVANCED CLIENT
BeforeScroll Triggered just before the dataset moves to a new record. This occurs OPERATIONS
when the dataset is opened during a First, Next, Prior, or Last DATASET
operation; during searches; and when a range or filter is applied
to the dataset.

In contrast to the BeforeXxx event handlers, which are triggered before an event actually
occurs (and therefore enable you to prevent the event from occurring), AfterXxx event han-
dlers are triggered after the event has occurred to let you know that the operation in question
has occurred successfully.
Table 4.2 lists the AfterXxx event handlers, which mirror the BeforeXxx event handlers.
Chapter 4
150

TABLE 4.2 Client Dataset AfterXxx Events


Event Description
AfterCancel Triggered after a Cancel method completes.
AfterClose Occurs just after the dataset is closed.
AfterDelete Triggered immediately after a record in the dataset is deleted.
AfterEdit Called after the dataset is put into edit mode as a result of an Edit
method call.
AfterInsert Called after the dataset is put into insert mode as a result of an
Insert or Append method call.
AfterOpen Occurs just after the dataset is opened.
AfterPost Triggered after a record is successfully posted to the dataset.
AfterScroll Triggered just after the dataset moves to a new record. This occurs
when the dataset is opened during a First, Next, Prior, or Last
operation; during searches; and when a range or filter is applied to
the dataset.

Table 4.3 lists the client dataset’s other notable event handlers.

TABLE 4.3 Other Event Handlers


Event Description
OnCalcFields As discussed in Chapter 6, “Data-Aware Grids,” this event is used to
provide values for calculated fields.
OnDeleteError Fired if there is an error when deleting a record from the dataset
(for example, if the dataset is read-only).
OnEditError Fired if there is an error when putting the dataset into edit mode
(for example, if the dataset is read-only).
OnFilterRecord As discussed in Chapter 6, this event is called to enable the user to
provide advanced filtering on a dataset.
OnNewRecord Triggered whenever a new record is created, but before it is edited
or posted to the dataset. This is a good place to set default values for
the record. Inside this event handler, the dataset is already in insert
mode, so you shouldn’t call TClientDataSet.Insert from within
this event handler.
OnPostError Fired if an error occurs when attempting to post a record to the
dataset (such as a key conflict).
Advanced Client Dataset Operations
151

You can learn a lot about how datasets work by handling all these events and logging their
calls to either a log file or a list box. In the following example, I’ve done just that. Listing 4.1
contains the complete source code for the EventLog application.

LISTING 4.1 EventLog—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, DB, DBClient, QComCtrls, QGrids, QDBGrids, QExtCtrls, QStdCtrls,
QDBCtrls;

type
TLogEventType = (logBeforeCancel, logBeforeClose, logBeforeDelete,
logBeforeEdit, logBeforeInsert, logBeforeOpen, logBeforePost,
logBeforeScroll, logAfterCancel, logAfterClose, logAfterDelete,
logAfterEdit, logAfterInsert, logAfterOpen, logAfterPost, logAfterScroll);

TfrmMain = class(TForm)
ClientDataSet1: TClientDataSet;
pnlClient: TPanel;
pnlLog: TPanel;
grid: TDBGrid;
lvLog: TListView;
DataSource1: TDataSource;
pnlBottom: TPanel;
btnConnect: TButton;
lblRecPos: TLabel; 4

ADVANCED CLIENT
btnDisconnect: TButton;
btnClearLog: TButton; OPERATIONS
DBNavigator1: TDBNavigator; DATASET
btnOptions: TButton;
procedure ClientDataSet1AfterCancel(DataSet: TDataSet);
procedure ClientDataSet1AfterClose(DataSet: TDataSet);
procedure ClientDataSet1AfterDelete(DataSet: TDataSet);
procedure ClientDataSet1AfterEdit(DataSet: TDataSet);
procedure ClientDataSet1AfterInsert(DataSet: TDataSet);
procedure ClientDataSet1AfterOpen(DataSet: TDataSet);
procedure ClientDataSet1AfterPost(DataSet: TDataSet);
procedure ClientDataSet1AfterScroll(DataSet: TDataSet);
procedure btnConnectClick(Sender: TObject);
Chapter 4
152

LISTING 4.1 Continued

procedure btnClearLogClick(Sender: TObject);


procedure btnDisconnectClick(Sender: TObject);
procedure ClientDataSet1BeforeCancel(DataSet: TDataSet);
procedure ClientDataSet1BeforeClose(DataSet: TDataSet);
procedure ClientDataSet1BeforeDelete(DataSet: TDataSet);
procedure ClientDataSet1BeforeEdit(DataSet: TDataSet);
procedure ClientDataSet1BeforeInsert(DataSet: TDataSet);
procedure ClientDataSet1BeforeOpen(DataSet: TDataSet);
procedure ClientDataSet1BeforePost(DataSet: TDataSet);
procedure ClientDataSet1BeforeScroll(DataSet: TDataSet);
procedure btnOptionsClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FLogScrollEvents: Boolean;
FPromptOnDelete: Boolean;
procedure Log(EventType: TLogEventType);
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses OptionsForm;

{$R *.xfm}

procedure TfrmMain.Log(EventType: TLogEventType);


const
EventText: array[TLogEventType] of string = (
‘BeforeCancel’, ‘BeforeClose’, ‘BeforeDelete’, ‘BeforeEdit’,
‘BeforeInsert’, ‘BeforeOpen’, ‘BeforePost’, ‘BeforeScroll’,
‘AfterCancel’, ‘AfterClose’, ‘AfterDelete’, ‘AfterEdit’,
‘AfterInsert’, ‘AfterOpen’, ‘AfterPost’, ‘AfterScroll’);
var
ListItem: TListItem;
begin
ListItem := lvLog.Items.Add;
ListItem.Caption := EventText[EventType];
end;
Advanced Client Dataset Operations
153

LISTING 4.1 Continued


procedure TfrmMain.FormCreate(Sender: TObject);
begin
FLogScrollEvents := True;
FPromptOnDelete := True;
end;

procedure TfrmMain.ClientDataSet1AfterCancel(DataSet: TDataSet);


begin
Log(logAfterCancel);
end;

procedure TfrmMain.ClientDataSet1AfterClose(DataSet: TDataSet);


begin
Log(logAfterClose);
end;

procedure TfrmMain.ClientDataSet1AfterDelete(DataSet: TDataSet);


begin
Log(logAfterDelete);
end;

procedure TfrmMain.ClientDataSet1AfterEdit(DataSet: TDataSet);


begin
Log(logAfterEdit);
end;

procedure TfrmMain.ClientDataSet1AfterInsert(DataSet: TDataSet);


begin
Log(logAfterInsert);
end;
4

ADVANCED CLIENT
OPERATIONS
procedure TfrmMain.ClientDataSet1AfterOpen(DataSet: TDataSet);
DATASET
begin
Log(logAfterOpen);
end;

procedure TfrmMain.ClientDataSet1AfterPost(DataSet: TDataSet);


begin
Log(logAfterPost);
end;
Chapter 4
154

LISTING 4.1 Continued


procedure TfrmMain.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
if FLogScrollEvents then
Log(logAfterScroll);

lblRecPos.Caption := Format(‘Record %d of %d’,


[DataSet.RecNo, DataSet.RecordCount]);
end;

procedure TfrmMain.ClientDataSet1BeforeCancel(DataSet: TDataSet);


begin
Log(logBeforeCancel);
end;

procedure TfrmMain.ClientDataSet1BeforeClose(DataSet: TDataSet);


begin
Log(logBeforeClose);
end;

procedure TfrmMain.ClientDataSet1BeforeDelete(DataSet: TDataSet);


begin
Log(logBeforeDelete);

if FPromptOnDelete then begin


if MessageDlg(‘Are you sure you want to delete the current record?’,
mtWarning, [mbYes, mbNo], 0) <> mrYes then
Abort;
end;
end;

procedure TfrmMain.ClientDataSet1BeforeEdit(DataSet: TDataSet);


begin
Log(logBeforeEdit);
end;

procedure TfrmMain.ClientDataSet1BeforeInsert(DataSet: TDataSet);


begin
Log(logBeforeInsert);
end;

procedure TfrmMain.ClientDataSet1BeforeOpen(DataSet: TDataSet);


begin
Log(logBeforeOpen);
end;
Advanced Client Dataset Operations
155

LISTING 4.1 Continued


procedure TfrmMain.ClientDataSet1BeforePost(DataSet: TDataSet);
begin
Log(logBeforePost);
end;

procedure TfrmMain.ClientDataSet1BeforeScroll(DataSet: TDataSet);


begin
if FLogScrollEvents then
Log(logBeforeScroll);
end;

procedure TfrmMain.btnConnectClick(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);


begin
ClientDataSet1.Close;
end;

procedure TfrmMain.btnClearLogClick(Sender: TObject);


begin
lvLog.Items.Clear;
end;

procedure TfrmMain.btnOptionsClick(Sender: TObject);


var
frmOptions: TfrmOptions;
begin
4

ADVANCED CLIENT
frmOptions := TfrmOptions.Create(nil);
try
OPERATIONS
DATASET
frmOptions.LogScrollEvents := FLogScrollEvents;
frmOptions.PromptOnDelete := FPromptOnDelete;

if frmOptions.ShowModal = mrOk then begin


FLogScrollEvents := frmOptions.LogScrollEvents;
FPromptOnDelete := frmOptions.PromptOnDelete;
end;
finally
frmOptions.Free;
end;
end;

end.
Chapter 4
156

Figure 4.1 shows the application at runtime. A data-aware grid occupies the top half of the
main form enabling the user to insert, edit, and delete records. The middle section of the form
contains a list view, which records a log of all activities performed on the dataset. On the bot-
tom of the form are buttons to open and close the dataset, clear the log, and set options for the
demo.
The user can select whether to log scroll events and whether to verify deletions by clicking the
Options… button at the bottom of the main form, as Figure 4.1 illustrates.

FIGURE 4.1
The EventLog application demonstrates client dataset events.

Listing 4.2 contains the source code for the application’s Options form, which allows the user
to specify whether the program should prompt him when deleting a record, and whether the
program should log dataset events to a list box.

LISTING 4.2 EventLog—OptionsForm.pas


unit OptionsForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QStdCtrls, QExtCtrls;

type
TfrmOptions = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Advanced Client Dataset Operations
157

LISTING 4.2 Continued

btnOk: TButton;
btnCancel: TButton;
cbLogScrollEvents: TCheckBox;
cbPromptOnDelete: TCheckBox;
private
procedure SetLogScrollEvents(const Value: Boolean);
function GetLogScrollEvents: Boolean;
function GetPromptOnDelete: Boolean;
procedure SetPromptOnDelete(const Value: Boolean);
{ Private declarations }
public
{ Public declarations }
property LogScrollEvents: Boolean
read GetLogScrollEvents write SetLogScrollEvents;
property PromptOnDelete: Boolean
read GetPromptOnDelete write SetPromptOnDelete;
end;

implementation

{$R *.xfm}

{ TfrmOptions }

function TfrmOptions.GetLogScrollEvents: Boolean;


begin
Result := cbLogScrollEvents.Checked;
end;

function TfrmOptions.GetPromptOnDelete: Boolean;


4

ADVANCED CLIENT
begin
OPERATIONS
Result := cbPromptOnDelete.Checked; DATASET
end;

procedure TfrmOptions.SetLogScrollEvents(const Value: Boolean);


begin
cbLogScrollEvents.Checked := Value;
end;

procedure TfrmOptions.SetPromptOnDelete(const Value: Boolean);


begin
cbPromptOnDelete.Checked := Value;
end;

end.
Chapter 4
158

Disabling Data-Aware Components


This topic actually applies to all Delphi datasets, but I’m discussing it within the context of
client datasets because you typically don’t connect data-aware components directly to a
dbExpress dataset. Usually, data-aware components are connected to a client dataset.
As you’ve learned in this chapter, data-aware components actively track the current record in
the dataset that they’re connected to. Although this is usually a good thing, at times you want
to prevent data-aware components from updating. This happens most often when you are
scrolling through a dataset to perform an operation on the records, and you don’t want to see
all the data-aware components rapidly updating as you do. For example, take a look at the
following code snippet:
var
Bookmark: TBookmarkStr;
begin
Bookmark := ClientDataSet1.Bookmark;
try
ClientDataSet1.First;
while not ClientDataSet1.EOF do begin
if ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then begin
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Salary’).AsFloat :=
ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;
ClientDataSet1.Post;
end;
ClientDataSet1.Next;
end;
finally
ClientDataSet1.Bookmark := Bookmark);
end;
end;

This code loops through all records in the dataset, giving anyone who makes less than $30,000
per year a 5% raise.
There is nothing wrong with this code from the standpoint that it does what it is intended to
do. It even remembers the current record so that it can reposition the dataset correctly when it’s
finished.
The problem with this code is that it’s slow. If you run it against the 10,000 record employee
dataset that we created in the preceding chapter, you’ll see the grid scroll through all the
records in the dataset as they are updated. (The example application at the end of this section
shows this effect.)
Advanced Client Dataset Operations
159

The solution is to disable all data-aware components (namely, the TDBGrid) attached to the
dataset before beginning this operation. To do that, you simply call the DisableControls
method before performing the lengthy operation, and then call EnableControls when you’re
finished. The following code snippet shows the updated procedure:
var
Bookmark: TBookmarkStr;
begin
ClientDataSet1.DisableControls;
try
Bookmark := ClientDataSet1.Bookmark;
try
ClientDataSet1.First;
while not ClientDataSet1.EOF do begin
if ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then begin
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Salary’).AsFloat :=
ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;
ClientDataSet1.Post;
end;
ClientDataSet1.Next;
end;
finally
ClientDataSet1.Bookmark := Bookmark;
end;
finally
ClientDataSet1.EnableControls;
end;
end;

As this code snippet shows, you want to wrap the code between the calls to DisableControls 4
and EnableControls in a try/finally block. If you don’t, and an exception occurs somewhere

ADVANCED CLIENT
in the code, the data-aware components cease to be updated. OPERATIONS
DATASET
Note that the calls to DisableControls and EnableControls are reference counted. If you call
DisableControls three times in your code, then you will need to call EnableControls three
times before data-aware controls are updated again.
Listing 4.3 contains the complete source code for the Updates application.

LISTING 4.3 Updates—MainForm.pas


unit MainForm;

interface
Chapter 4
160

LISTING 4.3 Continued

uses
Types, IdGlobal, SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids,
QDBCtrls;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnDisableEnable: TButton;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
btnBaseline: TButton;
procedure FormCreate(Sender: TObject);
procedure btnDisableEnableClick(Sender: TObject);
procedure btnBaselineClick(Sender: TObject);
private
{ Private declarations }
procedure PerformWork;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
end;

procedure TfrmMain.PerformWork;
var
Bookmark: TBookmark;
begin
Bookmark := ClientDataSet1.GetBookmark;
try
ClientDataSet1.First;
while not ClientDataSet1.EOF do begin
Advanced Client Dataset Operations
161

LISTING 4.3 Continued

if ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then begin


ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Salary’).AsFloat :=
ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;
ClientDataSet1.Post;
end;
ClientDataSet1.Next;
end;

ClientDataSet1.GotoBookmark(Bookmark);
finally
ClientDataSet1.FreeBookmark(Bookmark);
end;
end;

procedure TfrmMain.btnBaselineClick(Sender: TObject);


var
t1, t2: DWord;
begin
t1 := GetTickCount;
PerformWork;
t2 := GetTickCount;
ShowMessage(IntToStr(t2 - t1) + ‘ ms’);
end;

procedure TfrmMain.btnDisableEnableClick(Sender: TObject);


var
t1, t2: DWord;
begin
t1 := GetTickCount;
4

ADVANCED CLIENT
ClientDataSet1.DisableControls;
OPERATIONS
try
DATASET
PerformWork;
finally
ClientDataSet1.EnableControls;
end;

t2 := GetTickCount;
ShowMessage(IntToStr(t2 - t1) + ‘ ms’);
end;

end.
Chapter 4
162

The results of this test are impressive. The baseline test (which doesn’t disable controls) takes
about 19.6 seconds to run on my 1.4GHz Pentium 4. With the additional five lines to disable
and re-enable data-aware components, the code takes a mere 560ms to execute (and has no
annoying screen activity, to boot).

BLOBs
BLOBs, or Binary Large Objects, are a fundamental part of many modern database applications.
Whether you want to store images, formatted and unformatted notes, streamed components, or
any other chunk of bytes; BLOBs are an essential part of your database-programming repertoire.
In this section, I’ll show you how to effectively store BLOBs in a client dataset and how to
retrieve them later. In the pages to follow, I’ll focus specifically on notes, images, streamed
components, and generic BLOB storage.
As with other field types, you can create BLOB fields either at design time or at runtime. The
following code snippet shows how to create a BLOB field at runtime:
ClientDataSet1.FieldDefs.Add(‘Notes’, ftBlob);

This code creates a field named Notes, of type ftBlob.

Notes
One of the most common ways to use a BLOB field is to store notes, or free-format text. For
small amounts of text a string field typically suffices, but if you want to store entire memos,
you need to use a BLOB field.
Accessing a BLOB as a string is particularly easy. You can simply call the AsString method
on the field, like this:
Memo1.Text := ClientDataSet1.FieldByName(‘Notes’).AsString;

Similarly, to store the memo back to the field, you would write code like the following:
ClientDataSet1.FieldByName(‘Notes’).AsString := Memo1.Text;

Images
Another common use of BLOB fields is to store images. You might want to write a Delphi
application to catalog the pictures you’ve taken on your digital camera, or you might want to
track scanned documents in a paperless office. Either way, a BLOB field provides the necessary
support to store these images in a database.
Advanced Client Dataset Operations
163

Like formatted and unformatted memos, Delphi provides a data-aware version of an image:
TDBImage. TDBImage is lacking, however, because it only correctly stores and retrieves bitmaps
(.BMP files). A robust application should store bitmaps, JPGs, and (almost) any other image
type that the user might want to store.
There are at least three methods that you can use to store multiple image types in a dataset.
They include:
• Creating a separate field that will be used to store the image type. Use this field value to
determine how to store/load the image.
• Writing a value to the BLOB field indicating the image type, immediately followed by
the image data.
• Using a third-party imaging library to do the work for you.
The following sections discuss these options.

Using a Separate Field to Store the Image Type


One way to track the type of image stored in a dataset is to add a separate field, perhaps named
ImageType, to track the type of image. Say, for the sake of argument, that your application can
store BMPs and JPGs. You would set up constants for each image type, like this:
const
IMAGE_NONE = 0;
IMAGE_BMP = 1;
IMAGE_JPG = 2;

Presumably, the ImageType field contains the value IMAGE_NONE when the BLOB field is NULL.
To implement this method correctly, you must remember to set the ImageType field whenever
the user loads an image, and then reset it to IMAGE_NONE if the user clears the image. 4

ADVANCED CLIENT
Streaming the Image Type as Part of the BLOB Field OPERATIONS
With a little extra code, you can dispense with the additional field and store the image type in DATASET
the BLOB field along with the image itself. Figure 4.2 shows conceptually what is involved
with this method.
The following pseudo-code shows how you might implement this method:
procedure SaveImage;
begin
OpenOutputStream;
WriteImageType;
WriteImageData;
end;
Chapter 4
164

procedure LoadImage;
var
ImageType: Integer;
begin
OpenInputStream;
ImageType := ReadImageType;
case ImageType of
IMAGE_BMP: ReadBitmap;
IMAGE_JPG: ReadJPEG;
end;
end;

The sample application provided at the end of this section illustrates both of these techniques.

File extension or other designation


Fixed-Length Image Header to describe the image type
Image Data
Image data in a format suitable for
reading into an TBitmap, TJPEG,
or other image class.

FIGURE 4.2
The image type immediately precedes the image data in the BLOB field.

Third-Party Imaging Libraries


Third-party imaging libraries generally follow the steps outlined in the preceding section (that
is, they typically store a value to the stream indicating the type of image stored in the stream).
Immediately following the image type is the image itself.
However, other third-party libraries might always store an image internally in a proprietary
format, and then read and write that image format to and from the stream. The point is that
after you decide on an imaging library and implement it in your applications, you shouldn’t
expect that you can arbitrarily swap out the library with a different one at a later date. Switching
image libraries might require you to write a data conversion program for your BLOB data.
Advanced Client Dataset Operations
165

NOTE
If you’re looking for a good imaging library, check out Skyline Tools Imaging’s
ImageLib Corporate Suite. ImageLib Corporate Suite has won numerous awards as the
best Delphi imaging library available (including Delphi Informant Magazine’s coveted
Reader’s Choice award). I use ImageLib in my own applications and recommend it
highly. You can find Skyline Tools Imaging at http://www.imagelib.com.

Streamed Data
In addition to streaming images, there are times when you might want to stream out
unstructured data to a BLOB field. Perhaps you want to store a linked list of integers in a
single field, for example. The following code snippet shows how you can use a stream to save
data to a BLOB, and then read it back in later.
procedure SaveListToBlob(List: TList);
var
Stream: TStream;
Num: Integer;
Index: Integer;
begin
Stream := ClientDataSet1.CreateBlobStream(
ClientDataSet1.FieldByName(‘DATA’), bmWrite);
try
// Write out the number of integers
Num := List.Count;
Stream.Write(Num, sizeof(Num));

for Index := 0 to List.Count - 1 do begin


4

ADVANCED CLIENT
Num := Integer(List[Index]);
Stream.Write(Num, sizeof(Num)); OPERATIONS
end;
DATASET
finally
Stream.Free;
end;
end;

procedure LoadListFromBlob(List: TList);


var
Stream: TStream;
Count: Integer;
Index: Integer;
Num: Integer;
Chapter 4
166

begin
Stream := ClientDataSet1.CreateBlobStream(
ClientDataSet1.FieldByName(‘DATA’), bmRead);
try
List.Clear;

Stream.Read(Count, sizeof(Count));
for Index := 0 to Count - 1 do begin
Stream.Read(Num, sizeof(Num));
List.Add(Pointer(Num));
end;
finally
Stream.Free;
end;
end;

Note the use of Stream.Write and Stream.Read in the previous procedures. Both of these
methods take a reference to the data to be written as the first parameter, and the number of
bytes to write as the second parameter. Saving a block of data is as straightforward as making
repeated calls to TStream.Write. You must make sure to read the data in exactly the same
order as it was written in, or you will end up with an exception at best and corrupted data at
worst.
Note also the use of the TDataSet method CreateBlobStream to create a blob stream suitable
for the dataset. Many beginning Delphi database programmers attempt to call
TBlobStream.Create, like this:

var
Stream: TBlobStream;
begin
Stream := TBlobStream.Create(...);
try
// Read from or write to the stream here.
finally
Stream.Free;
end;
end;

The problem with this approach is that TBlobStream is specific to the BDE. Creating an
instance of TBlobStream will not work with non-BDE datasets, such as TClientDataSet. To
create a blob stream that will work with the current dataset, always call the dataset’s
CreateBlobStream method, like this:
Advanced Client Dataset Operations
167

var
Stream: TStream;
begin
Stream := TheDataSet.CreateBlobStream(...);
try
// Read from or write to the stream here.
finally
Stream.Free;
end;
end;

Notice that Stream is defined as a TStream, which is the ancestor class for streams. The actual
stream returned may in fact be a TBlobStream (for a BDE dataset) or another kind of stream.
Thanks to polymorphism, you can operate on the stream without knowing its exact class type.

Streamed Components
Although the concept of streaming data relies on you (the programmer) to make sure to read
and write data in the same order, by creating and streaming a component, you can let Delphi’s
built-in streaming mechanism do the work for you.
Delphi provides streaming support for components derived from TPersistent, as well as
helper functions for the TCollection family of classes.
To make streaming the list of integers that were described previously more automatic, let’s
create a component wrapper for the data.
TIntegerItem = class(TCollectionItem)
private
FNumber: Integer;
published
property Number: Integer read FNumber write FNumber;
4

ADVANCED CLIENT
end;
OPERATIONS
DATASET
TIntegerList = class(TComponent)
private
FIntList: TCollection;
published
property IntList: TCollection read FIntList;
end;

Granted, this might be overkill for a data structure as simple as a list of integers, but the same
concept works for a component containing multiple fields or complex subdata.
Chapter 4
168

NOTE
It is beyond the scope of this book to present a detailed discussion of Delphi’s streaming
support. For more information on streaming, please refer to the Component Writer’s
Guide or to the ultimate reference, the VCL/CLX source itself. If you are fortunate
enough to own a copy of Danny Thorpe’s Delphi Component Design, it also has an
informative chapter on Delphi’s streaming mechanism.

File BLOBs
Another common use of BLOB fields is storing an entire file inside a BLOB. For example, say
that you have a large number of PDF documents that you want to catalog and allow your users
to read.
By storing the PDF files in a BLOB field, you can fairly easily create an application that enables
the user to search for the PDF by category or keyword, and then view the file on his computer.
You can load an external file into a BLOB field by using the field’s LoadFromFile method, as
the following code snippet shows:
var
B: TBlobField;
begin
B := ClientDataSet1.FieldByName(‘AttachedFile’);
B.LoadFromFile(‘C:\PROPOSAL.PDF’);
end;

If you want to save the file back to disk (perhaps to load it into an application such as Acrobat
Reader), you would write the following:
var
B: TBlobField;
begin
B := ClientDataSet1.FieldByName(‘AttachedFile’);
B.SaveToFile(‘C:\TEMP.PDF’);
end;

Limitations of BLOB Fields


For all their usefulness, BLOBs do have a couple of limitations. Namely:
• You can’t (currently) perform a filter on a BLOB field. New versions of relational
databases, such as Informix, support searching on BLOBs. Once this functionality is
added to the core dbExpress technology, TClientDataSet might very well be updated to
support it also.
Advanced Client Dataset Operations
169

• You can’t perform a locate or other search technique on a BLOB field.


• The reconciliation features of DataSnap, which are discussed in Chapter 8, “DataSnap,”
don’t work with BLOB fields. However, there is a workaround, as you’ll see in that same
chapter.
The following example demonstrates some of the BLOB techniques discussed in this section.
Listing 4.4 shows the source code for the BLOBs application.

LISTING 4.4 BLOBs—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, QDBCtrls, QExtCtrls, QComCtrls, DBClient, JPEG;

type
TfrmMain = class(TForm)
pnlBottom: TPanel;
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
DBNavigator1: TDBNavigator;
PageControl1: TPageControl;
tabNotes: TTabSheet;
tabImage: TTabSheet;
DBMemo1: TDBMemo;
tabAttachment: TTabSheet;
Label1: TLabel;
DBText1: TDBText;
4

ADVANCED CLIENT
ClientDataSet1Notes: TBlobField;
OPERATIONS
ClientDataSet1ImageType: TStringField; DATASET
ClientDataSet1Image: TBlobField;
ClientDataSet1Attachment: TBlobField;
btnLoadAttachment: TButton;
btnSaveAttachment: TButton;
Label2: TLabel;
DBText2: TDBText;
ClientDataSet1AttachedFile: TStringField;
Bevel1: TBevel;
Image1: TImage;
btnLoadImage: TButton;
btnClearImage: TButton;
OpenPictureDialog1: TOpenDialog;
Chapter 4
170

LISTING 4.4 Continued

OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
procedure FormCreate(Sender: TObject);
procedure DataSource1DataChange(Sender: TObject; Field: TField);
procedure btnLoadImageClick(Sender: TObject);
procedure btnClearImageClick(Sender: TObject);
procedure btnLoadAttachmentClick(Sender: TObject);
procedure btnSaveAttachmentClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.CreateDataSet;
end;

procedure TfrmMain.DataSource1DataChange(Sender: TObject; Field: TField);


var
BlobStream: TStream;
JPEGImage: TJPegImage;
Ext: string;
begin
if (Field = nil) or (Field = ClientDataSet1Image) then begin
if ClientDataSet1ImageType.AsString <> ‘’ then begin
BlobStream := ClientDataSet1.CreateBlobStream(ClientDataSet1Image,
bmRead);
try
Ext := UpperCase(ClientDataSet1ImageType.AsString);
if Ext = ‘.BMP’ then
Image1.Picture.Bitmap.LoadFromStream(BlobStream)
else if Ext = ‘.JPG’ then begin
JPEGImage := TJPEGImage.Create;
try
JPEGImage.LoadFromStream(BlobStream);
Advanced Client Dataset Operations
171

LISTING 4.4 Continued


Image1.Picture.Assign(JPEGImage);
finally
JPEGImage.Free;
end;
end;
finally
BlobStream.Free;
end;
end else
Image1.Picture := nil;
end;
end;

procedure TfrmMain.btnLoadImageClick(Sender: TObject);


begin
if OpenPictureDialog1.Execute then begin
ClientDataSet1.Edit;
ClientDataSet1ImageType.AsString :=
ExtractFileExt(OpenPictureDialog1.FileName);
ClientDataSet1Image.LoadFromFile(OpenPictureDialog1.FileName);
end;
end;

procedure TfrmMain.btnClearImageClick(Sender: TObject);


begin
Image1.Picture := nil;
end;

procedure TfrmMain.btnLoadAttachmentClick(Sender: TObject);


begin
4

ADVANCED CLIENT
if OpenDialog1.Execute then begin
OPERATIONS
ClientDataSet1.Edit;
DATASET
ClientDataSet1AttachedFile.AsString := OpenDialog1.FileName;
ClientDataSet1Attachment.LoadFromFile(OpenDialog1.FileName);
end;
end;

procedure TfrmMain.btnSaveAttachmentClick(Sender: TObject);


begin
if SaveDialog1.Execute then
ClientDataSet1Attachment.SaveToFile(SaveDialog1.FileName);
end;

end.
Chapter 4
172

The BLOBs application enables you to store a note, an image, and a file attachment in a single
record. The notes are stored by using a TDBMemo component.
I elected to create a separate image type field, which I called ImageType, to track the type of
image stored in the Image field. If ImageType is blank, the image is assumed to be NULL.
Notice the way that the image is displayed—The program handles the data source’s
DataChange event. If the Field parameter sent to the event is nil (or if it references the Image
field), the Image component loads the picture from the Image field using one method for BMP
files and another method for JPG files. Of course, a real application would recognize more
image types than just the two.
Also note the parallel between the code used to load an image and the code used to load an
attachment. Any file can be loaded into a BLOB field through the field’s LoadFromFile
method, and can be saved back to disk through the field’s SaveToFile method.
The BLOBs application doesn’t save the data to disk (although it could do so by adding a
SaveToFile method in the code), and it is rather useless. However, it serves to illustrate the
correct way (or one of the correct ways, in the case of images) to use BLOB fields in your pro-
grams.
Figure 4.3 shows this application at runtime.

FIGURE 4.3
Notes and images are a part of many modern applications.

Nested Datasets
Nested datasets are TClientDataSet’s answer to master/detail relationships. Nested datasets
physically nest the detail dataset inside the master dataset as a field. Figure 4.4 illustrates this
concept.
Advanced Client Dataset Operations
173

Customers

Name

Address

Phone
Orders
E-mail
Quantity
Orders
Description

Unit Price

FIGURE 4.4
The Orders dataset is nested inside the Customers dataset.

Datasets can be nested more than one level deep, so you can set up a grandparent/parent/child
relationship between three datasets. You can also create a parent with multiple children, or a
mixture of both (where one master contains three details and each of those contains two
details).
When you save a nested dataset to a file or stream, the entire hierarchy is saved in a single file
or stream. To save a nested dataset, call SaveToFile or SaveToStream on the master dataset,
and all nested datasets are saved automatically. LoadFromFile and LoadFromStream reload all
the data and re-establish the master/detail relationships.
To create a nested dataset at design time, first create a dataset in the usual manner. Then, add a
field to it, giving it a type of DataSet. This completes the master dataset.
To create the detail dataset, drop a second TClientDataSet on the form or data module. Create
the fields that make up the detail dataset. 4

ADVANCED CLIENT
The only remaining piece of business is to link them together. To do this, click the detail OPERATIONS
dataset and set the DataSetField property to the name of the DataSet field that you created on DATASET
the master. That’s all there is to it.
At this point, you can connect data sources and data-aware components to either dataset. As
you scroll through the master dataset, the detail dataset is automatically updated to reflect only
the detail records that are associated with the current master record.
The following example application shows how to correctly set up nested datasets in an application.
It contains customer and order datasets, where one customer can place many orders. Two
data-aware grids enable you to scroll through the customers and view the orders for each one.
Chapter 4
174

Listing 4.5 contains the complete source code for the Nested application.

LISTING 4.5 Nested—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QExtCtrls, QDBCtrls, QGrids, QDBGrids, DB, DBClient, QStdCtrls;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
cdsCustomer: TClientDataSet;
cdsOrder: TClientDataSet;
dsCustomer: TDataSource;
dsOrder: TDataSource;
gridCustomer: TDBGrid;
gridOrder: TDBGrid;
navCustomer: TDBNavigator;
navOrder: TDBNavigator;
cdsCustomerName: TStringField;
cdsCustomerAddress: TStringField;
cdsCustomerCity: TStringField;
cdsCustomerState: TStringField;
cdsCustomerZip: TStringField;
cdsCustomerPhone: TStringField;
cdsCustomerOrders: TDataSetField;
cdsOrderQuantity: TIntegerField;
cdsOrderDescription: TStringField;
cdsOrderUnitPrice: TFloatField;
cdsOrderTotalPrice: TFloatField;
btnLoad: TButton;
btnSave: TButton;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
procedure cdsOrderCalcFields(DataSet: TDataSet);
procedure FormCreate(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
procedure btnLoadClick(Sender: TObject);
private
{ Private declarations }
public
Advanced Client Dataset Operations
175

LISTING 4.5 Continued

{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.cdsOrderCalcFields(DataSet: TDataSet);


begin
DataSet.FieldByName(‘TotalPrice’).AsFloat :=
DataSet.FieldByName(‘Quantity’).AsInteger *
DataSet.FieldByName(‘UnitPrice’).AsFloat;
end;

procedure TfrmMain.FormCreate(Sender: TObject);


begin
cdsCustomer.CreateDataSet;
end;

procedure TfrmMain.btnSaveClick(Sender: TObject);


begin
if SaveDialog1.Execute then
cdsCustomer.SaveToFile(SaveDialog1.FileName);
end;

procedure TfrmMain.btnLoadClick(Sender: TObject);


begin
4

ADVANCED CLIENT
if OpenDialog1.Execute then
OPERATIONS
cdsCustomer.LoadFromFile(OpenDialog1.FileName); DATASET
end;

end.

Looking at the code, you see that there are only four methods. FormCreate creates the master
dataset. It is important to understand that this creates the detail dataset(s) also. You seldom
need to manipulate the detail dataset directly in code.
Similarly, btnSaveClick and btnLoadClick save and load the master dataset to and from disk,
which takes care of saving and loading all detail datasets, as well.
Chapter 4
176

When you run this application, you must either enter some data from scratch, or load the
datasets from a file. Accompanying the source code for this book is a previously created data
file named NESTED.CDS. You might want to load this file instead of entering customers and
orders manually.
Figure 4.5 shows the Nested application at runtime.

FIGURE 4.5
Nested datasets automatically display the detail data for the current master record.

There are a couple of interesting points about this application. First, as you move from customer
to customer in the top grid, the orders for that customer are displayed in the bottom grid. This
is done automatically, with no coding effort.
Second, if you add or modify an order, you’ll notice that the customer record enters edit mode
(as evidenced by the glyph displayed in the indicator column of the current customer record).
If you programmatically manipulate nested datasets, you want to keep the following in mind:
The master record needs to be posted after adding or modifying detail records.

Undo Support
TCustomClientDataSet and its descendents support built-in undo functionality, so you can
provide for what-if scenarios in your application. For instance, you can enable the user to
change the values of certain fields in the dataset (perhaps graphing, or otherwise displaying, an
Advanced Client Dataset Operations
177

analysis of the data). If the user doesn’t like the results, he can revert to the previous data by
undoing his changes, either one at a time or in large chunks.

Cancel
You are probably already familiar with the Cancel method, but I’ll mention it here anyway for
completeness.
The lowest level of undo support is simply discarding changes to the current record before they
have been posted. The Cancel method provides this support:
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Carter’;

// Do more stuff here, and then decide not to save changes after all.

ClientDataSet1.Cancel;

The Change Log


To support more advanced undo operations, client datasets incorporate a change log. The
change log is used to remember each change that’s made to the dataset until the changes are
either merged into the data, undone, or canceled. The following sections examine the different
methods used to commit and roll back changes.
The change log is saved with the data when you call SaveToFile or SaveToStream. When the
dataset is read back in from the file or stream, the change log is in the same state that it was
prior to the save. This means that you can even perform undo operations between invocations
of your application.

LogChanges
4

ADVANCED CLIENT
In order for the change log to be active, the dataset’s LogChanges property must be set to True OPERATIONS
(which is the default). If you don’t intend to provide undo support in your application, you can DATASET
set LogChanges to False, slightly reducing memory requirements and increasing performance.

CAUTION
When creating applications that connect a client dataset to a dataset provider (as
discussed in Chapter 7, “Dataset Providers”), you should not set LogChanges to False.
This is because setting LogChanges to False prevents you from making changes to the
client dataset and applying those changes to the underlying database.
Chapter 4
178

UndoLastChange
You can undo the most recent change to the dataset (regardless of the record it was made to)
by calling UndoLastChange.
UndoLastChange takes a single Boolean parameter (FollowChange) that indicates whether the
dataset should position itself to the record that was affected by the undo operation. If
FollowChange is True, the client dataset positions its cursor to the record that was undone or
restored. If FollowChange is False, the most recently modified record is still restored, but the
current record is not changed.
ClientDataSet1.First;
ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Williams’;
ClientDataSet1.Post;

ClientDataSet1.First;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Carter’;
ClientDataSet1.Post;

ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘First’).AsString := ‘Sam’;
ClientDataSet1.Post;

ClientDataSet.Last;

ClientDataSet1.UndoLastChange(True);

The preceding code snippet first modifies the second record in the dataset, then modifies the
first record in the dataset, and then modifies the second record again. Finally, it moves to the
end of the dataset. The call to UndoLastChange undoes only the second change to the second
record, and repositions the dataset at the second record (because True was passed to
UndoLastChange).

If you were to issue a second call to UndoLastChange, the modification to the first record in the
dataset would be undone. A third call to UndoLastChange would undo the first modification to
the second record.

RevertRecord
RevertRecord undoes all changes to the current record in the dataset. Modifying the preceding
code snippet slightly, we get the following:
Advanced Client Dataset Operations
179

ClientDataSet1.First;
ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Williams’;
ClientDataSet1.Post;

ClientDataSet1.First;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Carter’;
ClientDataSet1.Post;

ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘First’).AsString := ‘Sam’;
ClientDataSet1.Post;

ClientDataSet.First;
ClientDataSet.Next;

ClientDataSet1.RevertRecord;

This code makes the same three changes that the previous code snippet made. It then moves
off the second record and back onto it. (This is just for the purpose of demonstration—You
don’t need to do it.) Finally, the call to RevertRecord undoes both changes that were made to
the second record, but it leaves the change to the first record intact.

SavePoint
SavePoint provides a means of establishing a baseline for database operations, and then
returning to that baseline at a later point in time.
For example, assume that the user made a change to a dataset. He then wants to experiment 4

ADVANCED CLIENT
with some other changes, but isn’t sure that he wants to save the results. After the first change,
OPERATIONS
you could retrieve the current value of SavePoint, like this: DATASET
var
Baseline: Integer;
begin
Baseline := ClientDataSet.SavePoint;

Later, after making modifications to the database, you can return to the baseline by setting the
SavePoint property:

ClientDataSet.SavePoint := Baseline;

Setting SavePoint discards all changes made to the dataset after the baseline was established.
Chapter 4
180

You can also retrieve multiple savepoints, like this:


var
Baseline1: Integer;
Baseline2: Integer;
begin
Baseline1 := ClientDataSet.SavePoint;
// Perform more dataset work here
Baseline2 := ClientDataSet.SavePoint;

Be careful when using SavePoint along with RevertRecord or UndoLastChange. If you


retrieve a SavePoint, and then undo your most recent modifications past the point of the save,
an exception is raised. The following code, shown in Listing 4.6, is just asking for trouble:

LISTING 4.6 Incorrect Use of SavePoints


ClientDataSet1.First;
ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Williams’; // Change 1
ClientDataSet1.Post;

ClientDataSet1.First;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘Last’).AsString := ‘Carter’; // Change 2
ClientDataSet1.Post;

Baseline := ClientDataSet1.SavePoint; // 2 changes on the “stack”

ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘First’).AsString := ‘Sam’; // Change 3
ClientDataSet1.Post;

ClientDataSet1.UndoLastChange(True); // 2 changes on the “stack”


ClientDataSet1.UndoLastChange(True); // 1 change on the “stack”
ClientDataSet1.SavePoint := Baseline; // Exception is raised here

In this code snippet, two changes are made, and then a baseline is established. Next, a third
change is made, and then the third and second changes are undone. Finally, the code attempts
to revert to the save point. Because the change log has been reversed past the point of the save
point, Delphi raises an exception.
Advanced Client Dataset Operations
181

CancelUpdates
The final level of undo support is undoing all changes in the change log. To do this, simply
call CancelUpdates, like this:
ClientDataSet1.CancelUpdates;

CancelUpdates discards all changes made to all records in the dataset by clearing the change
log.

ChangeCount
You can determine how many changes were made to the dataset by looking at the ChangeCount
property:
if ClientDataSet1.ChangeCount > 0 then
ShowMessage(‘It is okay to call UndoLastChange’);

MergeChangeLog
At some point in your application, you might want to merge changes in with the data to
commit any modifications that were made to the dataset. To do this, call MergeChangeLog.
MergeChangeLog takes no parameters.

NOTE
When a client dataset is connected to a provider, you seldom call MergeChangeLog
directly. Instead, you call ApplyUpdates, which makes a call to MergeChangeLog after
the changes have been applied to the underlying dataset.

StatusFilter 4

ADVANCED CLIENT
The StatusFilter property provides for a type of filter on the dataset, but I didn’t discuss it
OPERATIONS
in the “Ranges and Filters” section because it relates directly to the change log. DATASET
As records are added, modified, or deleted in a client dataset, they are tagged with a status.
That status can be one (or more) of the values shown in Table 4.4.

TABLE 4.4 TUpdateStatus Values

Value Description
usUnmodified The record has not been modified in any way.
usInserted The record has been newly inserted into the dataset.
usModified The record was modified.
usDeleted The record has been deleted.
Chapter 4
182

If you would like to view only those records that have been added to the dataset, you can set
StatusFilter to usInserted. To view only added or modified records, set StatusFilter to
usInserted, usModified.

Viewing the Change Log


Because, in reality, the change log is just another dataset, you can view it in a grid just like you
can with any other dataset. To do this, you need to assign the change log from the data of one
dataset to the data of another dataset, like this:
cdsChangeLog.Data := ClientDataSet1.Delta;

If ClientDataSet1’s change log is empty, this statement causes a Delta is empty exception
to be raised. So, you should always check the ChangeCount property before attempting to do
this.
The following sample application demonstrates the techniques discussed in this section. Listing
4.7 contains the source code for the main form of the application.

LISTING 4.7 ChangeLog—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QActnList, QGrids, QDBGrids;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
pnlBottom: TPanel;
btnRemoveFilter: TButton;
btnFilter: TButton;
btnUndo: TButton;
btnRevertRecord: TButton;
btnCancelUpdates: TButton;
btnSetSavepoint: TButton;
btnGotoSavepoint: TButton;
btnViewChangeLog: TButton;
procedure FormCreate(Sender: TObject);
procedure btnRemoveFilterClick(Sender: TObject);
procedure btnFilterClick(Sender: TObject);
Advanced Client Dataset Operations
183

LISTING 4.7 Continued

procedure btnUndoClick(Sender: TObject);


procedure btnRevertRecordClick(Sender: TObject);
procedure btnCancelUpdatesClick(Sender: TObject);
procedure btnSetSavepointClick(Sender: TObject);
procedure btnGotoSavepointClick(Sender: TObject);
procedure btnViewChangeLogClick(Sender: TObject);
private
{ Private declarations }
FSavePoint: Integer;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses ChangeLogForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
ClientDataSet1.MergeChangeLog;
end;

procedure TfrmMain.btnFilterClick(Sender: TObject);


begin
4

ADVANCED CLIENT
ClientDataSet1.StatusFilter := [usInserted];
OPERATIONS
end;
DATASET
procedure TfrmMain.btnRemoveFilterClick(Sender: TObject);
begin
ClientDataSet1.StatusFilter := [];
end;

procedure TfrmMain.btnUndoClick(Sender: TObject);


begin
ClientDataSet1.UndoLastChange(True);
end;

procedure TfrmMain.btnRevertRecordClick(Sender: TObject);


begin
ClientDataSet1.RevertRecord;
Chapter 4
184

LISTING 4.7 Continued


end;

procedure TfrmMain.btnCancelUpdatesClick(Sender: TObject);


begin
ClientDataSet1.CancelUpdates;
end;

procedure TfrmMain.btnSetSavepointClick(Sender: TObject);


begin
FSavePoint := ClientDataSet1.SavePoint;
end;

procedure TfrmMain.btnGotoSavepointClick(Sender: TObject);


begin
ClientDataSet1.SavePoint := FSavePoint;
end;

procedure TfrmMain.btnViewChangeLogClick(Sender: TObject);


var
frmChangeLog: TfrmChangeLog;
begin
if ClientDataSet1.ChangeCount > 0 then begin
frmChangeLog := TfrmChangeLog.Create(nil, ClientDataSet1);
try
frmChangeLog.ShowModal;
finally
frmChangeLog.Free;
end;
end else
ShowMessage(‘There are no changes to view.’);
end;

end.

Listing 4.8 shows the source code for the form that displays the change log.

LISTING 4.8 ChangeLog—ChangeLogForm.pas


unit ChangeLogForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QExtCtrls, QStdCtrls, QGrids, QDBGrids, DB, DBClient;
Advanced Client Dataset Operations
185

LISTING 4.8 Continued

type
TfrmChangeLog = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
btnClose: TButton;
Label1: TLabel;
procedure ClientDataSet1AfterScroll(DataSet: TDataSet);
private
{ Private declarations }
public
{ Public declarations }
constructor Create(AOwner: TComponent;
ADataSet: TCustomClientDataSet); reintroduce;
end;

implementation

{$R *.xfm}

{ TfrmChangeLog }

constructor TfrmChangeLog.Create(AOwner: TComponent;


ADataSet: TCustomClientDataSet);
begin
inherited Create(AOwner);

ClientDataSet1.Data := ADataSet.Delta;
4

ADVANCED CLIENT
end;
OPERATIONS
DATASET
procedure TfrmChangeLog.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
case ClientDataSet1.UpdateStatus of
usUnmodified: Label1.Caption := ‘Unmodified’;
usModified: Label1.Caption := ‘Modified’;
usInserted: Label1.Caption := ‘Inserted’;
usDeleted: Label1.Caption := ‘Deleted’;
end;
end;

end.

Figure 4.6 shows the ChangeLog application at runtime as it views the change log for the
EMPLOYEE.CDS dataset.
Chapter 4
186

FIGURE 4.6
The ChangeLog application shows how modifications to a dataset are efficiently stored.

Note the four lines in the change log. The first line shows the data for a newly added
employee. The second line shows the data for a deleted employee. The third and fourth lines
show modifications to an existing employee. The third line contains the employee data before
any modifications were made, but the fourth line contains data for only those fields that were
modified.

Cloning Data from Another Client Dataset


Client datasets have the unique capability to clone data from another client dataset. When you
clone a dataset, there is only one physical copy of the data, but there are two (or more) different
datasets accessing the same copy of that data. Changes to one dataset immediately affect the
other dataset’s view of the data.
Why would you want to do this? I have run into several situations in my projects where
cloning provides an elegant solution to an otherwise sticky situation. The following list outlines
a few of the benefits:
• You can traverse a clone of a dataset without disturbing the original dataset’s current
record pointer.
• When viewing a dataset in a grid, you can insert a new record in a dialog using data-aware
controls that are connected to the clone (without temporarily opening a new, empty line
in the grid).
• You can apply ranges or filters on the clone without affecting the display of the original
dataset.
Advanced Client Dataset Operations
187

To clone a dataset, create a second client dataset, and then call the second dataset’s
CloneCursor method, like this:

var
cdsClone: TClientDataSet;
begin
cdsClone := TClientDataSet.Create(nil);
try
cdsClone.CloneCursor(ClientDataSet1, False, False);

// Perform some operations on the clone here.


finally
cdsClone.Free;
end;
end;

This code snippet creates a clone of ClientDataSet1, performs some operation(s) on the
clone, and then frees the clone. Any Insert, Edit, or Delete operations performed on the
clone are automatically reflected in ClientDataSet1.
CloneCursor is defined like this:
procedure CloneCursor(Source: TCustomClientDataSet; Reset: Boolean;
KeepSettings: Boolean = False); virtual;

Source refers to the client dataset that you want to clone. You can’t clone a nonclient dataset,
such as a BDE dataset or a dbExpress dataset.
Reset and KeepSettings work hand in hand, and determine how the clone handles the
following attributes:
• Filter, Filtered, FilterOptions, OnFilterRecord
4
• IndexName

ADVANCED CLIENT
• MasterSource, MasterFields OPERATIONS
DATASET
• ReadOnly

• RemoteServer, ProviderName

If Reset and KeepSettings are set to False, all the previous properties are copied from the
original dataset to the clone. If Reset is False and KeepSettings is True, the previous
properties are not changed for the clone. If Reset is True (regardless of the value of
KeepSettings), the previous properties are cleared on the clone. Table 4.5 depicts this
relationship.
Chapter 4
188

TABLE 4.5 Relationship between Reset and KeepSettings


Reset KeepSettings Behavior
False False Properties are copied from the original dataset to the clone.
False True Properties are not changed for the clone.
True N/A Properties are cleared on the clone.

If you want the clone to copy some of the listed properties from the original dataset, but not to
copy others, you have to write some code. One way to handle this situation is to set both Reset
and KeepSettings to False copying all the properties listed previously from the original
dataset to the clone. Then, reset the clone’s properties that were overwritten by the original
dataset. Alternately, you could set Reset and KeepSettings to True, and then set the appropriate
properties on the clone.

NOTE
After cloning a client dataset, the clone does not contain any persistent fields types.
This means that you’ll typically use FieldByName to access fields in the clone. Also, the
clone does not inherit any standard calculated fields (internal calculated fields are
inherited) from the original dataset, so be careful not to try accessing any calculated
fields in the clone.

The following example program shows how to effectively use a cloned dataset in the situations
listed at the beginning of this section. Listing 4.9 contains the complete source code for the
Clone application.

LISTING 4.9 Clone—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QGrids, QDBGrids, DB, DBClient, QExtCtrls, QStdCtrls;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
ClientDataSet1: TClientDataSet;
Advanced Client Dataset Operations
189

LISTING 4.9 Continued

DataSource1: TDataSource;
DBGrid1: TDBGrid;
btnUpdate: TButton;
btnInsert: TButton;
btnRange: TButton;
btnInsert2: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnUpdateClick(Sender: TObject);
procedure btnInsertClick(Sender: TObject);
procedure btnRangeClick(Sender: TObject);
procedure btnInsert2Click(Sender: TObject);
private
{ Private declarations }
FCloneDS: TClientDataSet;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
4

ADVANCED CLIENT
ClientDataSet1.AddIndex(‘byID’, ‘ID’, [ixPrimary, ixUnique]);
OPERATIONS
ClientDataSet1.IndexName := ‘byID’;
DATASET
FCloneDS := TClientDataSet.Create(nil);
FCloneDS.CloneCursor(ClientDataSet1, False, False);
end;

procedure TfrmMain.FormDestroy(Sender: TObject);


begin
FCloneDS.Free;
end;

procedure TfrmMain.btnUpdateClick(Sender: TObject);


begin
Chapter 4
190

LISTING 4.9 Continued


Screen.Cursor := crHourglass;
try
FCloneDS.First;
while not FCloneDS.EOF do begin
FCloneDS.Edit;
FCloneDS.FieldByName(‘Salary’).AsFloat :=
FCloneDS.FieldByName(‘Salary’).AsFloat * 1.10;
FCloneDS.Post;

FCloneDS.Next;
end;
finally
Screen.Cursor := crDefault;
end;
end;

procedure TfrmMain.btnInsertClick(Sender: TObject);


begin
if FCloneDS.State <> dsBrowse then
Exit;

FCloneDS.Append;
FCloneDS.FieldByName(‘ID’).AsInteger := 99999;
FCloneDS.FieldByName(‘Name’).AsString := ‘Eric Harmon’;
FCloneDS.FieldByName(‘Birthday’).AsString := ‘1/1/1967’;
FCloneDS.FieldByName(‘Salary’).AsFloat := 1.00;
end;

procedure TfrmMain.btnInsert2Click(Sender: TObject);


begin
if FCloneDS.State <> dsInsert then
Exit;

try
FCloneDS.Post;
ClientDataSet1.GotoCurrent(FCloneDS);
except
FCloneDS.Cancel;
raise;
end;
end;

procedure TfrmMain.btnRangeClick(Sender: TObject);


begin
Advanced Client Dataset Operations
191

LISTING 4.9 Continued


Screen.Cursor := crHourglass;
try
FCloneDS.SetRange([100], [199]);
try
FCloneDS.First;
while not FCloneDS.EOF do begin
FCloneDS.Edit;
FCloneDS.FieldByName(‘Salary’).AsFloat := 50000.0;
FCloneDS.Post;

FCloneDS.Next;
end;
finally
FCloneDS.CancelRange;
end;
finally
Screen.Cursor := crDefault;
end;
end;

end.

Figure 4.7 shows the Clone application at runtime.

ADVANCED CLIENT
OPERATIONS
DATASET

FIGURE 4.7
Cloned datasets can be extremely useful for inserting and updating records.

Looking at this code, you might notice several things. One of them is that I’ve created the
cloned dataset in the FormCreate method and destroyed it in the FormDestroy method. I did
this for simplification. In a real application, you usually create a clone in the method(s) in
which it is needed, and immediately destroy it afterward. By creating and destroying it once in
this sample program, I saved a few lines of code.
Chapter 4
192

The Update button runs through the entire dataset and gives all employees a 10% raise. Notice
that even though the code doesn’t call DisableControls (discussed near the beginning of this
chapter), the grid still doesn’t scroll as the dataset is traversed. That’s because the grid is
connected to ClientDataSet1, and the update is performed on the clone dataset.
The Insert (Part 1) and Insert (Part 2) buttons perform two halves of an insert operation on the
clone. The first button appends a new record to the dataset and fills in the data. The second
button posts the new record to the dataset, and then calls the GotoCurrent method on the original
dataset (synchronizing the original dataset with the clone). GotoCurrent makes the original
dataset jump to the current record of the clone dataset.
The reason for separating the Insert operation into two buttons is so that you can easily see
what happens during an insert. Click Insert (Part 1). Now scroll down to the bottom of the grid.
You will not see a line in the grid for employee 99999—the newly inserted, but not-yet-posted
record. When you click Insert (Part 2), the new record appears in the grid. In contrast, if you
change the code in the Insert button event handlers to use ClientDataSet1, instead of
FCloneDS, you will see the new record appear in the grid before it is actually posted.

Finally, the Range button operates in much the same way as the Update button: It applies a
range to the dataset, and then sets the salary for all employees in that range to $50,000. The
grid is not updated to reflect the range because the range is applied only to the clone dataset
and not to ClientDataSet1.

Maintained Aggregates
Records in a dataset often aren’t completely isolated from one another. Many times, you want
to obtain the sum or the average of either an entire column, or some subset of that column. For
example, you might want to calculate the average salary of all employees in the Sales department,
or you might need to retrieve the number of employees whose last name is Jones.
If you’re using an SQL-based database, you can issue SQL statements to calculate these values.
For example,
SELECT AVG(SALARY) FROM EMPLOYEE WHERE DEPARTMENT = ‘SALES’;

SELECT COUNT(*) FROM EMPLOYEE WHERE LASTNAME = ‘Jones’;

However, you can’t execute SQL statements directly against a client dataset.

NOTE
If you connect a client dataset to a dbExpress dataset through a database provider,
you can then send SQL statements to the database backend and retrieve the results in
the client dataset. This technique is discussed in Chapter 7.
Advanced Client Dataset Operations
193

Instead, client datasets support a powerful feature called maintained aggregates. Maintained
aggregates automatically calculate the sum, average, count, minimum value, or maximum
value for the entire dataset (or for a group or records).

Creating a Maintained Aggregate at Design Time


Creating a maintained aggregate at design time is similar to creating a field at design time.
Like fields, maintained aggregates can be either persistent or nonpersistent. The following
sections show how to create each type.

Persistent Aggregates
You create a persistent aggregate in much the same way that you create a data, calculated, or
lookup field.
1. Right-click the client dataset in the form editor and select Field Editor… from the
pop-up menu.
2. Press Ins to create a new field.
3. Enter the field name and field type in the New Field dialog. The field type should be
Aggregate for maintained aggregates.
4. Select the Aggregate radio button in the Field type group box.
5. Click OK to create the aggregate.
Figure 4.8 shows the field editor after adding an aggregated field named AvgSalary.

ADVANCED CLIENT
OPERATIONS
DATASET

FIGURE 4.8
The field editor shows aggregates in a separate section.

After the aggregate is created, you need to set some additional properties (such as the expression
to aggregate on, and whether or not the aggregate is active).
1. Click the aggregate field in the field editor.
2. In the Object Inspector, enter the aggregate expression in the Expression property.
Aggregate expressions are discussed in the section titled “Aggregate Expressions.” For
now, you can use Avg(Salary).
Chapter 4
194

3. Set the Active property to True. By default, aggregates are not active, which means that
you aren’t able to access them. Set AggregatesActive to True to activate aggregate
fields.
Creating a persistent aggregate automatically creates a component of type TAggregateField,
which you can use to reference the aggregate value. Unlike most fields, however, you don’t use
the AsFloat property to obtain an aggregate’s value. Instead, you use the Value property, like
this:
ShowMessage(‘The average salary is ‘ + ClientDataSet1AvgSalary.Value);

Because Value is a variant, you can reference it as though it were either a string or a floating-
point number. Therefore, the following code is also correct:
var
AvgRaisedSalary: Double
begin
AvgRaisedSalary := ClientDataSet1AvgSalary.Value * 1.05;
end;

Nonpersistent Aggregates
To create a nonpersistent aggregate at design time, click the client dataset in the form editor,
and then double-click the Aggregates property in the Object Inspector to display the aggregate
editor.
The aggregate editor looks and acts a lot like the field editor. Click the Add New button on the
toolbar (or press Ins) to create a new aggregate.
Again, you need to set some additional properties (such as the expression to aggregate on, and
whether or not the aggregate is active).
1. Click the aggregate in the aggregate editor.
2. In the Object Inspector, enter a name for the aggregate, such as AvgSalary.
3. Type an aggregate expression into the Expression property, such as Avg(Salary).
4. Set the Active property to True.
5. Set AggregatesActive to True.
Unlike in the previous section, Delphi does not create a component for an aggregate created in
this manner. Instead, you access the aggregate through the dataset’s Aggregates property, like
this:
ShowMessage(‘The average salary is ‘ +
ClientDataSet1.Aggregates.Find(‘AvgSalary’).Value);
Advanced Client Dataset Operations
195

Alternately, if you know the index of the aggregate, you can access it directly:
ShowMessage(‘The average salary is ‘ + ClientDataSet1.Aggregates[0].Value);

Creating a Maintained Aggregate at Runtime


Creating a maintained aggregate at runtime is similar to creating a nonpersistent aggregate
because you make use of the Aggregates property. The following code snippet shows how to
create an aggregate at runtime:
var
Aggregate: TAggregate;
begin
Aggregate := ClientDataSet1.Aggregates.Add;
Aggregate.AggregateName := ‘AvgSalary’;
Aggregate.Expression := ‘Avg(Salary)’;
Aggregate.Active := True;
end;

Aggregate Expressions
In the previous code snippets, I used an expression of Avg(Salary). As you might guess, this
expression calculates the average value of the Salary field.
Delphi supports the aggregate types listed in Table 4.6.

TABLE 4.6 Aggregate Types


Aggregate Type Description
Sum Calculates the sum of a field.
Avg Calculates the average value of a field. 4

ADVANCED CLIENT
Count Calculates the number of values for a field that are not blank.
OPERATIONS
Min Calculates the minimum value of a field. DATASET
Max Calculates the maximum value of a field.

Sum and Avg can only be used with numeric field types; but Count, Min, and Max can be used
with numbers, strings, or date values.
Aggregate expressions do not have to be simple expressions, such as Avg(Salary). They can
include multiple functions, such as Sum(SalesPrice) - Sum(NetCost). However, you can’t
nest functions. Avg(Sum(SalesPrice)) is not a valid aggregate expression.
The Delphi help topic, “Specifying Aggregates,” provides additional examples of both valid
and invalid aggregate expressions.
Chapter 4
196

Aggregates Across a Group of Records


The simplest aggregate that you can create is one that totals or averages over the entire dataset.
The aggregates that we created previously aggregate across the whole dataset. Many times,
however, you want to calculate an aggregate based on a part of the dataset.
To create a grouped aggregate, you first need to make sure that there is at least one index
defined on the dataset. For purposes of the EMPLOYEE.CDS dataset, we’ll create an index
named byBirthday on the Birthday field.
1. Drop a TClientDataSet on the main form of your application.
2. Right-click the dataset, and select Load from MyBase Table… on the pop-up menu.
3. Select the file C:\EMPLOYEE.CDS and click Open.
4. Create an index named byBirthday on the Birthday field. (If you’ve forgotten how to
create an index, please refer to Chapter 3, “Client Dataset Basics.”)
5. Go into the field editor and create persistent fields for all dataset fields by selecting Add
All Fields from the field editor pop-up menu.
6. Create a persistent aggregate field, named NumSameBirthday, using the expression
Count(Birthday). Delphi will create a component named
ClientDataSet1NumSameBirthday.

7. Drop a TDataSource, TDBGrid, and TLabel on the main form and connect the data source
and grid to the client dataset.
8. In the data source’s OnDataChange event, write the following code:
Label1.Caption := ClientDataSet1NumSameBirthday.Value;

Now we have our starting application. If you run it, you should see the value 10000 appear in
the label (indicating that there are 10,000 records with a non-NULL birthday in the dataset).
Now that we’ve established our baseline, let’s change the aggregate so that it calculates the
number of employees who have the same birthday.
To do this, go back into the field editor and click the NumSameBirthday aggregate field. In the
Object Inspector, set the IndexName to byBirthday. This tells the aggregate field to calculate
its value using the byBirthday index. Next, set the GroupingLevel property to 1.
(GroupingLevel is a one-based value that tells Delphi which portion of the index to use when
calculating the aggregate value.)
For example, say that you have an index on the fields LastName;FirstName. If GroupingLevel
is set to 1, only the first field in the index is used to calculate the aggregate. For an expression
of Avg(Salary), the aggregate calculates the average salary for all employees having the same
last name as the current record.
Advanced Client Dataset Operations
197

If you set GroupingLevel to 2, the aggregate calculates the average salary for all employees
having the same last name and the same first name as the current employee.

Enabling and Disabling Aggregates


Usually, you leave aggregates enabled (Active = True). However, you can disable an individual
aggregate by setting its Active property to False, or you can disable all aggregates by setting
the dataset’s AggregatesActive property to False.
Disabling aggregates results in a slightly speedier application because Delphi doesn’t continually
have to recalculate aggregate values whenever a record is inserted, edited, or deleted. If you
plan to add a large number of records at one time, you might want to disable aggregates, add
the records, and then re-enable aggregates so that Delphi only has to calculate them once (at
the time that you re-enable them).

GetGroupState
You can determine the relative position of a record within an aggregate by calling the dataset’s
GetGroupState method. GetGroupState returns a value of gbFirst, gbMiddle, or gbLast
(depending on whether the current record is the first record in the group, the last record in the
group, or any other record in the group).
Rather than presenting a sample aggregate application here, I’ll refer you to the Aggregate
demo in Delphi’s DEMOS\MIDAS\AGGREGATE directory.

Miscellaneous Properties
This section lists several additional properties of the client dataset that don’t logically fall into
one of the previously discussed categories. 4

ADVANCED CLIENT
Constraints OPERATIONS
DATASET
Constraints provide a way of validating a record’s data before posting. Constraints are most
useful when the validation relies on a relationship between two or more fields in the record.
For example, a record’s data might (nonsensically) be determined invalid if the Salary field is
less than 1000 times the employee’s age. In other words, a 30-year-old employee must earn at
least $30,000.
Constraints are visually similar to filters. The constraint that I just mentioned looks like this:
Salary >= Age * 1000

To create a constraint on a dataset, double-click the Constraints property in the Object


Inspector. The constraints editor appears. Next, click the Add New toolbar button in the
constraints editor (or press the Ins key) to add a new constraint.
Chapter 4
198

Back in the Object Inspector, enter the constraint into the CustomConstraint property. In the
ErrorMessage property, type the message that you’d like to be displayed when the constraint is
not met—for example, “The salary must be at least 1000 times the employee’s age.”
Figure 4.9 shows a screen capture of the Object Inspector and the constraint editor after adding
a constraint.

FIGURE 4.9
Setting up a constraint.

You are not limited to a single constraint for a dataset. You can add as many constraints as are
necessary.
When the record is posted to the dataset, Delphi checks all the constraints imposed on the
dataset. If one of the constraints fails, the message specified in the ErrorMessage property is
displayed, and the post is aborted.

DisableStringTrim
Normally, when records are posted to a dataset, any trailing spaces in a string are automatically
removed. For example, if a user types John (note the two trailing spaces) in a data-aware
edit control, only four characters are actually written to the underlying field in the dataset
because the dataset automatically removes the additional two spaces.
Client datasets globally trim trailing spaces from string fields when the DisableStringTrim
property is set to False (which is the default). However, if you want to retain trailing spaces,
you can set DisableStringTrim to True.
Advanced Client Dataset Operations
199

DisableStringTrim is a global property, in that it affects all the string fields in the dataset. It
doesn’t allow you to trim trailing spaces from the FirstName field, and still retain trailing
spaces for the LastName field. If you want to retain trailing spaces for some fields and remove
them for others, you need to set DisableStringTrim to True, and then remove trailing spaces
manually from the appropriate fields (perhaps in the dataset’s BeforePost event handler).

ReadOnly
By default, client datasets are read/write datasets. You can make a client dataset read-only
(if the underlying data is stored on a CD-ROM drive, for instance) by setting the dataset’s
ReadOnly property to True.

Summary
In addition to the basic functionality presented in the preceding chapter, TClientDataSet
supports a number of advanced operations. In this chapter, you learned the following:
• Datasets provide a number of events that you can hook into and be notified when certain
operations occur. In addition, you can raise an exception during the BeforeXxx events to
prevent the operations from occurring.
• You can increase performance dramatically by disabling data-aware controls during
lengthy processes.
• Delphi datasets provide support for BLOBs, which can be used to store notes, images,
and other unformatted data.
• Nested datasets provide simplified master/detail support in client datasets.
• TClientDataSet’s undo support enables you to perform what-if scenarios in your
applications. 4

ADVANCED CLIENT
• By cloning a client dataset, you can perform operations on a clone of the data without
OPERATIONS
disturbing the settings of the original dataset. DATASET
• Maintained aggregates support the automatic calculation of sums, minimums, maximums,
counts, and averages for groups of records or for the entire dataset.
The following chapter begins a two-chapter introduction to data-aware components.
Data-Aware Components CHAPTER

5
IN THIS CHAPTER
• What Are Data-Aware Components? 202

• TDataSource 204

• Common Data-Aware Component


Characteristics 205

• Simple Data-Aware Components 211

• VCL-Only Data-Aware Controls 222

• Lookup Data-Aware Controls 222

• TDBNavigator 223

• Creating Your Own Data-Aware


Components 225

• Sample Application 232


Chapter 5
202

The preceding two chapters concentrated on client datasets, Delphi’s flexible in-memory
datasets. This chapter introduces the concept of data-aware components, which are ready-made
components that know how to display and edit information stored in a database.
Data-aware components can be used with differing datasets, including BDE, ADO, IBX, and
third-party datasets. However, this chapter shows how to use them with client datasets because
that is how they are used when dbExpress is the underlying data access technology.

What Are Data-Aware Components?


Data-aware components are components that can automatically load and store information
from and to a dataset. For example, consider a standard edit control. It has a Text property,
which the programmer is responsible for reading from and writing to. Where you obtain the
data, and what you do with the new string after the user enters it in the edit control, is entirely
up to you. You might store it in a dataset. You might store it in an INI file or in the Windows
registry. You might even simply use it to perform some sort of calculation, and never store it
anywhere at all.
Because displaying data obtained from a dataset is such a common application requirement,
Delphi provides a set of data-aware components that mirror the standard components. Table
5.1 lists the data-aware components, along with their standard counterparts.

TABLE 5.1 Delphi Data-Aware Components and Their Non–Data-Aware Equivalents


Data-Aware Component Non–Data-Aware Equivalent
TDBText TLabel
TDBEdit TEdit
TDBMemo TMemo
TDBCheckBox TCheckBox
TDBRadioGroup TRadioGroup
TDBComboBox TComboBox
TDBListBox TListBox
TDBLookupComboBox TComboBox
TDBLookupListBox TListBox
TDBImage TImage
TDBGrid TStringGrid
TDBRichEdit TRichEdit (VCL-only component)
TDBCtrlGrid None. Allows for the display of multiple fields in a format
that is not row oriented (VCL-only component).
Data-Aware Components
203

TABLE 5.1 Continued


Data-Aware Component Non–Data-Aware Equivalent
TDBNavigator None. Provides a visual means of navigating and manipu-
lating datasets without code.
TDataSource Not a data-aware component per se. Provides a conduit
between a dataset and one or more data-aware components.

As you can see in Table 5.1, the similarities between the standard components and the data-
aware components are self-explanatory (with the possible exception of the TDBLookupComboBox
and TDBLookupListBox components).
Except for TDataSource, you’ll find all the components listed in Table 5.1 on the Data
Controls tab of the component palette. TDataSource can be found on the Data Access tab.
All data-aware components discussed in this chapter provide two properties that you must set.
The DataSource property references the TDataSource component that provides the link
between the component and the dataset. The DataField property determines from which field
in the dataset the data-aware component retrieves its data.
Later sections in this chapter discuss each of these components, with the exception of TDBGrid.
Because the grid is such an involved component, I’ll spend the following chapter investigating
it. Rather than providing numerous small sample applications throughout this chapter, I’ll defer
an example until the end.
For the most part, the data-aware components mirror their non–data-aware counterparts, so I
have not spent a lot of time and space here discussing each of their properties, events, and
methods. Instead, I’ve concentrated on issues that are specific to the data-aware version of the
component. If you need basic information about the component’s properties or methods, please
refer to either the online help or to one of the excellent general-purpose Delphi books
available.

NOTE
Some Delphi programmers shy away from data-aware components—mostly because
they are aware of the implementation problems with data-aware components in
Visual Basic. They might also shy away because data-aware components received a 5
bad reputation in Delphi’s early days. Rest assured that data-aware components
DATA-AWARE

exhibit good performance characteristics under Delphi, especially when used with
COMPONENTS

TClientDataSets (which they are in this chapter).


Chapter 5
204

TDataSource
As Table 5.1 indicates, TDataSource provides a conduit between a dataset and one or more
data-aware controls that are connected to it. You cannot connect a data-aware component
directly to a dataset. Instead, you connect a TDataSource to the dataset, and then connect one
or more data-aware components to the data source (as Figure 5.1 illustrates).

Data aware component

Data aware component


Dataset Data Source
Data aware component

Data aware component

FIGURE 5.1
Relationship between datasets, data sources, and data-aware components.

TDataSource is a rather simple component, publishing just three events and three properties, in
addition to the Name and Tag properties common to all components. Table 5.2 lists the pub-
lished properties and Table 5.3 lists the data source’s events. OnDataChange and
OnStateChange (the most commonly used of the events) are applied in Listing 5.4, later in this
chapter.

TABLE 5.2 TDataSource Properties

Property Description
AutoEdit When True, the underlying dataset is automatically placed into edit
mode as soon as the user starts to type into a data-aware component
that is connected to this data source. When False, you must specifi-
cally call the dataset’s Edit method before the user can type into
any of the connected data-aware controls.
DataSet Provides a link to the dataset from which the data-aware compo-
nents retrieve data.
Enabled When Enabled is True, the data-aware components connected to
this data source display the data contained in the dataset. When it’s
False, data-aware controls are blank.
Data-Aware Components
205

TABLE 5.3 TDataSource Events

Event Description
OnDataChange Fires when the dataset’s current record data is changed; either
because the dataset’s cursor is moved to a new record, or because
one of the fields is modified.
OnStateChange Fires when the underlying dataset’s State property changes. For
example, when the dataset transitions from browse mode to edit
mode, or from insert mode to browse mode.
OnUpdateData Fires immediately before the underlying dataset posts changes to the
database.

It is easy to forget about TDataSource when writing database applications. After dropping the
data source on a form and connecting the data-aware components to it, the data source often
seems to serve no useful purpose. However, the three events listed in Table 5.3 are extremely
useful in a variety of situations. An example of their usefulness is shown in the sample applica-
tion at the end of this chapter.

Common Data-Aware Component Characteristics


Before discussing the specifics of each individual data-aware component, there are some com-
mon characteristics that you should understand. To effectively use data-aware components in
your applications, you should keep in mind the following considerations:

Modifying Component Data from Code


If you want to change the value that’s displayed in a data-aware component from within your
code, you should edit the underlying field rather than attempting to manipulate the data-aware
component.
For example, say you have a TDBEdit named ecFirstName connected to a field named
FirstName. If you want to programmatically set the edit control so that it displays John, you
might be tempted to write the following code:
ecFirstName.Text := ‘John’;

This is not the correct way, however. What you should do is set the underlying field value to
John, like this:
5
DATA-AWARE
COMPONENTS

ClientDataSet1.Edit;
ClientDataSet1FirstName.AsString := ‘John’;
Chapter 5
206

If you are not using persistent field objects, you would do this instead:
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(‘FirstName’).AsString := ‘John’;

The important thing to remember is to call the dataset’s Edit method before attempting to set
the field value. If the dataset is already in edit or insert mode, the redundant call to Edit does-
n’t have any adverse effects. If for some reason the dataset can’t be edited (for example, if the
dataset’s ReadOnly property is set to True), the call to Edit raises an exception, which you
should be prepared to handle gracefully.

Controlling When the User Is Allowed to Edit Data


By default, as soon as the user starts typing into a data-aware component, VCL/CLX puts the
associated dataset into edit mode. If you want to control the user’s ability to make edits from
within your code, you have four options:
• Set the dataset’s ReadOnly property to True, preventing the user from changing any val-
ues in the dataset. This also prevents any changes being made to the data through code.
• Set the underlying field’s ReadOnly property to True. This prevents the user from modi-
fying the field, and also prevents the field from being modified through code.
• Set the data-aware component’s ReadOnly property to True, preventing the user from
changing a single value in the dataset. Note that this has no effect on any changes that
you make in your code using the method described in the preceding section. Setting the
component’s ReadOnly property does not in any way make the field itself read-only. It
merely prevents the user from making direct modifications to the data through the com-
ponent.
• Set the data source’s AutoEdit property to False, preventing the dataset from automati-
cally entering an edit state when the user starts typing into a data-aware component. If
you go this route, you will typically provide a menu item or a button on the form, which
the user clicks to put the dataset into edit mode. Alternately, you can use a
TDBNavigator, which is discussed later in this chapter.

Formatting and Editing Field Values


Data-aware components don’t have a built-in mechanism for controlling the formatting of field
values during input and output, so at first glance, you might assume that there is no way to dis-
play nicely formatted numeric and string data. However, it turns out that data formatting is
taken care of at the field level rather than at the component level. For this reason, you can set a
specific output format for a field, and the same format will be used anywhere that a data-aware
component is used to display that field.
Data-Aware Components
207

Numeric Fields
When you connect a data-aware component to a numeric field, the data that is displayed in a
component is formatted according to the underlying field’s DisplayFormat property.
DisplayFormat is a string property that consists of up to three parts, separated by semicolons,
in the following format:
<Positive>;<Negative>;<Zero>

The different sections of the string determine how the value is displayed when it is positive,
negative, or zero (respectively). Null values are always displayed as a blank.
Table 5.4 lists the characters that can be used within the DisplayFormat string.

TABLE 5.4 DisplayFormat Specifiers

Character Description
# Digit placeholder. If the formatted value does not require a digit at that
position, the position is not filled. For example, the value 1.2 formatted
with a DisplayFormat of ###.## yields 1.2, with no leading or trailing
spaces.
0 Digit placeholder. If the formatted value does not require a digit at that
position, the position is filled with a 0. For example, the value 1.2 format-
ted with a DisplayFormat of 000.00 yields 001.20.
. Decimal point. Determines where the radix point occurs in the output
string. The decimal point is replaced by the character stored in the
DecimalSeparator global variable.
, Thousands separator. The occurrence of a comma in the DisplayFormat
indicates that the value should be formatted using thousands separators.
The comma does not occupy a position in the output string—it only
serves as an indication that thousands separators are needed. At runtime,
the comma is replaced by the character stored in the ThousandSeparator
global variable.
E+/– Scientific notation. If E+, E–, e+, or e– is present in the DisplayFormat,
the value is formatted using scientific notation. E+ indicates that all expo-
nents should be preceded by a sign. E– indicates that only negative expo-
nents should be preceded by a sign. The E+ or E– is followed by one to
four zeros, specifying the minimum number of digits to include in the 5
exponent.
DATA-AWARE

; Separator character. Used between positive, negative, and zero portions of


COMPONENTS

the string.
‘ or “ Literal. Characters enclosed in single or double quotes are copied literally
to the output string, and are not interpreted as formatting characters.
Chapter 5
208

Table 5.5 lists some examples of DisplayFormat settings.

TABLE 5.5 DisplayFormat Settings

Value DisplayFormat Output


1.2 ##0.00 1.20
1.2 000.## 001.2
1 #0.000 1.000
1 00.## 1(The radix point is not displayed because it is
not needed.)
12.34 $##0.00 $12.34
1234.56 $,0.00 $1,234.56
12.345 X=#.## X=12.35 (Notice that the part to the left of the
radix point is automatically expanded to show two
digits, but the part to the right of the radix point is
rounded to the specified number of digits.)
100000 #0.000E+00 10.000E+04
–15 ##0;(##0);zero (15)
0 ##0;(##0);zero zero
10 Room “#”#0 Room #10 (The # enclosed in quotes is copied
verbatim to the output result.)

If you only specify a single substring in DisplayFormat, it is used to format all numbers. To
use a different output format for negative or zero values, separate the specifiers with semi-
colons, like this:
$,0.00;($,0.00);<zero>

This DisplayFormat string formats positive numbers as dollars and cents, negative numbers
within parentheses, and zero values as the string <zero>.
You can omit a portion of the string by simply leaving its specifier empty. In this case, the pos-
itive format is used instead. For example:
$,0.00;;<zero>

In this case, positive and negative values are both formatted using $,0.00. Zero values are dis-
played as <zero>.
If the DisplayFormat property is left completely blank, the value is displayed using general
floating-point output with 15 significant digits.
Data-Aware Components
209

By default, the same format is used when editing a field’s value. You can set a different format
to use when editing by setting the field’s EditFormat property in addition to, or instead of the
DisplayFormat property. EditFormat works the same as DisplayFormat: It contains a semi-
colon-delimited set of formats to use when displaying positive, negative, and zero values.
For example, suppose that you have a floating-point field that you want displayed as 15.25%,
but when editing, you don’t want the percent sign displayed. You would set DisplayFormat to
#0.00%, and EditFormat to #0.00.

String Fields
String fields do not have separate DisplayFormat and EditFormat properties. Instead, they
have an EditMask property, which is used for both displaying and editing a field’s value.
EditMask holds a Paradox-style edit mask that determines how the string is both displayed and
edited.
Like the DisplayFormat and EditFormat properties, EditMask consists of three parts, sepa-
rated by semicolons. The first part is the mask to use when formatting the string value. The
second part contains a 0 to indicate that literals should not be saved as part of the string value.
Any other character in the second part of the string indicates that literals should be saved as
part of the string value. The third part represents the character that’s displayed to represent
blanks, or characters that have not yet been entered.
For example, the following EditMask accepts a U.S. Social Security number, storing the
hyphens in the underlying field and displaying underscores where numbers are to be entered.
000-000-0000;1;_

Table 5.6 shows the valid EditMask specifiers for string fields.

TABLE 5.6 EditMask Specifiers

Character Description
L Requires an alphabetic character.
l Allows an alphabetic character, but does not require it.
A Requires an alphanumeric character.
a Allows an alphanumeric character, but does not require it.
C Requires a character.
5
c Allows a character, but does not require it.
DATA-AWARE

Requires a numeric character.


COMPONENTS

0
9 Allows a numeric character, but does not require it.
# Allows a numeric character, or a plus or minus sign, but does not
require it.
Chapter 5
210

TABLE 5.6 Continued


Character Description
: Time separator. This character is replaced with the time separator that is
defined in the control panel under regional settings when it’s other than :.
/ Date separator. This character is replaced with the date separator that is
defined in the control panel under regional settings when it’s other than /.
_ Underscore. This character inserts a space in the text. When editing a
field, the cursor automatically skips over the _ character.
; Separator character. Used between mask, literal, and blank portions of the
string.
! If the ! character appears anywhere in the mask, extra and optional char-
acters are represented as leading blanks. Otherwise, extra and optional
characters are represented as trailing blanks.
> All characters following the > character are forced to uppercase until a <
character is encountered.
< All characters following the < character are forced to lowercase until a >
character is encountered.
<> All characters are accepted in whatever case the user enters them.
\ Literal. The character following the \ character is inserted in the string, lit-
erally, and is not interpreted as a mask character.

NOTE
Each character in the mask represents one byte in the string—not one character. For
that reason, when working with multibyte character sets, each character in the string
is represented by two characters in the EditMask. For example, AA and LL each repre-
sent a single multibyte character. When inserting a literal into a mask, only single-
byte literal characters can be entered.

Table 5.7 lists some examples of EditMask settings.

TABLE 5.7 EditMask Settings

Stored Value EditMask Displayed Value Remarks


5615551212 (000)_000-0000 (561) 555-1212 Phone number. Formatting
characters are not stored.
123-456-7890 000-000-0000;1;_ 123-456-7890 Social Security number.
Formatting characters
(hyphens) are stored.
Data-Aware Components
211

TABLE 5.7 Continued


Stored Value EditMask Displayed Value Remarks
33467-0708 00000-0000;1;_ 33467-0708 ZIP code. Hyphen is
stored.
5/28/01 !99/99/00;1;_ 5/28/01 Date. Slashes are stored.
Extra spaces are stored at
the beginning of the string
rather than at the end.

NOTE
The built-in Delphi edit masks leave something to be desired if you’re wanting to use
anything more than a simple edit mask. The standard data-aware components don’t
validate complex masks well (such as phone numbers, Social Security numbers, and
the like). For this reason, you might want to consider a third-party library to assist
you with data entry and validation. I use Orpheus, from TurboPower Software
Company. You can find TurboPower’s Web site at http://www.turbopower.com.

Simple Data-Aware Components


Most data-aware components fall into a category that I’ve defined as simple data-aware com-
ponents. For purposes of this discussion, a simple data-aware component is one that links to a
single field in a single record of a single dataset. For instance, a component that enables you to
display and edit values for the FirstName field of a dataset is a simple data-aware component.
This contrasts with more complex data-aware components; which either display multiple val-
ues from the same dataset (such as TDBGrid), or which look up information from one dataset
for inclusion in another dataset (such as TDBLookupComboBox and TDBLookupListBox).
In this section, I’ll discuss the simple data-aware components, and following sections will
cover some that are more complex. Don’t be too concerned at this point with the mechanics of
creating data-aware components. Near the end of this chapter, I’ll show you how to take a
non–data-aware component and create a data-aware descendent from it.
5
TDBText
DATA-AWARE
COMPONENTS

TDBText is the simplest of all the data-aware components. It is a display-only component, simi-
lar to the standard TLabel. To use it, drop a TDBText component on a form, and set the
DataSource and DataField properties. The data is displayed according to the output format
discussed in the preceding section.
Chapter 5
212

TDBEdit
TDBEdit corresponds to the standard TEdit component. It’s used to display and edit numeric,
string, or data/time data contained in a dataset.
Data is displayed according to the underlying field’s DisplayFormat or EditMask property, and
is edited according to the EditFormat or EditMask property (depending on the field type).

TDBMemo
TDBMemo is similar in concept to TDBEdit, except that it can display and edit multiline text
fields, such as unformatted notes. A TDBMemo is usually connected to a CLOB (Character Large
Object) field, although you can also use it to edit string fields.

TDBCheckBox
TDBCheckBox is used like a standard TCheckBox—to display and enter yes/no or true/false val-
ues. TDBCheckBox can be connected to a Logical or a Yes/No field in desktop databases, such
as Paradox or Access. Most SQL databases, however, don’t directly support these field types.
In these cases, you connect the TDBCheckBox to a string field, which is frequently a single char-
acter.
To define the relationship between checked/unchecked and the underlying field data, you set
the component’s ValueChecked and ValueUnchecked properties. ValueChecked refers to the
value of the field when the checkbox is marked. ValueUnchecked determines the value of the
field when the checkbox is not marked. When the form containing the TDBCheckBox is first dis-
played, and the underlying field contains a value that does not equal either of these two proper-
ties, the checkbox is initially grayed out.
By default, the values of these two properties are true and false, respectively. In my own appli-
cations, I use a single character field (VARCHAR(1)) for Boolean field types. I use T for true and
F for false. Because of this, whenever I drop a TDBCheckBox on a form, I set ValueChecked to T
and ValueUnchecked to F.

NOTE
If you decide to always use a single character field for Boolean field types, you might
want to consider creating a simple component derived from TDBCheckBox that sets
ValueChecked to T and ValueUnchecked to F (by default). That way, you don’t have to
manually set these properties every time you use the component.
In addition, you should probably consider creating a domain in the database to spec-
ify character fields. This is the domain that I create for my own InterBase databases:
CREATE DOMAIN DOM_BOOLEAN AS VARCHAR(1)
DEFAULT ‘F’ NOT NULL CHECK (VALUE IN (‘F’, ‘T’));
Data-Aware Components
213

TDBRadioGroup
TDBRadioGroup is used in cases when you want the user to select one option from a short list
of options. By default, the value of the selected item is stored in the underlying dataset field,
which means you typically connect the TDBRadioGroup component to a string field.
In my applications, I’ve found that I most often want to store the index of the selected item in
an Integer field. This is straightforward to accomplish if you make use of the component’s
Values property. Values is a string list that corresponds to the Items property in the following
manner:
• If Values is empty, the strings contained in the Items property are stored in the underly-
ing dataset.
• If Values is not empty, it should contain the same number of string values as the Items
property. When an item is selected in the radio group, the corresponding value in the
Values property is stored in the dataset.

Using the second rule, you can store a sequential list of numbers in the Values property and
connect the component to an Integer field. Delphi is then smart enough to store the numeric
representation of the selected item in the dataset. Figure 5.2 shows this concept. When the user
selects Tuesday from the list, the number three is stored in the associated field.

Items Value
Sunday 1
Monday 2
Tuesday 3
Wednesday 4
Thursday 5
Friday 6
Saturday 7

Tuesday 3

FIGURE 5.2
Relationship between TDBRadioGroup’s Items and Values properties.

5
TDBComboBox
DATA-AWARE
COMPONENTS

TDBComboBox works similarly to TDBRadioGroup because it enables the user to select an item
from a list and store it in a dataset. Unfortunately, it doesn’t support the Values property, so
you can’t use it to store the index of the selected item in a dataset. In my applications, this is
often a severe limitation, so I’ve created a descendent component named TETHDBComboBox that
supports assigning a value to each string contained in the Items property.
Chapter 5
214

NOTE
The Values property only comes into play when the component’s Style property is
set to csDropDownList, csOwnerDrawFixed, or csOwnerDrawVariable. If the style is set
to csDropDown or csSimple, the Values property is ignored because, in either case,
the user can enter any value in the edit portion of the combo box.

Listing 5.1 contains the source code for the TETHDBComboBox component.

LISTING 5.1 ETHDBComboBox.pas


unit ETHDBComboBox;

interface

uses
Windows, Messages, SysUtils, Classes, Controls, StdCtrls, DBCtrls;

type
TETHDBComboBox = class(TDBComboBox)
private
{ Private declarations }
FDataLink: TFieldDataLink;
FValues: TStrings;
procedure DataChange(Sender: TObject);
procedure UpdateData(Sender: TObject);
function GetComboValue(Index: Integer): string;
function GetComboText: string;
procedure SetComboText(const Value: string);
procedure SetValues(const Value: TStrings);
protected
{ Protected declarations }
procedure CreateWnd; override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
property Values: TStrings read FValues write SetValues;
end;
Data-Aware Components
215

LISTING 5.1 Continued


procedure Register;

implementation

procedure Register;
begin
RegisterComponents(‘ETH’, [TETHDBComboBox]);
end;

{ TETHDBComboBox }

constructor TETHDBComboBox.Create(AOwner: TComponent);


begin
FValues := TStringList.Create;

inherited Create(AOwner);
end;

destructor TETHDBComboBox.Destroy;
begin
FValues.Free;

inherited;
end;

procedure TETHDBComboBox.CreateWnd;
begin
inherited;

FDataLink := TFieldDataLink(SendMessage(Handle, CM_GETDATALINK, 0, 0));


FDataLink.OnDataChange := DataChange;
FDataLink.OnUpdateData := UpdateData;
end;

procedure TETHDBComboBox.SetValues(const Value: TStrings);


begin
FValues.Assign(Value);
DataChange(Self);
end;
5
function TETHDBComboBox.GetComboValue(Index: Integer): string;
DATA-AWARE
COMPONENTS

begin
if (Index < FValues.Count) and (FValues[Index] <> ‘’) then
Result := FValues[Index]
else if Index < Items.Count then
Result := Items[Index]
Chapter 5
216

LISTING 5.1 Continued


else
Result := ‘’;
end;

function TETHDBComboBox.GetComboText: string;


begin
if Style in [csDropDown, csSimple] then
Result := Text
else if ItemIndex >= 0 then
Result := GetComboValue(ItemIndex)
else
Result := ‘’;
end;

procedure TETHDBComboBox.SetComboText(const Value: string);


var
I: Integer;
Index: Integer;
Redraw: Boolean;
begin
if Value <> GetComboText then begin
if Style <> csDropDown then begin
Redraw := (Style <> csSimple) and HandleAllocated;
if Redraw then
SendMessage(Handle, WM_SETREDRAW, 0, 0);

try
if Value = ‘’ then
I := -1
else begin
I := -1;
for Index := 0 to Items.Count - 1 do
if Value = GetComboValue(Index) then begin
I := Index;
Break;
end;
end;

ItemIndex := I;
finally
if Redraw then begin
SendMessage(Handle, WM_SETREDRAW, 1, 0);
Invalidate;
end;
end;
Data-Aware Components
217

LISTING 5.1 Continued


if I >= 0 then
Exit;
end;

if Style in [csDropDown, csSimple] then


Text := Value;
end;
end;

procedure TETHDBComboBox.DataChange(Sender: TObject);


begin
if not (Style = csSimple) and DroppedDown then
Exit;

if FDataLink.Field <> nil then


SetComboText(FDataLink.Field.Text)
else if csDesigning in ComponentState then
SetComboText(Name)
else
SetComboText(‘’);
end;

procedure TETHDBComboBox.UpdateData(Sender: TObject);


begin
FDataLink.Field.Text := GetComboText;
end;

end.

Listing 5.1 contains some code that you might not be familiar with, so I’ll examine some of
the individual routines in more detail.
Create and Destroy simply create and free the new FValues property, and then pass control
onto TDBComboBox’s constructor and destructor.
CreateWnd sends a CM_GETDATALINK message to the component to obtain a reference to the
component’s internal FDataLink field. Because TETHDBComboBox derives from TDBComboBox,
we’re actually retrieving TDBComboBox’s FDataLink. TDBComboBox.FDataLink is private and
TDBComboBox doesn’t provide a property to access the value, so there’s no way to directly get a
5
hold of the data link. Fortunately, TDBComboBox supports the CM_GETDATALINK message, which
DATA-AWARE
COMPONENTS

accomplishes the same thing.


Chapter 5
218

When it has a reference to the data link, CreateWnd sets up new event handlers for
OnDataChange and OnUpdateData. OnDataChange is fired automatically when the underlying
field data changes because either the current record changed, or because a new value was
assigned to the field. OnUpdateData is fired when the user selects a value in the combo box,
and the underlying field should be updated.
TDBComboBox provides a handler for both of these methods, but the handlers don’t take into
account our new FValues property. So, it’s necessary to override them.
SetValues is called when you assign a new string list to the Values property, like this:
ETHDBComboBox1.Values := MyStringList;

It first assigns the string list, and then calls DataChange directly, which ensures that the combo
box is updated to display the correct data.
GetComboValue is a helper function that retrieves the correct value for a given index. It first
checks the Values property to see if a value was assigned to the item in question. If so, it
returns the value from that list. If not, it returns the value directly from the Items list.
GetComboText returns the text for the currently displayed item in the combo box. If the combo
box allows text entry—in other words, if the style is csDropDown or csSimple—the function
simply returns the text displayed in the combo box. Otherwise, it calls GetComboValue to
obtain the value of the current item.
SetComboText works in reverse. It determines the index of a given string and makes that the
current ItemIndex of the component.
DataChange, as mentioned earlier, fires when the underlying field data changes. This method
simply calls SetComboText to update the text displayed in the combo box.
Conversely, UpdateData updates the underlying field so that it contains the correct value for
the currently selected combo box item.
TDBComboBox is used in a manner similar to the TDBRadioGroup component. If you leave the
Values list empty, the item selected in the combo box is stored directly in the underlying field,
which should be a string field. If the Values list is populated, the corresponding value is stored
in the underlying field, which can be either a string field or a numeric field (depending on
whether the Values list contains text or numbers).

TDBListBox
TDBListBox is conceptually identical to TDBComboBox because it enables the user to select an
item from a list of items. It also has the same limitation of TDBComboBox because it does not
support a Values property. For that reason, I’ve created my own version of TDBListBox.
Data-Aware Components
219

Listing 5.2 contains the source code for TETHDBListBox (a descendent of TDBListBox that sup-
ports a Values property).

LISTING 5.2 ETHDBListBox.pas


unit ETHDBListBox;

interface

uses
Windows, Messages, SysUtils, Classes, Controls, StdCtrls, DBCtrls;

type
TETHDBListBox = class(TDBListBox)
private
{ Private declarations }
FDataLink: TFieldDataLink;
FValues: TStrings;
procedure DataChange(Sender: TObject);
procedure UpdateData(Sender: TObject);
function GetListValue(Index: Integer): string;
function IndexOfItem(const Value: string): Integer;
procedure SetValues(const Value: TStrings);
protected
{ Protected declarations }
procedure CreateWnd; override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
property Values: TStrings read FValues write SetValues;
end;

procedure Register;

implementation

procedure Register;
begin 5
RegisterComponents(‘ETH’, [TETHDBListBox]);
DATA-AWARE
COMPONENTS

end;

{ TETHDBListBox }
Chapter 5
220

LISTING 5.2 Continued


constructor TETHDBListBox.Create(AOwner: TComponent);
begin
FValues := TStringList.Create;

inherited Create(AOwner);
end;

destructor TETHDBListBox.Destroy;
begin
FValues.Free;

inherited;
end;

procedure TETHDBListBox.CreateWnd;
begin
inherited;

FDataLink := TFieldDataLink(SendMessage(Handle, CM_GETDATALINK, 0, 0));


FDataLink.OnDataChange := DataChange;
FDataLink.OnUpdateData := UpdateData;
end;

procedure TETHDBListBox.SetValues(const Value: TStrings);


begin
FValues.Assign(Value);
DataChange(Self);
end;

function TETHDBListBox.GetListValue(Index: Integer): string;


begin
if (Index < FValues.Count) and (FValues[Index] <> ‘’) then
Result := FValues[Index]
else if Index < Items.Count then
Result := Items[Index]
else
Result := ‘’;
end;

function TETHDBListBox.IndexOfItem(const Value: string): Integer;


var
I: Integer;
Index: Integer;
Data-Aware Components
221

LISTING 5.2 Continued


begin
I := -1;
for Index := 0 to Items.Count - 1 do
if Value = GetListValue(Index) then begin
I := Index;
Break;
end;

Result := I;
end;

procedure TETHDBListBox.DataChange(Sender: TObject);


begin
if FDataLink.Field <> nil then
ItemIndex := IndexOfItem(FDataLink.Field.Text)
else
ItemIndex := -1;
end;

procedure TETHDBListBox.UpdateData(Sender: TObject);


begin
if ItemIndex >= 0 then
FDataLink.Field.Text := GetListValue(ItemIndex)
else
FDataLink.Field.Text := ‘’;
end;

end.

The source code for TETHDBListBox is similar to that of TETHDBComboBox, so I won’t go into it
in detail here.

TDBImage
TDBImage is used to display bitmaps contained in a dataset’s BLOB field. Unfortunately,
TDBImage cannot be used to display nonbitmap images (such as JPEG, PNG, and the like).
Chapter 4, “Advanced Client Dataset Operations,” explains how you can store and retrieve
nonbitmap images from database BLOB fields. 5
DATA-AWARE
COMPONENTS

NOTE
Listing 3.4 showed how to display image data from a dataset without using a
TDBImage data-aware component.
Chapter 5
222

VCL-Only Data-Aware Controls


VCL supports a few additional data-aware controls that are not supported under CLX. These
include
• TDBRichEdit

• TDBChart

• TDBCtrlGrid

These components are not included with CLX because they rely on one of the following:
underlying Win32 implementations (TDBRichEdit), not-yet-available third-party components
(TDBChart), or unsupported/obsolete functionality (TDBCtrlGrid).
Nevertheless, these components have use in VCL applications, so I’ll mention TDBRichEdit in
this chapter and TDBCtrlGrid in the next. Because date entry is something that many applica-
tions require, I’ll present a data-aware implementation of the Win32 TDateTimePicker compo-
nent later in this chapter.
TDBRichEdit is similar to TDBMemo because it is used to display and edit multiline text.
However, TDBMemo displays and edits unformatted text, while TDBRichEdit works with rich text
(text formatted using RTF, or rich text format).
Rich text enables the user to format paragraphs, words, or individual characters using different
font styles and formatting techniques—such as bullets, numbering, tabs, and indentation.
Although TDBRichEdit and its non–data-aware counterpart, TRichEdit, support this function-
ality through a wide array of properties and methods, it is up to you to provide the user with a
menu, a toolbar, or both to call the appropriate methods.
Without writing any code whatsoever, TDBRichEdit can still be used to display formatted text.

Lookup Data-Aware Controls


The preceding section discussed simple data-aware components that connect to a single field in
a single dataset. In this section, I’ll introduce lookup components. Lookup components store
data to a single field in a dataset, but display a list of available data from another dataset.
For example, let’s assume that we’re dealing with a standard order-entry system containing an
ORDERDETAIL table and a PARTS table. The PARTS table consists of a PartNumber field and a
Description field (among others). The ORDERDETAIL table also contains a PartNumber field,
which references the PARTS table.
In your application, you might want the user to be able to view a list of part numbers and their
descriptions, select a part, and have the corresponding part number automatically stored in the
ORDERDETAIL table.
Data-Aware Components
223

This is what the lookup data-aware controls are designed for—displaying a list from one
dataset and enabling the user to select an item to be stored in another dataset.
To complete the link to the lookup dataset, lookup data-aware components provide four addi-
tional properties: ListSource, ListField, KeyField, and ListFieldIndex.
• ListSource references the data source of the dataset from which to retrieve the list of
values.
• ListField is a semicolon-delimited list of field names that are to be displayed in the
component.
• KeyField determines the field whose value is to be stored in the dataset.
• ListFieldIndex is a zero-based number that determines the field to be used for incre-
mental searching in the component. For example, say that you set the ListField prop-
erty to FirstName;LastName. This instructs the component to display the first name and
the last name in the list. If you set ListFieldIndex to 1, as the user types into the con-
trol, it performs automatic incremental searching on the LastName field.
Lookup data-aware components consist of TDBLookupComboBox and TDBLookupListBox. These
components look and act like TDBComboBox and TDBListBox (respectively), except that rather
than populating the items manually, TDBLookupComboBox and TDBLookupListBox retrieve their
items from the dataset referenced through the component’s ListSource property.

NOTE
You can duplicate the functionality of the TETHDBComboBox and TETHDBListBox com-
ponents by using TDBLookupComboBox and TDBLookupListBox. To do this, create a
TClientDataSet that contains the Items and Values associations that are set in the
TETHDBComboBox or in the TETHDBListBox. If you only have a single occurrence of this
in your application, you might elect to go this route. However, if you have numerous
occurrences, your form becomes littered with lookup datasets and you might find it
easier to use TETHDBComboBox and TETHDBListBox instead.

TDBNavigator
The remaining components discussed in this chapter are used for displaying and editing data,
but TDBNavigator provides a code-free means of navigating and manipulating a dataset.
5
Visually, TDBNavigator looks like a toolbar because it contains a horizontal array of prede-
DATA-AWARE
COMPONENTS

fined buttons (actually, TSpeedButtons), which are listed in Table 5.8.


Chapter 5
224

TABLE 5.8 TDBNavigator Buttons

Button Dataset Method


First

Prior

Next

Last

Insert

Delete

Edit

Post

Cancel

Refresh (discussed in Chapter 7, “Dataset Providers”)

When the user clicks one of the buttons in the TDBNavigator, VCL/CLX calls the correspond-
ing dataset method automatically. You can control which buttons are displayed through the
VisibleButtons property, which is implemented as a Pascal set: Simply remove the buttons
that you don’t want shown from the VisibleButtons property.
It is possible to change the image that appears on one or more of the buttons, although the
method for doing this isn’t well documented. TDBNavigator encapsulates a number of
TSpeedButtons to display the individual images, so you can access an individual speed button
through the controls array (as the following code snippet illustrates):
(DBNavigator1.Controls[0] as TSpeedButton).Glyph.LoadFromFile(‘C:\First.bmp’);

The index into the Controls array is a number between zero and nine, which refers to the
absolute position of the button within the navigator’s button array. However, a more fail-safe
method of accessing the button involves using the TNavigateBtn enumerated type, which is
defined in DBCtrls.pas like this:
TNavigateBtn = (nbFirst, nbPrior, nbNext, nbLast,
nbInsert, nbDelete, nbEdit, nbPost, nbCancel, nbRefresh);
Data-Aware Components
225

Using this type, we can write the following code instead:


(DBNavigator1.Controls[Ord(nbFirst)] as TSpeedButton).Glyph.LoadFromFile(
‘C:\First.bmp’);

Creating Your Own Data-Aware Components


Creating your own data-aware components isn’t all that difficult when you understand the steps
that you must take to provide a data-aware version of an existing standard control. In this sec-
tion, I provide working code for a data-aware version of the Win32 TDateTimePicker compo-
nent. Along the way, I’ll provide an overview of the steps required to create a data-aware
component. While reading the following sections, please refer to the source code shown in
Listing 5.3.

TFieldDataLink
TFieldDataLink is a helper class that establishes a link between the data-aware component
and the underlying dataset field. TFieldDataLink provides only a small number of methods,
properties, and events that you need to concern yourself with when writing a data-aware com-
ponent. Tables 5.9, 5.10, and 5.11 list the most often-used methods, properties, and events
(respectively).

TABLE 5.9 TFieldDataLink Methods

Method Description
Edit Try to put the dataset into edit mode. Edit returns False if the dataset
does not allow editing, and returns True otherwise.
Modified Call the Modified method when the data-aware component is changed;
either because the user types into it, or because the contents of the
component were changed in some other way (such as through a click
or other interaction with the component).
Reset Call the Reset method when an action occurs that causes the contents
of the underlying field to be reset to its original value. For example, a
data-aware component might support a key (such as Ctrl+R) that resets
the original value of the field.

5
TABLE 5.10 TFieldDataLink Properties
DATA-AWARE
COMPONENTS

Property Description
CanModify Read-only property that returns True if the corresponding field can be
modified, and returns False if it cannot. CanModify returns False if
the dataset, the field, or the data-aware component is read-only.
Control References the link data-aware control.
Chapter 5
226

TABLE 5.10 Continued


Property Description
Field References the field object to which the data-aware control is bound.
The field object might be a persistent field, or it might be an automati-
cally generated field object for a nonpersistent field.
FieldName The name of the field to which the data-aware component is bound.

TABLE 5.11 TFieldDataLink Events

Event Description
OnDataChange Fires when there is a change to the underlying field.
OnEditingChange Fires when the associated data source changes from an editing mode to
a browse mode, or vice versa.
OnUpdateData Fires when the data contained in the data-aware component should be
written out to the dataset.
OnActiveChange Fires when the underlying dataset changes from active to inactive, or
vice versa.

The following sections explain how to incorporate a TFieldDataLink class into a component
to create a data-aware version of that component. They also show the proper way to make use
of the methods, properties, and events listed in the preceding tables.

Setting Up the TFieldDataLink


The first step in creating a data-aware component is to add a private field of type
TFieldDataLink to the component.

Next, override the Create and Destroy constructor and destructor.


Create is responsible for creating the TFieldDataLink object and establishing the connection
to this component through the Control property. Notice in Listing 5.3 that the Create method
adds the csReplicatable setting to the ControlStyle property. This informs the component
that it can be used in a TDBCtrlGrid, as discussed in the following chapter.
Create also sets up event handlers for the TFieldDataLink’s OnDataChange and OnUpdateData
events. You can also create handlers for the OnEditingChange and OnActiveChange events if
you want or need to, but I haven’t done that here.
Destroy simply frees the TFieldDataLink component, and then calls the inherited Destroy
method.
Data-Aware Components
227

Finally, you should handle the CM_GETDATALINK message and return a reference to the internal
TFieldDataLink field. CMGetDataLink provides this service in Listing 5.3.

NOTE
If you remember from the section titled “TDBComboBox,” we took advantage of the
CM_GETDATALINK message when writing the TETHDBComboBox and TETHDBListBox
components. If the authors of TDBComboBox and TDBListBox had not provided the
CM_GETDATALINK message handler, we would have no way of obtaining a reference to
the component’s internal TFieldDataLink.

Setting Up a Connection to the Data Source


The next step that you will take is to create properties for DataSource and DataField. These
properties simply make calls to GetDataSource/SetDataSource and
GetDataField/SetDataField. For most data-aware components, you can copy the code pre-
sented in Listing 5.3 for these methods verbatim.
In addition, you should provide an overridden Notification method, which is called when a
linked component is freed. In the case of most data-aware components, we want to be notified
if the user removes the associated TDataSource component from the form or from the data
module at design time. If this occurs, the reference to the data source is no longer valid, so we
set the DataSource property to nil.

Responding to Changes in the Dataset


At this point, you should create a DataChange event handler. DataChange does the job of
updating the data-aware component so that it reflects the current state of the linked data field.
In the example presented here, DataChange sets the component’s Date property to the value of
the associated field. If there is no associated field, the component displays today’s date.
Next, you should provide an overridden implementation of the Loaded event. Loaded simply
calls the DataChange event when the component is in design mode. At runtime, DataChange
automatically gets called.

Updating the Dataset 5


Now that the component updates itself correctly when the underlying data changes, we need to
DATA-AWARE
COMPONENTS

write the code that updates the data when the component changes. To do that, we need to write
the UpdateData event handler.
Chapter 5
228

In many cases, UpdateData contains a single line of code, which gets the current value from
the data-aware component and writes it to the data field (as Listing 5.3 shows).
You also need to write one or more event handlers for the data-aware component that fires
when the value of the component is changed. In many cases, this includes a Change event han-
dler. In some cases, it requires a Click handler instead of (or in addition to) the Change event
handler. You should be familiar with the component that you are working with so that you
know what events might be fired as a result of a change to the component’s value.
In this case, I’ve overridden TDateTimePicker’s Click and Change dynamic methods to add
calls to the data link’s Edit and Modified methods. The logic is this: First, call Edit to attempt
putting the underlying dataset into edit mode. Next, call the component’s inherited method.
Finally, call Modified to let the data link know that the field was changed.

Message Handlers
Typically, a data-aware component updates the dataset when focus leaves the component. To
accomplish this, we must provide a message handler for the CM_EXIT message in the form of
the CMExit method shown in Listing 5.3.
The CMExit method attempts to update the dataset. If it fails for any reason, focus is set back to
the component and the exception is raised again. You can generally copy this message han-
dler’s code into your own data-aware components without modification.

Action Handlers
The final two methods that you should provide in your data-aware component are overrides for
ExecuteAction and UpdateAction. These overridden methods ensure that the component
works correctly with the standard DataSet actions provided with Delphi. Again, you can copy
the code verbatim from this component into your own data-aware components.

Data-Aware TDateTimePicker
Listing 5.3 contains the complete source code for TETHDBDateTimePicker (a data-aware
descendent of TDateTimePicker).

LISTING 5.3 ETHDBDateTimePicker.pas


unit ETHDBDateTimePicker;

interface

uses
Windows, Messages, SysUtils, Classes, Controls, ComCtrls, DB, DBCtrls;
Data-Aware Components
229

LISTING 5.3 Continued


type
TETHDBDateTimePicker = class(TDateTimePicker)
private
{ Private declarations }
FDataLink: TFieldDataLink;
function GetDataField: string;
function GetDataSource: TDataSource;
procedure SetDataField(const Value: string);
procedure SetDataSource(const Value: TDataSource);
function GetField: TField;
procedure DataChange(Sender: TObject);
procedure UpdateData(Sender: TObject);
procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK;
protected
{ Protected declarations }
procedure Loaded; override;
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
procedure Change; override;
procedure Click; override;
procedure CMExit(var Message: TCMExit); message CM_EXIT;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function ExecuteAction(Action: TBasicAction): Boolean; override;
function UpdateAction(Action: TBasicAction): Boolean; override;
property Field: TField read GetField;
published
{ Published declarations }
property DataField: string read GetDataField write SetDataField;
property DataSource: TDataSource read GetDataSource write SetDataSource;
end;

procedure Register;

implementation

procedure Register;
begin
5
RegisterComponents(‘ETH’, [TETHDBDateTimePicker]);
DATA-AWARE
COMPONENTS

end;

{ TETHDBDateTimePicker }
Chapter 5
230

LISTING 5.3 Continued


constructor TETHDBDateTimePicker.Create(AOwner: TComponent);
begin
inherited Create(AOwner);

ControlStyle := ControlStyle + [csReplicatable];


FDataLink := TFieldDataLink.Create;
FDataLink.Control := Self;
FDataLink.OnDataChange := DataChange;
FDataLink.OnUpdateData := UpdateData;
end;

destructor TETHDBDateTimePicker.Destroy;
begin
FDataLink.Free;
FDataLink := nil;
inherited Destroy;
end;

procedure TETHDBDateTimePicker.Loaded;
begin
inherited Loaded;
if (csDesigning in ComponentState) then
DataChange(Self);
end;

procedure TETHDBDateTimePicker.Notification(AComponent: TComponent;


Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and
(FDataLink <> nil) and
(AComponent = DataSource) then
DataSource := nil;
end;

procedure TETHDBDateTimePicker.CMGetDataLink(var Message: TMessage);


begin
Message.Result := Integer(FDataLink);
end;

procedure TETHDBDateTimePicker.Change;
begin
FDataLink.Edit;
inherited Change;
FDataLink.Modified;
end;
Data-Aware Components
231

LISTING 5.3 Continued


procedure TETHDBDateTimePicker.Click;
begin
FDataLink.Edit;
inherited Click;
FDataLink.Modified;
end;

function TETHDBDateTimePicker.GetDataSource: TDataSource;


begin
Result := FDataLink.DataSource;
end;

procedure TETHDBDateTimePicker.SetDataSource(const Value: TDataSource);


begin
if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then
FDataLink.DataSource := Value;
if Value <> nil then
Value.FreeNotification(Self);
end;

function TETHDBDateTimePicker.GetDataField: string;


begin
Result := FDataLink.FieldName;
end;

procedure TETHDBDateTimePicker.SetDataField(const Value: string);


begin
FDataLink.FieldName := Value;
end;

function TETHDBDateTimePicker.GetField: TField;


begin
Result := FDataLink.Field;
end;

procedure TETHDBDateTimePicker.DataChange(Sender: TObject);


begin
if FDataLink.Field <> nil then
Date := FDataLink.Field.AsDateTime
else
5
Date := Now;
DATA-AWARE
COMPONENTS

end;

procedure TETHDBDateTimePicker.UpdateData(Sender: TObject);


Chapter 5
232

LISTING 5.3 Continued


begin
FDataLink.Field.AsDateTime := Date;
end;

procedure TETHDBDateTimePicker.CMExit(var Message: TCMExit);


begin
try
FDataLink.UpdateRecord;
except
SetFocus;
raise;
end;
end;

function TETHDBDateTimePicker.ExecuteAction(Action: TBasicAction): Boolean;


begin
Result := inherited ExecuteAction(Action) or (FDataLink <> nil) and
FDataLink.ExecuteAction(Action);
end;

function TETHDBDateTimePicker.UpdateAction(Action: TBasicAction): Boolean;


begin
Result := inherited UpdateAction(Action) or (FDataLink <> nil) and
FDataLink.UpdateAction(Action);
end;

end.

Sample Application
Listing 5.4 is a sample application that makes use of many (but not all) of the data-aware com-
ponents discussed in this chapter. As you can see from Listing 5.4, there is very little code in
this application. Thanks to VCL/CLX, the data-aware components encapsulate almost every-
thing needed to display and update datasets in your applications.

LISTING 5.4 DataAware—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DB, DBClient,
QStdCtrls, QExtCtrls, QButtons, Mask, QComCtrls, QDBCtrls, QMask;
Data-Aware Components
233

LISTING 5.4 Continued


type
TfrmMain = class(TForm)
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
cdsLookup: TClientDataSet;
dsLookup: TDataSource;
cdsLookupID: TIntegerField;
cdsLookupDescription: TStringField;
DBNavigator1: TDBNavigator;
pnlBottom: TPanel;
ClientDataSet1Weekday: TStringField;
ClientDataSet1WeekdayValue: TIntegerField;
ClientDataSet1Image: TBlobField;
ClientDataSet1Active: TStringField;
ClientDataSet1Age: TIntegerField;
ClientDataSet1ItemID: TIntegerField;
ClientDataSet1Salary: TFloatField;
ClientDataSet1Enabled: TBooleanField;
lblCurrent: TLabel;
lblState: TLabel;
OpenDialog1: TOpenDialog;
Panel1: TPanel;
lbDSEvents: TListBox;
Label3: TLabel;
pnlClient: TPanel;
PageControl1: TPageControl;
tabSimple: TTabSheet;
txtAge: TDBText;
Label1: TLabel;
txtWeekday: TDBText;
txtSalary: TDBText;
Label6: TLabel;
Label7: TLabel;
ecAge: TDBEdit;
DBRadioGroup1: TDBRadioGroup;
cbActive: TDBCheckBox;
cbEnabled: TDBCheckBox;
ecSalary: TDBEdit;
tabComboList: TTabSheet;
Label2: TLabel;
5
cbWeekday: TDBComboBox;
DATA-AWARE
COMPONENTS

lbWeekday: TDBListBox;
tabLookup: TTabSheet;
Label4: TLabel;
lbLookup: TDBLookupListBox;
Chapter 5
234

LISTING 5.4 Continued


cbLookup: TDBLookupComboBox;
tabImage: TTabSheet;
Label5: TLabel;
img: TDBImage;
btnLoad: TButton;
btnClear: TButton;
procedure FormCreate(Sender: TObject);
procedure DataSource1DataChange(Sender: TObject; Field: TField);
procedure ClientDataSet1NewRecord(DataSet: TDataSet);
procedure DataSource1StateChange(Sender: TObject);
procedure btnLoadClick(Sender: TObject);
procedure btnClearClick(Sender: TObject);
procedure DataSource1UpdateData(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);

procedure AddLookupItem(ID: Integer; const Description: string);


begin
cdsLookup.Append;
cdsLookupID.AsInteger := ID;
cdsLookupDescription.AsString := Description;
cdsLookup.Post;
end;

begin
// Create the lookup dataset and populate with some data
cdsLookup.CreateDataSet;
AddLookupItem(1, ‘Widgit’);
AddLookupItem(2, ‘Gadget’);
AddLookupItem(3, ‘Thingamabob’);

ClientDataSet1.CreateDataSet;
end;
Data-Aware Components
235

LISTING 5.4 Continued


procedure TfrmMain.ClientDataSet1NewRecord(DataSet: TDataSet);
begin
DataSet.FieldByName(‘Enabled’).AsString := ‘T’;
DataSet.FieldByName(‘Active’).AsString := ‘T’;
end;

procedure TfrmMain.DataSource1DataChange(Sender: TObject; Field: TField);


begin
if Field = nil then
lbDSEvents.Items.Add(‘Data Change: Field = nil’)
else
lbDSEvents.Items.Add(‘Data Change: Field = ‘ + Field.FieldName);

lblCurrent.Caption := Format(‘(Record %d of %d)’,


[ClientDataSet1.RecNo, ClientDataSet1.RecordCount]);
end;

procedure TfrmMain.DataSource1StateChange(Sender: TObject);


begin
lbDSEvents.Items.Add(‘State Change’);

case DataSource1.State of
dsInactive: lblState.Caption := ‘Inactive’;
dsBrowse: lblState.Caption := ‘Browse’;
dsEdit: lblState.Caption := ‘Edit’;
dsInsert: lblState.Caption := ‘Insert’;
end;
end;

procedure TfrmMain.DataSource1UpdateData(Sender: TObject);


begin
lbDSEvents.Items.Add(‘Update Data’)
end;

procedure TfrmMain.btnLoadClick(Sender: TObject);


begin
if OpenDialog1.Execute then begin
ClientDataSet1.Edit;
ClientDataSet1Image.LoadFromFile(OpenDialog1.FileName);
end;
5
end;
DATA-AWARE
COMPONENTS

procedure TfrmMain.btnClearClick(Sender: TObject);


Chapter 5
236

LISTING 5.4 Continued


begin
if not ClientDataSet1Image.IsNull then begin
ClientDataSet1.Edit;
ClientDataSet1Image.Clear;
end;
end;

end.

Figure 5.3 shows the data-aware application at runtime.

FIGURE 5.3
DataAware demonstrates the use of many of the provided data-aware components.

Summary
This chapter introduced you to data-aware components. The components that we’ve covered
are
• TDataSource provides a high-level conduit between data-aware components and datasets.
• TDBText and TDBEdit are useful for displaying and editing simple field values.
• TDBMemo provides a means of displaying and editing unformatted multiline text.
• TDBRichEdit (a VCL-only data-aware component) can be used to display and edit for-
matted multiline text.
• TDBCheckBox and TDBRadioGroup support the selection of one or more options from an
available list of options.
• TDBComboBox and TDBListBox enable the user to select a field value from a list of prede-
fined values. I also provided you with code for descendents of these two components that
allows finer control over the value stored in the associated data field.
Data-Aware Components
237

• TDBLookupComboBox and TDBLookupListBox elaborate on TDBComboBox and TDBListBox


by obtaining the list of items from another dataset, and then saving the primary key of
the lookup dataset back to the dataset that’s being edited.
• TDBImage is used to display bitmaps that are stored in a dataset’s BLOB field.
• TDBNavigator can be used to provide a code-free form of dataset navigation and manip-
ulation.
• With a little effort, you can create data-aware versions of standard VCL/CLX compo-
nents.
The following chapter continues this discussion of data-aware components with a look at data-
aware grids.

5
DATA-AWARE
COMPONENTS
Data-Aware Grids CHAPTER

6
IN THIS CHAPTER
• TDBGrid 240

• TClientDataSetGrid 263

• TDBCtrlGrid 266

• Third-Party Data-Aware Grids 271


Chapter 6
240

The preceding chapter introduced you to data-aware components—in particular, to data-aware


components that display and edit one field at a time. This chapter discusses data-aware grids,
which display information from a number of records at one time.
In this chapter, I’ll examine three different data-aware grids: TDBGrid, TClientDataSetGrid,
and TDBCtrlGrid. TDBGrid is the only one of the three that comes standard with both Delphi
and Kylix. TClientDataSetGrid is a derivative work, written by John Kaster, that provides
built-in support for user-configurable columns as well as code that can automatically sort a
client dataset when the user clicks a column heading (more on that later in this chapter).
TDBCtrlGrid is supplied with Delphi, but not with Kylix. It allows for a nonlinear grid
layout—for example, a grid where each record occupies several lines instead of a single line.

TDBGrid
TDBGridprovides the cornerstone for Delphi’s grid-based, data-aware components. Using
TDBGrid,you can create screens that look like the one shown in Figure 6.1 without a lot of
programming effort.

FIGURE 6.1
A sample screen created using a TDBGrid.

Later in this chapter, we’ll investigate the code required to produce this screen. In this section,
I’ll explore the TDBGrid component. The following sections introduce two other grids that are
either included with Delphi or are available as a free download.

TDBGrid Basic Operation


Like the components discussed in the preceding chapter, TDBGrid publishes a DataSource
property, which indirectly determines the dataset from which the grid retrieves data. However,
because a grid can display data from multiple fields at the same time, there is no DataField
property. Instead, TDBGrid provides a Columns property that enables you to specify which
fields to display in the grid as well as the ordering of the fields and other display-related settings.
These are discussed in detail in the following section, “Customizing Columns.”
Data-Aware Grids
241

The simplest way to use a TDBGrid is to drop it on a form, connect the data source, open the 6
dataset, and then run the application. If you do this, you’ll see a fairly mundane grid using all
default settings, as shown in Figure 6.2.

DATA-AWARE
GRIDS
FIGURE 6.2
The default grid is functional, but not eye-catching.

Figure 6.2 points out examples of title cells, data cells, indicator cells, and grid background.
Using a combination of the grid’s properties and events, you can create a grid that looks much
more pleasing to the eye. In the following sections, I’ll examine those properties and events
in detail.

Customizing Columns
Generally, the most basic level of customization that you want to perform is adjusting either
the number of columns that are displayed, or the order in which the columns are displayed.
TDBGrid published a Columns property, which provides access to the list of columns displayed
in the grid.

NOTE
You can think of the Columns property as being similar to a dataset’s Fields property.
If there are no columns specifically defined, the grid simply displays all columns in the
order that they appear in the dataset. If persistent fields are defined for the dataset,
the grid displays columns only for those fields.
To create persistent column objects for the grid (similar to a dataset’s persistent field
objects), you use the columns editor.
Chapter 6
242

Double-click the grid component at design time (or right-click it and select Columns Editor…
from the pop-up menu) to display the columns editor. The columns editor works like most
collection editors in Delphi—press Ins to create a new TColumn object, or right-click and select
Add from the pop-up menu.
Each column supports a number of properties that can be used to customize the column’s look
and feel. (For the ultimate in display flexibility, see the “Custom Drawing” section later in this
chapter.) These properties are listed in Table 6.1.

TABLE 6.1 Basic TColumn Properties


Property Description
Alignment Sets the alignment of the data displayed in the column to left-justified,
centered, or right-justified.
Color Sets the background color of the individual column.
FieldName Specifies the name of the field in the underlying dataset that is to be
displayed in this column. Any field can be displayed (including data
fields, calculated fields, lookup fields, and aggregate fields).
Font Customizes the font used to display the column data.
ReadOnly When True, the column data cannot be edited, even if the underlying
field and dataset allow editing.
Title Enables customization of the column’s title cell. This property is
discussed later in the “Column Titles” section.
Visible When False, the column is not displayed.
Width Sets the width of the column in screen pixels.

When you set the FieldName property, Delphi sets the Alignment and Width properties
automatically (based on the size and type of the field). Unfortunately, even though Delphi sets
Alignment to taRightJustify for a numeric field, it doesn’t automatically set the title’s
alignment to taRightJustify. So, you need to set the title’s alignment manually.

NOTE
It is possible to add a column for which no underlying data field exists. To do so,
insert a new column and leave the FieldName property blank. When doing this, you
need to set the column’s Alignment and Width properties manually, and you must use
the grid’s custom draw functionality to paint the cell contents for that column. For
example, you might want to create a column with no associated field to display an
icon in certain rows.
Data-Aware Grids
243

Column Types 6
Most columns are displayed and edited as a simple string. For cases in which you want the

DATA-AWARE
user to select from a list of values, or want to display a dialog that enables the user to select
the cell value, TDBGrid supports two types of embellishments that can be made to a column’s

GRIDS
active cell:
• A column can display a lookup combo box to enable the user to select from a predefined
list of values. If a column is linked to a lookup field in a dataset, the column automatically
displays a combo box of acceptable values when the user is editing that column.
• A column can display an ellipsis button, which can be programmed to display a dialog,
or programmed to perform some other function when the user clicks it.
The properties listed in Table 6.2 are used to set options for the various column styles.

TABLE 6.2 Additional TColumn Properties


Property Description
ButtonStyle When set to bsAuto (the default value), the column automatically
displays a combo box for lookup fields. You can manually set this
property to bsEllipsis (displaying an ellipsis button) or to bsNone
(suppressing the combo box for lookup fields).
DropDownRows Specifies the maximum number of items to display in the column’s
combo box when it is dropped down.
PickList For columns that are not connected to a lookup field, you can specify a
list of acceptable field values in the PickList property. If this property
is used, the column automatically displays a combo box when it is
edited (unless the column’s ButtonStyle property is set to bsNone).

For columns with a ButtonStyle of bsEllipsis, the grid’s OnEditButtonClick event is fired
when the user clicks the ellipsis button. The sample program presented at the end of this section
shows how you might respond to that event.

Column Titles
In addition to customizing the look of the column data, you can customize the look of the
columns’ titles. To change the font used for all column titles at the same time, you can set the
grid’s TitleFont property accordingly.
However, for control over each column title, you should resort to the individual column’s
Title property. The Title property expands to enable the following properties to be set for the
column title.
Chapter 6
244

TABLE 6.3 TColumn Title Properties

Property Description
Alignment Sets the alignment of the column title to left-, center-, or right-justified.
Caption Specifies the text to be displayed in the column title.
Color Sets the background color of the column title.
Font Sets the font for the text displayed in the column title.

As mentioned earlier, Delphi does not automatically set the title’s Alignment property to
taRightJustify for numeric fields. So, you should make sure that you check the title’s
alignment when creating persistent columns.

Grid Options
After you have set up the columns that you want to be displayed in the grid, you can set
gridwide options that determine the overall look and feel of the grid. Table 6.4 lists the available
options.

TABLE 6.4 TDBGrid Options

Option Description
dgEditing The grid is editable. The user must press F2 to begin editing the
current cell. Note that individual columns can still be set to
read-only, which prevents editing in those columns. Setting the
dgRowSelect option automatically forces dgEditing off.
dgAlwaysShowEditor The grid is automatically placed into edit mode as soon as the user
tabs into a cell. The user does not need to press F2 to begin editing.
Like dgEditing, dgAlwaysShowEditor is forced off if dgRowSelect
is set.
dgTitles When this option is set, column titles are displayed.
dgIndicator This option forces the display of a narrow column at the extreme
left of the grid that shows the state of the current record (insert, edit,
or browse mode).
dgColumnResize Setting this option enables individual columns to be moved or
resized at runtime.
dgColLines When set, vertical lines are drawn between columns.
dgRowLines When set, horizontal lines are drawn between rows.
dgTabs When set, the user can press the Tab and Shift+Tab keys to move
from cell to cell in the grid. When clear, pressing Tab or Shift+Tab
causes focus to move to the next or the preceding control on the
form, respectively.
Data-Aware Grids
245

TABLE 6.4 Continued 6


Option Description

DATA-AWARE
dgRowSelect When set, clicking a row highlights the entire row rather than

GRIDS
selecting an individual cell. Row highlighting can also be
accomplished manually by custom drawing the grid, as explained
later in this chapter.
dgAlwaysShow Set this option to highlight the current cell even when the grid does
Selection not have focus.
dgConfirmDelete If the grid’s ReadOnly property is not set, this option causes the
VCL to display a delete confirmation message when the user
presses Ctrl+Delete while in the grid. If this option is not set, the
current record is deleted when the user presses Ctrl+Delete. Note that
Ctrl+Delete deletes the current record even if dgEditing is not set.
dgCancelOnExit This option affects how newly inserted rows are treated when the
user tabs out of the grid. When set, newly inserted rows for which
no data has been entered are canceled. If not set, inserted rows that
are left empty are posted to the dataset.
dgMultiSelect When set, the user can select multiple rows in the grid by pressing
Ctrl and clicking individual rows.

By default, Options is set to [dgEditing, dgTitles, dgIndicator, dgColumnResize,


dgColLines, dgRowLines, dgTabs, dgConfirmDelete, dgCancelOnExit]. I find that when I use
a TDBGrid, I turn off the dgEditing and dgIndicator options, and set the grid’s ReadOnly
property to True. Instead of allowing my users to edit directly in the grid, I display a dialog
when they press Enter and enable them to edit field values for the current record there. Of
course, your mileage might vary, and you’ll determine your own favorite set of options as you
use the grid in your applications.

Events
In addition to the properties listed previously, TDBGrid provides a number of events that you
can respond to for finer control over the grid’s display and functionality. These events are
listed in Table 6.5.

TABLE 6.5 TDBGrid Events

Event Description
OnCellClick Fires when the user clicks a cell. Does not fire when the user clicks
a title cell, the indicator, or the grid background.
OnColEnter Fires immediately after focus enters the current column.
Chapter 6
246

TABLE 6.5 Continued


Event Description
OnColExit Fires immediately before focus leaves the current column. Calling
Abort in this handler prevents the grid from switching to a new
column.
OnColumnMoved Fires after the user moves (but not after the user resizes) a column
at runtime.
OnDrawColumnCell Fires when a cell is about to be drawn. It’s used to implement
custom drawing, which is explained later in this chapter.
OnDrawDataCell Obsolete and included for backward compatibility only.
OnEditButtonClick Fires when the user clicks the ellipsis button in a cell.
OnTitleClick Occurs when the user clicks a title cell (assuming that the option
dgTitles is set). TClientDataSetGrid makes internal use of this
event to automatically sort the underlying dataset when the user
clicks a column title.

The following example program, shown in Listing 6.1, demonstrates when the different grid
events are fired. The next section, “Custom Drawing” explores the OnDrawColumnCell event in
more detail.

LISTING 6.1 Options—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DB, QGrids,
QDBGrids, DBClient, QExtCtrls, QStdCtrls, QDBCtrls;

type
TfrmMain = class(TForm)
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
pnlOptions: TPanel;
pnlClient: TPanel;
grid: TDBGrid;
cbEditing: TCheckBox;
cbAlwaysShowEditor: TCheckBox;
cbTitles: TCheckBox;
cbIndicator: TCheckBox;
Data-Aware Grids
247

LISTING 6.1 Continued 6


cbColumnResize: TCheckBox;

DATA-AWARE
cbColLines: TCheckBox;

GRIDS
cbRowLines: TCheckBox;
cbTabs: TCheckBox;
cbRowSelect: TCheckBox;
cbAlwaysShowSelection: TCheckBox;
cbConfirmDelete: TCheckBox;
cbCancelOnExit: TCheckBox;
cbMultiSelect: TCheckBox;
btnShowSelections: TButton;
DBNavigator1: TDBNavigator;
lbEvents: TListBox;
Label1: TLabel;
Label2: TLabel;
btnClearEventLog: TButton;
procedure FormCreate(Sender: TObject);
procedure gridCellClick(Column: TColumn);
procedure gridColExit(Sender: TObject);
procedure gridColEnter(Sender: TObject);
procedure gridColumnMoved(Sender: TObject; FromIndex,
ToIndex: Integer);
procedure gridEditButtonClick(Sender: TObject);
procedure cbEditingClick(Sender: TObject);
procedure cbAlwaysShowEditorClick(Sender: TObject);
procedure cbTitlesClick(Sender: TObject);
procedure cbIndicatorClick(Sender: TObject);
procedure cbColumnResizeClick(Sender: TObject);
procedure cbColLinesClick(Sender: TObject);
procedure cbRowLinesClick(Sender: TObject);
procedure cbTabsClick(Sender: TObject);
procedure cbRowSelectClick(Sender: TObject);
procedure cbAlwaysShowSelectionClick(Sender: TObject);
procedure cbConfirmDeleteClick(Sender: TObject);
procedure cbCancelOnExitClick(Sender: TObject);
procedure cbMultiSelectClick(Sender: TObject);
procedure btnShowSelectionsClick(Sender: TObject);
procedure btnClearEventLogClick(Sender: TObject);
private
procedure RetrieveOptions;
procedure UpdateOption(Option: TDBGridOption; Active: Boolean);
{ Private declarations }
public
{ Public declarations }
end;
Chapter 6
248

LISTING 6.1 Continued

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);

RetrieveOptions;
end;

// Options set/get methods

procedure TfrmMain.RetrieveOptions;
begin
cbEditing.Checked := (dgEditing in grid.Options);
cbAlwaysShowEditor.Checked := (dgAlwaysShowEditor in grid.Options);
cbTitles.Checked := (dgTitles in grid.Options);
cbIndicator.Checked := (dgIndicator in grid.Options);
cbColumnResize.Checked := (dgColumnResize in grid.Options);
cbColLines.Checked := (dgColLines in grid.Options);
cbRowLines.Checked := (dgRowLines in grid.Options);
cbTabs.Checked := (dgTabs in grid.Options);
cbRowSelect.Checked := (dgRowSelect in grid.Options);
cbAlwaysShowSelection.Checked := (dgAlwaysShowSelection in grid.Options);
cbConfirmDelete.Checked := (dgConfirmDelete in grid.Options);
cbCancelOnExit.Checked := (dgCancelOnExit in grid.Options);
cbMultiSelect.Checked := (dgMultiSelect in grid.Options);
end;

procedure TfrmMain.UpdateOption(Option: TDBGridOption; Active: Boolean);


begin
if Active then
grid.Options := grid.Options + [Option]
else
grid.Options := grid.Options - [Option];

RetrieveOptions;
end;

procedure TfrmMain.cbEditingClick(Sender: TObject);


Data-Aware Grids
249

LISTING 6.1 Continued 6


begin

DATA-AWARE
UpdateOption(dgEditing, cbEditing.Checked);

GRIDS
end;

procedure TfrmMain.cbAlwaysShowEditorClick(Sender: TObject);


begin
UpdateOption(dgAlwaysShowEditor, cbAlwaysShowEditor.Checked);
end;

procedure TfrmMain.cbTitlesClick(Sender: TObject);


begin
UpdateOption(dgTitles, cbTitles.Checked);
end;

procedure TfrmMain.cbIndicatorClick(Sender: TObject);


begin
UpdateOption(dgIndicator, cbIndicator.Checked);
end;

procedure TfrmMain.cbColumnResizeClick(Sender: TObject);


begin
UpdateOption(dgColumnResize, cbColumnResize.Checked);
end;

procedure TfrmMain.cbColLinesClick(Sender: TObject);


begin
UpdateOption(dgColLines, cbColLines.Checked);
end;

procedure TfrmMain.cbRowLinesClick(Sender: TObject);


begin
UpdateOption(dgRowLines, cbRowLines.Checked);
end;

procedure TfrmMain.cbTabsClick(Sender: TObject);


begin
UpdateOption(dgTabs, cbTabs.Checked);
end;

procedure TfrmMain.cbRowSelectClick(Sender: TObject);


begin
UpdateOption(dgRowSelect, cbRowSelect.Checked);
end;
Chapter 6
250

LISTING 6.1 Continued

procedure TfrmMain.cbAlwaysShowSelectionClick(Sender: TObject);


begin
UpdateOption(dgAlwaysShowSelection, cbAlwaysShowSelection.Checked);
end;

procedure TfrmMain.cbConfirmDeleteClick(Sender: TObject);


begin
UpdateOption(dgConfirmDelete, cbConfirmDelete.Checked);
end;

procedure TfrmMain.cbCancelOnExitClick(Sender: TObject);


begin
UpdateOption(dgCancelOnExit, cbCancelOnExit.Checked);
end;

procedure TfrmMain.cbMultiSelectClick(Sender: TObject);


begin
UpdateOption(dgMultiSelect, cbMultiSelect.Checked);
end;

// Grid event handlers

procedure TfrmMain.gridColExit(Sender: TObject);


begin
lbEvents.Items.Add(‘OnColExit - Col ‘ + IntToStr(grid.SelectedIndex) +
‘, Field ‘ + grid.SelectedField.FieldName + ‘)’);

// By calling Abort here, you can prevent focus from leaving this column
// Abort;
end;

procedure TfrmMain.gridColEnter(Sender: TObject);


begin
lbEvents.Items.Add(‘OnColEnter - Col ‘ + IntToStr(grid.SelectedIndex) +
‘, Field ‘ + grid.SelectedField.FieldName + ‘)’);
end;

procedure TfrmMain.gridEditButtonClick(Sender: TObject);


begin
lbEvents.Items.Add(‘OnEditButtonClick - Col ‘ +
IntToStr(grid.SelectedIndex) + ‘, Field ‘ +
grid.SelectedField.FieldName + ‘)’);
end;
Data-Aware Grids
251

LISTING 6.1 Continued 6


procedure TfrmMain.gridCellClick(Column: TColumn);

DATA-AWARE
begin

GRIDS
lbEvents.Items.Add(‘OnCellClick - Col ‘ + IntToStr(grid.SelectedIndex) +
‘, Field ‘ + grid.SelectedField.FieldName + ‘)’);
end;

procedure TfrmMain.gridColumnMoved(Sender: TObject; FromIndex,


ToIndex: Integer);
begin
lbEvents.Items.Add(‘Column moved from ‘ + IntToStr(FromIndex) +
‘ to ‘ + IntToStr(ToIndex));
end;

// Command buttons

procedure TfrmMain.btnClearEventLogClick(Sender: TObject);


begin
lbEvents.Items.Clear;
end;

procedure TfrmMain.btnShowSelectionsClick(Sender: TObject);


var
Index: Integer;
s: string;
begin
if not (dgMultiSelect in grid.Options) then
raise Exception.Create(‘dgMultiSelect not set’);

if grid.SelectedRows.Count = 0 then
raise Exception.Create(‘No rows selected’);

for Index := 0 to grid.SelectedRows.Count - 1 do begin


ClientDataSet1.Bookmark := grid.SelectedRows[Index];
if s <> ‘’ then
s := s + #13;
s := s + Format(‘%d: %s’, [ClientDataSet1.FieldByName(‘ID’).AsInteger,
ClientDataSet1.FieldByName(‘Name’).AsString]);
end;

ShowMessage(s);
end;

end.
Chapter 6
252

Figure 6.3 shows the Options application at runtime.

FIGURE 6.3
The Options application lets you experiment with the TDBGrid component’s options.

Custom Drawing
As you can see from Figure 6.2, the grid’s default appearance is pleasing to look at, but not
especially eye-catching. Using custom drawing, we can spruce up the look of the grid
considerably.
To implement custom drawing in your grid, you need to handle the grid’s OnDrawColumnCell
event. You might notice that the grid contains a similarly named event, OnDrawDataCell.
OnDrawDataCell is an obsolete event that is included for backward compatibility with early
versions of Delphi. You should not use it in any new programming efforts.
A newly created handler for the OnDrawColumnCell event looks like this:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin

end;

As with all grid events, the Sender parameter references the grid object. Rect refers to the
bounding rectangle of the cell that is about to be drawn. DataCol is a zero-based index into the
absolute position of the column that is about to be drawn. State is a set containing one or
more of the values listed in Table 6.6.
Data-Aware Grids
253

TABLE 6.6 TGridDrawState Values


6
Value Description

DATA-AWARE
gdSelected The cell is selected.

GRIDS
gdFocused The cell has the focus.
gdFixed The cell is fixed (that is, it’s the indicator cell).

The difference between gdSelected and gdFocused can get confusing (especially because
these values change meaning slightly as the Options property changes), so I’ll clarify it here.
When dgRowSelect is not set (the default), only the current cell has the gdSelected value set.
If the grid currently has focus, the current cell has gdFocused set in addition to having the
gdSelected value set.

When dgRowSelect is set, all cells in the current row have the gdSelected value set. In
addition, if the grid has focus, the first cell in the row (excluding the indicator) has gdFocused
set. You probably want to ignore the gdFocused value when using dgRowSelect, as it has no
useful meaning.

The DefaultDrawing Property


The grid’s DefaultDrawing property determines how drawing is performed in the grid. When
this property is True (the default), VCL/CLX draws each cell in the grid as usual, and then
passes control to the OnDrawColumnCell handler that you set up. OnDrawColumnCell is called
for every cell in the grid, so you want to make sure that whatever code you write in that event
handler executes quickly.
When DefaultDrawing is False, Delphi paints the cell with the appropriate background color,
and sets the grid’s Brush and Font properties in readiness to draw the cell. Then, it calls
OnDrawColumnCell so that you can draw the contents of the cell yourself.

In practice, you will often find that when you implement custom drawing, Delphi’s default
drawing code does about 90% of what you need. You might simply want to change the color of
selected cells, draw an image in a given column, or perhaps draw negative values in red.
It might seem that the DefaultDrawing property goes to extremes. On the one hand, if it is
True, the cell is drawn using its default settings, and then you turn around and draw over the
top of it. On the other hand, if it is False, you need to draw every single cell manually—even
those that you don’t need any special drawing for.
Fortunately, this isn’t the case. The solution is to set DefaultDrawing to False, and then inside
the OnCustomDrawColumn event handler, call the grid’s DefaultDrawColumnCell, like this:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
Chapter 6
254

begin
if Column.FieldName = ‘Salary’ then begin
if Column.Field.AsFloat > 50000.0 then begin
DBGrid1.Canvas.Brush.Color := clYellow;

if gdFocused in State then


DBGrid1.Canvas.Font.Color := clRed;
end;
end;

DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State);


end;

This code snippet only changes the way the Salary column is drawn. If the salary is greater
than $50,000, the background of the cell is drawn in yellow. If the cell is focused, the salary is
drawn in red.
For all other cells, and for salaries that are less than or equal to $50,000, the cell is drawn
normally. The call to DefaultDrawColumnCell takes care of drawing the cell after the appropriate
changes (if any) are made to the brush and font colors.
The way that Delphi’s internal VCL/CLX painting code works, all you need to do in this
handler is to set the canvas’ Brush and Font properties so that they reflect the color and font
that you want to use when painting the cell. The call to DefaultDrawColumnCell then uses the
settings that you specified when drawing the cell contents.

NOTE
Notice, in the preceding code snippet, that I checked the FieldName property of the
column to see if the code is drawing the Salary column. You might be tempted to
use the DataCol parameter to check for this. However, DataCol is the zero-based
absolute index of the cell being drawn. If the user reorders the columns at runtime,
this value changes.

The following sample program demonstrates several ways to custom draw grid cells. Listing
6.2 contains the complete source code for the CustomDraw application.

LISTING 6.2 CustomDraw—MainForm.pas


unit MainForm;

interface
Data-Aware Grids
255

LISTING 6.2 Continued 6


uses

DATA-AWARE
SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, DB,

GRIDS
DBClient, QGrids, QDBGrids, QExtCtrls, DateUtils;

type
TfrmMain = class(TForm)
pnlClient: TPanel;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
ClientDataSet1: TClientDataSet;
Image1: TImage;
procedure FormCreate(Sender: TObject);
procedure DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
private
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);
end;

procedure TfrmMain.DBGrid1DrawColumnCell(Sender: TObject;


const Rect: TRect; DataCol: Integer; Column: TColumn;
State: TGridDrawState);
var
RetirementBirthdate: TDateTime;
X: Integer;
begin
if Odd(ClientDataSet1.RecNo) then
DBGrid1.Canvas.Brush.Color := clAqua
else
DBGrid1.Canvas.Brush.Color := clWhite;
Chapter 6
256

LISTING 6.2 Continued

if gdSelected in State then begin


DBGrid1.Canvas.Font.Color := clGreen;
DBGrid1.Canvas.Font.Style := [fsBold];
end;

if Column.ID = 0 then begin


DBGrid1.Canvas.FillRect(Rect);

RetirementBirthdate := IncYear(Date, -50);


if ClientDataSet1.FieldByName(‘Birthday’).AsDateTime <=
RetirementBirthdate then begin
// Eligible for retirement
X := (Column.Width - Image1.Picture.Width) div 2 + Rect.Left;
DBGrid1.Canvas.Draw(X, Rect.Top, Image1.Picture.Graphic);
end;
end else
DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State);
end;

end.

Figure 6.1 showed the CustomDraw application at runtime.


Several things happen in the DBGrid1DrawColumnCell method in Listing 6.2. First, odd rows
are drawn using a background color of clAqua, and even rows are drawn using a background
color of clWhite. This gives a checkbook-style look to the grid.
Second, the current row (determined by the fact that the State parameter includes the
gdSelected option) is drawn in a bold, green font. The State parameter includes the
gdSelected option because dgRowSelect is specified in the grid’s options.

Finally, the code checks the birthday of the employee. If the employee is 50 years old (or
older), a watch icon is drawn in the first column. The first column has an ID of 0. Notice that
the code doesn’t check the DataCol parameter to see if it’s 0 because the user could rearrange
the columns at runtime. Instead, it checks for the column ID, which is a zero-based integer that
was established at design time and doesn’t change.

NOTE
If the dgIndicator option were turned on, the column ID would be 1 instead of 0
because the indicator column would have an ID of 0.
Data-Aware Grids
257

Solutions to Common Grid Questions 6


In this section, I’ll attempt to answer a number of commonly asked questions about the

DATA-AWARE
TDBGrid component. The solutions to these problems are not overly difficult, but in most cases

GRIDS
they involve more than simply calling a method or setting a property.

Determining the Current Row or Column


Sometimes you might need to determine what row, column, or cell is currently focused.
Depending on the information that you need, there are several ways to go about this.
If all you need to know is what row has the focus, the easiest thing to do is to check the
underlying dataset. The dataset’s current record is the one that has focus. So, if you want to
get the value of the current Salary column, you can do the following:
var
CurrentSalary: Double;
begin
CurrentSalary := DBGrid1.DataSource.DataSet.FieldByName(‘Salary’).AsFloat;
...
end;

To get the index of the focused column, you can access the grid’s SelectedIndex property.
SelectedIndex is a zero-based number indicating the absolute position of the selected column.
SelectedIndex adjusts for the indicator, so if the indicator is present, the first data column is
index 1. If the indicator is not displayed, the first data column is index 0.
To retrieve the field object for the current column, you can reference the grid’s SelectedField
property. SelectedField returns the underlying dataset’s TField object, so you can directly
access it to retrieve the value of the current cell.
ShowMessage(‘The current cell value is ‘ + DBGrid1.SelectedField.AsString);

NOTE
If the current column is not connected to a dataset field, SelectedIndex returns –1
and SelectedField returns nil. If you display any columns in your grid that are not
tied to a field, you should always check for –1 or nil before attempting to do some-
thing with the SelectedIndex or SelectedField properties.

Getting the Cell at a Given Mouse Coordinate


With a little effort, you can determine the row and column of the cell at any mouse position.
Normally, you don’t need to do this because you can use the dataset’s current record and the
grid’s SelectedIndex or SelectedField properties to determine the current cell. However,
you might want to know about a cell that isn’t current.
Chapter 6
258

For example, say that you want to write an event handler that tracks the current position of the
mouse and provides information about the field at that location. A more ambitious project
might be to write code that displays a tooltip when the mouse hovers over a cell whose contents
are too long to be fully displayed in the cell.
In either case, you can call the grid’s MouseCoord method passing in the X and Y coordinates
of the mouse relative to the grid control. MouseCoord passes back a TGridCoord structure,
which contains X and Y fields representing the absolute column and row indexes of the cell at
that mouse position.
TGridCoord’s X and Y values deserve a little explanation. These are absolute indexes, meaning
that they take the indicator column and title rows into account. If the indicator is displayed, the
X position of the first data column is 1. If the indicator is not displayed, it is 0. This is also true
for the rows: If the grid titles are not displayed (dgTitles is not set in the grid’s Options
property), the Y position of the first row of data is 0. If titles are displayed, it is 1.
If the mouse position is not over a cell (for instance, the mouse is over the background area of
the grid), the returned TGridCoord’s X and Y values are both –1.
The following code snippet shows how you might update a label to show the X and Y
positions, as well as the field name, of the cell at the current mouse position.
procedure TForm1.DBGrid1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var
GC: TGridCoord;
IndicatorOffset: Integer;
TitleOffset: Integer;
begin
GC := DBGrid1.MouseCoord(X, Y);
if GC.X = -1 then
Label1.Caption := IntToStr(GC.X) + ‘, ‘ + IntToStr(GC.Y)
else begin
if dgIndicator in DBGrid1.Options then
IndicatorOffset := 1
else
IndicatorOffset := 0;

if dgTitles in DBGrid1.Options then


TitleOffset := 1
else
TitleOffset := 0;

if GC.X < IndicatorOffset then


Label1.Caption := IntToStr(GC.X) + ‘, ‘ + IntToStr(GC.Y) +
Data-Aware Grids
259

‘ - Indicator’
else
6
Label1.Caption := IntToStr(GC.X) + ‘, ‘ + IntToStr(GC.Y) + ‘ - ‘ +

DATA-AWARE
DBGrid1.Columns[GC.X - IndicatorOffset].FieldName;

GRIDS
// To move the dataset to the corresponding record, clone the dataset,
// and set the clone’s RecNo property to GC.Y - TitleOffset.
end;
end;

At the end of that code snippet is a comment explaining how to retrieve the data for the
appropriate cell. Here is the code required to do that (assuming that the grid is connected to
a client dataset):
var
CloneDS: TClientDataSet;
FieldValue: string;
begin
...

CloneDS := TClientDataSet.Create(nil);
try
CloneDS.CloneCursor(DBGrid1.DataSource.DataSet);
CloneDS.RecNo := GC.Y - TitleOffset;

// Now access the fields of the cloned dataset


FieldValue := CloneDS.FieldByName(DBGrid1.Columns[GC.X –
IndicatorOffset].FieldName).AsString;
finally
CloneDS.Free;
end;
end;

Setting Edit Mode Manually


If dgAlwaysShowEditor is True, the grid automatically enters edit mode when the user enters a
cell. If dgAlwaysShowEditor is False, the grid enters edit mode when the user presses F2.
(The user can simply start typing a value to overwrite a cell’s contents.) However, what if (for
compatibility with another software package) you want to enter edit mode when the user
presses a different key—perhaps F9 instead of F2?
To achieve this, you need to take control over when the grid enters edit mode. The best place
to do this is in the grid’s OnKeyDown event. Within that handler, set the grid’s EditorMode
property to True to enter edit mode.
The following code snippet shows how to enter edit mode when the user presses F9 (not when
he or she presses F2).
Chapter 6
260

procedure TForm1.DBGrid1KeyDown(Sender: TObject; var Key: Word;


Shift: TShiftState);
begin
if Shift = [] then begin
case Key of
VK_F2: Key := 0;
VK_F9: DBGrid1.EditorMode := True;
end;
end;
end;

NOTE
For this code to work, you must make sure the dgEditing option is set. If it isn’t, the
grid doesn’t enter edit mode even when the code sets EditorMode to True.

Detecting When a Column Is Resized


TDBGrid provides an OnColumnMoved event, but no OnColumnSized event. This means that you
can easily tell when a column is moved, but it’s more difficult to determine when a column is
resized. Fortunately, we can achieve this functionality by writing a grid descendent with a single
overridden method.
Listing 6.3 contains the source code for the TETHDBGrid component.

LISTING 6.3 ETHDBGrid.pas


unit ETHDBGrid;

interface

uses
Windows, Messages, SysUtils, Classes, Controls, Grids, DBGrids;

type
TETHDBGrid = class(TDBGrid)
private
{ Private declarations }
FOnColumnSized: TNotifyEvent;
protected
{ Protected declarations }
procedure ColWidthsChanged; override;
public
{ Public declarations }
published
{ Published declarations }
Data-Aware Grids
261

LISTING 6.3 Continued 6


property OnColumnSized: TNotifyEvent

DATA-AWARE
read FOnColumnSized write FOnColumnSized;

GRIDS
end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents(‘ETH’, [TETHDBGrid]);
end;

{ TETHDBGrid }

procedure TETHDBGrid.ColWidthsChanged;
begin
inherited ColWidthsChanged;

if Assigned(FOnColumnSized) then
FOnColumnSized(Self);
end;

end.

The overridden method, ColWidthsChanged, calls the inherited version, and then fires the
OnColumnSized event (if you provide an event handler for it in your application).

This code does nothing more than notify you that a column was resized. It doesn’t tell you
which column was resized, what the old width was, or what the new width is. Providing that
additional information would require a lot more work and duplicate a good deal of code that is
in TDBGrid. I prefer to keep my descendent components simple and direct.

NOTE
TClientDataSetGrid (discussed later in this chapter) does not provide this event han-
dler, either. So, if you intend to use TClientDataSetGrid instead of TDBGrid in your
applications, you might want to change the code shown here so that it derives from
TClientDataSetGrid.
Chapter 6
262

Persisting Grid Settings


Users expect customizations to be persistent across program invocations, and grid settings are
no exception. When your users resize or reorder the columns in a grid, they usually expect the
column order and size to be the same the next time they run the application.
It isn’t difficult to persist grid column settings. There are two approaches that you can take:
• Save the settings to a stream or to a separate file.
• Save the settings to the Windows registry or to an ini file.
The first option is simpler, so I’ll cover it first. TDBGrid’s Columns property provides
SaveToFile and SaveToStream methods that you can use to save column configurations, to
either a file or a stream, with a minimum of fuss. The following code snippet shows how you
can save column settings to a file:
DBGrid1.Columns.SaveToFile(‘GRID.CFG’);

Similarly, to reload the settings:


DBGrid1.Columns.LoadFromFile(‘GRID.CFG’);

The drawback to this approach is that if you have many different grids in your application, you
need separate configuration files to persist each one. If that is the case, you might want to look
at SaveToStream instead of SaveToFile. With a little effort, you can use SaveToStream to save
a grid’s column configuration to an ini file or to the Windows registry. The following procedure
saves column information to an ini file:
procedure SaveColumnConfiguration(const FileName: string; Grid: TDBGrid;
const SectionName: string; const Name: string);
var
ini: TIniFile;
MemStream: TMemoryStream;
begin
MemStream := TMemoryStream.Create;
try
Grid.Columns.SaveToStream(MemStream);
MemStream.Seek(0, soFromBeginning);

ini := TIniFile.Create(FileName);
try
ini.WriteBinaryStream(SectionName, Name, MemStream);
finally
ini.Free;
end;
finally
MemStream.Free;
end;
end;
Data-Aware Grids
263

This code first creates a memory stream and saves the column configuration to that stream. 6
Next, it writes the stream out to the ini file. With minor modifications, you could change this
code to use the Windows registry instead of an ini file.

DATA-AWARE
GRIDS
Similarly, the following procedure loads the column information back from the ini file:
procedure LoadColumnConfiguration(const FileName: string; Grid: TDBGrid;
const SectionName: string; const Name: string);
var
ini: TIniFile;
MemStream: TMemoryStream;
begin
MemStream := TMemoryStream.Create;
try
ini := TIniFile.Create(FileName);
try
ini.ReadBinaryStream(SectionName, Name, MemStream);
if MemStream.Size > 0 then
Grid.Columns.LoadFromStream(MemStream);
finally
ini.Free;
end;
finally
MemStream.Free;
end;
end;

Limitations
For all its power, TDBGrid does have some limitations. The most notable one is that it doesn’t
display memos or images. You can draw images or memos manually using the custom drawing
features of the grid, but because each grid row is the same height, this can lead to difficulties
when one memo is three lines long and another is thirty lines long.
This limitation (as well as others) is removed by many of the third-party grids available. At the
end of this chapter, there is a quick overview of some of the third-party grids that you might
want to look into.

TClientDataSetGrid
TClientDataSetGrid is a TDBGrid descendent written by John Kaster. It takes advantage of
some of the functionality of client datasets to provide automatic sorting of the grid when the
user clicks a column title.
In addition, TClientDataSetGrid can automatically persist column information to and from a
separate configuration file. It doesn’t support saving and loading column information to and
Chapter 6
264

from an ini file, or to and from the Windows registry, so you might still want to make use of
the SaveColumnConfiguration and LoadColumnConfiguration procedures provided in the
preceding section. (TClientDataSetGrid is available as ID 15099 on Code Central at
http://codecentral.borland.com.)

Automatic Sorting
As indicated previously, TClientDataSetGrid enables the user to sort the grid in ascending or
descending order, on a single column or on multiple columns. It can even sort one column in
ascending order and another column in descending order.
To enable this functionality, you must set the component’s TitleSort property to True. (It is
False by default, allowing the component to be used with nonclient datasets. If you set
TitleSort to True, TClientDataSetGrid does not work with datasets that do not derive from
TCustomClientDataSet.)

To indicate the current sort order, TClientDataSetGrid draws three-dimensional arrows in the
title of the sorted column(s). The colors used to draw these arrows are set through the
ArrowColor, ArrowHighlight, and ArrowShade properties.

Figure 6.4 shows how the grid looks when sorted by Name, and then by Birthday.

FIGURE 6.4
TClientDataSetGrid provides visual feedback about the current sort order.

To sort, click one of the column titles. An up arrow will be drawn in the title cell of that column.
To switch to descending order, click the column title again.
If you want to sort on more than one column, press Shift and click the next column title to sort
on. Press Shift and click the column title a second time to sort in descending order on that
column only. Pressing Shift and clicking the column a third time removes it from the current
sort order. Repeat this for every column you want to sort on.
Data-Aware Grids
265

This is actually easier done than said. If the previous explanation sounds complicated, you 6
might want to play with clicking, and pressing shift and clicking, column titles to see the effect
for yourself.

DATA-AWARE
GRIDS
Column Customization
In addition to automatic sorting capabilities, TClientDataSetGrid provides a separate dialog
that can be used to set the visible columns for the grid (as shown in Figure 6.5). To display this
dialog, call the grid’s ConfigureColumns method, like this:
ClientDataSetGrid1.ConfigureColumns;

FIGURE 6.5
TClientDataSetGrid enables the user to hide columns that he doesn’t want to see.

Using this dialog, the user can hide or show individual columns.
If you want the grid to automatically save and restore its column configuration (including
column order and the visibility state of individual columns), set the ConfigFile property to
the name of the file that you want to use for persisting the column information. Make sure to
use a different filename for each grid, as the current version of this component doesn’t support
saving multiple configurations in a single file.
Chapter 6
266

NOTE
Because the code for TClientDataSetGrid is freely available, I hope to see some
enterprising Delphi programmers providing enhancements to it in the future. My per-
sonal wish list includes
• Saving and loading column configuration to and from an ini file and the
Windows registry.
• Enhancing the Configure Columns dialog to support column reordering in addi-
tion to hiding or showing columns.
• Enhancing the Configure Columns dialog to support locking individual columns
so that they cannot be hidden or moved.
• Adding support for an OnColumnSized event.

TDBCtrlGrid
I’m not going to spend a lot of time on TDBCtrlGrid because it isn’t CLX-compatible, and
because there isn’t any new development going on in terms of TDBCtrlGrid.
TDBCtrlGrid is a grid-like component, although it relies on other data-aware components to
perform the actual data input and output. To use a TDBCtrlGrid in your application, drop it on
a form and connect the DataSource property to your data source. Then, populate the grid with
other data-aware components, such as TDBEdit, TDBCheckBox, and so on.
TDBCtrlGrid replicates these components at runtime, displaying each component for every
record displayed in the grid. Every cell in the grid corresponds to a single record in the dataset.
Most data-aware controls are replicable (that is, they can be used in a TDBCtrlGrid). Some are
not (including the TETHDBDateTimePicker component that I created in the preceding chapter).
In order for the control to be replicable, it must include the csReplicatable option in its
ControlStyle property (typically set in the component’s constructor). The following is a snippet
from TDBEdit’s constructor:
constructor TDBEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
inherited ReadOnly := True;
ControlStyle := ControlStyle + [csReplicatable];
...
end;

The following sections discuss the properties and events that TDBCtrlGrid introduces.
Data-Aware Grids
267

Properties 6
TDBCtrlGrid introduces a handful of properties that you can use to customize its look and feel.

DATA-AWARE
Table 6.7 lists these properties.

GRIDS
TABLE 6.7 TDBCtrlGrid Properties

Property Description
AllowInsert When True, the user can scroll past the last record in the grid to
insert a new record.
AllowDelete When True, the user can delete the current record by pressing
Ctrl+Delete.
ColCount Determines the number of columns displayed in the grid.
Orientation Determines the direction in which the grid scrolls to display more data.
PanelBorder Possible values are gbNone and gbRaised. gbRaised causes the grid
to have a raised look. You can achieve other looks, such as lowered
or bump, by setting PanelBorder to gbNone and dropping the
TDBCtrlGrid on a panel with the desired bevel.
PanelHeight Refers to the height, in pixels, of a single panel (cell).
PanelWidth Refers to the width, in pixels, of a single panel (cell).
RowCount Determines the number of rows displayed at a time in the grid.
SelectedColor Determines the background color of the current cell.
ShowFocus When True, a focus rectangle is drawn around the current cell.

The ColCount, PanelWidth, and Width properties are directly related. ColCount×PanelWidth is
approximately equal to Width (allowing for the grid border and vertical scrollbar). Setting
ColCount automatically adjusts the Width, as long as the Align setting does not prevent it.
(Setting Align to alClient, for example, does not allow the grid to resize. In this case, setting
ColCount automatically adjusts PanelWidth.)

Similarly, the RowCount, PanelHeight, and Height properties are related. Setting one property
affects the others.

Events
TDBCtrlGrid only contains one new event, named OnPaintPanel. OnPaintPanel fires just
before each panel is about to be drawn. OnPaintPanel looks like this:
procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;
Index: Integer);
begin

end;
Chapter 6
268

Index refers to the zero-based index of the panel about to be drawn, and is a number between
zero and RowCount—one, inclusive.
You might notice that there is no Rect parameter passed to this function, so at first glance it
isn’t obvious how to determine the bounding rectangle of the current cell. Upon entry to this
method, the grid canvas’ origin is set to the upper-left corner of the current panel. In other
words, point (0, 0) on the canvas refers to the upper-left corner of the panel. Point
(PanelWidth, PanelHeight) references the lower-right corner. This enables you to use the
canvas for such things as drawing a background image on the panel (as the following code
snippet taken from Listing 6.4 shows).
procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;
Index: Integer);
begin
if Index <> DBCtrlGrid.PanelIndex then
DBCtrlGrid1.Canvas.Draw(0, 0, Image1.Picture.Graphic);
end;

This code checks the index passed into the method to see if we’re drawing the current panel.
(The public property PanelIndex contains the number of the current panel.) All noncurrent
panels are drawn with a background graphic.
Listing 6.4 contains the complete source code for the CtrlGrid demo application, which
enables you to play with some of TDBCtrlGrid’s properties.

LISTING 6.4 CtrlGrid—MainForm.pas


unit MainForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, dbcgrids, ExtCtrls, DB, DBClient, StdCtrls, Mask, DBCtrls,
ComCtrls;

type
TForm1 = class(TForm)
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
pnlClient: TPanel;
DBCtrlGrid1: TDBCtrlGrid;
ecID: TDBEdit;
ecName: TDBEdit;
ecSalary: TDBEdit;
Label1: TLabel;
Label2: TLabel;
Data-Aware Grids
269

LISTING 6.4 Continued 6


Label3: TLabel;

DATA-AWARE
Label4: TLabel;

GRIDS
ecBirthday: TDBEdit;
pnlBottom: TPanel;
cbAllowInsert: TCheckBox;
cbAllowDelete: TCheckBox;
ecRowCount: TEdit;
ecColCount: TEdit;
cbShowFocus: TCheckBox;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
cbOrientation: TComboBox;
Image1: TImage;
procedure FormCreate(Sender: TObject);
procedure cbAllowInsertClick(Sender: TObject);
procedure cbAllowDeleteClick(Sender: TObject);
procedure cbShowFocusClick(Sender: TObject);
procedure ecRowCountChange(Sender: TObject);
procedure ecColCountChange(Sender: TObject);
procedure cbOrientationClick(Sender: TObject);
procedure DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;
Index: Integer);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);


begin
ClientDataSet1.LoadFromFile(‘C:\Employee.cds’);
end;

procedure TForm1.cbAllowInsertClick(Sender: TObject);


begin
DBCtrlGrid1.AllowInsert := cbAllowInsert.Checked;
end;
Chapter 6
270

LISTING 6.4 Continued

procedure TForm1.cbAllowDeleteClick(Sender: TObject);


begin
DBCtrlGrid1.AllowDelete := cbAllowDelete.Checked;
end;

procedure TForm1.cbShowFocusClick(Sender: TObject);


begin
DBCtrlGrid1.ShowFocus := cbShowFocus.Checked;
end;

procedure TForm1.ecRowCountChange(Sender: TObject);


begin
try
DBCtrlGrid1.RowCount := StrToInt(ecRowCount.Text);
except
DBCtrlGrid1.RowCount := 1;
end;
end;

procedure TForm1.ecColCountChange(Sender: TObject);


begin
try
DBCtrlGrid1.ColCount := StrToInt(ecColCount.Text);
except
DBCtrlGrid1.ColCount := 1;
end;
end;

procedure TForm1.cbOrientationClick(Sender: TObject);


begin
DBCtrlGrid1.Orientation := TDBCtrlGridOrientation(cbOrientation.ItemIndex);
end;

procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;


Index: Integer);
begin
if Index <> DBCtrlGrid.PanelIndex then
DBCtrlGrid1.Canvas.Draw(0, 0, Image1.Picture.Graphic);
end;

end.

Figure 6.6 shows CtrlGrid at runtime.


Data-Aware Grids
271

DATA-AWARE
GRIDS
FIGURE 6.6
CtrlGrid demonstrates TDBCtrlGrid’s behavior.

Third-Party Data-Aware Grids


Though TDBGrid and TClientDataSetGrid are useful grid components, they don’t come close
to the flexibility and power that’s provided by third-party data-aware grids. This section lists
several of the more popular third-party data-aware grids that you might want to investigate if
you’re looking for more functionality than the built-in Delphi grids offer.
Table 6.8 lists some of the better known third-party TDBGrid replacements.

TABLE 6.8 TDBGrid Replacement Components

Product Description
Orpheus TurboPower Software Company’s flagship Delphi-VCL add-on
library contains two data-aware grid components that can be used
for formatted data entry, multilevel data display, and automatic
subtotaling and totaling of columns. Visit www.turbopower.com
for more information about Orpheus or to download a free trial
version.
InfoPower 2000 This popular general-purpose Delphi library includes the
TwwDBGrid component, which works like TDBGrid and adds new
functionality (such as memo display and automatic footers). Go
to www.woll2woll.com for more information about the
InfoPower product.
ExpressQuantumGrid An extremely powerful TDBGrid replacement that offers numerous
advanced grid features—too many to list here. Some of the
notable features include multiline data display, runtime sorting
Chapter 6
272

TABLE 6.8 Continued


Product Description
and grouping of data, and extensive customization (both at
design time and at runtime). For more information, visit
www.devexpress.com.
TopGrid Provides both data-aware and non—data-aware versions of its
powerful grid component, which allow display and editing of
multiline notes, cells that contain controls (such as combo boxes
and check boxes), and numerous customization features. You can
find TopGrid at www.objectinsight.com/TopGridOverview.htm.
Others Numerous other freeware, shareware, and commercial grids are
available to Delphi and Kylix programmers. The ones listed in
this table are just some of the more popular and more widely
known grids that are commercially available today.

Summary
This chapter continued the discussion of data-aware components with an overview of data-aware
grids, which allow multiple rows of a dataset to be displayed on the screen at the same time.
Specifically, this chapter taught you the following:
• You can create a quick and dirty grid by dropping a TDBGrid component on a form and
using the default values. To customize the columns in the resulting grid, use the Columns
property.
• Various grid options exist for altering the look and feel of the grid. (See Table 6.4 for a
summary.)
• You can use various grid events to gain control over what happens when the user clicks a
cell or performs some other action. (See Table 6.5 for a summary.)
• For the ultimate control over how the grid looks, use the OnDrawColumnCell event.
• TClientDataSetGrid is a free component that you can use for automatic column sorting
and additional column customization.
• TDBCtrlGrid is a VCL-specific component that offers the capability to arrange data for a
single record in a nonlinear format.
In addition, this chapter showed how to deal with several commonly encountered grid issues,
such as detecting when a column is moved and persisting column states.
The following chapter begins a two-chapter exploration of client datasets.
Dataset Providers CHAPTER

7
IN THIS CHAPTER
• What Is a Dataset Provider? 274

• Connecting to a Dataset 275

• Resolving Changes to Data 276

• Provider Options 293

• Provider Events 295

• Changing Field Values on the Server 297

• Intercepting Data 298

• Optional Parameters 300

• Master/Detail Relationships 301

• Providing and Resolving Data from Stored


Procedures and Joins 302

• Connecting to a Local Database 308


Chapter 7
274

Earlier in this book, you learned about dbExpress—a high-performance, low-overhead data-
base technology. You learned that it is read-only and unidirectional, making it cumbersome to
use directly for most interactive database applications.
Later, I introduced you to client datasets. Client datasets are especially well suited to database
application front ends because they are fast, flexible, and powerful. However, they are inher-
ently single user because they are RAM-based. Also, they read from and write to a proprietary
file format.
So far, these may seem like disparate technologies used to solve different types of program-
ming problems. This chapter and the next tie them together, showing you how you can use
client datasets to read and write data using dbExpress as the underlying database technology.
This chapter shows you how to use providers to create multitier database applications.

What Is a Dataset Provider?


A dataset provider forms a conduit between a client dataset and an external data store—typi-
cally another dataset, such as a TSQLDataSet. It provides data to the client dataset on request
and sends data back to the underlying data store when the client changes it—a technique called
resolving.
The component used to accomplish this is TDataSetProvider, found on the Data Access tab of
the component palette.

NOTE
Nothing about dataset providers ties them to dbExpress specifically. The information
presented in this chapter is applicable to other database technologies as well, such as
BDE and dbGo (formerly called ADOExpress).
For a dataset to be compatible with TDataSetProvider, it must support the
IProviderSupport interface, defined in DB.pas like this:
IProviderSupport = interface
procedure PSEndTransaction(Commit: Boolean);
procedure PSExecute;
function PSExecuteStatement(const ASQL: string; AParams: TParams;
ResultSet: Pointer = nil): Integer;
procedure PSGetAttributes(List: TList);
function PSGetDefaultOrder: TIndexDef;
function PSGetKeyFields: string;
function PSGetParams: TParams;
function PSGetQuoteChar: string;
function PSGetTableName: string;
Dataset Providers
275

function PSGetIndexDefs(IndexTypes: TIndexOptions =


[ixPrimary..ixNonMaintained]): TIndexDefs;
function PSGetUpdateException(E: Exception;
Prev: EUpdateError): EUpdateError;
function PSInTransaction: Boolean;
function PSIsSQLBased: Boolean;
function PSIsSQLSupported: Boolean;
procedure PSReset;
procedure PSSetParams(AParams: TParams);
procedure PSSetCommandText(const CommandText: string); 7
procedure PSStartTransaction;
function PSUpdateRecord(UpdateKind: TUpdateKind;

PROVIDERS
Delta: TDataSet): Boolean;

DATASET
end;
TDataSet implements stub functions for these methods, which generally do nothing
or raise an exception. The datasets included with Delphi (BDE, dbExpress, and dbGo)
override these methods to provide a specific implementation.

Connecting to a Dataset
In this section I’ll show you how to connect a client dataset to another dataset. To do so, follow
these steps:
1. Start a new application and drop a TSQLConnection and TSQLDataSet on the main form.
Connect these components to a database, using the techniques discussed in Chapters 1
and 2.
2. Drop a TDataSetProvider on the main form. Set the DataSet property to the
TSQLDataSet component. For now, leave all other properties set to their default values.

3. Drop a TClientDataSet on the main form. Set its ProviderName property to the dataset
provider you created in the previous step.
4. Connect a TDataSource component to the client dataset and hook up a TDBGrid compo-
nent to the data source.
5. Create an event handler for the form’s FormCreate event and add the following line of
code to it:
ClientDataSet1.Open;

6. Run the application.


If you set everything up correctly, you should see the data from the dbExpress database dis-
played in the grid. Notice that you can scroll forward and backward through the data, and you
Chapter 7
276

can make changes to the data. However, if you leave the application and run it again, none of
your changes have been saved to the database. We’ll remedy that situation in the next section.
For now, I just want you to see how easy it is to establish the relationship between a client
dataset and another dataset. This relationship is important because dbExpress datasets don’t
support editing or bidirectional scrolling on their own—they must be connected to a client
dataset to provide these capabilities.
Figure 7.1 shows the results of the preceding steps after connecting to the CONMAN database.

FIGURE 7.1
Sample application at design time and runtime.

You should leave this sample application loaded in Delphi or save it to disk somewhere; we’ll
embellish on it in the following sections.

Resolving Changes to Data


If you’ll recall from Chapter 4, client datasets store changes to data in a change log rather than
applying changes to the underlying data immediately. Because of that, when you changed the
grid’s data in the preceding section, those changes didn’t get reflected in the underlying data-
base.

Applying Updates
To save changes to the database permanently, you need to call the client dataset’s
ApplyUpdates method. ApplyUpdates detects that the dataset is connected to a provider and
takes care of sending changes back through the provider to the database.
Dataset Providers
277

Add a button to your sample application and create an OnClick handler for it. In the OnClick
handler, add the following code:
if ClientDataSet1.ApplyUpdates(0) > 0 then
ShowMessage(‘Failed to update database’);

Now run the application again, modify some data, and click the button. If you quit the applica-
tion and rerun it, you’ll see that the changes were indeed saved to the database.
The call to ApplyUpdates takes a single parameter, which indicates the “tolerance level” for
errors. In this case, I’ve specified a zero-error tolerance level. What this means is that if any
errors occur during the update process, the changes are rolled back and none of the updates are 7
committed to the underlying database.

PROVIDERS
DATASET
NOTE
When resolving data to a database, VCL/CLX automatically wraps the updates in a
transaction, so either all the changes are made or none of them are. You don’t need
to write any code to deal with transactions in this case.

At times, you might be willing to tolerate one or more errors when resolving data. For
instance, if the user changes three rows in the grid, but only two of the changes can be saved
successfully, you might still want those two changes saved. If this is the case, pass the maxi-
mum number of errors that you will allow to ApplyUpdates. If you don’t care how many errors
occur, call ApplyUpdates with a parameter of –1.
After the call to ApplyUpdates, any successful updates are removed from the client dataset’s
change log. If any rows could not be updated, they are left in the change log.
You may be wondering why the provider might not be able to save changes to the underlying
database. The most common reason is that another user changed the same row while you were
viewing it or deleted the row before you had a chance to save your changes. Other reasons
may include a broken connection to the database server.

NOTE
TDataSetProvider and TClientDataSet give you control over how to detect and
respond to data clashes. Later in this chapter, I’ll cover some of the various techniques
you can use.
Chapter 7
278

Resolving to a Dataset
TDataSetProvider publishes a property named ResolveToDataSet. By default it is False,
indicating that the provider resolves data directly to the database server associated with the
provider’s DataSet. This is generally the most efficient way to resolve data.
In some cases, you must set ResolveToDataSet to True. The most common reasons are listed
next:
• The provider’s DataSet is not connected to a database—for example, it is a
TClientDataSet.

• The provider’s DataSet does not provide the necessary implementation of the
IProviderSupport interface.

In these cases, updates will be applied to the dataset referenced by the provider’s DataSet
property. You can then handle the provider’s AfterApplyUpdates method to make those
changes persistent (in the case of a TClientDataSet, you could call the dataset’s SaveToFile
method, for example). AfterApplyUpdates, as well as other provider events, are discussed in
the section titled “Provider Events,” later in this chapter.

Reconciliation Errors
By default, if one or more errors occur during reconciliation, ApplyUpdates returns a number
greater than zero, indicating the number of errors that occurred. This is fine if all you want to
know is whether there were errors. However, it doesn’t give you any control over how to han-
dle reconciliation errors.
For greatest control over reconciliation errors, you should provide an event handler for the
client dataset’s OnReconcileError event. An empty handler looks like the following:
procedure TfrmMain.ClientDataSet1ReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
Begin

end;

DataSet refers to the client dataset for which the reconciliation error occurred. E is an excep-
tion that gives more information about the error. UpdateKind is one of the values listed in
Table 7.1. You should set Action to one of the values listed in Table 7.2, which instructs the
VCL as to what action to take for the offending record.
The OnReconcileError event is fired for each offending record. If eight updates are resolved
back to the provider and two of them conflict with prior changes made by another user,
OnReconcileError is fired twice. Depending on the action taken for each record, the number
returned from ApplyUpdates may be 0, 1, or 2. This is explained in more detail in Table 7.2.
Dataset Providers
279

The value returned from ApplyUpdates is also dependent on the parameter passed to
ApplyUpdates. The return value will never be more than one greater than the value specified
by the parameter. For instance, if you pass 0 to ApplyUpdates (which is typically what’s done),
the return value will be either 0 or 1.

TABLE 7.1 TUpdateKind Values


Value Description
ukInsert The record is a newly inserted record.
ukModify Modifications were made to an existing record. 7
ukDelete The record refers to a deleted record.

PROVIDERS
DATASET
TABLE 7.2 TReconcileAction Values

Value Description
raSkip Don’t apply updates to this record. Leave the unapplied changes in the
client dataset’s change log. This record will be counted in the return
value from ApplyUpdates.
raAbort Abort the whole operation. No updates are made to the underlying
database at all, and all changes are left in the client dataset’s change
log. All records are counted in the return value from ApplyUpdates.
raMerge Merge the record with the record in the underlying database. This
works only if the fields that are changed in the record don’t conflict
with fields that were changed by someone else. This record will not be
counted in the return value from ApplyUpdates. You must set the
pfInKey flag (discussed later in the section titled “Update Modes”) for
all fields in the primary key for this option to be available.
raCorrect Indicates that changes were made to the current record inside the
OnReconcileError event handler. VCL/CLX should try to update again
with the new field values.
raCancel Cancel changes to this record, reverting to the original record data.
This record will not be counted in the return value from ApplyUpdates.
raRefresh Cancel changes to this record and reread the record data from the data-
base. This record will not be counted in the return value from
ApplyUpdates. You must set the pfInKey flag for all fields in the pri-
mary key for this option to be available.
Chapter 7
280

Fortunately, in most situations you don’t have to worry about writing a complicated event han-
dler for OnReconcileError. Delphi comes with a prewritten class that you can use to handle
reconcile errors. To use this class, perform the following steps:
1. From the Delphi main menu, Select File, New, Other.
2. On the Dialogs tab of the New Items dialog, select the Reconcile Error Dialog icon.
Make sure Copy is selected in the option buttons below the list of icons (see Figure 7.2).
3. Click OK.
4. Save the resulting unit as something like ReconcileErrorForm.pas. (the name doesn’t
matter).
5. Add the new unit to the uses clause of the form that contains the client dataset (in this
example, it’s the main form).
6. Add the following code to the OnReconcileError event for the client dataset:
Action := HandleReconcileError(DataSet, UpdateKind, E);

I won’t go into detail about the inner working of HandleReconcileError here. You should take
a look at the unit’s source code to gain an understanding of how it works.

FIGURE 7.2
Inserting a TReconcileErrorForm into your application.

Listing 7.1 contains the source code for the main form of the Updates sample application,
which illustrates the concepts discussed so far in this chapter.
Dataset Providers
281

LISTING 7.1 Updates—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms, QStdCtrls,
QDialogs, QExtCtrls, DBXpress, FMTBcd, QGrids, QDBGrids, DB, Provider,
DBClient, SqlExpr, QDBCtrls, QTypes;

type 7
TfrmMain = class(TForm)
pnlClient: TPanel;

PROVIDERS
DATASET
pnlBottom: TPanel;
SQLConnection1: TSQLConnection;
SQLDataSet1: TSQLDataSet;
ClientDataSet1: TClientDataSet;
DataSetProvider1: TDataSetProvider;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
btnApplyUpdates: TButton;
btnCancelUpdates: TButton;
lblUpdates: TLabel;
Timer1: TTimer;
DBNavigator1: TDBNavigator;
SQLDataSet1CONTACTID: TIntegerField;
SQLDataSet1FIRST: TStringField;
SQLDataSet1LAST: TStringField;
SQLDataSet1DEAR: TStringField;
SQLDataSet1TITLE: TStringField;
SQLDataSet1COMPANYNAME: TStringField;
SQLDataSet1ADDRESS1: TStringField;
SQLDataSet1ADDRESS2: TStringField;
SQLDataSet1CITY: TStringField;
SQLDataSet1STATE: TStringField;
SQLDataSet1POSTALCODE: TStringField;
SQLDataSet1COUNTRY: TStringField;
SQLDataSet1PHONE: TStringField;
SQLDataSet1FAX: TStringField;
SQLDataSet1CELLULAR: TStringField;
SQLDataSet1PAGER: TStringField;
SQLDataSet1EMAIL: TStringField;
SQLDataSet1IMAGE: TBlobField;
SQLDataSet1NOTES: TMemoField;
sqlID: TSQLDataSet;
lbEvents: TListBox;
Chapter 7
282

LISTING 7.1 Continued


Label1: TLabel;
btnClearEventLog: TButton;
procedure FormCreate(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure btnApplyUpdatesClick(Sender: TObject);
procedure btnCancelUpdatesClick(Sender: TObject);
procedure ClientDataSet1ReconcileError(DataSet: TCustomClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
procedure DataSetProvider1BeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
procedure DataSetProvider1AfterApplyUpdates(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1AfterExecute(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1AfterGetParams(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1AfterGetRecords(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1AfterRowRequest(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1AfterUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind);
procedure DataSetProvider1BeforeApplyUpdates(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1BeforeExecute(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1BeforeGetParams(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1BeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
procedure DataSetProvider1BeforeRowRequest(Sender: TObject;
var OwnerData: OleVariant);
function DataSetProvider1DataRequest(Sender: TObject;
Input: OleVariant): OleVariant;
procedure DataSetProvider1GetData(Sender: TObject;
DataSet: TCustomClientDataSet);
procedure DataSetProvider1GetDataSetProperties(Sender: TObject;
DataSet: TDataSet; out Properties: OleVariant);
procedure DataSetProvider1GetTableName(Sender: TObject;
DataSet: TDataSet; var TableName: String);
procedure DataSetProvider1UpdateData(Sender: TObject;
DataSet: TCustomClientDataSet);
Dataset Providers
283

LISTING 7.1 Continued


procedure DataSetProvider1UpdateError(Sender: TObject;
DataSet: TCustomClientDataSet; E: EUpdateError;
UpdateKind: TUpdateKind; var Response: TResolverResponse);
procedure ClientDataSet1AfterApplyUpdates(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1AfterExecute(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1AfterGetParams(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1AfterGetRecords(Sender: TObject; 7
var OwnerData: OleVariant);
procedure ClientDataSet1AfterRefresh(DataSet: TDataSet);

PROVIDERS
DATASET
procedure ClientDataSet1AfterRowRequest(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1BeforeApplyUpdates(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1BeforeExecute(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1BeforeGetParams(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1BeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
procedure ClientDataSet1BeforeRefresh(DataSet: TDataSet);
procedure ClientDataSet1BeforeRowRequest(Sender: TObject;
var OwnerData: OleVariant);
procedure btnClearEventLogClick(Sender: TObject);
private
function GetNextID: Integer;
procedure Log(const s: string);
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses RecErrorForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


Chapter 7
284

LISTING 7.1 Continued


begin
ClientDataSet1.Open;
end;

procedure TfrmMain.Timer1Timer(Sender: TObject);


begin
lblUpdates.Caption := IntToStr(ClientDataSet1.ChangeCount) + ‘ Update(s)’;
end;

procedure TfrmMain.btnApplyUpdatesClick(Sender: TObject);


begin
ShowMessage(‘ApplyUpdates returned a value of ‘ +
IntToStr(ClientDataSet1.ApplyUpdates(0)));
end;

procedure TfrmMain.btnCancelUpdatesClick(Sender: TObject);


begin
ClientDataSet1.CancelUpdates;
end;

procedure TfrmMain.ClientDataSet1ReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E);
end;

function TfrmMain.GetNextID: Integer;


begin
sqlID.ExecSQL;
Result := sqlID.ParamByName(‘AValue’).AsInteger;
end;

procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
begin
Log(‘TDataSetProvider.BeforeUpdateRecord’);

if UpdateKind = ukInsert then


if DeltaDS.FieldByName(‘ID’).OldValue <= 0 then
DeltaDS.FieldByName(‘ID’).NewValue := GetNextID;
end;
Dataset Providers
285

LISTING 7.1 Continued


procedure TfrmMain.Log(const s: string);
begin
lbEvents.Items.Add(s);
end;

procedure TfrmMain.DataSetProvider1AfterApplyUpdates(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.AfterApplyUpdates’);
end; 7
procedure TfrmMain.DataSetProvider1AfterExecute(Sender: TObject;

PROVIDERS
DATASET
var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.AfterExecute’);
end;

procedure TfrmMain.DataSetProvider1AfterGetParams(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.AfterGetParams’);
end;

procedure TfrmMain.DataSetProvider1AfterGetRecords(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.AfterGetRecords’);
end;

procedure TfrmMain.DataSetProvider1AfterRowRequest(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.AfterRowRequest’);
end;

procedure TfrmMain.DataSetProvider1AfterUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind);
begin
Log(‘TDataSetProvider.AfterUpdateRecord’);
end;

procedure TfrmMain.DataSetProvider1BeforeApplyUpdates(Sender: TObject;


var OwnerData: OleVariant);
Chapter 7
286

LISTING 7.1 Continued


begin
Log(‘TDataSetProvider.BeforeApplyUpdates’);
end;

procedure TfrmMain.DataSetProvider1BeforeExecute(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.BeforeExecute’);
end;

procedure TfrmMain.DataSetProvider1BeforeGetParams(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.BeforeGetParams’);
end;

procedure TfrmMain.DataSetProvider1BeforeGetRecords(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.BeforeGetRecords’);
end;

procedure TfrmMain.DataSetProvider1BeforeRowRequest(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TDataSetProvider.BeforeRowRequest’);
end;

function TfrmMain.DataSetProvider1DataRequest(Sender: TObject;


Input: OleVariant): OleVariant;
begin
Log(‘TDataSetProvider.OnDataRequest’);
end;

procedure TfrmMain.DataSetProvider1GetData(Sender: TObject;


DataSet: TCustomClientDataSet);
begin
Log(‘TDataSetProvider.OnGetData’);
end;

procedure TfrmMain.DataSetProvider1GetDataSetProperties(Sender: TObject;


DataSet: TDataSet; out Properties: OleVariant);
begin
Log(‘TDataSetProvider.OnDataSetProperties’);
end;
Dataset Providers
287

LISTING 7.1 Continued


procedure TfrmMain.DataSetProvider1GetTableName(Sender: TObject;
DataSet: TDataSet; var TableName: String);
begin
Log(‘TDataSetProvider.OnGetTableName’);
end;

procedure TfrmMain.DataSetProvider1UpdateData(Sender: TObject;


DataSet: TCustomClientDataSet);
begin
Log(‘TDataSetProvider.OnUpdateData’); 7
end;

PROVIDERS
DATASET
procedure TfrmMain.DataSetProvider1UpdateError(Sender: TObject;
DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind;
var Response: TResolverResponse);
begin
Log(‘TDataSetProvider.OnUpdateError’);
end;

procedure TfrmMain.ClientDataSet1AfterApplyUpdates(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.AfterApplyUpdates’);
end;

procedure TfrmMain.ClientDataSet1AfterExecute(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.AfterExecute’);
end;

procedure TfrmMain.ClientDataSet1AfterGetParams(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.AfterGetParams’);
end;

procedure TfrmMain.ClientDataSet1AfterGetRecords(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.AfterGetRecords’);
end;

procedure TfrmMain.ClientDataSet1AfterRefresh(DataSet: TDataSet);


Chapter 7
288

LISTING 7.1 Continued


begin
Log(‘TClientDataSet.AfterRefresh’);
end;

procedure TfrmMain.ClientDataSet1AfterRowRequest(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.AfterRowRequest’);
end;

procedure TfrmMain.ClientDataSet1BeforeApplyUpdates(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.BeforeApplyUpdates’);
end;

procedure TfrmMain.ClientDataSet1BeforeExecute(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.BeforeExecute’);
end;

procedure TfrmMain.ClientDataSet1BeforeGetParams(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.BeforeGetParams’);
end;

procedure TfrmMain.ClientDataSet1BeforeGetRecords(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.BeforeGetRecords’);
end;

procedure TfrmMain.ClientDataSet1BeforeRefresh(DataSet: TDataSet);


begin
Log(‘TClientDataSet.BeforeRefresh’);
end;

procedure TfrmMain.ClientDataSet1BeforeRowRequest(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(‘TClientDataSet.BeforeRowRequest’);
end;
Dataset Providers
289

LISTING 7.1 Continued


procedure TfrmMain.btnClearEventLogClick(Sender: TObject);
begin
lbEvents.Items.Clear;
end;

end.

Run two occurrences of this application on your computer. In the first occurrence, change
John’s first name to Eddie and apply updates. In the second occurrence, notice that the record 7
still shows a first name of John. Change John’s last name to Smith and apply updates. The
OnReconcileError event fires, indicating that the record has been changed by someone else in

PROVIDERS
DATASET
the meantime (see Figure 7.3).

FIGURE 7.3
Delphi’s reconcile error handler allows the user to decide how to deal with record conflicts.

By default, this dialog shows conflicting values only—in other words, fields that were changed
by another user. Uncheck the Show Conflicting Fields Only check box to display all field val-
ues. If you do, you’ll see that this application changed the LAST field from Lombardo to Smith.
Another application changed the FIRST field from John to Eddie.
The upper-right corner of the dialog contains a list of option buttons that the user can use to
instruct Delphi how to deal with the error. The option buttons correspond to the raSkip,
raCancel, raCorrect, raRefresh, and raMerge values for TReconcileAction. No raAbort
option is listed, but clicking the Cancel button will result in a value of raAbort being returned
from the event handler.
Chapter 7
290

Like several of the other sample applications presented in this book, the Updates application
contains an event log list box that shows you when and in what order interesting events fire for
both the dataset provider and the client dataset. These events are discussed in the section titled
“Provider Events,” later in this chapter.

Resolving Changes to BLOB Fields


TDataSetProvider doesn’t automatically check for conflicts on BLOB fields, including
memos. If two users change the contents of a BLOB field or memo, the second user’s changes
will overwrite the first user’s changes without warning.
Probably the best way to deal with this situation is to add a corresponding integer field to your
database for each BLOB field. The integer field contains an “update number” for the BLOB
field. For instance, if the database table contains a column named IMAGE, add an integer col-
umn named IMAGEUNIQUE, or something similar. Create a trigger for the IMAGE column such
that the IMAGEUNIQUE column is incremented whenever the value of the IMAGE column changes.
Using this technique, your application can detect changes on the IMAGEUNIQUE column, which
indicates that the IMAGE column was changed also.

Refreshing Data from the Server


Earlier chapters discussed the TDBNavigator component, with the exception of one button—
the Refresh button.
TDBNavigator’s Refresh button (which calls the TDataSet.Refresh method) is used to refresh
the dataset from the underlying database. For TClientDataSet, Refresh refreshes the data by
refetching all rows through the dataset provider.

CAUTION
Refresh will raise an exception if the dataset’s change log is not empty. Before you
call TClientDataSet.Refresh directly, you should check the ChangeCount property to
see if it is zero. If it is nonzero, either apply or cancel the updates first (using
ApplyUpdates or CancelUpdates) or refrain from calling Refresh.

Refreshing data from the provider is useful in those cases in which a reconciliation error
occurs and you want to retrieve the latest data from the underlying database. You can also call
it at other times to ensure that the local copy of the data is up to date.
Dataset Providers
291

Rather than calling Refresh to refresh the entire dataset, you can also call
TClientDataSet.RefreshRecord to refresh only the current record. RefreshRecord does not
raise an exception if the dataset’s change log is not empty. Rather, it leaves the entire change
log intact, including any changes that may have been made to the refreshed record.

NOTE
RefreshRecord requires that pfInKey be set for all fields in the primary key (as dis-
cussed in the following section). In addition, you should usually call RefreshRecord
only when the UpdateStatus of the current record is usUnmodified. 7
if ClientDataSet1.UpdateStatus = usUnmodified then

PROVIDERS
ClientDataSet1.RefreshRecord;

DATASET
Update Modes
When resolving data to the database, TDataSetProvider automatically builds the necessary
SQL statement to send to the server to perform the update. An example of such an SQL state-
ment is
UPDATE CONTACTS SET LAST = ‘Smith’ WHERE ID = 5

TDataSetProvider allows you some control over how the SQL statement is built. The first
level of control is afforded by the TDataSetProvider.UpdateMode property. UpdateMode may
be set to one of the values shown in Table 7.3.

TABLE 7.3 TUpdateMode Values

Value Description
upWhereAll All designated fields are included in the WHERE clause.
upWhereChanged Only key fields as well as modified fields are included in the WHERE
clause.
upWhereKeyOnly Only key fields are included in the WHERE clause.

Assume a table named EMPLOYEES has four columns: ID, NAME, BIRTHDAY, and SALARY. ID is the
primary key for the table. One particular record in the table contains the values 1, “John
Smith”, 5/1/1958, $40,000.
The following SQL statements show what SQL statement would be generated for the various
settings of UpdateMode, assuming John Smith’s SALARY field was changed from $40,000 to
$45,000.
Chapter 7
292

When UpdateMode = upWhereAll, the SQL statement would be


UPDATE EMPLOYEES SET SALARY TO 45000 WHERE (ID = 1) AND (NAME = ‘John Smith’)
AND (BIRTHDAY = ‘5/1/1958’) AND (SALARY = 40000)

When UpdateMode = upWhereChanged, the SQL statement would be


UPDATE EMPLOYEES SET SALARY TO 45000 WHERE (ID = 1) AND (SALARY = 40000)

Finally, when UpdateMode = upWhereKeyOnly, the SQL statement would be


UPDATE EMPLOYEES SET SALARY TO 45000 WHERE ID = 1

Table 7.3 describes designated fields and key fields. So what constitutes a designated field or
key field, anyway? Each persistent field created on the server data module has a
ProviderFlags property, which is used to instruct the provider how to treat the field.
ProviderFlags is a set property and can contain any or all the values listed in Table 7.4.

ProviderFlags is a property of TField, which means you don’t need to have persistent fields
to use ProviderFlags. You can set ProviderFlags for a nonpersistent field, like this:
sqlClients.FieldByName(‘ID’).ProviderFlags := [pfInWhere, pfInKey];

TABLE 7.4 TUpdateMode Values

Value Description
pfInUpdate The field can be modified.
pfInWhere The field is included in the WHERE clause when UpdateMode is set to
upWhereAll or upWhereChanged.
pfInKey The field is part of the primary key and controls such features as
refreshing records through a call to RefreshRecord, as well as recon-
ciliation options such as merging.
pfHidden The field is included in data packets sent to and from the client only to
serve as a way to make each record unique. The client dataset can’t see
or modify the field.

NOTE
Nonpersistent fields also have a ProviderFlags property, which is automatically set to
[pfInUpdate, pfInWhere].

Continuing with the previous EMPLOYEES table example, you would set up persistent fields for
the table as shown in Table 7.5.
Dataset Providers
293

TABLE 7.5 ProviderFlags Settings for the Fictitious EMPLOYEES Table Fields

Field ProviderFlags
ID [pfInUpdate, pfInWhere, pfInKey]
NAME [pfInUpdate, pfInWhere]
BIRTHDAY [pfInUpdate, pfInWhere]
SALARY [pfInUpdate, pfInWhere]

ID is the primary key for the table. 7


NAME, BIRTHDAY, and SALARY can all be updated, and they should all be included in the WHERE

PROVIDERS
clause of an update SQL statement when the provider’s UpdateMode property is set to

DATASET
upWhereAll.

Provider Options
TDataSetProvider supports various options that determine the way in which data is sent to the
client dataset, what changes are allowed to the data, and the way in which updates to the data
are handled. TDataSetProvider.Options is a set property that enables you to customize these
options to your liking.
Table 7.6 shows the valid settings for the Options property.

TABLE 7.6 TDataSetProvider Options

Option Description
poFetchBlobsOnDemand When True, BLOBs are not returned from the server as
part of the data packet. The client application must call
TClientDataSet.FetchBlobs to retrieve BLOB data.
When False, BLOBs are returned as part of the data
packet.
poFetchDetailsOnDemand Used when the provider is part of a master/detail relation-
ship. When True, detail records are not returned from the
server as part of the data packet. The client application
must call TClientDataSet.FetchDetails to retrieve detail
records. When False, detail records are returned as part of
the data packet.
poIncFieldProps When True, field properties including Alignment,
Currency, DisplayFormat, DisplayLabel, DisplayValues,
DisplayWidth, EditFormat, EditMask, MaxValue,
MinValue, and Visible are sent to the client along with the
data.
Chapter 7
294

TABLE 7.6 Continued


Option Description
poCascadeDeletes Used when the provider is part of a master/detail relation-
ship. When True, the server deletes detail records automati-
cally when the master record is deleted.
poCascadeUpdates Used when the provider is part of a master/detail relation-
ship. When True, the server automatically updates detail
records when the key value(s) of the master record
changes.
poReadOnly When True, you can’t edit the data in the client dataset.
poAllowMultiRecordUpdates When True, allows updates that affect multiple records.
When False, updates that affect more than one record raise
an exception.
poDisableInserts When True, client datasets will not be able to Insert or
Append new records.
poDisableEdits When True, client datasets will not be able to Edit existing
records.
poDisableDeletes When True, client datasets will not be able to Delete exist-
ing records.
poNoReset When True, calls to AS_GetRecords ignore the reset flag.
poAutoRefresh When True, the provider automatically refreshes updated
records with the latest data from the database.
It is important to note that this option is not yet imple-
mented in Delphi as of version 6.
poPropogateChanges When True, any changes made to data during the
BeforeUpdateRecord or AfterUpdateRecord event han-
dlers are sent back to the client.
poAllowCommandText When True, the client dataset can override the
CommandText property of the provider’s dataset. When
False, attempting to set the client dataset’s CommandText
property raises an exception.
poRetainServerOrder When True, alerts the client dataset that it should not
attempt to sort the data returned from the server.

At the time of this writing, the poAutoRefresh option is not implemented. poAutoRefresh will
be useful for refreshing an updated record from the database when the database fills in field
values automatically.
Dataset Providers
295

For example, a database might include a trigger for autogenerating a table’s primary key, using
a generator. At the time a new record is posted to the database, the value of that field may be
NULL. During the post operation, the database will fill in the value of the primary key, and
poAutoRefresh will then reread the record so the Delphi application knows the value of the
primary key.
Without the poAutoRefresh option, you can still autoassign primary key values to a record, but
you will need to perform a little work in the server-side data module. This technique is
explained later, in the section titled “Changing Field Values on the Server.”
7
Provider Events

PROVIDERS
publishes two types of events—OnXxx and BeforeXxx/AfterXxx. The

DATASET
TDataSetProvider
BeforeXxx/AfterXxx events fire before and after “interesting” things happen in the provider,
such as when updates are applied to the underlying database.
Table 7.7 lists the Before and After events supported by TDataSetProvider.

TABLE 7.7 TDataSetProvider BeforeXxx/AfterXxx Events

Event Description
AfterApplyUpdates Fired after the updates to the database are complete.
AfterExecute Fired after the server executes the query or stored procedure
that will ultimately return data to the client.
AfterGetParams Fired after the server returns output parameters from the
dataset to the client dataset.
AfterGetRecords Fired after the provider creates the data packet to send to the
client.
AfterRowRequest Fired after the provider refreshes the current record because of
a call to TClientDataSet.RefreshRecord or any other
method that fetches data.
AfterUpdateRecord Fired after a record is successfully updated.
BeforeApplyUpdates Fired before updates are applied to the database.
BeforeExecute Fired before the server executes a query or stored procedure.
BeforeGetParams Fired before the server returns output parameters from the
dataset to the client dataset.
BeforeGetRecords Fired before the provider creates the data packet to send to the
client.
Chapter 7
296

TABLE 7.7 Continued


Event Description
BeforeRowRequest Fired before the provider refreshes the current record because
of a call to TClientDataSet.RefreshRecord or any other
method that fetches data.
BeforeUpdateRecord Fired before each record’s updates are applied to the database.

Most of the BeforeXxx and AfterXxx events pass a parameter named OwnerData. OwnerData is
a variant that contains user-defined data. The data is passed from the client dataset to the
provider and back to the client dataset during certain method calls, such as ApplyUpdates. The
flow is as follows:
In the client dataset BeforeXXX event (such as BeforeGetRecords), OwnerData can be set to
anything that you want to pass to the provider. Because it is a variant, it can contain a simple
value such as an integer—or something more complex, such as an array of values.
As flow passes to the provider, the OwnerData parameter is passed to the provider’s BeforeXxx
event. The value of OwnerData may be changed in the BeforeXxx event, if needed.
Next, flow continues to the provider’s AfterXxx event, where OwnerData may again be
changed if necessary.
Finally, flow passes to the client dataset’s AfterXxx event, when you may inspect the (possibly
modified) value of OwnerData. The client dataset’s AfterXxx event is the end of the line, so
there is no reason to modify the value of OwnerData in the client dataset’s AfterXxx event.
The following chapter shows an example of how you can use the OwnerData parameter to
implement a stateless server. OnXxx events are fired to allow the application code to “hook”
into the various stages of providing data to the client and resolving it back to the server.
Table 7.8 lists the events supported by the TDataSetProvider component, along with their use.

TABLE 7.8 TDataSetProvider OnXxx Events

Event Description
OnGetData Fired after data is fetched from the underlying database but
before the data is returned to the client. You can handle this
event to modify the data in some way before passing it on
to the client. For example, you might encrypt fields, com-
press the data, or weed out certain data that the client
should never see. This event is discussed in the
“Intercepting Data” section later in this chapter.
Dataset Providers
297

TABLE 7.8 Continued


Event Description
OnGetDataSetProperties Fired after data is fetched from the underlying database but
before the data is returned to the client. Using the optional
parameters (discussed later in this chapter in the section
titled “Optional Parameters”), you can send additional
information to the client.
OnGetTableName Used when the provider returns data from a join or stored
procedure. You can handle this event to instruct the 7
provider which table to apply updates to. This option is
discussed in the section titled “Providing and Resolving

PROVIDERS
Data from a Join,” later in this chapter.

DATASET
OnUpdateData OnUpdateData is the counterpart of OnGetData. It is fired
just before updates are sent to the database server. You can
handle this event to decrypt data before it is saved to the
database, for example. This event is discussed in the
“Intercepting Data” section later in this chapter.
OnUpdateError Fired when an error occurs while reconciling data. If you
don’t handle this event, the error is sent back to the client
application. You can handle this event to ignore certain
errors or attempt to correct them on the server before send-
ing them back to the client.

Some of the more interesting events will be discussed in more detail in the sections that
follow.

Changing Field Values on the Server


Sometimes, you will want the server to make modifications to data that is passed to it by the
client. The most common example is when a table contains an ID field that is the primary key
for the table. The database server is often responsible for assigning unique IDs to each individ-
ual record.
To accomplish this, you must follow this procedure:
1. Create a stored procedure in the database that will return the next unique ID.
2. Include the poPropogateChanges setting in the TDataSetProvider’s Options property.
3. Include the pfInKey setting in the ID field’s ProviderFlags.
4. Provide an event handler for the provider’s BeforeUpdateRecord event.
Chapter 7
298

In the BeforeUpdateRecord event handler for a newly inserted record, obtain the next value of
the ID field and assign it to the record. The following code snippet shows a typical implemen-
tation for the BeforeUpdateRecord event handler.
procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
begin
if UpdateKind = ukInsert then
if DeltaDS.FieldByName(‘ID’).OldValue <= 0 then
DeltaDS.FieldByName(‘ID’).NewValue := GetNextID;
end;

Where GetNextID looks like this:


function TfrmMain.GetNextID: Integer;
begin
sqlID.ExecSQL;
Result := sqlID.ParamByName(‘AValue’).AsInteger;
end;

sqlID is a TSQLDataSet component that executes a stored procedure on the database server,
which in turn returns the next unique ID number in an output parameter named AValue.
I won’t provide a program example using this technique at this time, but you’ll see this tech-
nique implemented in later examples in this chapter.

Intercepting Data
Table 7.4 lists two events, OnGetData and OnUpdateData, that can be used to intercept data on
its way from the provider to the client and also on its way from the client to the provider.
In this chapter, we’re working with a single application; in other words, the client and server
portions of the data are both contained in a single application. In the next chapter, we’ll create
client and server applications that may exist on separate machines that can be located down the
hall from each other or on different continents.
When data travels from machine to machine, unfortunately there is always the chance that
some hacker may be attempting to listen in on the exchange of data. If the data includes any-
thing sensitive, such as account numbers or the like, you might want to consider encrypting
that data before sending it over the wire or across cyberspace.
OnGetData and OnUpdateData provide the two hooks on the server side for implementing this
functionality. The following code snippet shows implementations of both OnGetData and
OnUpdateData that encrypt data on its way to the client and decrypt data on its way back. The
functions EncryptData and DecryptData referenced by the code are fictitious routines that you
would need to supply if you were to implement this functionality in your own applications.
Dataset Providers
299

NOTE
For a good third-party encryption library that supports encryption standards such as
DES, Blowfish, and Rijndael (AES), take a look at TurboPower Software’s LockBox
product at www.tpx.turbopower.com/products/LockBox.

procedure TForm1.ProviderGetData(Sender: TObject;


DataSet: TCustomClientDataSet);
begin 7
while not DataSet.EOF do begin
DataSet.Edit;

PROVIDERS
DATASET
DataSet.FieldByName(‘AccountNumber’).AsString :=
EncryptData(DataSet.FieldByName(‘AccountNumber’).AsString);
DataSet.Post;
DataSet.Next;
end;
end;

procedure TfrmMain.DataSetProvider1UpdateData(Sender: TObject;


DataSet: TCustomClientDataSet);
begin
while not DataSet.EOF do begin
if DataSet.UpdateStatus <> usDeleted then begin
DataSet.Edit;
DataSet.FieldByName(‘AccountNumber’).AsString :=
DecryptData(DataSet.FieldByName(‘AccountNumber’).AsString);
DataSet.Post;
DataSet.Next;
end;
end;
end;

On the client side, the AccountNumber field will be encrypted.


ClientDataSet1.FieldByName(‘AccountNumber’).AsString will return an encrypted account
code, which you should then decrypt before displaying. Note that the code shown here is a
rather simplistic implementation of encrypting/decrypting data. For one thing, only the account
number is encrypted and decrypted. In a real application, you might want to encrypt and
decrypt all string fields by looping through all fields in the dataset.
Chapter 7
300

Optional Parameters
Optional parameters are custom data that pertain to the dataset passed to the client. Optional
parameters relate to the dataset as a whole, rather than to individual records. You can use an
optional parameter to pass data back to the client, such as the date and time the data was pro-
vided, the length of time required to run the query on the server, or any other data.
To pass optional parameters to the client, provide an event handler for TDataSetProvider’s
OnGetDataSetProperties event. Within this event handler, use the Properties parameter to
set the values to send back to the client. The following code snippet shows how to send both
the time required to execute the query and the time the query was executed.
procedure TForm1.DataSetProvider1GetDataSetProperties(Sender: TObject;
DataSet: TDataSet; out Properties: OleVariant);
begin
Properties := VarArrayCreate([0, 1], varVariant);
Properties[0] := VarArrayOf([‘TimeQueried’, Now, True]);
Properties[1] := VarArrayOf([‘QueryPerformance’, FTimeToQuery, True]);
end;

Properties is a variant array of variant arrays. This code snippet creates an array of two vari-
ants. Each variant in the array is an array of three values: the name of the optional parameter,
the value of the parameter, and a Boolean value that indicates whether the optional parameter
should be sent back to the server as part of the delta.
In this example, FTimeToQuery is a private variable that is calculated using the following code:
procedure TForm1.SQLDataSet1BeforeOpen(DataSet: TDataSet);
begin
FTimeToQuery := GetTickCount;
end;

procedure TForm1.SQLDataSet1AfterOpen(DataSet: TDataSet);


begin
FTimeToQuery := GetTickCount - FTimeToQuery;
end;

SQLDataSet1 is the underlying dataset for the TDataSetProvider.


On the client, you can retrieve these values by using the following code:
procedure TForm1.btnGetPropertiesClick(Sender: TObject);
var
QP: DWord;
TimeQueried: TDateTime;
begin
QP := ClientDataSet1.GetOptionalParam(‘QueryPerformance’);
TimeQueried := ClientDataSet1.GetOptionalParam(‘TimeQueried’);
Dataset Providers
301

ShowMessage(‘The query took ‘ + IntToStr(QP) + ‘ms and was executed on ‘ +


DateToStr(TimeQueried) + ‘ at ‘ + TimeToStr(TimeQueried));
end;

Master/Detail Relationships
Back in Chapter 2, “dbExpress Datasets,” you learned how to create a master/detail relation-
ship between two or more dbExpress datasets.
Later, in Chapter 4, you learned how to create nested TClientDataSets to create a
master/detail relationship at the TClientDataSet level. 7
Using providers, you will establish the master/detail relationship on the server-side data mod-

PROVIDERS
DATASET
ule, using TSQLDataSet components. You will then connect a TDataSetProvider component to
the master dataset only. Drop a single TClientDataSet component on the client-side data mod-
ule and connect it to the master dataset’s provider. This will automatically create a nested
dataset on the client side.
It is good practice to create one data module for the dbExpress components and dataset
providers and another data module for the client datasets. I refer to these data modules as
server-side data modules and client-side data modules. This is explained more fully in the sec-
tion titled, “Connecting to a Local Database,” later in this chapter.
The MasterDetail sample application, included in the downloads for this book, illustrates this
technique. I haven’t included a listing here, because almost no code is required—everything is
done at design time.
Figure 7.4 shows the main form of the MasterDetail application at design time. For simplicity,
I put the database components directly on the main form instead of creating separate data
modules for the server and client.

FIGURE 7.4
Components needed for a master/detail relationship.
Chapter 7
302

Providing and Resolving Data from Stored


Procedures and Joins
So far, the examples in this chapter focused on providing data from a single table using a sim-
ple select statement, such as SELECT * FROM CONTACTS. Frequently, data is provided from a
stored procedure on the server or is generated as a join between multiple tables, as the follow-
ing two snippets show:
// Stored procedure
SELECT * FROM CONTACTSBYSTATE(‘FL’);

or
// Join
SELECT CONTACTS.FIRST, CONTACTS.LAST, TODOS.SCHEDULED, TODOS.DESCRIPTION
FROM CONTACTS, TODOS
WHERE CONTACTS.ID = TODOS.CONTACTID;

Providing and Resolving Data from a Stored Procedure


In the preceding example, the stored procedure returns all contacts for a given state—in this
case, Florida. TDataSetProvider attempts to intelligently determine what database table to
update when resolving data, but in this case, CONTACTSBYSTATE is a stored procedure and not a
table.
TDataSetProvider needs a little help to know what table should be updated with any data
changes. To do this, you need to provide an event handler for the provider’s OnGetTableName
event. In the event handler, specify the name of the table to be updated, like this:
procedure TForm1.DataSetProvider1GetTableName(Sender: TObject;
DataSet: TDataSet; var TableName: String);
begin
TableName := ‘CONTACTS’;
end;

In addition, you need to set the ProviderFlags to [] for all fields in the stored procedure
dataset that are not updated.

Providing and Resolving Data from a Join


By definition, a join returns data from more than one table. In many cases, the user will be able
to update data from only one of the tables. For example, in the select statement shown previ-
ously, data is retrieved from the CONTACTS table and the TODOS table. On the client side, the
user may add a new TODO to the list, but the only table that is affected is the TODOS table.
(Presumably, if the user wants to add a new contact, he would not do so on the same screen
that he’s viewing todos on).
Dataset Providers
303

If this is the case, you can specify the TODOS table name in the OnGetTableName event handler,
as explained earlier.
Sometimes, though, the user will be able to update multiple tables at once. To handle this situ-
ation, you will need to write some code. Again, we turn to TDataSetProvider’s
BeforeUpdateRecord event handler. The trick is to apply the necessary updates to the individ-
ual tables ourselves inside the event handler.
The following code snippet shows the general outline that you will follow to apply updates to
multiple tables at once. It is not compilable code.
procedure TForm1.SQLClientDataSet1BeforeUpdateRecord(Sender: TObject;
7
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;

PROVIDERS
var Applied Boolean);

DATASET
var
SQL: string;
Connection: TSQLConnection;
begin
// Obtain a pointer to the connection from the source dataset
Connection := (SourceDS as TCustomSQLDataSet).SQLConnection;

case UpdateKind of
ukInsert: begin
// Insert into the first table
SQL := // SQL INSERT STATEMENT FOR TABLE 1
Connection.Execute(SQL, nil, nil);

// Insert into the second table


SQL := // SQL INSERT STATEMENT FOR TABLE 2
Connection.Execute(SQL, nil, nil);
end;

ukModify: begin
// Update the first table
SQL := // SQL UPDATE STATEMENT FOR TABLE 1
Connection.Execute(SQL, nil, nil);

// Update the second table


SQL := // SQL UPDATE STATEMENT FOR TABLE 2
Connection.Execute(SQL, nil, nil);
end;

ukDelete: begin
// Delete from the first table
SQL := // SQL DELETE STATEMENT FOR TABLE 1
Connection.Execute(SQL, nil, nil);
Chapter 7
304

// Delete from the second table


SQL := // SQL DELETE STATEMENT FOR TABLE 2
Connection.Execute(SQL, nil, nil);
end;
end;

Applied := True;
end;

As you can see from the preceding listing, the general idea is to determine what kind of opera-
tion is taking place (Insert, Modify, or Delete) and then call the TSQLConnection component to
execute the appropriate SQL statements directly. At the end of the method, set Applied to True
so the provider knows that you’ve already handled the update manually.
This process works equally well for three-, four-, or n-table joins.
The following example shows how you can resolve updates in a simple two-way join.
Listing 7.2 contains the source code for the main form of the Joins application.

LISTING 7.2 Joins—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
QStdCtrls, DBXpress, FMTBcd, DB, SqlExpr, QGrids, QDBGrids, Provider,
DBClient, Variants;

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
DBGrid1: TDBGrid;
SQLConnection1: TSQLConnection;
SQLDataSet1: TSQLDataSet;
DataSetProvider1: TDataSetProvider;
ClientDataSet1: TClientDataSet;
sqlID: TSQLDataSet;
btnApplyUpdates: TButton;
procedure FormCreate(Sender: TObject);
procedure DataSetProvider1BeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
procedure btnApplyUpdatesClick(Sender: TObject);
procedure ClientDataSet1NewRecord(DataSet: TDataSet);
Dataset Providers
305

LISTING 7.2 Continued


private
{ Private declarations }
FNextID: Integer;
function GetNextID: Integer;
public
{ Public declarations }
end;

var
frmMain: TfrmMain; 7
implementation

PROVIDERS
DATASET
{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
ClientDataSet1.Open;
end;

procedure TfrmMain.ClientDataSet1NewRecord(DataSet: TDataSet);


begin
Dec(FNextID);
DataSet.FieldByName(‘CONTACTID’).AsInteger := FNextID;
end;

function TfrmMain.GetNextID: Integer;


begin
sqlID.ExecSQL;
Result := sqlID.ParamByName(‘AValue’).AsInteger;
end;

procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
var
SQL: string;
Connection: TSQLConnection;
ID: Integer;
begin
// Obtain a pointer to the connection from the source dataset
Connection := (SourceDS as TCustomSQLDataSet).SQLConnection;

case UpdateKind of
ukInsert: begin
ID := GetNextID;
Chapter 7
306

LISTING 7.2 Continued

// Insert into the first table


SQL := Format(‘INSERT INTO CONTACTS (CONTACTID, FIRST, LAST) ‘ +
‘VALUES (%d, %s, %s)’,
[ID, QuotedStr(DeltaDS.FieldByName(‘FIRST’).NewValue),
QuotedStr(DeltaDS.FieldByName(‘LAST’).NewValue)]);
Connection.Execute(SQL, nil, nil);

// Insert into the second table


SQL := Format(‘INSERT INTO CONTACTS2 (CONTACTID, SPOUSE) ‘ +
‘VALUES (%d, %s)’,
[ID, QuotedStr(DeltaDS.FieldByName(‘SPOUSE’).NewValue)]);
Connection.Execute(SQL, nil, nil);
end;

ukModify: begin
// Update the first table
SQL := ‘’;

if not VarIsEmpty(DeltaDS.FieldByName(‘FIRST’).NewValue) then


SQL := SQL + Format(‘FIRST = %s’,
[QuotedStr(DeltaDS.FieldByName(‘FIRST’).NewValue)]);

if not VarIsEmpty(DeltaDS.FieldByName(‘LAST’).NewValue) then begin


if SQL <> ‘’ then
SQL := SQL + ‘, ‘;
SQL := SQL + Format(‘LAST = %s’,
[QuotedStr(DeltaDS.FieldByName(‘LAST’).NewValue)]);
end;

if SQL <> ‘’ then begin


ID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;
SQL := Format(‘UPDATE CONTACTS SET %s ‘ +
‘WHERE CONTACTID = %d’, [SQL, ID]);
Connection.Execute(SQL, nil, nil);
end;

// Update the second table


SQL := ‘’;

if not VarIsEmpty(DeltaDS.FieldByName(‘SPOUSE’).NewValue) then begin


ID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;

if VarIsNull(DeltaDS.FieldByName(‘SPOUSE’).OldValue) then
SQL := Format(‘INSERT INTO CONTACTS2 (CONTACTID, SPOUSE) ‘ +
‘VALUES (%d, %s)’,
Dataset Providers
307

LISTING 7.2 Continued


[ID, QuotedStr(DeltaDS.FieldByName(‘SPOUSE’).NewValue)])
else
SQL := Format(‘UPDATE CONTACTS2 SET SPOUSE = %s ‘ +
‘WHERE CONTACTID = %d’,
[QuotedStr(DeltaDS.FieldByName(‘SPOUSE’).NewValue), ID]);

Connection.Execute(SQL, nil, nil);


end;
end;
7
ukDelete: begin
ID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;

PROVIDERS
DATASET
// Delete from the second table
SQL := Format(‘DELETE FROM CONTACTS2 WHERE CONTACTID = %d’, [ID]);
Connection.Execute(SQL, nil, nil);

// Delete from the first table


SQL := Format(‘DELETE FROM CONTACTS WHERE CONTACTID = %d’, [ID]);
Connection.Execute(SQL, nil, nil);
end;
end;

Applied := True;
end;

procedure TfrmMain.btnApplyUpdatesClick(Sender: TObject);


begin
ClientDataSet1.ApplyUpdates(0);
end;

end.

The CONMAN database contains a CONTACTS2 table, which has a one-to-one correspondence to
the CONTACTS table. CONTACTS2 is only in the database for purposes of this example.
The DataSetProvider1BeforeUpdateRecord method handles all insert, modify, and delete
requests by dynamically building the appropriate SQL statements and sending them directly to
the database.
To keep the SQL statements simple, this sample application works with just two fields from
the CONTACTS table and a single field from the CONTACTS2 table.
Chapter 7
308

Connecting to a Local Database


This chapter explains how to take advantage of providers in a multitier application in which the
client and server are both physically located in the same executable. Even under this arrange-
ment, it is beneficial to structure the application such that it is a relatively simple matter to
move the server into its own application if you ever decide to move to separate server and
client applications.
To facilitate this, you should put server-side components on a separate data module from
client-side components. Under this arrangement, your application would have the following
structure:
• Server data module This contains the connection to the database, along with the neces-
sary datasets and providers.
• Client data module This contains the client datasets and (optionally) data sources.
• Forms and units The forms and supports units in the application will reference the data
from the client data module.
Other than from inside the server data module, you should not reference any components on
the server side of the equation, including the database connection or datasets. All data access
should be performed through the client data module. If you impose this restriction on your
code, it will go a long way toward simplifying moving the server data module out into its own
application.

Using Providers Located on a Different Form


After you move the data access components and providers onto a separate data module, the
client dataset won’t be able to directly connect to the dataset provider. In other words, the
TClientDataSet’s ProviderName property won’t list the provider in its drop-down list, because
the provider is not on the same form (or data module).
To provide access to the provider(s) on the server-side data module to your client datasets, you
either need to call TClientDataSet.SetProvider or use a TLocalConnection component, dis-
cussed in the next chapter.
For now, at runtime, you can call TClientDataSet.SetProvider to establish a connection to a
provider on a different form or data module. The following line of code shows how this is
done:
ClientDataSet1.SetProvider(ServerDM.pvContacts);
Dataset Providers
309

One-Stop Shopping: TSQLClientDataSet


If you write a lot of applications using dbExpress, and you don’t want to plan ahead for easy
migration to a separate application server, you may find that the TSQLClientDataSet compo-
nent simplifies your application somewhat.
TSQLClientDataSet encapsulates a TSQLDataSet, TDataSetProvider, and TClientDataSet
component into a single component. You still need to set up a TSQLConnection component as
explained in Chapter 2, but then you can drop a single TSQLClientDataSet component on your
data module rather than setting up three components for each dataset.
7
The downside to using TSQLClientDataSet is that if you later decide to move to a separate
application server, you will need to create new TSQLDataSet and TDataSetProvider compo-

PROVIDERS
DATASET
nents on the server side and then replace the TSQLClientDataSet component with a
TClientDataSet component on the client side. In addition, TSQLClientDataSet limits the
amount of control you have over individual provider and dataset events.
It doesn’t require much more effort to set up the separate components to begin with, and it
makes migration to a multitier application much easier in the future. For that reason, I don’t
recommend using TSQLClientDataSet at all in most applications.

Limiting the Amount of Data Returned by the Server


By default, when you open a client dataset, all data returned by the server-side datasets are
returned to the client application. In many cases, this is acceptable. In other cases, you may
want to limit the number of records returned at a single time. For example, if a query returns a
result set of 10,000 records, it may not be a good idea to return all records to the client at one
time when the client application is across a slow LAN or even slower WAN.
To limit the amount of data returned at once from the server, set the TClientDataSet’s
PacketRecords property. By default, this property is set to –1, meaning that all records should
be returned at once from the server.
If you set PacketRecords to a number greater than zero, it determines the maximum number
of records to return at a given time from the server. When PacketRecords is set to zero, only
metadata information is returned from the server—no actual row data is returned.
Generally, each data packet is fetched from the server automatically as you scroll through
the client dataset, whether in code using Next commands or through use of a data-aware
component such as TDBGrid. If you set TClientDataSet.FetchOnDemand to False,
additional data packets are not automatically retrieved. In this case, you must call
TClientDataSet.GetNextPacket to return the next data packet from the server.
Chapter 7
310

Fetching BLOBs Manually


One way to limit the amount of data sent from the server to the client is to fetch BLOB data
manually, rather than automatically. By default, BLOB data is returned by the provider along
with the rest of the data packet. However, if you don’t always need BLOB information, you
can turn on the TDataSetProvider’s poFetchBlobsOnDemand option. You also need to set
TClientDataSet.FetchOnDemand to False.

NOTE
If your client application has no need for BLOB data at all, you should change the
TSQLDataset’s CommandText property so that the SELECT statement doesn’t retrieve
the BLOB field from the database. poFetchBlobsOnDemand is used when the client
application sometimes needs the BLOB data, but not always.

When you set the poFetchBlobsOnDemand option, BLOB data is not returned to the client
dataset. If you try to access a BLOB field in the client dataset, an exception will be raised. If
the client needs BLOB information for the current record, it should call the client dataset’s
FetchBlobs method, like this:

ClientDataSet1.FetchBlobs;

FetchBlobs retrieves all BLOB fields for the current record only. If the dataset contains
three BLOB fields, there is no way to retrieve only a single BLOB value from the server—
FetchBlobs will return all three BLOBs.

Note that if the client dataset’s FetchOnDemand property is set to True, the client dataset will
call FetchBlobs automatically. To fetch BLOBs manually, you must set both the
poFetchBlobsOnDemand option on the provider and the client dataset’s FetchOnDemand
property.

Fetching Detail Records Manually


Another way to limit the amount of data returned by the server is to fetch detail records from a
master/detail relationship manually. By default, all detail records (in the form of a nested
dataset) are sent to the client along with the master record. If you don’t always need detail
information, you can turn on the TDataSetProvider’s poFetchDetailsOnDemand option.
When you set the poFetchDetailsOnDemand option, detail data is not returned to the client
dataset. Any nested datasets will return a RecordCount of –2147483648 (-MaxInt – 1). If the
client needs detail information for the current record, it should call the client dataset’s
FetchDetails method, like this:

ClientDataSet1.FetchDetails;
Dataset Providers
311

As with fetching BLOBs, you must set the client dataset’s FetchOnDemand property to False,
or the client dataset will automatically call FetchDetails, even if the
poFetchDetailsOnDemand option is set on the provider.

FetchDetails retrieves all detail fields for the current record only. If the dataset contains three
nested datasets, there is no way to retrieve details for only a single nested dataset—
FetchDetails will return records for all three nested datasets.

The following sample application illustrates how you can use FetchBlobs and FetchDetails
to limit the amount of data returned from the server.
7
Listing 7.3 shows the source code for the main form of the DataFetch application. To simplify
the sample somewhat, all components are on the main form of the application. In a real appli-

PROVIDERS
DATASET
cation, you should create separate server-side and client-side data modules for the data access
components.

LISTING 7.3 DataFetch—MainForm.pas


unit MainForm;

interface

uses
SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
QStdCtrls, DBXpress, FMTBcd, Provider, SqlExpr, DB, DBClient, QGrids,
QDBGrids, QDBCtrls;

type
TfrmMain = class(TForm)
cdsContacts: TClientDataSet;
conn: TSQLConnection;
sqlContacts: TSQLDataSet;
pvContacts: TDataSetProvider;
sqlTodos: TSQLDataSet;
dsContacts: TDataSource;
dsClientContacts: TDataSource;
gridContacts: TDBGrid;
btnFetchBlobs: TButton;
btnFetchDetails: TButton;
cdsTodos: TClientDataSet;
cdsContactsID: TIntegerField;
cdsContactsFIRST: TStringField;
cdsContactsLAST: TStringField;
cdsContactsDEAR: TStringField;
cdsContactsTITLE: TStringField;
cdsContactsCOMPANYNAME: TStringField;
Chapter 7
312

LISTING 7.3 Continued


cdsContactsADDRESS1: TStringField;
cdsContactsADDRESS2: TStringField;
cdsContactsCITY: TStringField;
cdsContactsSTATE: TStringField;
cdsContactsPOSTALCODE: TStringField;
cdsContactsCOUNTRY: TStringField;
cdsContactsPHONE: TStringField;
cdsContactsFAX: TStringField;
cdsContactsCELLULAR: TStringField;
cdsContactsPAGER: TStringField;
cdsContactsEMAIL: TStringField;
cdsContactsIMAGE: TBlobField;
cdsContactsNOTES: TMemoField;
cdsContactssqlTodos: TDataSetField;
lblDetails: TLabel;
lblBlobs: TLabel;
procedure FormCreate(Sender: TObject);
procedure btnFetchDetailsClick(Sender: TObject);
procedure btnFetchBlobsClick(Sender: TObject);
procedure cdsContactsAfterScroll(DataSet: TDataSet);
private
procedure ShowTodoCount;
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
cdsContacts.Open;
end;

procedure TfrmMain.btnFetchDetailsClick(Sender: TObject);


begin
cdsContacts.FetchDetails;
ShowTodoCount;
end;
Dataset Providers
313

LISTING 7.3 Continued


procedure TfrmMain.btnFetchBlobsClick(Sender: TObject);
begin
cdsContacts.FetchBlobs;
ShowTodoCount;
end;

procedure TfrmMain.cdsContactsAfterScroll(DataSet: TDataSet);


begin
ShowTodoCount;
end; 7
procedure TfrmMain.ShowTodoCount;

PROVIDERS
DATASET
var
TestStream: TStream;
begin
try
TestStream := cdsContacts.CreateBlobStream(cdsContactsNOTES, bmRead);
TestStream.Free;
lblBLOBs.Caption := ‘BLOBs have been fetched for this record’;
except
lblBLOBs.Caption := ‘BLOBs have not been fetched for this record’;
end;

case cdsTodos.RecordCount of
-MaxInt - 1:
lblDetails.Caption := ‘Todos have not been fetched for this record’;
1:
lblDetails.Caption := ‘1 todo has been fetched for this record’;
else
lblDetails.Caption := IntToStr(cdsTodos.RecordCount) +
‘ todos have been fetched for this record’;
end;
end;

end.

A few interesting things are going on in this application. Let’s take a look at the methods one
at a time.
First, however, let’s examine the structure of the application.
Figure 7.5 shows the main form of the DataFetch application at design time.
Chapter 7
314

FIGURE 7.5
DataFetch main form showing relationships between components.

Two dbExpress datasets—sqlContacts and sqlTodos—are linked in a master/detail relation-


ship. A TDataSetProvider is connected to the sqlContacts (master) dataset. Remember that a
provider is not needed for the detail dataset. This constitutes the server side of the equation.
Next, two client datasets—cdsContacts and cdsTodos—represent the tables on the client side.
cdsTodos is a nested dataset inside the cdsContacts dataset.

The FormCreate method opens the cdsContacts client dataset. This causes a chaining effect
that opens the sqlContacts dbExpress dataset and sends the results back to the client through
the pvContacts provider.
Two buttons on the form, labeled Fetch Details and Fetch Blobs, call the
btnFetchDetailsClick and btnFetchBlobsClick events, respectively. These two events, in
turn, call the client dataset’s FetchDetails and FetchBlobs methods. They then call the
ShowTodoCount method, which we’ll examine shortly.

cdsContactsAfterScroll is fired whenever the current record changes on the cdsContacts


dataset. It also makes a call to ShowTodoCount.
cdsContactsApplyUpdates is a method that is connected to cdsContacts’ AfterDelete and
AfterPost events. What it does is ensure that whenever a record is posted to the cdsContacts
dataset, it is automatically resolved back to the underlying database. This eliminates the need
to place an Apply Updates button in the sample application.
ShowTodoCount is a method that checks the current contact record to see if BLOBs and/or
details have been fetched for it. To check the status of BLOBs, it attempts to create a BLOB
stream on the NOTES field. The call to CreateBlobStream will raise an exception if BLOBs
have not been fetched, and the program updates a label on the main form accordingly.
Dataset Providers
315

To check whether details have been fetched for the current contact, the code looks at
cdsTodos.RecordCount. RecordCount will be –MaxInt – 1 if details have not yet been fetched.

Figure 7.6 shows the main form of the DataFetch application at runtime.

PROVIDERS
DATASET
FIGURE 7.6
DataFetch shows how to fetch details and BLOBs on demand.

Summary
This chapter introduced you to providers and multitier database development, including the
following key concepts:
• A TDatasetProvider is a conduit between a client dataset and an external data store. It
provides data to the client dataset on request and resolves data back to the database when
the client applies updates.
• You need to call the client dataset’s ApplyUpdates method to save changes to a database
permanently.
• For greatest control over reconciliation errors, you should provide an event handler for
the client dataset’s OnReconcileError event. Delphi comes with a prewritten function,
named HandleReconcileError, that you can use to facilitate this process.
• By calling TClientDataSet.Refresh, you can ensure that the client always has the latest
copy of the data from the server.
• You can set the provider’s update mode to upWhereAll, upWhereChanged, or
upWhereKeyOnly for finer control over how each record is updated.

• You can take advantage of numerous provider options and events for finer control over
the entire provide/resolve process.
• When creating a master/detail relationship on the server-side components, the client
dataset will represent that relationship as a nested dataset.
Chapter 7
316

• By handling the provider’s OnGetTableName event, you can update data returned from a
stored procedure.
• By handling the provider’s BeforeUpdateRecord event, you can update data returned
from a join operation.
• You can call TClientDataSet.SetProvider at runtime to establish a connection between
client datasets and providers on different forms or data modules.
• To replace separate TSQLDataSet, TDataSetProvider, and TClientDataSet components,
use TSQLClientDataSet.
• To limit the amount of data returned by the server at a single time, set the provider’s
poFetchBlobsOnDemand and/or poFetchDetailsOnDemand options, and then call
TClientDataSet.FetchBlobs or TClientDataSet.FetchDetails manually.

The next chapter expands on this discussion to show you how to create a multitier application
with separate server and client executables.
DataSnap CHAPTER

8
IN THIS CHAPTER
• What Is DataSnap? 318

• Creating the Application Server 318

• Creating the Client Application 329

• A Complete Example 336

• The Briefcase Model 340

• Stateless Servers 341

• Sharing a Connection Between Multiple Client


DataSets 343

• Brokering Connections Between Multiple


Servers 348
Chapter 8
318

The previous chapter gave an overview of providers and explained how to use them in a multi-
tier application where the client and application server are physically located in a single exe-
cutable.
This chapter expands the discussion to show you how to create multitier database applications
where the client and application server are in different executables, which may then run on the
same machine or, more likely, on two separate computers.
Delphi supports a number of underlying protocols to connect to an application server, includ-
ing DCOM, CORBA, HTTP, and SOAP. This book does not attempt to explain any of these
technologies in detail. Rather, it describes how to set up the required connection component for
each of the technologies. It is assumed that you already have the necessary software installed
and functioning properly for the protocol that you will use to connect to the application server.

What Is DataSnap?
DataSnap is the technology that allows client applications to connect to providers in an appli-
cation server. DataSnap is implemented by a number of components that can be used to con-
nect different machines through such underlying technologies as sockets, DCOM, CORBA,
HTTP, and SOAP.

NOTE
In earlier versions of Delphi, DataSnap was named MIDAS. Because of international
trademark considerations, the name MIDAS has been changed to DataSnap.

Creating the Application Server


This section describes the steps necessary to create an application server. The application
server contains all the code necessary to connect to the underlying database and provide the
resulting data to the client application. As you’ll learn later in this section, the application
server can also provide other, possibly non-database related, services to the client.

Remote Data Modules


When creating the application server, the first order of business is to set up one or more
remote data modules. A remote data module is simply a data module that can be accessed
remotely from a client application. The remote data module contains the components that we
placed on the server-side data module in the previous chapter.
DataSnap
319

In many, if not most, application servers, a single remote data module will suffice. However,
you can create multiple remote data modules in a single application server if you want. Some
reasons for doing so include the following:
• The application server needs to connect to multiple databases, and you want to create a
distinct remote data module for each database connection.
• Two types of users will run this application—perhaps a “normal” user and an administra-
tor. You may want to create two separate remote data modules, in which the remote data
module used for administrators contains additional tables or queries for sensitive data.
• One data module is used to provide access to the underlying database, and another data
module is used to provide other, non-data-aware services, such as numeric calculations.
This capability is discussed later, in the section titled “Adding Methods to the Remote
Data Module.”

Creating a Remote Data Module


Creating a remote data module for non-SOAP applications is a straightforward operation.
• Create a new application, which will be the application server.
• From Delphi’s main menu, select File, New, Other. Delphi’s New Items dialog is dis- 8
played. Select the Multitier tab. The New Items dialog should look like the image shown

DATASNAP
in Figure 8.1.
• Select one of the remote data modules from the list and click OK. The different data
modules are discussed in the following sections.

FIGURE 8.1
Delphi supports three remote data modules—Remote Data Modules, Transactional Data Modules, and CORBA Data
Modules.
Chapter 8
320

Creating a Standard Remote Data Module


The examples presented in this book all use a standard remote data module. This type of data
module is used for clients connecting through sockets, DCOM, or HTTP connections.
To create a TRemoteDataModule, select Remote Data Module from the Multitier page of the
New Items dialog and click OK. The dialog shown in Figure 8.2 appears.

FIGURE 8.2
Creating a standard remote data module.

Enter a CoClass name for the data module, such as ContactDataServer or some other mean-
ingful name. The application name and CoClass together form the name by which the remote
data module is referenced. For example, if the application name is ContactServer and the
CoClass name is ContactDataServer, the remote data module is referenced from the client
application as ContactServer.ContactDataServer.
Select the type of instancing to use for the remote data module. Table 8.1 lists the possible val-
ues for this selection.

TABLE 8.1 Instancing Values

Value Description
Internal The remote data module cannot be created from an external
client. Internal remote data modules are always created from
within the server application.
Single Instance Each client that attempts to connect to the server will cause a
separate instance of the server executable to run.
Multiple Instance Only one copy of the server executable will run, but it will
instantiate a separate remote data module for each client that
connects to it.

In most situations, you should leave the Instancing combo box set to Multiple Instance.

Lastly, select the threading model to use for the remote data module. Table 8.2 lists the possi-
ble values for the Threading Model combo.
DataSnap
321

TABLE 8.2 Threading Model Values


Value Description
Single The data module will receive requests from only a single client at a
time. Because of this, you don’t need to deal with threading issues.
Apartment Each instance of the data module will service only a single request at a
time. However, the server may handle multiple requests on multiple
data modules at the same time. Therefore, you need to handle multi-
threading issues on global data.
Free The data module can receive multiple requests from multiple clients at
the same time. In addition to dealing with thread conflicts on global
data, you must also protect instance data.
Both The same as Free with the exception that any callbacks made to client
interfaces are serialized.
Neutral Multiple clients can make requests to the same data module at the
same time, but COM ensures that no two requests conflict with each
other. This is supported only under COM+. When not running under
COM+, this is treated the same as the Apartment model.
8
After you have filled in the appropriate values (the default values for Instancing and

DATASNAP
Threading Model are fine in most cases) you can click OK to create the remote data module.

Creating a MTS Remote Data Module


A TMTSDataModule should be used for application servers that will be installed under MTS or
COM+. These data modules can also be used for clients connecting through sockets, DCOM,
or HTTP connections.
TMTSDataModules can be created only in an ActiveX library—not in an application.
To create a TMTSDataModule, select Transactional Data Module from the Multitier page of
the New Items dialog. The dialog shown in Figure 8.3 appears.

FIGURE 8.3
Creating a transactional remote data module.
Chapter 8
322

Enter a CoClass name for the data module, such as ContactDataServer, or some other mean-
ingful name.
Select the threading model to use for the remote data module. Transactional data modules sup-
port the Single, Apartment, and Both threading models from Table 8.2.
Finally, select the transaction model to use for the data module. Because we won’t be creating
TMTSDataModules in this book, I’ll refer you to the Delphi help for an explanation of the differ-
ent transactional models.
Click OK to create the TMTSDataModule.

Creating a CORBA Remote Data Module


You should create a TCORBADataModule if you want to use the application server to provide
data to clients across a CORBA connection.
To create a TCORBADataModule, select CORBA Data Module from the Multitier page of the New
Items dialog and click OK. The dialog shown in Figure 8.4 appears.

FIGURE 8.4
Creating a CORBA remote data module.

Enter a CoClass name for the data module, such as ContactDataServer, or some other mean-
ingful name.
Select the instancing type and threading models to use for the remote data module. Because
we won’t be creating TCORBADataModules in this book, I’ll refer you to the Delphi help for an
explanation of the valid instancing types and threading models for a CORBA data module.
Click OK to create the TCORBADataModule.

Creating a SOAP Remote Data Module


TSOAPDataModule is used to provide data to clients that are set up to access data from a Web
service.
DataSnap
323

To create a remote data module for a SOAP application server, perform the following steps.
• From Delphi’s main menu, select File, New, Other. Delphi’s New Items dialog is dis-
played. Select the WebServices tab.
• Select SOAP Server Application from the New Items dialog and click OK. The dialog
shown in Figure 8.5 appears.

FIGURE 8.5
Delphi supports five types of SOAP server applications.

• Select the type of SOAP server application to create. If you’re creating a Web App 8
Debugger executable, enter a CoClass name to use for the COM object that the Web App

DATASNAP
Debugger uses to call your Web module. Click OK to create the SOAP Server
Application.
• Again, select File, New, Other from Delphi’s main menu. Select the WebServices tab in
the New Items dialog.
• Select SOAP Server Data Module on the WebServices tab and click OK. The dialog
shown in Figure 8.6 appears.

FIGURE 8.6
Creating a SOAP Server Data Module.

• Enter a class name for the data module and click OK. The class name may be anything
you want, such as MyDataServer, CustomerDataServer, or any other meaningful name.
Chapter 8
324

Placing Components on the Remote Data Module


Regardless of which remote data module you create, it is now time to populate it with compo-
nents.
Basically, the components that go on the remote data module are the same components that
you put on the server-side data module in Chapter 7—namely, the database connection compo-
nent, datasets, and providers.
Because we’re using dbExpress for these examples, this means that you’ll populate the data
module with a TSQLConnection, one or more TSQLDataSets, and one or more
TDataSetProvider components. If you were using BDE, ADO, or a third-party database server
instead of dbExpress, you would use the corresponding third-party connection and dataset
components.

Adding Methods to the Remote Data Module


In addition to serving data in the form of datasets, remote data modules can also provide other
services to a client application. These services might include anything from returning the
server machine’s date and time to calculating pi to 1,000 decimal places.
To add a method to the remote data module, right-click anywhere in the source code editor for
the remote data module. From the context menu, select the Add to Interface menu item. The
Add To Interface dialog appears.
Type the declaration for the new method call in the Declaration edit box and click OK.
Figure 8.7 shows a filled out Add To Interface dialog.

FIGURE 8.7
Adding a method to a remote data module.

When you click OK, Delphi writes an empty method for you, using the declaration you just
entered. At this point, you need to flesh out the method call. The following code snippet shows
the simple GetServerTime method created in Figure 8.7.
function TMethodsDM.GetServerTime: TDateTime;
begin
Result := Now;
end;

Later in this chapter, you’ll see how to call this method from a client application.
DataSnap
325

Callbacks
In addition to receiving calls from a client, the server application can also make calls to a
client application, using a callback interface.

NOTE
This section assumes that you have some knowledge of interfaces, what they are, and
how to use them. If you do not have this knowledge, I suggest picking up a copy of
my book, Delphi COM Programming.

To create a callback interface, select View, Type Library from the Delphi main menu. The type
library editor appears, shown in Figure 8.8.

DATASNAP
FIGURE 8.8
Creating a callback interface.

Click the New Interface button (the first button on the toolbar) and name the new interface
something original like ITestCallback. Now click the New Method toolbar button, and name
the new method Test. To keep things simple, Test won’t take any parameters or return a
result.
Close the type library editor.
Now that the server application knows about ITestCallback, you’ll need a way for the client
to pass an ITestCallback interface to the server. This is accomplished by creating a method
on the server that takes a parameter of type ITestCallback and saves the value in a variable
local to the remote data module, like this:
Chapter 8
326

type
TMethodsDM = class(TRemoteDataModule, IMethodsDM)
...
private
{ Private declarations }
FCallback: ITestCallback;
...
end;
...
procedure TMethodsDM.SetCallback(const Callback: ITestCallback);
begin
FCallback := Callback;
end;

The client application will call this method at some point to set up a callback interface with the
server. The server can then make calls to the interface later.
This technique is illustrated in the Methods sample application, presented later in this chapter
in the section titled “A Complete Example.”

NOTE
Callbacks have a couple of limitations with certain types of connections (discussed
later in this chapter, in the section titled “Creating the Client Application”). First,
when using a socket connection, you must make sure to set
TSocketConnection.SupportsCallbacks to True. In addition, the callback interface
must derive from IDispatch. Second, TWebConnection and TSOAPConnection don’t
support callbacks at all, so keep that in mind if you intend to use HTTP as the commu-
nication protocol between the client and server applications.

Creating the Application Server’s User Interface


Usually, the application server will run on a dedicated server machine that is not being used as
a workstation. Often nobody is sitting at the server to see error messages, enter data, respond
to prompts, and so on. For that reason, the application server generally won’t have much of a
user interface.
However, it is often useful to provide a minimal user interface for the application server to dis-
play information such as how many users are logged in, database statistics, or other pertinent
information. Even if you don’t want to distribute your application with this user interface, it
can still be invaluable while debugging a multitier application to see a list of current connec-
tions to the server.
Figure 8.9 shows an example of such a minimal user interface.
DataSnap
327

FIGURE 8.9
Application server with minimal user interface.

To create this interface, all you need to do is notify the main form each time a remote data
module is created or destroyed. A reasonable place to do this is in the data module’s OnCreate
and OnDestroy event handlers.
Listing 8.1 shows the source code from the main form of the MethodsServer application server
(presented a little later in this chapter). This code is typical of the main form of an application
server.

LISTING 8.1 MethodsServer—MainForm.pas

unit MainForm;

interface 8
uses

DATASNAP
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

const
UM_CONNECT = WM_USER + 1;

type
TfrmMain = class(TForm)
lblConnections: TLabel;
private
{ Private declarations }
FConnections: Integer;
procedure UpdateConnections;
procedure UMConnect(var Msg: TMessage); message UM_CONNECT;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation
Chapter 8
328

LISTING 8.1 Continued


{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);


begin
FConnections := FConnections + Msg.WParam;
UpdateConnections;
end;

procedure TfrmMain.UpdateConnections;
begin
if FConnections = 1 then
lblConnections.Caption := ‘1 connection’
else
lblConnections.Caption := IntToStr(FConnections) + ‘ connections’;
end;

end.

The remote data module then calls the main form’s Connect and Disconnect methods on cre-
ation and destruction, as the following code snippet shows.
procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);
begin
PostMessage(frmMain.Handle, UM_CONNECT, 1, 0); end;

procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);


begin
PostMessage(frmMain.Handle, UM_CONNECT, -1, 0);
end;

Preparing the Application Server for Testing


When you have finished writing the application server, you should install it onto the server
machine and register it for testing. I highly recommend that you test it on your development
machine before moving it to another computer for testing, for a couple of very good reasons:
• When you first start creating application servers, you may find that you make numerous
mistakes. You’ll frequently need to modify, recompile, and redeploy the application
server. It’s a lot easier to do this on your local development machine.
• Your application server may be prone to hang up or crash before it is debugged. It’s a
much better practice to crash your development machine than to crash a production
server.
DataSnap
329

After the application server is debugged locally, you should then move it to a server machine
for further testing and debugging.
For application servers implementing TRemoteDataModule, registering the server is a simple
matter of running it once. Running the server automatically registers it with the computer.
When you modify the application server or move it to a new directory or computer for testing,
you should run it again.
Other remote data modules, such as TMTSDataModule or TCORBADataModule, will require regis-
tering the server with MTS or CORBA.

Creating the Client Application


Now that the application server has been created and registered, it’s time to create the client
application.
The client application is functionally identical to the applications created in the previous chap-
ter, except that it doesn’t contain the server-side data module. In addition, the data module in
the client application will contain a remote connection component, discussed in the following
sections. 8
To get the client application started, select File, New from the Delphi main menu, and then

DATASNAP
select Application (at this time, CLX applications don’t support DataSnap).
Now create a data module, which will house the client datasets that retrieve data from the
application server.

Connecting to a Local Database Connection


In the previous chapter, you learned how to connect to a provider that is on the same form or
data module as a client dataset. You also learned how to call TClientDataSet.SetProvider at
runtime to connect to a provider on a different form or data module.
This section shows you how you can use the TLocalConnection component to connect to
providers on different forms or data modules at design time.
To set up a local connection, do the following:
1. Drop a TLocalConnection on the data module that contains the server-side datasets and
providers. TLocalConnection can be found on the DataSnap page of the component
palette, along with the remote connection components.
2. Add the server-side data module to the uses clause of the client-side data module.
Chapter 8
330

3. In the client-side data module, set the client dataset’s RemoteServer property to the
TLocalConnection component.

4. In the client dataset’s ProviderName property, select the provider from the drop-down
list.
After you complete these steps, all you need to do is open the client dataset to pull the data
from the database into the client dataset.
The LocalConn application example, included with the source code for this book, shows the
correct way to set up an application that uses a local connection to access data. Because almost
no source code is in the application that isn’t generated by Delphi for you, I haven’t listed the
source code here.

Connecting to a Remote Database Connection


Remote connection components allow you to access providers that are located in a separate
executable from the client application. The server application may be located on the same
machine or on a machine half a world away.
Whereas the TLocalConnection component is placed on the server-side data module along
with the dbExpress datasets and providers, remote connection components are placed on the
client’s data module, along with the client dataset components. Delphi supports five types of
remote connections.
The following sections discuss the remote connection components supported by Delphi.

Socket Connections
TSocketConnection uses sockets as a transport between the application server and client appli-
cation. It is the simplest of the connection protocols to set up and use and is the one that is
used in this book’s examples. No additional software or licensing fees are required when using
sockets (other than the standard DataSnap licensing issues, which are discussed in Appendix A
of this book). Also, sockets are supported by any machine that has a TCP/IP address.
The disadvantage of a socket connection is that sockets don’t provide very good support for
security. However, you can work around this limitation by writing a data packet interceptor,
discussed later in this chapter in the section titled “Intercepting Data Packets.”
In addition, sockets can’t always be used to connect to a server located behind a firewall
(unless the firewall is configured to allow access to the port that the socket server listens on,
which is discussed in the following section).
Table 8.3 lists the most important properties for TSocketConnection.
DataSnap
331

TABLE 8.3 TSocketConnection Properties

Property Description
Address Specifies the IP address of the remote machine to connect to. You
may specify the name of the remote machine instead of the IP
address by setting the Host property rather than the Address
property.
Host Specifies the name of the remote machine to connect to. You may
specify the IP address of the remote machine instead of the name
by setting the Address property rather than the Host property.
InterceptGUID Specifies the GUID of a COM object that intercepts data packets
sent between the client and the application server. You may spec-
ify the ProgID of the COM object rather than the GUID by set-
ting the InterceptName property instead of InterceptGUID. Data
interceptors are discussed in the section titled “Intercepting Data
Packets,” later in this chapter.
InterceptName Specifies the ProgID of a COM object that intercepts data pack-
ets sent between the client and the application server. You may
specify the GUID of the COM object rather than the ProgID by 8
setting the InterceptGUID property instead of InterceptName.
Data interceptors are discussed in the section titled “Intercepting

DATASNAP
Data Packets,” later in this chapter.
Port Specifies the port that the socket server uses to listen to socket
requests. By default, this is set to 211.
ServerGUID Specifies the GUID of the application server that the client will
connect to. You can set the server name using the ServerName
property instead of ServerGUID.
ServerName Specifies the name of the application server that the client will
connect to. You can set the server GUID using the ServerGUID
property instead of ServerName.
SupportCallbacks Set to True if your server supports callbacks to the client applica-
tion. If this property is True, WinSock 2 must be present on the
server machine. When False, the server machine does not
require WinSock 2 to operate properly, but it also doesn’t support
callbacks.

The Socket Server


To connect to a remote application server using sockets, you must run a socket server on the
server machine. Although you can write your own socket server if you want, Delphi ships with
a prewritten server named ScktSrvr. Source code for the server is included with the VCL
source, so you can modify it or use it as a basis for writing your own server.
Chapter 8
332

You must run the socket server on the remote machine before attempting to connect to an
application server using sockets. If you don’t run the socket server first, the attempted connec-
tion to the server will appear to hang and will eventually time out.

NOTE
You can install the socket server as a service on a Windows NT machine by
running scktsrvr –install at the command prompt. To remove the service, run
scktsrvr –uninstall. After installing as a service, you must either reboot your com-
puter or start the service manually before it will run the first time.

The Borland socket server usually sits inconspicuously in the tray. However, you can double-
click it to open the socket server, as Figure 8.10 shows.

FIGURE 8.10
Socket server enables you to set the port number and other properties for the socket server.

The default port on which the socket server listens is 211. Unless you have a good reason for
doing so, you should leave it set to the default value. If you change the port, you will also need
to change the TSocketConnection component in any client applications to use the same port.
The intercept GUID can be set to the GUID of a COM object used to intercept data packets
transferred between the client and server. The following section discusses this in more detail.
DataSnap
333

Intercepting Data Packets


Some applications may deal with sensitive data, such as account numbers, and the like. By
default, data transmitted between the server and client applications is not encrypted or com-
pressed in any way, meaning that anyone bright enough to figure out how to listen in on the
socket server port could obtain this data for himself.
When using sockets, you can write a COM object that intercepts data being sent over the
socket connection. This object can compress, encrypt, or otherwise modify the data to keep it
from prying eyes.
To implement an interceptor object, you need to perform the following steps:
1. Write a COM object that implements the IDataIntercept interface. Because Delphi
ships with a demo that shows how to write this object (located in C:\Program Files\
Borland\Delphi6\Demos\Midas\Intrcpt), I won’t duplicate that example in this book.

2. Register the COM object on both the server machine and all client machines.
3. Set the TSocketConnection’s ServerName property to the name of the application server.
4. Inside the socket server, set the Intercept Name to the same name.
5. Run the application as usual. 8
You should debug your application without the interceptor to make sure it works correctly

DATASNAP
before adding the interceptor into the mix.

DCOM Connections
A TDCOMConnection uses Distributed COM (DCOM) as its underlying protocol, tying it to the
Windows operating system. DCOM is a little tricky to set up properly, especially if the appli-
cation server isn’t running on a Windows domain server.
I assume at this point that if you’re going to use TDCOMConnection, you know how to set up
and use DCOM. If you need additional information on configuring DCOM, you can look in
my COM book or see Dan Miser’s Web page at www.distribucon.com.
Table 8.4 lists the most important properties for TDCOMConnection.

TABLE 8.4 TDCOMConnection Properties


Property Description
ComputerName Specifies the name of the remote computer to connect to. If
ComputerName is blank, TDCOMConnection assumes that the application
server is located on the local machine.
ServerGUID Specifies the GUID of the application server that the client will con-
nect to. You can set the server name using the ServerName property
instead of ServerGUID.
Chapter 8
334

TABLE 8.4 Continued


Property Description
ServerName Specifies the name of the application server that the client will connect
to. You can set the server GUID using the ServerGUID property instead
of ServerName.

HTTP Connections
TWebConnection uses HTTP as its transport protocol, which makes it useful for writing appli-
cations that run over the Internet. HTTP offers some additional features over a socket connec-
tion so that you can take advantage of SSL security. Also, you can connect to a server
computer that is located behind a firewall.
When using a TWebConnection, you must be sure to redistribute the Web server application
(httpsrvr.dll) along with the server application. httpsrvr.dll is an ISAPI extension that brokers
the HTTP call from the client to the COM object on the server application. In addition, the
client machine must contain a copy of wininet.dll. Wininet.dll is included with Internet
Explorer version 3 and later.
Table 8.5 lists the most important properties for TWebConnection.

TABLE 8.5 TWebConnection Properties

Property Description
Password Valid password used for authentication on the host machine. This may
be left blank if the host does not require authentication.
Proxy Semicolon-delimited list of proxy servers that can be used to resolve
the IP address of the host machine.
ServerGUID Specifies the GUID of the application server that the client will con-
nect to. You can set the server name using the ServerName property
instead of ServerGUID.
ServerName Specifies the name of the application server that the client will connect
to. You can set the server GUID using the ServerGUID property instead
of ServerName.
URL The URL used to locate httpsrvr on the host machine.
UserName Valid username used for authentication on the host machine. This may
be left blank if the host does not require authentication.
DataSnap
335

SOAP Connections
TSOAPConnection uses SOAP to communicate between the client and server applications.
TSOAPConnection is similar to TWebConnection in that it uses HTTP as the underlying
protocol.
As with TWebConnection, the client machine must contain a copy of wininet.dll.
Table 8.6 lists the most important properties for TSOAPConnection.

TABLE 8.6 TSOAPConnection Properties

Property Description
Password Valid password used for authentication on the host machine. This may
be left blank if the host does not require authentication.
Proxy Semicolon-delimited list of proxy servers that can be used to resolve
the IP address of the host machine.
URL The URL used to locate the application server on the host machine.
UserName Valid username used for authentication on the host machine. This may
be left blank if the host does not require authentication. 8

DATASNAP
CORBA Connections
TCORBAConnection uses CORBA, or IIOP, as its underlying protocol. To establish a CORBA
connection to an application server, you must be running a CORBA Smart Agent somewhere
on your network.
Table 8.7 lists the most important properties for TCORBAConnection.

TABLE 8.7 TCORBAConnection Properties

Property Description
HostName Specifies the machine where the application server is located. If this
property is blank, the connection component will connect to the first
available machine that supports the interface specified by the
RepositoryID property.
ObjectName Set this property if the interface specified by RepositoryID must be
implemented by a specific instance of the object. Leave this property
blank if only one object supports the specified interface.
RepositoryID Specifies the CORBA data module to connect to. May take one of two
forms:
IDL:ProjectName/CorbaDataModuleName:1.0
or
ProjectName/CorbaDataModuleName
Chapter 8
336

A Complete Example
The following example uses a TSocketConnection to connect to the application server
remotely. Also, it demonstrates how to call additional methods on the server and how the
server can use a callback interface to call methods on the client or fire events back to the client
application.
Listing 8.2 contains the source code for the server’s remote data module.

LISTING 8.2 MethodsServer—ServerDataModule.pas

unit ServerDataModule;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses
Windows, Messages, SysUtils, Classes, ComServ, ComObj, VCLCom, DataBkr,
DBClient, MethodsServer_TLB, StdVcl, DBXpress, FMTBcd, DB, SqlExpr,
Provider, Variants;

type
TMethodsDM = class(TRemoteDataModule, IMethodsDM)
conn: TSQLConnection;
sqlContacts: TSQLDataSet;
pvContacts: TDataSetProvider;
procedure RemoteDataModuleCreate(Sender: TObject);
procedure RemoteDataModuleDestroy(Sender: TObject);
private
{ Private declarations }
FCallback: OleVariant;
protected
class procedure UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string); override;
function GetServerTime: TDateTime; safecall;
procedure SetCallback(Callback: OleVariant); safecall;
procedure TestCallbacks; safecall;
public
{ Public declarations }
end;

implementation
DataSnap
337

LISTING 8.2 Continued


uses
MainForm;

{$R *.DFM}

class procedure TMethodsDM.UpdateRegistry(Register: Boolean;


const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end; 8
function TMethodsDM.GetServerTime: TDateTime;

DATASNAP
begin
Result := Now;
end;

procedure TMethodsDM.TestCallbacks;
var
Index: Integer;
begin
if not VarIsEmpty(FCallback) then
for Index := 1 to 3 do
FCallback.Test;
end;

procedure TMethodsDM.SetCallback(Callback: OleVariant);


begin
FCallback := Callback;
end;

procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);


begin
PostMessage(frmMain.Handle, UM_CONNECT, 1, 0);
end;
Chapter 8
338

LISTING 8.2 Continued


procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);
begin
PostMessage(frmMain.Handle, UM_CONNECT, -1, 0);
end;

initialization
TComponentFactory.Create(ComServer, TMethodsDM,
Class_MethodsDM, ciMultiInstance, tmApartment);
end.

Listing 8.3 contains the source code for the main form of the client application.

LISTING 8.3 MethodsClient—MainForm.pas

unit MainForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, Grids, DBGrids, StdCtrls, MethodsServer_TLB, ComObj, ActiveX;

type
TTest = class(TAutoIntfObject, ITestCallback)
protected
procedure Test; safecall;
end;

TfrmMain = class(TForm)
DBGrid1: TDBGrid;
DataSource1: TDataSource;
btnGetServerTime: TButton;
btnTestCallbacks: TButton;
procedure FormCreate(Sender: TObject);
procedure btnGetServerTimeClick(Sender: TObject);
procedure btnTestCallbacksClick(Sender: TObject);
private
{ Private declarations }
FTest: TTest;
public
{ Public declarations }
end;
DataSnap
339

LISTING 8.3 Continued


var
frmMain: TfrmMain;

implementation

uses DataModule;

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);


begin
DM.cdsContacts.Open;
end;

procedure TfrmMain.btnGetServerTimeClick(Sender: TObject);


var
D: TDateTime;
begin
D := DM.SocketConnection1.AppServer.GetServerTime;
ShowMessage(TimeToStr(D)); 8
end;

DATASNAP
procedure TfrmMain.btnTestCallbacksClick(Sender: TObject);
var
typelib: ITypeLib;
begin
OleCheck(LoadRegTypeLib(LIBID_MethodsServer, 1, 0, 0, typelib));
FTest := TTest.Create(typelib, ITestCallback);

DM.SocketConnection1.AppServer.SetCallback(FTest as IDispatch);

DM.SocketConnection1.AppServer.TestCallbacks;
end;

procedure TTest.Test;
begin
ShowMessage(‘In callback’);
end;

end.
Chapter 8
340

The Briefcase Model


Multitier applications lend themselves to something called the briefcase model. When in the
office, the user is presumably connected to the server and is running an application using live
data. Before leaving the office, the user downloads any data that may be needed onto the user’s
local machine, for use while away from the office. The general overview of the briefcase
model is as follows:
When in the office, the user works with live data directly from the application server. While on
the road, the user works with a local copy of the data, making additions or modifications to the
data as needed. Upon return to the office, the user reconnects to the server and reconciles all
updates back to the server.
This magic is performed using the techniques you have already learned in this book. When
connected to the server, dataset providers provide a live copy of the data to the client applica-
tion’s client datasets. Before the user leaves the office, the data is saved locally by calling
TClientDataSet.SaveToFile.

While on the road, the client is unable to connect to the application server; instead, it loads the
local copy of the data by calling TClientDataSet.LoadFromFile. Changes are accumulated in
the change log, which is persisted by calling TClientDataSet.SaveToFile before the user
exits the application.
When the user returns to the office, the client application again calls
TClientDataSet.LoadFromFile to load the local copy of the data, but then calls
TClientDataSet.ApplyUpdates to resolve any updates back to the application server.

Note that the server application does not change in any way—only the client application con-
tains code to support the briefcase model.
Because Delphi ships with a useful briefcase demo application, I won’t duplicate its function-
ality here. Instead, I encourage you to take a look at the Delphi demo. For a typical Delphi
installation, look at C:\Program Files\Borland\Delphi6\Demos\Midas\Brfcase.

NOTE
Rather than calling LoadFromFile and SaveToFile in your application, you can simply
assign the client dataset’s FileName property to the name of the local file to save to
and load from. Delphi will then take care of loading and saving the data at the
appropriate times.
DataSnap
341

Stateless Servers
In the last chapter, I showed you how you can limit the amount of data returned at a given time
from the application server. The catch with limiting the amount of data returned from the
server is that you must let the server know from where to start retrieving data each time it
fetches a new data packet. Delphi database applications are stateless, meaning that the server-
side database components don’t keep track of their current location in the dataset. When the
provider retrieves data from a TSQLDataSet or other dataset component, the dataset is opened,
data is sent to the provider, and then the dataset is closed. Because the dataset is accessed only
for a short period of time, a single server-side dataset can service multiple client applications,
reducing memory load and increasing performance. This isn’t much of an issue with a local
connection, but it becomes an important consideration when using remote connections.
Conceptually, incremental data fetching on a stateless server works like this:
• The provider fetches a set of records, remembering the ID of the last record in the batch.
• The provider notifies the client application of the last record.
• The client remembers the last record fetched.
• When the client fetches the next batch of records from the server, it notifies the server of 8
the last record previously retrieved.

DATASNAP
• The server retrieves the next batch of records that occurs after the last record previously
retrieved.
To implement a stateless server, you need to supply event handlers for three provider events on
the server, as well as for two client dataset events on the client. Let’s start with the server:
In the application server, you need to set up the TSQLDataSet as a parameterized query, like
this:
SELECT * FROM CONTACTS WHERE ID > :MinID ORDER BY ID

This will select only contacts with an ID greater than the last ID that has already been fetched.
Now, provide an event handler for the provider’s OnGetData event. In this event, you’ll save
the last ID retrieved from the contact table.
procedure TStatelessServerData.pvContactsGetData(Sender: TObject;
DataSet: TCustomClientDataSet);
begin
DataSet.Last;
pvContacts.Tag := DataSet.FieldByName(‘ID’).AsInteger;
end;
Chapter 8
342

The call to DataSet.Last does not operate directly on the TSQLDataSet component. Instead, it
moves to the last record of the batch of records that is about to be sent back to the client appli-
cation. The ID of that last fetched record is then saved temporarily in the Tag property of the
provider.
Next, write an event handler for the provider’s AfterGetRecords event handler. In this handler,
you’ll pass the last retrieved ID back to the client application via the OwnerData parameter.
procedure TStatelessServerData.pvContactsAfterGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
OwnerData := pvContacts.Tag;
end;

Finally, on the server, you need to handle the provider’s BeforeGetRecords event handler. In
this event, you’ll set the value of the MinID parameter so that the query knows what record to
fetch next. The following code snippet shows how to accomplish this:
procedure TStatelessServerData.pvContactsBeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
if not VarIsEmpty(OwnerData) then begin
sqlContacts.Params.ParamValues[‘MinID’] := OwnerData;
sqlContacts.Refresh;
end;
end;

That’s all you need to do on the server. In the client application, you need to first provide an
event handler for the client dataset’s AfterGetRecords event. In this event handler, you’ll save
the last ID retrieved from the server, as the following code snippet illustrates:
procedure TForm1.cdsContactsAfterGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
cdsContacts.Tag := OwnerData;
end;

In this example, the last retrieved ID (provided in the OwnerData parameter) is again stored in
the client dataset’s Tag property. You could also set up a form variable named FLastContactID
to save the ID.
Finally, provide an event handler for the client dataset’s BeforeGetRecords event. In this event
handler, you’ll pass the last retrieved ID back to the server so that it knows where to start
fetching data from.
DataSnap
343

procedure TForm1.cdsContactsBeforeGetRecords(Sender: TObject;


var OwnerData: OleVariant);
begin
if cdsContacts.Active then
OwnerData := cdsContacts.Tag;
end;

The Stateless application example incorporates these concepts into a stateless server and client.
Because most of the source code was listed previously in the form of code snippets, I won’t
show it again here.

Sharing a Connection Between Multiple Client


DataSets
Some real-world applications allow the user to choose between connection protocols; for
example, the user may opt for a socket connection, a DCOM connection, or an HTTP connec-
tion. In this way, the client application lets the user make the decision as to which connection
type is most appropriate for a particular situation.
To provide support for multiple connection types, you need to place multiple connection com- 8
ponents on the client’s data module. For example, you may drop a TSocketConnection, a
TDCOMConnection, and a TWebConnection component on the data module. Switching from one

DATASNAP
connection to another means that you need to set the RemoteServer property for each
TClientDataSet component to the correct connection at runtime.

In small sample applications, this isn’t a big problem, because most examples use only one or
two client datasets. However, most real-world applications contain many more than that. For
example, I am currently working on an application that utilizes more than 50 TClientDataSet
components in the client application.
To facilitate switching from one connection component to another, Delphi provides a
TConnectionBroker component. To use a TConnectionBroker, drop one on the client’s data
modules and set the ConnectionBroker property for each TClientDataSet component to the
TConnectionBroker. Then, set TConnectionBroker’s RemoteServer property to the correct
connection component. Switching connections is then a trivial matter of changing the connec-
tion broker’s RemoteServer property to the desired connection component.
In addition to easily switching from connection to connection, you gain another advantage by
using a connection broker. If you make calls to the application server in your code, such as the
following:
SocketConnection1.AppServer.ExecuteSomeMethod;
Chapter 8
344

You don’t need to change that code if the connection type changes. Instead of calling the
method on SocketConnection1 (or any other connection component), you can call it on the
connection broker, like this:
ConnectionBroker1.Connection.AppServer.ExecuteSomeMethod;

In this way, the code works regardless of the active connection.

Brokering Connections Between Multiple Servers


In large-scale development, a single instance of the application server may not be enough to
handle the large number of clients connected to it. You also may want to run a copy of the
application server on multiple server machines so that if one machine goes down, the client can
still connect to an application server running on a different server machine.
When running multiple copies of the application server, the client needs some way of choosing
between them at runtime. If it selects a server that is down at the moment, it should try again
with a different server.
To incorporate connection brokering into your application, drop a TSimpleObjectBroker com-
ponent onto your client’s data module. In the Servers property, enter a list of servers that the
client application can attempt to connect to. Set the connection object’s ObjectBroker property
to the name of the TSimpleObjectBroker.
When the TSimpleObjectBroker’s LoadBalanced property is set to False, the client will
attempt to connect to the first server in the Servers property. If that connection fails, it will try
the second server, and so on. When LoadBalanced is True, the broker attempts to select a dif-
ferent server for each connection in the current client application only. If the client application
contains a single remote connection, setting LoadBalanced to True has no effect.
A more common scenario is to have several instances of the application server running and a
number of clients that connect to the server. In this case, your best bet is to randomize the list
of servers at runtime so that each client has the potential of connecting to a different server.
For example, suppose that you have three servers, named Server1, Server2, and Server3. If
there are six instances of the client application, and you randomize the order of the servers for
each client, potentially each client’s server order could be the following:
Client 1: Server1, Server2, Server3
Client 2: Server1, Server3, Server2
Client 3: Server2, Server1, Server3
Client 4: Server2, Server3, Server1
DataSnap
345

Client 5: Server3, Server1, Server2


Client 6: Server3, Server2, Server1
Assuming that all servers are up and running, this will balance the load equally over the three
servers, with two clients connected to each server.

Summary
This chapter discussed DataSnap, Delphi’s remote access technology for multitier database
development.
• The first step in creating a separate application server is to set up one or more remote
data modules. Delphi supports the creation of standard, MTS, CORBA, and SOAP
remote data modules.
• To create methods for use with an application server, use the Add to Interface menu item.
To create a callback interface, use the Type Library editor to create a new interface.
• To create a simple user interface for the application server, post a custom message to the
main form whenever a remote data module is created or destroyed. A reasonable place to
do this is in the data module’s OnCreate and OnDestroy event handlers.
8
• You can connect to the application server from a client application, using a variety of

DATASNAP
connection protocols, including TSocketConnection, TDCOMConnection,
TWebConnection, TSOAPConnection, and TCORBAConnection.

• The briefcase model allows your application to download data to the local machine for
use when it is impossible to connect to the server.
• To facilitate the use of multiple application servers in a large-scale application, Delphi
provides the TConnectionBroker component.
The next chapter introduces a complete application that uses many of the techniques discussed
in this chapter and throughout the rest of the book.
The ConMan Application CHAPTER

9
IN THIS CHAPTER
• What Is ConMan? 348

• Database Structure 349

• Overview of the Code 352

• The Server Application 352

• The Client Application 358

• Room for Improvement 373


Chapter 9
348

In this book, I’ve discussed quite a number of techniques for writing database applications in
Delphi and Kylix. Along the way, I’ve presented a number of small sample applications to
solidify many of the important points discussed in each chapter.
In this chapter, I’ll present a complete multiuser database application that incorporates many of
the ideas discussed in this book. The application is called ConMan (short for Contact
Manager).

What Is ConMan?
ConMan is a rather simple contact manager that can be used to remember names, addresses,
and phone numbers of important clients, friends, and family. You can also enter notes for each
contact and store reminders.
Multiple users can run this application simultaneously, and provisions in the application allow
a user to store the database onto a local machine to use while away from the office (briefcase
model).
Although ConMan demonstrates many important multiuser database concepts, it isn’t going to
replace your production-quality contact manager (such as ACT! or GoldMine) anytime soon.
Many important features of a commercial contact manager are missing from ConMan, includ-
ing the capability to dial the phone, automatically write letters, faxes, and memos using your
favorite word processor, and so on. The intent of ConMan is not to write a salable product.
After all, if that were the case, I wouldn’t be including the source code for free in a database
book. Rather, the intent is to show a real-life application that makes use of important Delphi
database technologies to give you a concrete understanding of how you might take advantage
of the same technologies in your own applications.
Figure 9.1 shows ConMan at runtime.
Because ConMan takes advantage of technologies not yet supported by Kylix—such as
DataSnap—it is presented as a VCL application only.
The ConMan Application
349

FIGURE 9.1
ConMan displays information about a contact, as well as his or her picture, notes, and scheduled todos.

Database Structure
You have already accessed some of the tables in the ConMan database through the sample pro-
grams provided with earlier chapters. Listing 9.1 contains the partial SQL script used to create
a new, empty version of the CONMAN database.
The Contacts table is where company names, addresses, phone numbers, and the like are
stored. One record is created for each contact, so if multiple contacts exist for the same com- 9
pany, the company data is stored with each contact.

THE CONMAN
APPLICATION
NOTE
In reality, this database should also contain a Companies table, with each contact
holding a reference back to the company to which he or she is associated. To keep
the size of this application manageable, I elected to store both company and contact
data in the Contacts table.
Chapter 9
350

The Todos table stores reminders for each contact. For example, you might want to store a
reminder to phone a certain contact on a given date to place an order. The Todos table and
Contacts table are linked through the Todos table’s ContactID field.

In addition to the tables, the database contains a stored procedure named ContactsByState.
This procedure is provided for use in the sample applications shown in Chapters 1 and 2; it
won’t be used in the ConMan application.
Finally, the script creates a generator used to populate the primary keys of the Contacts and
Todos tables and two triggers that run when a new contact and todo are entered, respectively.
The triggers ensure that new contacts and todos always receive a unique primary key.

LISTING 9.1 ConMan.SQL


/* Table: CONTACTS, Owner: SYSDBA */

CREATE TABLE CONTACTS


(
CONTACTID INTEGER NOT NULL,
FIRST VARCHAR(20),
LAST VARCHAR(30),
DEAR VARCHAR(40),
TITLE VARCHAR(30),
COMPANYNAME VARCHAR(50),
ADDRESS1 VARCHAR(50),
ADDRESS2 VARCHAR(50),
CITY VARCHAR(30),
STATE VARCHAR(20),
POSTALCODE VARCHAR(10),
COUNTRY VARCHAR(30),
PHONE VARCHAR(20),
FAX VARCHAR(20),
CELLULAR VARCHAR(20),
PAGER VARCHAR(20),
EMAIL VARCHAR(40),
IMAGE BLOB SUB_TYPE 0 SEGMENT SIZE 4096,
NOTES BLOB SUB_TYPE TEXT SEGMENT SIZE 4096,
PRIMARY KEY (CONTACTID)
);

CREATE UNIQUE INDEX IX_CONNAME ON CONTACTS (LAST, FIRST);

CREATE TABLE TODOS


(
The ConMan Application
351

LISTING 9.1 Continued


TODOID INTEGER NOT NULL,
CONTACTID INTEGER NOT NULL,
DESCRIPTION VARCHAR(50),
SCHEDULED TIMESTAMP,
COMPLETED TIMESTAMP,
PRIMARY KEY (TODOID),
FOREIGN KEY (CONTACTID) REFERENCES CONTACTS (CONTACTID) ON DELETE CASCADE
);

/* Generators */
CREATE GENERATOR ID_GENERATOR;

SET TERM ^ ;

/* Stored Procedures */

CREATE PROCEDURE ID_GEN


RETURNS (
AVALUE INTEGER
)
AS
BEGIN
AValue = GEN_ID(ID_GENERATOR, 1);
END ^

CREATE TRIGGER CONTACTS_INSERT FOR CONTACTS


ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
IF (New.CONTACTID IS NULL) THEN
9
New.CONTACTID = GEN_ID(ID_GENERATOR, 1);

THE CONMAN
APPLICATION
END ^

CREATE TRIGGER TODOS_INSERT FOR TODOS


ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
IF (New.TODOID IS NULL) THEN
New.TODOID = GEN_ID(ID_GENERATOR, 1);
END ^

SET TERM ; ^
Chapter 9
352

Overview of the Code


ConMan is separated into an application server and a client, providing true multitier function-
ality.
The entire source code for ConMan is contained in seven units—two of which Delphi automat-
ically writes for us. The server-side source units include the following:
• MainForm.pas Contains the code for the main form of the server (which does nothing
but show us the number of clients currently connected).
• RemoteDataModule.pas Houses the remote data module where the database connection
components reside.
• ConManServer_TLB.pas Type library import file for the application server. Delphi auto-
matically creates and maintains this unit.
The client application is composed of the following units:
• DataModule.pas Client-side data module, including DataSnap components and
TClientDataSets.

• RecErrorForm.pas Form to handle reconciliation errors. This form is automatically


generated by Delphi (see Chapter 7 for more information on handling reconciliation
errors).
• MainForm.pas The application’s main form.
• TodoForm.pas A form that facilitates adding or editing a todo.
The following sections describe the server- and client-side applications in more detail.

The Server Application


As noted in the preceding section, the application server consists of three source files, only two
of which you write code for.
Listing 9.2 contains the source code for the application server’s remote data module. As you
can see from Figure 9.2, the remote data module contains six components. These are
• conn A TSQLConnection component for connecting to the database.
• sqlContacts A TSQLDataSet component for retrieving data from the Contacts table.
• sqlTodos A TSQLDataSet component for retrieving data from the Todos table.
• sqlID A TSQLDataSet that calls the stored procedure ID_GEN in the database to retrieve
a unique ID for each inserted record.
The ConMan Application
353

• dsContacts A TDataSource component used to establish a server-side master/detail


relationship between sqlContacts and sqlTodos.
• pvContacts A TDataSetProvider component used to provide contacts and todo
records to the client application. Because the contacts and todos are connected in a mas-
ter/detail relationship, only one provider is needed at the master level (refer to Chapter 7
if you need to revisit server-side master/detail relationships).
Figure 9.2 shows the remote data module at design time.

FIGURE 9.2
The remote data module and its data access components.

LISTING 9.2 ConManServer—RemoteDataModule.pas


unit RemoteDataModule;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses
Windows, Messages, SysUtils, Classes, ComServ, ComObj, VCLCom, DataBkr, 9
DBClient, ConManServer_TLB, StdVcl, DBXpress, FMTBcd, DB, SqlExpr,

THE CONMAN
Provider;
APPLICATION
type
TConManDataServer = class(TRemoteDataModule, IConManDataServer)
conn: TSQLConnection;
sqlContacts: TSQLDataSet;
sqlTodos: TSQLDataSet;
dsContacts: TDataSource;
pvContacts: TDataSetProvider;
sqlID: TSQLDataSet;
sqlContactsCONTACTID: TIntegerField;
sqlContactsFIRST: TStringField;
sqlContactsLAST: TStringField;
sqlContactsDEAR: TStringField;
Chapter 9
354

LISTING 9.2 Continued


sqlContactsTITLE: TStringField;
sqlContactsCOMPANYNAME: TStringField;
sqlContactsADDRESS1: TStringField;
sqlContactsADDRESS2: TStringField;
sqlContactsCITY: TStringField;
sqlContactsSTATE: TStringField;
sqlContactsPOSTALCODE: TStringField;
sqlContactsCOUNTRY: TStringField;
sqlContactsPHONE: TStringField;
sqlContactsFAX: TStringField;
sqlContactsCELLULAR: TStringField;
sqlContactsPAGER: TStringField;
sqlContactsEMAIL: TStringField;
sqlContactsIMAGE: TBlobField;
sqlContactsNOTES: TMemoField;
sqlTodosTODOID: TIntegerField;
sqlTodosCONTACTID: TIntegerField;
sqlTodosDESCRIPTION: TStringField;
sqlTodosSCHEDULED: TSQLTimeStampField;
sqlTodosCOMPLETED: TSQLTimeStampField;
procedure RemoteDataModuleCreate(Sender: TObject);
procedure RemoteDataModuleDestroy(Sender: TObject);
procedure pvContactsBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
private
{ Private declarations }
function GetNextID: Integer;
protected
class procedure UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string); override;
public
{ Public declarations }
end;

implementation

uses MainForm;

resourcestring
SDatabaseIsOpen = ‘Cannot perform this operation on an open database’;

{$R *.DFM}
The ConMan Application
355

LISTING 9.2 Continued


class procedure TConManDataServer.UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;

procedure TConManDataServer.RemoteDataModuleCreate(Sender: TObject);


begin
PostMessage(frmMain.Handle, UM_CONNECT, 1, 0);
end;

procedure TConManDataServer.RemoteDataModuleDestroy(Sender: TObject);


begin
PostMessage(frmMain.Handle, UM_CONNECT, -1, 0);
end;

procedure TConManDataServer.pvContactsBeforeUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
begin
9
if UpdateKind = ukInsert then

THE CONMAN
APPLICATION
if SourceDS = sqlContacts then begin
if DeltaDS.FieldByName(‘CONTACTID’).OldValue <= 0 then
DeltaDS.FieldByName(‘CONTACTID’).NewValue := GetNextID;
end else begin
if DeltaDS.FieldByName(‘TODOID’).OldValue <= 0 then
DeltaDS.FieldByName(‘TODOID’).NewValue := GetNextID;
end;
end;

function TConManDataServer.GetNextID: Integer;


begin
sqlID.ExecSQL;
Result := sqlID.ParamByName(‘AValue’).AsInteger;
Chapter 9
356

LISTING 9.2 Continued


end;

initialization
TComponentFactory.Create(ComServer, TConManDataServer,
Class_ConManDataServer, ciMultiInstance, tmApartment);
end.

Notice that the RemoteDataModuleCreate and RemoteDataModuleDestroy methods post a


UM_CONNECT message to the main form. You’ll see in Listing 9.3 how the main form responds
to that message to update its user interface accordingly.
pvContactsBeforeUpdateRecord is called automatically just before updates are made to the
database. Because sqlContacts and sqlTodos are connected in a master/detail relationship,
this method is called for both datasets. If a new record is being inserted, the code checks to see
if the primary key for that record is less than or equal to zero (you’ll see on the client side that
primary keys are generated with a negative value, which I’ll explain when I discuss the client-
side code). If so, the code calls the GetNextID method, which executes the GEN_ID stored pro-
cedure to retrieve the next available ID for the record.
Listing 9.3 contains the source code for the application server’s main form. As discussed in
Chapter 8, application servers often don’t display a main form, but I’ve found that it is often
useful to display a small main form that shows the number of active connections.

LISTING 9.3 ConManServer—MainForm.pas


unit MainForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, ComCtrls;

const
UM_CONNECT = WM_USER + 101;

type
TfrmMain = class(TForm)
StatusBar1: TStatusBar;
pnlConnections: TPanel;
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
private
The ConMan Application
357

LISTING 9.3 Continued


{ Private declarations }
FConnections: Integer;
procedure UMConnect(var Msg: TMessage); message UM_CONNECT;
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

resourcestring
SOneConnection = ‘1 Connection’;
SConnections = ‘%d Connections’;

SHeapAllocated = ‘%s bytes allocated’;

{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);


begin
Inc(FConnections, Msg.WParam);
if FConnections = 1 then
pnlConnections.Caption := SOneConnection
else
pnlConnections.Caption := Format(SConnections, [FConnections]);
end; 9

THE CONMAN
APPLICATION
procedure TfrmMain.Timer1Timer(Sender: TObject);
var
HS: THeapStatus;
begin
HS := GetHeapStatus;

StatusBar1.SimpleText := Format(SHeapAllocated,
[FloatToStrF(HS.TotalAllocated, ffNumber, 10, 0)]);
end;

end.
Chapter 9
358

Listing 9.3 defines a user-defined message, UM_CONNECT. The remote data module posts this
message to the main form on creation and destruction. The UMConnect method fires in response
to the UM_CONNECT message and updates a label showing the number of current connections to
the application server.
The only other code in Listing 9.3 is the Timer1Timer method, which fires every second to dis-
play the amount of RAM currently being used by the application server.
Figure 9.3 shows the application server at runtime.

FIGURE 9.3
The application server with a minimal user interface.

The Client Application


With the application server out of the way, we can now turn our attention to the client applica-
tion. The first unit that we’ll look at is the data module. Listing 9.4 contains the source code
for the client-side data module.

LISTING 9.4 ConMan—DataModule.pas


unit DataModule;

interface

uses
SysUtils, Classes, SConnect, DB, DBClient, MConnect, Dialogs;

type
TDM = class(TDataModule)
SocketConnection1: TSocketConnection;
cdsContacts: TClientDataSet;
cdsTodos: TClientDataSet;
cdsContactsCONTACTID: TIntegerField;
cdsContactsFIRST: TStringField;
cdsContactsLAST: TStringField;
cdsContactsDEAR: TStringField;
cdsContactsTITLE: TStringField;
cdsContactsCOMPANYNAME: TStringField;
cdsContactsADDRESS1: TStringField;
The ConMan Application
359

LISTING 9.4 Continued


cdsContactsADDRESS2: TStringField;
cdsContactsCITY: TStringField;
cdsContactsSTATE: TStringField;
cdsContactsPOSTALCODE: TStringField;
cdsContactsCOUNTRY: TStringField;
cdsContactsPHONE: TStringField;
cdsContactsFAX: TStringField;
cdsContactsCELLULAR: TStringField;
cdsContactsPAGER: TStringField;
cdsContactsEMAIL: TStringField;
cdsContactsIMAGE: TBlobField;
cdsContactsNOTES: TMemoField;
cdsContactssqlTodos: TDataSetField;
cdsTodosTODOID: TIntegerField;
cdsTodosCONTACTID: TIntegerField;
cdsTodosDESCRIPTION: TStringField;
cdsTodosSCHEDULED: TSQLTimeStampField;
cdsTodosCOMPLETED: TSQLTimeStampField;
cdsContactsFullName: TStringField;
procedure DataModuleCreate(Sender: TObject);
procedure SocketConnection1BeforeConnect(Sender: TObject);
procedure cdsContactsReconcileError(DataSet: TCustomClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
procedure cdsContactsCalcFields(DataSet: TDataSet);
procedure DataModuleDestroy(Sender: TObject);
procedure cdsContactsNewRecord(DataSet: TDataSet);
procedure cdsTodosNewRecord(DataSet: TDataSet);
private
{ Private declarations }
9
public

THE CONMAN
APPLICATION
{ Public declarations }
function GetNextID(DataSet: TCustomClientDataSet;
const PrimaryKey: string): Integer;
end;

var
DM: TDM;

implementation

uses RecErrorForm;
Chapter 9
360

LISTING 9.4 Continued


resourcestring
SConnectCaption = ‘Database Server’;
SConnectPrompt = ‘Server:’;

{$R *.dfm}

procedure TDM.DataModuleCreate(Sender: TObject);


begin
cdsContacts.Open;
end;

procedure TDM.DataModuleDestroy(Sender: TObject);


begin
cdsContacts.Close;
SocketConnection1.Close;
end;

// Dataset events

procedure TDM.cdsContactsNewRecord(DataSet: TDataSet);


begin
DataSet.FieldByName(‘CONTACTID’).AsInteger :=
GetNextID(DataSet as TCustomClientDataSet, ‘CONTACTID’);
end;

procedure TDM.cdsContactsCalcFields(DataSet: TDataSet);


begin
DataSet.FieldByName(‘FullName’).AsString :=
DataSet.FieldByName(‘FIRST’).AsString + ‘ ‘ +
DataSet.FieldByName(‘LAST’).AsString;
end;

procedure TDM.cdsContactsReconcileError(DataSet: TCustomClientDataSet;


E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E);
end;

procedure TDM.cdsTodosNewRecord(DataSet: TDataSet);


begin
DataSet.FieldByName(‘TODOID’).AsInteger :=
GetNextID(DataSet as TCustomClientDataSet, ‘TODOID’);
end;
The ConMan Application
361

LISTING 9.4 Continued


function TDM.GetNextID(DataSet: TCustomClientDataSet;
const PrimaryKey: string): Integer;
var
CloneDS: TClientDataSet;
begin
CloneDS := TClientDataSet.Create(nil);
try
CloneDS.CloneCursor(DataSet, False);
CloneDS.IndexFieldNames := PrimaryKey;
CloneDS.First;
if CloneDS.FieldByName(PrimaryKey).AsInteger > 0 then
Result := -1
else
Result := CloneDS.FieldByName(PrimaryKey).AsInteger - 1;
finally
CloneDS.Free;
end;
end;

// Connection events

procedure TDM.SocketConnection1BeforeConnect(Sender: TObject);


var
Server: string;
begin
if InputQuery(SConnectCaption, SConnectPrompt, Server) then
SocketConnection1.Address := Server;
end;

end.
9

THE CONMAN
The first two methods of interest are the DataModuleCreate and DataModuleDestroy methods. APPLICATION
DataModuleCreate opens the cdsContacts table, which attempts to load local data from the
file CONMAN.CDS because the component’s FileName property is set to CONMAN.CDS. If
the file does not exist, the program will display a blank screen—it doesn’t attempt to connect
to the application server automatically.
DataModuleDestroy closes the cdsContacts table and also the connection to the application
server, if a connection was established. Because the cdsContacts.FileName property is set, the
program automatically writes the data to the file CONMAN.CDS. In addition, the todo dataset
is stored in the same CDS file because it is a nested dataset of cdsContacts.
Chapter 9
362

The cdsContactsNewRecord and cdsTodosNewRecord events make a call to the local method
GetNextID. GetNextID clones the dataset in question and moves to the first record in the
dataset. The first record is the record with the lowest numbered ID. It then assigns the next
lower number to the ID of that record, ensuring that the record has a negative primary key.
The reason this is done is to ensure that all records that have not yet been added to the data-
base have a negative ID. The server-side data module checks for a negative ID and then calls
the database’s GEN_ID stored procedure to assign a unique ID. This ensures that all added
records receive a unique ID, regardless of what computer they were added from. (Remember
that multiple users may be running this application at the same time.)

NOTE
An alternative to using the cloned dataset to retrieve the next negative ID is to set up
a private variable and retrieve the next ID at program startup. That way you can sim-
ply decrement the variable to obtain the next ID. The reason I don’t do this is because
in real-world applications I sometimes have 40 or 50 datasets in my data module. I
don’t like to set up 40 or 50 variables—one for each dataset. Instead, I use the com-
mon function to retrieve the next ID on the fly.
Note that cloning a dataset does temporarily require a small amount of memory, and
also takes a small amount of time to do. If you need to generate temporary IDs as
quickly as possible, you should consider using a private variable instead.

Listing 9.5 contains the source code for the client’s main form, which is where the bulk of the
code for this application lies.

LISTING 9.5 ConMan—MainForm.pas


unit MainForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DBActns, StdActns, ActnList, ImgList, ActnCtrls, ToolWin,
ActnMan, ActnMenus, ComCtrls, BandActn, ExtCtrls, Menus, Grids, DBGrids,
DB, StdCtrls, DBCtrls, Mask, ExtDlgs;

type
TfrmMain = class(TForm)
StatusBar1: TStatusBar;
ImageList1: TImageList;
The ConMan Application
363

LISTING 9.5 Continued


MainMenu1: TMainMenu;
ActionList1: TActionList;
FileExit1: TFileExit;
EditCut1: TEditCut;
EditCopy1: TEditCopy;
EditPaste1: TEditPaste;
EditSelectAll1: TEditSelectAll;
EditUndo1: TEditUndo;
EditDelete1: TEditDelete;
DataSetFirst1: TDataSetFirst;
DataSetPrior1: TDataSetPrior;
DataSetNext1: TDataSetNext;
DataSetLast1: TDataSetLast;
DataSetInsert1: TDataSetInsert;
DataSetDelete1: TDataSetDelete;
DataSetEdit1: TDataSetEdit;
DataSetPost1: TDataSetPost;
DataSetCancel1: TDataSetCancel;
DataSetRefresh1: TDataSetRefresh;
File1: TMenuItem;
Exit1: TMenuItem;
Edit1: TMenuItem;
Copy1: TMenuItem;
Cut1: TMenuItem;
Paste1: TMenuItem;
SelectAll1: TMenuItem;
Delete1: TMenuItem;
Undo1: TMenuItem;
N1: TMenuItem;
ToolBar1: TToolBar;
9
ToolButton1: TToolButton;

THE CONMAN
APPLICATION
ToolButton2: TToolButton;
ToolButton3: TToolButton;
pnlClient: TPanel;
PageControl1: TPageControl;
tabGrid: TTabSheet;
tabForm: TTabSheet;
gridContacts: TDBGrid;
pnlTodos: TPanel;
dsContacts: TDataSource;
dsTodos: TDataSource;
PageControl2: TPageControl;
tabTodos: TTabSheet;
tabNotes: TTabSheet;
gridTodos: TDBGrid;
Chapter 9
364

LISTING 9.5 Continued


memoNotes: TDBMemo;
Dataset1: TMenuItem;
First1: TMenuItem;
Prior1: TMenuItem;
Next1: TMenuItem;
Last1: TMenuItem;
N2: TMenuItem;
Insert1: TMenuItem;
Edit2: TMenuItem;
Post1: TMenuItem;
Delete2: TMenuItem;
Cancel1: TMenuItem;
N3: TMenuItem;
Refresh1: TMenuItem;
FileConnect1: TAction;
ConnecttoDatabaseServer1: TMenuItem;
N4: TMenuItem;
DataSetNumUpdates: TAction;
DataSetApplyUpdates: TAction;
DataSetCancelUpdates: TAction;
N5: TMenuItem;
ApplyUpdates1: TMenuItem;
CancelUpdates1: TMenuItem;
popupTodos: TPopupMenu;
Markdone1: TMenuItem;
TodoMarkDone1: TAction;
TodoAdd: TAction;
AddTodo1: TMenuItem;
N6: TMenuItem;
btnLoadImage: TButton;
btnClearImage: TButton;
ImageLoad1: TAction;
ImageClear1: TAction;
OpenPictureDialog1: TOpenPictureDialog;
ToolButton4: TToolButton;
ToolButton5: TToolButton;
ToolButton6: TToolButton;
ToolButton7: TToolButton;
ToolButton8: TToolButton;
ToolButton9: TToolButton;
ToolButton10: TToolButton;
ToolButton11: TToolButton;
ToolButton12: TToolButton;
ToolButton13: TToolButton;
ToolButton14: TToolButton;
The ConMan Application
365

LISTING 9.5 Continued


ToolButton16: TToolButton;
ToolButton17: TToolButton;
imgPhoto: TImage;
TodoEdit: TAction;
TodoDelete: TAction;
EditTodo1: TMenuItem;
PageControl3: TPageControl;
tabClientGeneral: TTabSheet;
tabClientAddress: TTabSheet;
tabClientPhones: TTabSheet;
Label1: TLabel;
ecCompanyName: TDBEdit;
Label2: TLabel;
ecFirst: TDBEdit;
Label3: TLabel;
ecLast: TDBEdit;
Label4: TLabel;
ecDear: TDBEdit;
Label5: TLabel;
ecTitle: TDBEdit;
Label6: TLabel;
ecAddress1: TDBEdit;
ecAddress2: TDBEdit;
Label7: TLabel;
Label8: TLabel;
ecCity: TDBEdit;
ecState: TDBEdit;
Label9: TLabel;
Label10: TLabel;
ecPostalCode: TDBEdit;
9
ecCountry: TDBEdit;

THE CONMAN
APPLICATION
Label11: TLabel;
Label12: TLabel;
ecPhone: TDBEdit;
Label13: TLabel;
ecFax: TDBEdit;
ecCellular: TDBEdit;
Label14: TLabel;
Label15: TLabel;
ecPager: TDBEdit;
ecEmail: TDBEdit;
Label16: TLabel;
DeleteTodo1: TMenuItem;
procedure FileConnect1Update(Sender: TObject);
Chapter 9
366

LISTING 9.5 Continued


procedure FileConnect1Execute(Sender: TObject);
procedure StatusBar1DrawPanel(StatusBar: TStatusBar;
Panel: TStatusPanel; const Rect: TRect);
procedure DataSetNumUpdatesUpdate(Sender: TObject);
procedure DataSetApplyUpdatesExecute(Sender: TObject);
procedure DataSetCancelUpdatesExecute(Sender: TObject);
procedure OnHaveUpdates(Sender: TObject);
procedure TodoMarkDone1Execute(Sender: TObject);
procedure TodoAddExecute(Sender: TObject);
procedure TodoMarkDone1Update(Sender: TObject);
procedure ImageLoad1Execute(Sender: TObject);
procedure ImageClear1Execute(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure dsContactsDataChange(Sender: TObject; Field: TField);
procedure FormCreate(Sender: TObject);
procedure TodoEditExecute(Sender: TObject);
procedure TodoEditUpdate(Sender: TObject);
procedure TodoDeleteExecute(Sender: TObject);
private
procedure DoConnectDisconnect(Sender: TObject);
{ Private declarations }
public
{ Public declarations }
end;

var
frmMain: TfrmMain;

implementation

uses DataModule, TodoForm;

resourcestring
SConnect = ‘&Connect to Database Server’;
SDisconnect = ‘&Disconnect from Database Server’;

SOneUpdate = ‘1 update pending’;


SUpdates = ‘%d updates pending’;

SOnline = ‘Online’;
SOffline = ‘Offline’;

SChangesPending = ‘The current contact has been changed. ‘ +


‘Do you want to save changes to this record?’;
The ConMan Application
367

LISTING 9.5 Continued


{$R *.dfm}

// Form event handlers

procedure TfrmMain.FormCreate(Sender: TObject);


begin
DM.SocketConnection1.AfterConnect := DoConnectDisconnect;
DM.SocketConnection1.AfterDisconnect := DoConnectDisconnect;
end;

procedure TfrmMain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);


begin
if DM.cdsContacts.State <> dsBrowse then begin
case MessageDlg(SChangesPending, mtWarning, [mbYes, mbNo, mbCancel], 0) of
mrYes: DM.cdsContacts.Post;
mrNo: DM.cdsContacts.Cancel;
mrCancel: CanClose := False;
end;
end;
end;

procedure TfrmMain.DoConnectDisconnect(Sender: TObject);


begin
// Repaint the status bar to reflect the new connection status
StatusBar1.Invalidate;
end;

// Status bar event handlers

procedure TfrmMain.StatusBar1DrawPanel(StatusBar: TStatusBar; 9


Panel: TStatusPanel; const Rect: TRect);

THE CONMAN
APPLICATION
begin
if Panel.Index = 0 then begin
if DM.SocketConnection1.Connected then begin
StatusBar.Canvas.Font.Color := clGreen;
StatusBar.Canvas.TextRect(Rect, Rect.Left + 2, Rect.Top + 1, SOnline);
end else begin
StatusBar.Canvas.Font.Color := clRed;
StatusBar.Canvas.TextRect(Rect, Rect.Left + 2, Rect.Top + 1, SOffline);
end;
end;
end;
Chapter 9
368

LISTING 9.5 Continued


// Database component event handlers

procedure TfrmMain.dsContactsDataChange(Sender: TObject; Field: TField);


var
BlobStream: TStream;
begin
if (Field = nil) or (Field = DM.cdsContactsIMAGE) then begin
BlobStream := DM.cdsContacts.CreateBlobStream(DM.cdsContactsIMAGE, bmRead);
try
if BlobStream.Size = 0 then
imgPhoto.Picture := nil
else
imgPhoto.Picture.Bitmap.LoadFromStream(BlobStream);
finally
BlobStream.Free;
end;
end;
end;

// Image controls

procedure TfrmMain.ImageLoad1Execute(Sender: TObject);


begin
if OpenPictureDialog1.Execute then begin
DM.cdsContacts.Edit;
DM.cdsContactsIMAGE.LoadFromFile(OpenPictureDialog1.FileName);
end;
end;

procedure TfrmMain.ImageClear1Execute(Sender: TObject);


begin
DM.cdsContacts.Edit;
DM.cdsContactsIMAGE.Clear;
end;

// File menu

procedure TfrmMain.FileConnect1Execute(Sender: TObject);


begin
DM.SocketConnection1.Connected := not DM.SocketConnection1.Connected;
end;

procedure TfrmMain.FileConnect1Update(Sender: TObject);


The ConMan Application
369

LISTING 9.5 Continued


begin
with Sender as TAction do begin
if DM.SocketConnection1.Connected then
Caption := SDisconnect
else
Caption := SConnect
end;
end;

// Dataset menu

procedure TfrmMain.DataSetApplyUpdatesExecute(Sender: TObject);


begin
if DM.cdsContacts.ApplyUpdates(0) = 0 then
DM.cdsContacts.Refresh;
end;

procedure TfrmMain.DataSetCancelUpdatesExecute(Sender: TObject);


begin
DM.cdsContacts.CancelUpdates;
end;

procedure TfrmMain.OnHaveUpdates(Sender: TObject);


begin
(Sender as TAction).Enabled := (DM.cdsContacts.ChangeCount > 0);
end;

procedure TfrmMain.DataSetNumUpdatesUpdate(Sender: TObject);


begin
if DM.cdsContacts.ChangeCount = 1 then 9
StatusBar1.Panels[1].Text := SOneUpdate

THE CONMAN
APPLICATION
else
StatusBar1.Panels[1].Text := Format(SUpdates,
[DM.cdsContacts.ChangeCount]);
end;

// Todo popup menu

procedure TfrmMain.TodoAddExecute(Sender: TObject);


var
frmTodo: TfrmTodo;
DT: TDateTime;
Chapter 9
370

LISTING 9.5 Continued


begin
frmTodo := TfrmTodo.Create(nil);
try
if frmTodo.ShowModal = mrOk then begin
DM.cdsTodos.Append;
DM.cdsTodosDescription.AsString := frmTodo.ecDescription.Text;
DT := frmTodo.dtDate.Date;
ReplaceTime(DT, frmTodo.dtTime.Time);
DM.cdsTodosScheduled.AsDateTime := DT;
DM.cdsTodos.Post;
end;
finally
frmTodo.Free;
end;
end;

procedure TfrmMain.TodoEditExecute(Sender: TObject);


var
frmTodo: TfrmTodo;
DT: TDateTime;
begin
frmTodo := TfrmTodo.Create(nil);
try
frmTodo.ecDescription.Text := DM.cdsTodosDescription.AsString;
frmTodo.dtDate.Date := DM.cdsTodosScheduled.AsDateTime;
frmTodo.dtTime.Time := DM.cdsTodosScheduled.AsDateTime;
if frmTodo.ShowModal = mrOk then begin
DM.cdsTodos.Edit;
DM.cdsTodosDescription.AsString := frmTodo.ecDescription.Text;
DT := frmTodo.dtDate.Date;
ReplaceTime(DT, frmTodo.dtTime.Time);
DM.cdsTodosScheduled.AsDateTime := DT;
DM.cdsTodos.Post;
end;
finally
frmTodo.Free;
end;
end;

procedure TfrmMain.TodoEditUpdate(Sender: TObject);


begin
(Sender as TAction).Enabled := not DM.cdsTodos.IsEmpty;
end;
The ConMan Application
371

LISTING 9.5 Continued


procedure TfrmMain.TodoDeleteExecute(Sender: TObject);
begin
DM.cdsTodos.Delete;
end;

procedure TfrmMain.TodoMarkDone1Execute(Sender: TObject);


begin
DM.cdsTodos.Edit;
DM.cdsTodosCompleted.AsDateTime := Now;
DM.cdsTodos.Post;
end;

procedure TfrmMain.TodoMarkDone1Update(Sender: TObject);


begin
(Sender as TAction).Enabled := (not DM.cdsContacts.IsEmpty) and
(not DM.cdsTodos.IsEmpty) and DM.cdsTodosCompleted.IsNull;
end;

end.

The source code shown in Listing 9.5 deserves some discussion. I have grouped related sets of
event handlers in the source code.
The form’s OnCreate event handler sets the data module’s TSocketConnection.AfterConnect
and AfterDisconnect event handlers to point to the main form’s DoConnectDisconnect
method. This way, when a connection is established to or broken from the application server,
the main form will learn of the connect or disconnect. DoConnectDisconnect simply invali-
dates the status bar, which will then be updated to show the new connection status. 9
These event handlers are assigned at runtime rather than design time because the event handler

THE CONMAN
is not located in the same unit as the data module. Because the data module doesn’t use the APPLICATION
main form, the event handlers can’t be assigned at design time.
dsContactsDataChange is fired whenever the contact data source detects a change in the data.
The event handler checks to see if the modified field is nil (meaning the current record
changed) or if it references the IMAGE field (meaning the image for the current record
changed). If so, the code loads the current image from the dataset and displays it on the main
form.
ImageLoad1Execute and ImageClear1Execute fire when the user loads a new image for the
current contact or clears the contact’s image, respectively. Both event handlers put the contact
dataset into edit mode and then either clear the image or load a new image, respectively. As a
result of either of these events, dsContactsDataChange fires automatically, causing the dis-
played image to update.
Chapter 9
372

The only interesting action on the File menu is the Connect/Disconnect menu item.
FileConnect1Execute simply toggles the socket connection’s Connected property to connect
to or disconnect from the application server. FileConnect1Update takes care of setting the
action’s caption appropriately, depending on the current connection status.
Finally, the todo pop-up menu contains several actions for manipulating the todo items for the
current contact. TodoAddExecute and TodoEditExecute display the frmTodo form, which
allows the user to enter or modify a todo. TodoMarkDone1Execute sets the completed date and
time for the current todo to the current date and time.
The rest of the code in the main form should be fairly self-explanatory, so I won’t go over it in
detail.
The final unit in this application is TodoForm.pas. Listing 9.6 contains the source code for this
file, which enables the user to enter a new todo for a contact or edit an existing todo.

LISTING 9.6 ConMan—TodoForm.pas


unit TodoForm;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, ComCtrls, StdCtrls;

type
TfrmTodo = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Label1: TLabel;
ecDescription: TEdit;
Label2: TLabel;
dtDate: TDateTimePicker;
dtTime: TDateTimePicker;
Label3: TLabel;
btnCancel: TButton;
btnOk: TButton;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
The ConMan Application
373

LISTING 9.6 Continued


implementation

{$R *.dfm}

procedure TfrmTodo.FormCreate(Sender: TObject);


begin
dtDate.Date := Date;
dtTime.Time := StrToTime(‘12:00 pm’);
end;

end.

The todo form’s FormCreate handler simply defaults the date and time of the todo to noon of
the current date. The user will modify the date and time accordingly.

Room for Improvement


Because ConMan is a sample application, there is considerable room for improvement. In
addition to the omissions mentioned in the section titled “What Is ConMan?” at the beginning
of this chapter, the following improvements would be useful additions to this application:
• A dialog that pops up to alert the user as to when a todo is imminent.
• A screen that shows a list of todos for all contacts, ordered by date.
• The capability to assign a todo to a specific user, to provide rudimentary scheduling
capabilities.
• The capability to query the database. As it grows to a large size, it may take a consider-
able amount of time to load all the data onto a client machine. 9

THE CONMAN
I’ll leave it to you to implement these suggestions if you see fit. That will give you a spring-
board to learning multitier development techniques on your own. APPLICATION

Summary
This chapter pulled together many of the concepts discussed in prior chapters to create a com-
plete multitier database application. Specifically, this application implements the following:
• The application server makes use of dbExpress components such as TSQLConnection and
TSQLDataSet for connecting to an Interbase database. These components are discussed in
Chapters 1 and 2.
Chapter 9
374

• The application server contains a minimal user interface that displays the number of cur-
rent connections along with the amount of RAM in use by the server, as shown in
Chapter 8.
• The server’s pvContactsBeforeUpdateRecord event handler shows how to ensure that
each record has a unique primary key just before posting to the database, as discussed in
Chapter 7.
• The client application makes use of a calculated field on the cdsContact dataset.
Calculated fields were discussed in Chapter 3.
• The client application’s dsContactsDataChange event handler takes care of displaying an
image from the dataset in a non data-aware TImage component. This concept was dis-
cussed in Chapter 4.
• The client application’s cdsContactsReconcileError event handler takes advantage of
Delphi’s built-in reconciliation form to handle reconciliation errors, as discussed in
Chapter 7.
Redistributing dbExpress APPENDIX

A
Applications

IN THIS APPENDIX
• Redistributable Files 376

• Licensing Issues 378

• CD-ROM-Based Applications 378


Appendix A
376

This appendix tells you what you need to know to redistribute your database applications after
you’ve written them. Because this book assumes that you’re using dbExpress as the data access
technology behind your applications, I won’t explain how to redistribute the BDE or ADO.
Instead, I’ll concentrate specifically on dbExpress and DataSnap.

Redistributable Files
Redistributing an application built with dbExpress and DataSnap is incredibly simple—espe-
cially so if you’re used to messing around with a huge BDE- or ADO-based installation set. To
support dbExpress and DataSnap, you need to redistribute only two files along with your appli-
cation.

NOTE
Well, that’s two files in addition to whatever else your application might need. For
example, if you build your application with runtime packages enabled, you will also
need to redistribute the required runtime packages for the application.

Redistributing a Windows Application


For a VCL or CLX application running under Microsoft Windows, you need to redistribute the
driver for the database back end that your application uses. The database engines and their
associated Windows drivers are listed in Table A.1.

TABLE A.1 dbExpress Windows Drivers


Database Windows Driver
InterBase Dbexpint.dll
Oracle dbexpora.dll
DB2 dbexpdb2.dll
MySQL Dbexpmys.dll

In addition to the dbExpress driver, you need to redistribute MIDAS.DLL, which contains the
code necessary for the client dataset technology.
You can place both the dbExpress driver and MIDAS.DLL anywhere where your application
can find them, which could be either the application directory or the SYSTEM32 directory.
Neither file needs to be registered with Windows.
Redistributing dbExpress Applications
377

If you prefer, you can statically link DataSnap into your client application by including
MidasLib to your project’s uses clause and rebuilding. If you do this, you don’t need to redis-
tribute MIDAS.DLL with the client application (it must be redistributed with the server appli-
cation, however).
You can also statically link the dbExpress drivers with your application by linking in the
appropriate file(s), as listed in Table A.2.

TABLE A.2 dbExpress Statically-Linked Driver Units


Database Unit
InterBase Dbexpint
Oracle Dbexpora
DB2 dbexpdb2
MySQL Dbexpmy

NOTE
Even though Delphi 6 supports statically linking dbExpress and/or DataSnap to your
application, you should be aware that the static linking is a new technology that isn’t
completely compatible with all DataSnap code. For example, attempting to call
TClientDataSet.SaveToFile to save a client dataset to an XML file doesn’t save to
the same format when statically linking as when dynamically linking. The bottom line
is, if you decide to statically link your application, please test thoroughly before
releasing to your clients.

Redistributing a Linux Application


For a CLX application running under Linux, you need to redistribute the appropriate Linux
dbExpress driver. Table A.3 lists the database engines currently supported by dbExpress and
the associated Linux driver.

TABLE A.3 dbExpress Linux Drivers


Database Linux Driver A
InterBase libsqlib.so
REDISTRIBUTING
APPLICATIONS
DBEXPRESS

Oracle libsqlor.so
DB2 libsqldb2.so
MySQL libsqlmy.so
Appendix A
378

In addition to the dbExpress driver, you need to redistribute LIBMIDAS.SO. The redistrib-
utable files should be placed in /home/<usrname>/delphidir/bin.

Licensing Issues
If you’re writing database applications that work with local data (that is, data does not travel
from one machine to another), you don’t need to worry about any licensing fees for your appli-
cations. However, if data packets travel from one machine to another, you will need to pay a
small redistribution fee to Borland for use of the DataSnap technology.
For the particulars about when you need to pay the redistribution fees and how much they are,
visit Borland’s Web site at http://www.borland.com/midas.

CD-ROM-Based Applications
A common requirement of many database applications is that they need to be able to access a
(read-only) database on a CD. These applications, such as phone lists, parts databases, and so
on, are often redistributed with a large database on CD. The end user doesn’t need to update
the data—the user needs only to access the data from the CD.
To redistribute a read-only InterBase database on CD, you need to first run the gfix command
against the database, like this:
gfix –read-only mydata.gdb

This marks the database as read-only.


In your application, you should set the poReadOnly option for all dataset providers and set
ReadOnly to True for any TClientDataSets. This will ensure that none of the database-related
components attempt to update any data in the database.
dbExpress Plus APPENDIX

B
IN THIS APPENDIX
• What Is dbExpress Plus? 380

• For More Information 384


Appendix B
380

This appendix takes a quick look at dbExpress Plus, Thomas Miller’s open source add-on
library for dbExpress. dbExpress Plus can be downloaded from CodeCentral at
codecentral.borland.com. It is ID #15945.

What Is dbExpress Plus?


dbExpress Plus is an open source effort to extend dbExpress by introducing new components
in three key areas:
• Scripting
• Enhanced metadata
• Data pumping
The following sections provide a quick overview of these components.

Scripting
TSQLScript provides a means of executing multiple SQL statements at once through what is
known as a script. You’re probably familiar with SQL script files. The following snippet shows
a simple script file. This is actually part of the SQL script used to create the CONMAN database.
CREATE TABLE CONTACTS
(
CONTACTID INTEGER NOT NULL,
FIRST VARCHAR(20),
LAST VARCHAR(30),
DEAR VARCHAR(40),
TITLE VARCHAR(30),
COMPANYNAME VARCHAR(50),
ADDRESS1 VARCHAR(50),
ADDRESS2 VARCHAR(50),
CITY VARCHAR(30),
STATE VARCHAR(20),
POSTALCODE VARCHAR(10),
COUNTRY VARCHAR(30),
PHONE VARCHAR(20),
FAX VARCHAR(20),
CELLULAR VARCHAR(20),
PAGER VARCHAR(20),
EMAIL VARCHAR(40),
IMAGE BLOB SUB_TYPE 0 SEGMENT SIZE 4096,
NOTES BLOB SUB_TYPE TEXT SEGMENT SIZE 4096,
PRIMARY KEY (CONTACTID)
);
dbExpress Plus
381

CREATE UNIQUE INDEX IX_CONNAME ON CONTACTS (LAST, FIRST);


B

DBEXPRESS
CREATE TABLE TODOS
(
TODOID INTEGER NOT NULL,
CONTACTID INTEGER NOT NULL,

PLUS
DESCRIPTION VARCHAR(50),
SCHEDULED TIMESTAMP,
COMPLETED TIMESTAMP,
PRIMARY KEY (TODOID),
FOREIGN KEY (CONTACTID) REFERENCES CONTACTS (CONTACTID) ON DELETE CASCADE
);

Ordinarily, you’d need to execute these three statements (CREATE TABLE, CREATE INDEX, and
CREATE TABLE) separately by calling TSQLConnection.ExecuteDirect three times, once for
each statement.
Using TSQLScript, you can use the following two lines of code:
SQLScript1.SQL.LoadFromFile(ScriptFile);
SQLScript1.ExecuteDirect;

This code snippet assumes that ScriptFile is a string variable that contains the filename of
the SQL script file.

Enhanced Metadata
TSQLMetaData provides additional, easy-to-use methods to retrieve metadata information from
a database. Because it derives from TSQLConnection, you can use TSQLMetaData in place of a
TSQLConnection in your projects in which you need enhanced metadata.

Retrieving Table, View, and Field Names


TSQLMetaData allows easy retrieval of table names, system table names, view names, and
synonym names through the GetTableNames, GetSysTableNames, GetViewNames, and
GetSynonymNames methods, respectively. Each of these methods takes a single parameter,
which designates a string list into which the results are returned.
SQLMetaData1.GetTableNames(ListBox1.Items);

TSQLMetaData.GetFieldNames retrieves the fields that make up a table. Pass in the table name
to retrieve the field names for a string list for the results, and a flag that indicates whether
fields should be returned in the order in which they are declared or in alphabetical order.
TSQLMetaData.GetFieldNames is defined like this:

procedure GetFieldNames(const ATableName: string; AList: TStrings;


ASortOrder: TMetaDataSortOrder = soPosition); overload;
Appendix B
382

By default, fields are returned in the order in which they were declared. By passing soName as
the final parameter to GetFieldNames, the field names are returned in alphabetical order.

Retrieving Additional Field Metadata


If you want to retrieve additional metadata for a field, call TSQLMetaData.GetFieldMetaData,
which is defined like this:
function GetFieldMetaData(const ATableName,
AColumnName: string): TFieldMetaData;

This method takes the table name and field name as parameters and returns a TFieldMetaData
record containing information about the field in question. TFieldMetaData is defined like this:
TFieldMetaData = record
ColumnName: string;
ColumnPosition: LongInt;
ColumnDataType: LongInt;
ColumnTypeName: string;
ColumnSubtype: LongInt;
ColumnLength: LongInt;
ColumnPrecision: LongInt;
ColumnScale: LongInt;
ColumnNullable: LongInt; // 1=Not Nullable, 0=Nullable
end;

As you can see, it specifies the column (or field) name, the zero-based index, the type of data
contained in the column, and other pertinent information about the field.

Retrieving Index Data


One useful method that TSQLMetaData provides is a way to get information about an index on
a table. Two forms are provided. The first form retrieves information about any index, given its
name. Two methods are provided for this use.
procedure GetIndexFieldNames(const ATableName, AIndexName: string;
AList: TStrings);

GetIndexFieldNames returns a string list composed of the fields that make up the index
AIndexName on table ATableName.

function GetIndexFields(const ATableName, AIndexName: string): string;

GetIndexFields returns the same list as a semicolon-delimited list.


The second form of index data retrieval is to retrieve data about the primary key, rather than an
arbitrary index. To retrieve a list of fields that make up the primary key, you could pass in the
primary key name either to GetIndexFieldNames or GetIndexFields. However, you might not
dbExpress Plus
383

know the name of the primary key. In that case, you can call either GetPrimaryKeyFieldNames B
or GetPrimaryKeyFields.

DBEXPRESS
procedure GetPrimaryKeyFieldNames(const ATableName: string;
AList: TStrings);
function GetPrimaryKeyFields(const ATableName: string): string;

PLUS
These two methods perform the same service as GetIndexFieldNames and GetIndexFields,
except that they always work on the primary key.

Additional Methods
TSQLMetaData also provides a collection of methods to return the INSERT, UPDATE, and SELECT
SQL statements for a given table. The three methods are listed next:
function GetInsertStatement(const ATableName: string;
ASQL: TStrings): Integer;
function GetUpdateStatement(const ATableName: string;
ASQL: TStrings): Integer;
function GetSelectStatement(const ATableName: string;
ASQL: TStrings): Integer;

Given a table name, these three methods return the corresponding INSERT, UPDATE, or SELECT
SQL statement in the ASQL parameter.

Data Pumping
TSQLDataPump provides an answer to the BDE’s TBatchMove component, allowing you to eas-
ily move data from one table to another.
To do this, TSQLDataPump publishes SQLMetaDataSource and SQLMetaDataDestination prop-
erties, which reference the TSQLMetaData components that point to the source and destination
database connections, respectively.
After you specify the source and destination databases, set the SQLSource property to a valid
SELECT statement for the source database, such as

SELECT EMP_NO, FIRST_NAME, LAST_NAME, PHONE_EXT, HIRE_DATE, DEPT_NO,


JOB_CODE, JOB_GRADE, JOB_COUNTRY, SALARY FROM EMPLOYEE

Next, set the DestinationTable property to the name of the table in the destination database,
and then double-click the DestinationFields property, which allows you to set up a mapping
between the fields in the source table and the fields in the destination table.
Finally, set the DataMoveMode to the appropriate action to perform. Table B.1 lists the modes
that may be used.
Appendix B
384

TABLE B.1 Valid DataMoveMode Values


Value Description
dmAlwaysInsert Inserts all selected records into the destination table.
DmAppend Appends selected records to the destination table.
dmAppendUpdate Updates the record if it already exists in the destination table.
Otherwise, appends the record to the destination table.
DmDelete Deletes matching records from the destination table.
DmUpdate Updates matching records in the destination table.

At this point, all the required properties are set, so you need to call only
TSQLDataPump.Execute to perform the batch move operation.

For More Information


This has been only a brief overview of the capabilities provided by dbExpress Plus. Because it
is an open source project, it stands to reason that the code may grow to provide more functionality
than that discussed here. The dbExpress Plus download also comes with a demo application
that shows how to use many of the methods discussed in this appendix.
SYMBOLS
INDEX
+ (Addition) operator, 128
* (asterisk) operator, 129
\ (backslash), 210
/ (data separator), 210
. (decimal point), 207
# (digit placeholder), 207
0 (digit placeholder), 207
[db] (Division) operator, 128
“ (double quotation mark), 207
= (equality test) operator, 127
! (exclamation point), 210
> (greater than) operator, 127
>= (greater than or equal to) operator,
127
> (greater than sign), 210
< > (greater than/less than sign), 210
< > (inequality test) operator, 127
< (less than) operator, 127
<= (less than or equal to) operator, 127
< (less than sign), 210
[ts] (multiplication) operator, 128
# (pound sign), 209
; (separator character), 207, 210
‘ (single quotation mark), 207
0 specifier, 209
9 specifier, 209
– (Subtraction) operator, 128
, (thousands separator), 207
: (time separator), 210
_ (underscore), 210
A specifier
386

A Address property, 331 grouped, 196


Advanced application maintained, 192-193
A specifier, 209 (MainForm.pas), code creating at design
accessing listing, 76-79 time, 193-195
fields (client datasets), advantages creating at runtime,
103 client datasets, 94-95 195
nonpersistent fields, ConMan, 373 Aggregates property,
104-105 AfterApplyUpdates 194-195
persistent fields, 103 event, 295 Alignment property,
Fields objects, 64 AfterCancel event, 150 242-244
providers on different AfterClose event, 150 AllowDelete property,
form, 308 AfterConnect event, 14 267
ACID (Atomic, AfterDelete event, 150 AllowInsert property,
Consistent, Isolated, AfterDisconnect 267
Durable) event, 14 And operator, 128
properties, 37 AfterEdit event, 150 Apartment value, 321
action handlers, 228 AfterExecute event, Append method, 105
activating indexes 295 application servers
(client datasets), 121 AfterGetParams event, ConMan, 352-353
Active property, 60 295 creating, 318
Add Connection AfterGetRecords event, testing, 328-329
button, 10 295, 342 applications. See also
Add method, 102 AfterInsert event, 150 individual
Add to Interface AfterOpen event, 150 applications
command (remote AfterPost event, 150 client
data module context AfterRowRequest ConMan, 358,
menu), 324 event, 295 361-362, 371-372
Add To Interface dialog AfterScroll event, 150 CORBA connections,
box, 324 AfterUpdateRecord 335
AddFieldDef method, event, 295 creating, 329-330
102 aggregate expressions, DCOM connections,
AddIndex method, 121 195 333
adding Aggregate fields, 96 HTTP connections,
fields (client datasets), aggregate types, 195 334
102 aggregates intercepting data
methods (remote data across datasets, 196-197 packets, 333
modules), 324 determining position of multiple connection
Addition (+) operator, record, 197 types, 343-344
128 enabling/disabling, 197
ButtonStyle property
387

SOAP connections, BDE (Borland Database storing


335 Engine), 59 files, 168
socket connections, BeforeApplyUpdates images, 162-164
330 event, 295 notes, 162
socket servers, BeforeCancel event, streamed data,
331-332 149 165-167
RangeFilter, 135 BeforeClose event, 149 streaming components,
redistributing, 376 BeforeConnect 167
CD-ROM-based, 378 event, 14 support, 69-70
fees, 378 BeforeDelete event, BLOBs application
Linux, 377-378 149 (MainForm.pas), code
Windows, 376-377 BeforeDisconnect listing, 169-171
server, creating user event, 14 BlobSize property, 70
interface, 326-328 BeforeEdit event, 149 bookmarks, 114-115
ApplyUpdates method, BeforeExecute event, Borland Database
276-277 295 Engine (BDE), 59
arithmetic operators BeforeGetParams Borland Web site, 378
(filters)), 128 event, 295 Both value, 321
AsString method, 162 BeforeGetRecords briefcase model, 340
AsString property, 103 event, 295, 342 btnRetrieveClick
asterisk (*) operator, BeforeInsert event, 149 method, 87
129 BeforeOpen event, 149 buffering messages, 50
AutoCalcFields prop- BeforePost event, 149 buttons
erty, 98 BeforeRowRequest Add Connection, 10
AutoEdit property, 204 event, 296 Cancel, 224
automatic sorting BeforeScroll event, 149 Delete, 224
(grids), 264-265 BeforeUpdateRecord Edit, 224
AutoSave property, 49 event, 296 First, 224
Avg aggregate type, Binary Large Objects. Insert, 224
195 See BLOBs Last, 224
BLANK operator, 128 New Interface, 325
BLOB column, Next, 224
B removing, 70 Post, 224
BLOBs (Binary Large Prior, 224
backslash (\), 210
Objects), 162 Refresh, 224, 290
Basic application, code
fetching manually, 310 TDBNavigator, 224
listings
fields ButtonStyle property,
DatasetTypeForm.pas,
limitations, 168-169, 243
68-69
172
MainForm.pas, 65-68
resolving changes,
290
c specifier
388

C change log, 177 client applications


properties ConMan, 358, 361-362,
c specifier, 209 CancelUpdates, 181 371-372
calculated fields, 98 ChangeCount, 181 CORBA connections,
internal, 98-99 LogChanges, 177 335
providing values, MergeChangeLog, creating, 329
99-100 181 local connections,
standard, 98 RevertRecord, 329-330
callback handlers, 178-179 remote connections,
removing, 49 SavePoint, 179-180 330
callback events, trace StatusFilter, 181-182 DCOM connections,
(SQL operations UndoLastChange, 333
feedback), 47-49 178 HTTP connections, 334
callbacks, 325-326 viewing, 182, 186 intercepting data
Cancel button, 224 ChangeCount property, packets, 333
Cancel method, 177 181 multiple connection
CancelUpdates CHANGEINDEX index, types, 343-344
property, 181 123 SOAP connections, 335
CanModify property, ChangeLog applica- socket connections, 330
225 tion, code listings socket servers, 331-332
Caption property, 244 ChangeLogForm.pas, client datasets, 94
CaseInsFields property, 184-185 advantages/
120 MainForm.pas, 182-185 disadvantages, 94-95
cbProcedureClick ChangeLogForm.pas cloning, 186-188,
method, 25 (ChangeLog 191-192
cbUseCallbackClick application), code creating, 95
event handler, 54 listing, 184-185 design-time, 96
CD-ROM-based changing example, 108
applications, cursor (SQL operations runtime, 101-103
redistributing, 378 feedback), 47 field definitions,
CDS (ClientDataset) field data (servers), creating, 96
application 297-298 fields
(MainForm.pas), code check boxes accessing, 103-105
listing, 108-111 Log Trace, 54 adding, 102
CDSIndex application Use Callback, 54 calculated, 98-100
(MainForm.pas), code classes data, 96-97
listing, 124-126 TCustomClientDataSet, lookup, 100-101
cell (grids), 257-259 94 file formats, 107
TFieldDataLink,
225-226
code listings
389

indexes, 118-119 CMExit method, 228 ConManServer


activating, 121 Code Central Web site, MainForm.pas,
creating, 119-121 264, 380 356-357
retrieving code listings RemoteDataModule.
information, Advanced application pas, 353-357,
122-123 (MainForm.pas), 359-361, 363-371,
switching between, 76-79 373
121-122 Basic application creating empty data-
manipulating DatasetTypeForm.pas, bases from a resource,
deleting records, 106 68-69 29
example, 108 MainForm.pas, CtrlGrid application
modifying records, 65-68 (MainForm.pas),
105-106 BLOBs application 268-270
navigating, 113 (MainForm.pas), CustomDraw
code listing, 116-118 169-171 (MainForm.pas),
random-access navi- CDS (ClientDataset) 254-256
gation, 114-116 application DataAware application
sequential naviga- (MainForm.pas), (MainForm.pas),
tion, 113 108-111 232-236
populating, 105 CDSIndex application DataFetch application
example, 108 (MainForm.pas), (MainForm.pas),
from a file, 106-107 124-126 311-313
from a stream, ChangeLog application DDLSQL
106-107 ChangeLogForm.pas, (MainForm.pas), 33-36
from another data- 184-185 ETHDBComboBox.pas,
base, 106 MainForm.pas, 214-217, 220-221
manually, 105 182-185 ETHDBDateTimePicker
ClientData field, 48 Clone application .pas, 228-236
Clone application (MainForm.pas), ETHDBGrid.pas,
(MainForm.pas), code 188-191 260-261
listing, 188-191 ConMan ETHDBListBox.pas,
CloneCursor method, DataModule.pas, 219-221
187 358-361 EventLog application
cloning client datasets, MainForm.pas, MainForm.pas,
186-188, 191-192 362-371 151-155
closing TodoForm.pas, OptionsForm.pas,
database connections, 372-373 156-157
64 ConMan.SQL, 350-351 Events application
datasets, 64 (MainForm.pas), 14-17
code listings
390

feedback Search application columns editor, 242


(MainForm.pas), MainForm.pas, Columns property,
51-54 141-143 240-241
Joins application SearchForm.pas, 144 combo boxes
(MainForm.pas), Trans application Driver Name, 10
304-307, 312-313 (MainForm.pas), Instancing, 320
MetaData 42-46 Threading Model, 321
(MainForm.pas), Updates application comma (,), 207
20-25 (MainForm.pas), commands
MethodsClient 159-161, 281-289 DDL, 27
application ColCount property, 267 databases, creating,
(MainForm.pas), colon (:), 210 28-29
338-339 Color property, 242-244 executing, 27
MethodsServer column (grid) tables, creating,
application current, determining, 27-28
MainForm.pas, 257 DML, 29
327-328 mouse coordinate, parameterized SQL
ServerDataModule. determining, 257-259 statements, 30-32
pas, 336-339 column titles, 243-244 SELECT statement,
navigating client column types, 243 32-33
datasets, 116-118 columns simple SQL
Nested application BLOB, removing, 70 statements, 30
(MainForm.pas), customizing, 241-242, File menu (New), 319
174-175 265 gfix, 378
options column titles, New menu (Other), 280,
(MainForm.pas), 243-244 319
246-251 column types, 243 remote data module
RangeFilter resized, detecting, context menu (Add to
FilterForm.pas, 133 260-261 Interface), 324
MainForm.pas, schema CommandText
130-132 stColumns value, 89 property, 60, 310
RangeForm.pas, stIndexes value, CommandType
134-135 90-91 property, 60
SavePoint property, 180 stProcedureParams committing
Schema application value, 89-90 transactions, 40
(MainForm.pas), stProcedures value, comparison operators
81-87 88 (filters), 127-128
stSystemTables
value, 88
stTables value, 88
components
391

components TConnectionBroker, TDBNavigator,


conn, 352 343 223-225, 290
data-aware, 202-203 TCORBAConnection, TDBRadioGroup, 213
controlling user 335 TDBText, 211
editing, 206 TDataSetProvider, 274, TDCOMConnection,
creating, 225 293-295 333-334
disabling, 158-159, TDBCheckBox, 212 TFieldDataLink,
162 TDBComboBox, 213, 226-227
formatting/editing 217-218 TLocalConnection,
field values, 206 TDBCtrlGrid, 266 329-330
numeric fields, 207-209 events, 267-268 TSimpleObjectBroker,
string fields, 209-210 properties, 267 344
modifying data from TDBEdit, 212 TSOAP, 335
code, 205-206 TDBGrid, 240-241 TSocketConnection,
non-data-aware custom drawing, 330-331
equivalents, 252-254 TSQLClientDataSet,
202-203 customizing columns, 309
simple. See simple 241-244 TSQLDataPump, 383
data-aware edit mode, setting, TSQLDataSet, 60
components 259-260 general-purpose data
data-aware. See events, 245-246 access, 62-63
individual components grid options, properties, 60-61
dsContacts, 353 244-245 query-level access,
populating remote data grid settings, persist- 61-62
modules, 324 ing, 262-263 stored procedure
pvContacts, 353 limitations, 263 access, 62
sqlContacts, 352 resized columns, table-level access, 61
sqlID, 352 detecting, 260-261 TSQLMetaData, 381
sqlTodos, 352 row/column/cell, field metadata, 382
streaming (BLOBs), 167 determining, index data, 382-383
TClientDataSet, 94. See 257-259 methods, 383
also client datasets TDBImage, 221 table/view/field
TClientDataSetGrid, TDBListBox, 218-221 names, 381-382
261-264 TDBLookupComboBox TSQLMonitor, 49, 54
automatic sorting, , 223 TSQLQuery, 60
264-265 TDBLookupListBox, TSQLScript, 380-381
columns, 223 TSQLStoredProc, 60
customizing, 265 TDBMemo, 212 TSQLTable, 60
TWebConnection, 334
ComputerName property
392

ComputerName connecting to datasets, creating


property, 333 275-276 application servers, 318
ConfigFile property, connection brokering, callbacks, 325-326
265 344-345 client application, 329
ConfigureColumns Connection Editor, local connections,
method, 265 10-11 329-330
ConMan, 348 Connection Name list remote connections,
advantages/ box, 10 330
disadvantages, 373 ConnectionName client datasets, 95
application server, property, 8 design-time, 96
352-353 connections example, 108
client application, 358, database, closing, 64 runtime, 101-103
361-362, 371-372 DCOM (Distributed data-aware components,
database structure, COM), 333 225
349-350 HTTP, 334 databases (DDL
code listings creating commands), 28-29
DataModule.pas, local, 329-330 DataChange event
358-361 remote, 330 handler, 227
MainForm.pas, named, 9-11 empty databases from a
362-371 SOAP, 335 resource, code listing,
TodoForm.pas, sockets, 330 29
372-373 unnamed, 11 field definitions (client
ConMan.SQL, code constraints, 197-198 datasets), 96
listing, 350-351 Constraints property, grouped aggregates, 196
ConManServer_TLB. 197 indexes (client datasets),
pas, 352 Contacts table, 349 119
conn component, 352 ContactsByState at design-time,
connect events, 14, 17 procedure, 350 119-120
Connected property, 8 Control property, 225 at runtime, 121
connecting to data- controlling login, 12-13 maintained aggregates
bases, 8-9 controls (data-aware) design time, 193-195
controlling login, 12-13 lookup, 222-223 runtime, 195
dbExpress, 8 VCL-only, 222 master/detail
local, 308 CORBA remote data relationships, 74-76
named connections, modules, creating, remote data modules,
9-11 322 319
setting database Count aggregate type, CORBA remote data
parameters, 11-12 195 modules, 322
unnamed CreateBlobStream MTS remote data
connections, 11 method, 166 modules, 321-322
data-aware grids
393

SOAP remote data returned, limiting, modifying data from


modules, 322-323 309-311, 314-315 code, 205-206
standard remote data server, refreshing, non-data-aware
modules, 320-321 290-291 equivalents, 202-203
tables (DDL com- streamed, storing simple, 211
mands), 27-28 (BLOBs), 165-168 TDBCheckBox, 212
TColumn object, 242 data clashes, 277 TDBComboBox, 213,
user interfaces (server Data Definition 217-218
applications), 326-328 Language (DDL) TDBEdit, 212
CtrlGrid application statements, 27 TDBImage, 221
(MainForm.pas), code data fields, 96-97 TDBListBox,
listing, 268-270 Data Manipulation 218-221
cursor, changing (SQL Language (DML) TDBLookupComboB
operations statements, 27 ox, 223
feedback), 47 data modules, remote, TDBLookupListBox,
custom drawing, 318-319 223
252-254 adding methods, 324 TDBMemo, 212
CustomContraint creating, 319-322 TDBRadioGroup,
property, 198 populating with 213
CustomDraw components, 324 TDBText, 211
(MainForm.pas), code data packets, inter- TDataSource, 204-205
listing, 254-256 cepting, 333 TDBNavigator, 223-225
CustomIsolation data pumping data-aware controls
field, 39 (dbExpress Plus), lookup, 222-223
customizing columns, 383-384 VCL-only, 222
241-242, 265 data sources, setting data-aware grids
column titles, 243-244 up connection to, 227 TClientDataSetGrid,
column types, 243 data-aware compo- 261, 263-264
nents, 202-203 automatic sorting,
controlling user editing, 264-265
D 206 columns, customiz-
creating, 225 ing, 265
Dan Miser Web site,
disabling, 158-159, 162 TDBCtrlGrid, 266
333
formatting/editing field events, 267-268
data
values, 206 properties, 267
fields (servers), chang-
numeric fields, TDBGrid, 240-241
ing, 297-298
207-209 custom drawing,
intercepting, 298-299
string fields, 209-210 252-254
ordering, 73
customizing columns,
queries, 74
241-244
tables, 73
data-aware grids
394

edit mode, setting, databases dataset events, 148


259-260 connecting to, 8-9 AfterXxx events,
events, 245-246 controlling login, 149-150
grid options, 244-245 12-13 BeforeXxx events, 149
grid settings, dbExpress, 8 event handlers, 150
persisting, 262-263 named connections, DataSet property, 204
limitations, 263 9-11 dataset providers,
resized columns, setting database 274-275
detecting, 260-261 parameters, 11-12 datasets. See also
row/column/cell, unnamed queries; stored
determining, connections, 11 procedures; tables
257-259 creating (DDL aggregates, 196-197
third party, 271-272 commands), 28-29 client, 94
ExpressQuantumGrid, creating empty advantages/disad-
271 databases from a vantages, 94-95
InfoPower 2000, 271 resource, code calculated fields,
Orpheus, 271 listing, 29 98-100
TopGrid, 272 disconnecting from, 13 cloning, 186-188,
DataAware application automatically, 13-14 191-192
(MainForm.pas), code manually, 13 creating, 95-96,
listing, 232-236 local, connecting to, 101-103, 108
database connections, 308 data fields, 96-97
closing, 64 saving changes, field definitions, cre-
database events, 276-277 ating, 96
monitoring, 49 DataChange event fields, accessing,
buffering messages, 50 handler, creating, 227 103-105
logging messages, DataFetch application fields, adding, 102
49-50 (MainForm.pas), code file formats, 107
Database Login dialog listing, 311-313 indexes. See client
box, 12 DataModule.pas, 352, datasets, indexes
database metadata, 358-361 lookup fields,
retrieving, 18 DataModuleCreate 100-101
database parameters, method, 361 manipulating,
11-12 DataModuleDestroy 105-108
Database property, 12 method, 361 navigating, 113-118
database structure DataMoveMode populating, 105-108
(ConMan), 349-350 values, 384 closing, 64
connecting to, 275-276
dgRowSelect option
395

constraints, 197-198 DCOM (Distributed indexes, creating,


dbExpress, 58-59 COM) connections, 119-120
field contents, 333 maintained aggregates,
retrieving, 64-65 DDL (Data Definition creating, 193-195
navigating, 65, 68 Language) detail records, fetching
nested, 172-176 statements, 27 manually, 310-311,
opening, 63-64 DDL commands, 27 314-315
resolving to, 278 creating tables, 28 detecting
responding to changes, databases, creating, resized columns,
227 28-29 260-261
updating, 227-228 executing, 27 transaction support, 38
DatasetTypeForm.pas tables, creating, 27 determining
(Basic application), DDLSQL position of record in
code listing, 68-69 (MainForm.pas), code aggregate, 197
DataSize property, 107 listing, 33-36 row/cell
DataSnap, 318 decimal point (.), 207 current, 257
DataSource property, Declaration edit box, mouse coordinate,
60, 240 324 257-259
Date function, 129 DefaultDrawing Developer Express Web
date separator (/), 210 property, 253-254 site, 272
date/time functions DEFAULT_ORDER dgAlwaysShowEditor
(filters), 129 index, 123 option, 244, 259
Day function, 129 Delete button, 224 dgAlwaysShowSelectio
dbExpress Delete method, 106 n option, 245
connecting to DeleteIndex method, dgCancelOnExit option,
databases, 8 121 245
Linux drivers, 377 deleting dgColLines option, 244
statically-linked driver indexes (client datasets), dgColumnResize
unites, 377 121 option, 244
dbExpress Connection records (client dgConfirm option, 245
Editor, 10-11 databases), 106 dgEditing option, 244,
dbExpress datasets, Delphi Randomizer, 260
58-59 111 dgIndicator option, 244
dbExpress Plus, 380 DescFields property, dgMultiSelect option,
data pumping, 383-384 120 245
metadata, 381-383 design-time dgRowLines option,
scripting, 380-381 client datasets, 244
dbExpress Windows creating, 96 dgRowSelect option,
drivers, 376 245, 253
dgTabs option
396

dgTabs option, 244 DmDelete value, 384 field values, 206


dgTitles option, 244 DML (Data numeric fields,
dialog boxes Manipulation 207-209
Add to Interface, 324 Language) string fields, 209-210
Database Login, 12 statements, 27 EditKey method, 140
New Field, 97 DML commands, 29 EditMask property, 209
New Items, 280, 319 SELECT statement, settings, 210-211
Dialogs tab, 280 32-33 specifiers, 209-210
digit placeholders, 207 SQL statements EditorMode property,
DisableControls parameterized, 30-32 259
method, 159 simple, 30 editors (columns), 242
DisableStringTrim DmUpdate value, 384 empty databases, cre-
property, 198-199 double quotation mark ating from a resource
disabling (“), 207 (code listing), 29
aggregates, 197 drawing, 252-254 EmptyDataSet method,
data-aware components, Driver Name combo 106
158-159, 162 box, 10 EnableControls
disadvantages DriverName property, 8 method, 159
client datasets, 95 drivers Enabled property, 204
ConMan, 373 Linux, 377 enabling aggregates,
disconnect events, statistically-linked, 377 197
14, 17 Windows, 376 encryption libraries,
disconnecting from DropDownRows 299
databases, 13 property, 243 equality test (=)
automatically, 13-14 dsContacts component, operator, 127
manually, 13 353 ETHDBComboBox.pas,
DisplayFormat code listing, 214-217,
property, 207-209 220-221
settings, 208 E ETHDBDateTimePicker.
specifiers, 207 pas, code listing,
Distributed COM E+/–, 207 228-236
connections, 333 edit boxes, ETHDBGrid.pas, code
Division ([db]) Declaration, 324 listing, 260-261
operator, 128 Edit button, 224 ETHDBListBox.pas,
dmAlwaysInsert value, Edit method, 105, 225 code listing, 219-221
384 edit mode, setting, eTraceCat field, 48
DmAppend value, 384 259-260 event handlers
dmAppendUpdate editing cbUseCallbackClick, 54
value, 384 data-aware component DataChange, 227
data, 206
feedback
397

stateless servers, BeforeInsert, 149 OnPaintPanel, 267-268


341-343 BeforeOpen, 149 OnPostError, 150
writing, 50 BeforePost, 149 OnReconcileError, 278
EventLog application, BeforeRowRequest, 296 OnStateChange, 205
code listings BeforeScroll, 149 OnTitleClick, 246
MainForm.pas, 151-155 BeforeUpdateRecord, OnTrace, 50
OptionsForm.pas, 296 OnUpdateData, 205,
156-157 connect/disconnect, 226, 297-298
events 14, 17 OnUpdateError, 297
AfterApplyUpdates, 295 database, monitoring, TDataSetProvider
AfterCancel, 150 49-50 component, 295-297
AfterClose, 150 dataset, 148-150 TDataSource, 205
AfterConnect, 14 OnActiveChange, 226 TDBCtrlGrid
AfterDelete, 150 OnCalcFields, 150 component, 267-268
AfterDisconnect, 14 OnCellClick, 245 TDBGrid, 245-246
AfterEdit, 150 OnColEnter, 245 TFieldDataLink, 226
AfterExecute, 295 OnColExit, 246 Events application
AfterGetParams, 295 OnColumnMoved, 246 (MainForm.pas), code
AfterGetRecords, 295, OnDataChange, 205, listing, 14-17
342 226 exclamation point (!),
AfterInsert, 150 OnDeleteError, 150 210
AfterOpen, 150 OnDrawColumnCell, ExecuteAction method,
AfterPost, 150 246, 252 228
AfterRowRequest, 295 OnDrawDataCell, 246, ExecuteDirect method,
AfterScroll, 150 252 27-28
AfterUpdateRecord, 295 OnEditButtonClick, 246 executing DDL
BeforeApplyUpdates, OnEditError, 150 commands, 27
295 OnEditingChange, 226 Expression property,
BeforeCancel, 149 OnFilterRecord, 130, 120
BeforeClose, 149 150 expressions
BeforeConnect, 14 OnGetData, 296-298, (aggregates), 195
BeforeDelete, 149 341 ExpressQuantumGrid,
BeforeDisconnect, 14 OnGetDataSetProperties, 271
BeforeEdit, 149 297
BeforeExecute, 295 OnGetTableName, 297
BeforeGetParams, 295 OnLogin, 12, 14 F
BeforeGetRecords, 295, OnLogTrace, 50
feedback
342 OnNewRecord, 150
MainForm.pas, code
listing, 51-54
feedback
398

SQL operations, 46-47 fields file formats (client


changing cursor, 47 Aggregate, 96 datasets), 107
multiple feedback, BLOBs File menu commands
50-51, 54 limitations, 168-169, (New), 319
trace callback events, 172 FileName property, 49
47-49 resolving changes, files
fees, redistributing 290 BLOBs, storing, 168
applications, 378 calculated, 98 MyBase, 107
FetchBlobs method, internal, 98-99 Filter property, 130
310 providing values, FilterForm.pas
FetchDetails method, 99-100 (RangeFilter
310 standard, 98 application), code
fetching client dataset listing, 133
BLOBs, 310 accessing, 103-105 filters, 126-127, 130
detail records, 310-311, adding, 102 arithmetic operators,
314-315 ClientData, 48 128
FetchOnDemand CustomIsolation, 39 comparison operators,
property, 310-311 data, 96-97 127-128
field data (servers), eTraceCat, 48 date/time functions, 129
changing, 297-298 FSupportsMultiTrans, functions, 129
field definitions (client 41 logical operators, 128
datasets), creating, 96 GlobalID, 39 operators, 129
field names, retrieving, IsolationLevel, 39-40 string functions,
18-19, 381-382 lookup, 100-101 128-129
field objects, metadata, retrieving, FindField method, 104
retrieving, 257 382 FindKey method,
Field property, 226 nonpersistent, 104-105 138-139
field values, persistent, 103 FindNearest method,
formatting/editing, pszTrace, 48 139
206 retrieving contents from First button, 224
numeric fields, 207-209 datasets, 64-65 First method, 113
string fields, 209-210 separate, storing images Font property, 242-244
FieldByName method, (BLOBs), 163 formats, files (client
104 TransactionID, 39 datasets), 107
FieldCount property, 65 uTotalMsgLen, 48 formatting field values,
FieldDefs property, 102 Fields object, access- 206
FieldName property, ing, 64 numeric fields, 207-209
226, 242 Fields property, 104, string fields, 209-210
120
indexes
399

FormCreate method, GetFieldNames H-I


111, 175 property, 18-19
Free value, 321 GetGroupState handlers (callback),
freeing bookmarks, 114 method, 197 removing, 49
FSupportsMultiTrans GetIndexNames Host property, 331
field, 41 method, 122 HostName property,
functions GetIndexNames 335
Date, 129 property, 19 Hour function, 129
date/time (filters), 129 GetProcedureNames HTTP connections, 334
Day, 129 property, 19
filters, 129 GetProcedureParams icons, Reconcile Error
GetDate, 129 property, 19-20, 25-27 Dialog, 280
Hour, 129 GetTableNames ImageLib Corporate
Lower, 128 property, 18 Suite, 165
Minute, 129 gfix command, 378 images (BLOBs),
Month, 129 GlobalID field, 39 storing, 162-164
Second, 129 GotoKey method, IN operator, 129
string (filters), 128-129 139-140 index names,
SubString, 128 GotoNearest method, retrieving, 19
Time, 129 140 IndexDefs property,
Trim, 128 greater than (>) 119
TrimLeft, 129 operator, 127 indexed search
TrimRight, 129 greater than or equal methods, 138
Upper, 128 to (>=) operator, 127 FindKey, 138-139
Year, 129 greater than sign (>), FindNearest, 139
210 GotoKey, 139-140
greater than/less than GotoNearest, 140
indexes
G sign (< >), 210
grid options (TDBGrid), CHANGEINDEX, 123
gdFixed value, 253 244-245 client datasets, 118-119
gdFocused value, 253 grid settings, persist- activating, 121
gdSelected value, 253 ing, 262-263 creating, 119-121
general-purpose data grids deleting, 121
access (TSQLDataSet data-aware. See individ- retrieving
component), 62-63 ual data-aware grids information,
GetDate function, 129 sorting, 126, 264-265 122-123
GetDriverFunc grouped aggregates, switching between,
property, 8 creating, 196 121-122
GroupingLevel prop- data, retrieving, 382-383
erty, 120
indexes
400

DEFAULT_ORDER, IxExpression option, licensing, 378


123 120 LIKE operator, 129
retrieving, 257 IxNonMaintained limitations
IndexFieldNames option, 120 callbacks, 326
property, 73, 122 IxPrimary option, 120 fields (BLOBs),
IndexName property, IxUnique option, 120 168-169, 172
73 TDBGrid component,
inequality test (< >) 263
operator, 127 J-K limiting returned data,
InfoPower 2000, 271 309
Insert button, 224 joins, providing/ BLOBs, fetching manu-
Insert method, 105 resolving data, ally, 310
Instancing combo box, 302-304, 307 detail records, fetching
320 Joins application manually, 310-311,
InterceptGUID (MainForm.pas), code 314-315
property, 331 listing, 304-307, Linux
intercepting 312-313 applications,
data, 298-299 redistributing, 377-378
data packets, 333 KeepConnection drivers, 377
InterceptName property, 8 list boxes, Connection
property, 331 KeepSettings property, Name, 10
interfaces (callbacks) 187-188 Load1E XEcute
creating, 325-326 KeyFields parameter, method, 112
limitations, 326 136 LoadBalanced property,
internal calculated KeyValues parameter, 344
fields, 98-99 136 LoadFromFile method,
Internal value, 320 168
InTransaction LoadParamsOnConnect
property, 41 L property, 9
IS NOT NULL operator, L specifier, 209 local connections
128 Last button, 224 (client applications),
IS NULL operator, 128 Last method, 113 creating, 329-330
IsolationLevel field, less than (<) operator, local databases,
39-40 127 connecting to, 308
IxCaseInsensitive less than or equal to loCaseInsensitve
option, 120 (<=) operator, 127 option, 136
IxDescending option, less than sign (<), 210 Locate method,
120 LibraryName 136-137
property, 8
methods
401

Log Trace check box, 54 ConManServer maintained aggregates,


LogChanges property, application, 356-357 192-193
177 CtrlGrid application, creating at design time,
logging messages, 268-270 193
49-50 CustomDraw applica- nonpersistent
logical operators tion, 254-256 aggregates,
(filters), 128 DataAware application, 194-195
login, controlling, 232-236 persistent aggregates,
12-13 DataFetch application, 193-194
LoginPrompt 311-313 creating at runtime, 195
property, 9 DDLSQL application, manipulating client
lookup data-aware 33-36 datasets
controls, 222-223 EventLog application, deleting records, 106
lookup fields, 100-101 151-155 example, 108
Lookup method, Events application, modifying records,
137-138 14-17 105-106
loPartialKey option, feedback, 51-54 manually populating
136 Joins application, client datasets, 105
Lower function, 128 304-307, 312-313 master/detail
MetaData, 20-25 relationships, 74-76,
MethodsClient 301
M application, 338-339 Max aggregate type,
MethodsServer 195
MainForm.pas code application, 327-328 MaxBlobSize
listings, 352 navigating client property, 61
Advanced application, datasets, 116-118 MergeChangeLog
76-79 Nested application, property, 181
Basic application, 65-68 174-175 message handlers, 228
BLOBs application, options, 246-251 messages
169-171 RangeFilter application, buffering, 50
CDS (ClientDataset) 130-132 logging, 49-50
application, 108-111 Schema application, metadata (dbExpress
CDSIndex, 124-126 81-87 Plus), 381-383
ChangeLog application, Search application, MetaData application
182-185 141-143 (MainForm.pas), code
Clone application, Trans, 42-46 listing, 20-25
188-191 Updates application, methods
ConMan application, 159-161, 281-289 Add, 102
362-371
AddFieldDef, 102
AddIndex, 121
methods
402

adding to remote data Load1E XEcute, 112 Min aggregate type,


modules, 324 LoadFromFile, 168 195
Append, 105 Locate, 136-137 Minute function, 129
ApplyUpdates, 276-277 Lookup, 137-138 modes, update,
AsString, 162 Modified, 225 291-293
btnRetrieveClick, 87 MonitorTrace, 54 Modified method, 225
Cancel, 177 Next, 113 modifying
cbProcedureClick, 25 OnCalcFields, 99 data-aware component
CloneCursor, 187 Populate1E XEcute, data from code,
CMExit, 228 111 205-206
ConfigureColumns, 265 Prior, 113 records (client
CreateBlobStream, 166 Refresh, 290 databases), 105-106
DataModuleCreate, 361 RefreshRecord, 291 monitoring database
DataModuleDestroy, Reset, 225 events, 49
361 Save1E XEcute, 112 buffering messages, 50
Delete, 106 SaveToFile, 50, 106 logging messages,
DeleteIndex, 121 SaveToStream, 106 49-50
DisableControls, 159 StartTransaction, 39 MonitorTrace
Edit, 105, 225 Statistics1E XEcute, method, 54
EditKey, 140 112 Month function, 129
EmptyDataSet, 106 TFieldDataLink, 225 MTS remote data
EnableControls, 159 TIndexDefs, 123 modules, creating,
Execute Direct, 27-28 TSQLMetaData 321-322
ExecuteAction, 228 component, 383 multiple connection
FetchBlobs, 310 UpdateAction, 228 types (client
FetchDetails, 310 MethodsClient applications), 343-344
FieldByName, 104 application multiple feedback (SQL
FindField, 104 (MainForm.pas), code operations), 50-51, 54
FindKey, 138-139 listing, 338-339 Multiple Instance
FindNearest, 139 MethodsServer value, 320
First, 113 application, code multiple transactions,
FormCreate, 111, 175 listings 40-42
GetGroupState, 197 MainForm.pas, 327-328 Multiplication ([ts])
GetIndexNames, 122 ServerDataModule.pas, operator, 128
GotoKey, 139-140 336-339 Multitier tab, 319
GotoNearest, 140 Microsoft ADO, MyBase,
Insert, 105 incompatibility with incompatibility with
Last, 113 MyBase, 107 Microsoft ADO, 107
MIDAS, 318 MyBase file, 107
OnTrace event
403

N nonpersistent OnColumnMoved
aggregates, creating event, 246
Name property, 120 at design time, OnDataChange event,
named connections, 194-195 205, 226
9-11 nonpersistent fields, OnDeleteError event,
navigating 104-105 150
client datasets, 113 Not operator, 128 OnDrawColumnCell
random-access notes (BLOBs), storing, event, 246, 252
navigation, 114-116 162 OnDrawDataCell event,
sequential numeric fields, 246, 252
navigation, 113 formatting/editing, one-to-many
datasets, 65, 68 207-209 relationships, 74
Nested application OnEditButtonClick
(MainForm.pas), code event, 246
listing, 174-175
O OnEditError event, 150
nested datasets, OnEditingChange
172-176 Object-Insight Web
event, 226
nested transactions, 40 site, 272
OnFilterRecord event,
Neutral value, 321 ObjectBroker property,
130, 150
New command (File 344
OnGetData event,
menu), 319 ObjectName property,
296-298, 341
New Field dialog box, 335
OnGetDataSetPropertie
97 objects
s event, 297
New Interface button, field, retrieving, 257
OnGetTableName
325 Fields, accessing, 64
event, 297
New Items dialog box, TColumn, creating, 242
OnLogin event, 12, 14
280, 319 TFieldDef, 102
OnLogTrace event, 50
New menu commands ObjectView
OnNewRecord event,
(Other), 280, 319 property, 61
150
New Method toolbar OnActiveChange
OnPaintPanel event,
button, 325 event, 226
267-268
Next button, 224 OnCalcFields event,
OnPostError event, 150
Next method, 113 150
OnReconcileError
nonindexed search OnCalcFields
event, 278
methods, 136 method, 99
OnStateChange event,
Locate method, 136-137 OnCellClick event, 245
205
Lookup method, OnColEnter event, 245
OnTitleClick event, 246
137-138 OnColExit event, 246
OnTrace event, 50
OnUpdateData event
404

OnUpdateData event, options poFetchDetailsOnDema


205, 226, 297-298 dgAlwaysShowEditor, nd, 293, 310
OnUpdateError event, 244, 259 poIncFieldProps, 293
297 dgAlwaysShowSelectio poNoReset, 294
opening datasets, n, 245 poPropogateChanges,
63-64 dgCancelOnExit, 245 294
operators dgColLines, 244 poReadOnly, 294
Addition (+), 128 dgColumnResize, 244 poRetainServerOrder,
And, 128 dgConfirm, 245 294
arithmetic (filters), 128 dgEditing, 244, 260 TDataSetProvider
asterisk (*), 129 dgIndicator, 244 component, 293-295
BLANK, 128 dgMultiSelect, 245 Options parameter, 136
comparison (filters), dgRowLines, 244 Options property, 120,
127-128 dgRowSelect, 245, 253 293-294
Division ([db]), 128 dgTabs, 244 OptionsForm.pas
equality test (=), 127 dgTitles, 244 (EventLog
filters, 129 grid (TDBGrid), application), code
greater than (>), 127 244-245 listing, 156-157
greater than or equal to IxCaseInsensitive, 120 Or operator, 128
(>=), 127 IxDescending, 120 ordering data, 73
IN, 129 IxExpression, 120 queries, 74
inequality test (< >), IxNonMaintained, 120 tables, 73
127 IxPrimary, 120 Orientation property,
IS NOT NULL, 128 IxUnique, 120 267
IS NULL, 128 loCaseInsensitive, 136 Orpheus, 271
less than (<), 127 loPartialKey, 136 Other command (New
less than or equal to MainForm.pas, code menu), 280, 319
(<=), 127 listing, 246-251 overlapped
LIKE, 129 poAllowCommandText, transactions, 40
logical (filters), 128 294
Multiplication ([ts]), poAllowMultiRecordUp
128 dates, 294 P
Not, 128 poAutorRefresh, 294
PacketRecords
Or, 128 poCascadeUpdates, 294
property, 309
Subtraction (–), 128 poDisableDeletes, 294
PanelBorder property,
optional parameters, poDisableEdits, 294
267
300-301 poDisableInserts, 294
PanelHeight property,
poFetchBlobsOnDemand,
267
293, 310
properties
405

PanelWidth property, poAutoRefresh option, properties


267 294 Active, 60
ParamCheck property, poCascadeUpdates Address, 331
61, 72-73 option, 294 Aggregates, 194-195
parameterized queries, poDisableDeletes Alignment, 242-244
71-73, 75 option, 294 AllowDelete, 267
parameterized SQL poDisableEdits option, AllowInsert, 267
statements, 30-32 294 AsString, 103
parameters poDisableInserts AutoCalcFields, 98
database, 11-12 option, 294 AutoEdit, 204
KeyFields, 136 poFetchBlobsOnDeman AutoSave, 49
KeyValues, 136 d option, 293, 310 BlobSize, 70
optional, 300-301 poFetchDetailsOnDema ButtonStyle, 243
Options, 136 nd option, 293, 310 CancelUpdates, 181
Properties, 300 poIncFieldProps option, CanModify, 225
retrieving, 19-20, 25-27 293 Caption, 244
Params property, poNoReset option, 294 CaseInsFields, 120
9-12, 61 poPropogateChanges ChangeCount, 181
Password property, 12, option, 294 ColCount, 267
334-335 Populate1E XEcute Color, 242-244
passwords, setting method, 111 Columns, 240-241
(named populating CommandText, 60, 310
connections), 11 client datasets, 105-108 CommandType, 60
period (.), 207 remote data modules, ComputerName, 333
persistent aggregates, 324 ConfigFile, 265
creating at design poReadOnly option, Constraints, 197
time, 193-194 294 Control, 225
persistent fields, 103 poRetainServerOrder CustomContraint, 198
persisting grid settings, option, 294 Database, 12
262-263 Port property, 331 DataSet, 204
pfHidden value, 292 Post button, 224 DataSize, 107
pfInKey value, 292 pound sign (#), DataSource, 60, 240
pfInUpdate value, 292 207-209 DefaultDrawing,
pfInWhere value, 292 Prior button, 224 253-254
PickList property, 243 Prior method, 113 DescFields, 120
poAllowCommandText procedures DisableStringTrim,
option, 294 ContactsByState, 350 198-199
poAllowMultiRecordUp stored, DisplayFormat, 207-209
dates option, 294 providing/resolving
data, 302. See also
datasets
properties
406

settings, 208 MaxBlobSize, 61 SupportCallbacks, 331


specifiers, 207 MergeChangeLog, 181 TableScope, 18
DropDownRows, 243 Name, 120 TColumn, 242-243
EditMask, 209 ObjectBroker, 344 TCORBAConnection
settings, 210-211 ObjectName, 335 component, 335
specifiers, 209-210 ObjectView, 61 TDataSource, 204
EditMode, 259 Options, 120, 293-294 TDBCtrlGrid
Enabled, 204 Orientation, 267 component, 267
Expression, 120 PacketRecord, 309 TDCOMConnection,
FetchOnDemand, PanelBorder, 267 333-334
310-311 PanelHeight, 267 TFieldDataLink,
Field, 226 PanelWidth, 267 225-226
FieldCount, 65 ParamCheck, 61, 72-73 Title, 242-244
FieldDefs, 102 Params, 61 TitleSort, 264
FieldName, 226, 242 Password, 12, 334-335 TraceList, 50
Fields, 104, 120 PickList, 243 TransactionSupported,
FileName, 49 Port, 331 38
Filter, 130 ProviderFlags, 292-293 TSOAPConnection, 335
Font, 242, 244 Proxy, 334-335 TSocketConnection
GetFieldNames, 18-19 ReadOnly, 199, 242 component, 331
GetIndexNames, 19 RecNo, 115-116 TSQLConnection, 8-9
GetProcedureNames, 19 RepositoryID, 335 Connected, 8
GetProcedureParams, Reset, 187-188 ConnectionName, 8
19-20, 25-27 ResolveToDataSet, 278 DriverName, 8
GetTableNames, 18 RevertRecord, 178-179 GetDriverFunc, 8
GroupingLevel, 120 RowCount, 267 KeepConnection, 8
Host, 331 SavePoint, 179-180 LibraryName, 8
HostName, 335 SelectedColor, 267 LoadParamsOn-
IndexDefs, 119 SelectedField, 257 Connect, 9
IndexFieldNames, 73, SelectedIndex, 257 LoginPrompt, 9
122 ServerGUID, 331-334 Params, 9, 11-12
IndexName, 73 ServerName, 331, 334 TableScope, 9
InterceptGUID, 331 ShowFocus, 267 VendorLib, 9
InterceptName, 331 SortFieldNames, 61 TSQLDataSet
InTransaction, 41 Source, 120 component, 60-61
KeepSettings, 187-188 SQLConnection, 61 TWebConnection
LoadBalanced, 344 SQLHourGlass, 47 component, 334
LogChanges, 177 StatusFilter, 181-182 UndoLastChange, 178
remote data modules
407

UpdateMode, 291 Raize Software Web modifying (client data-


URL, 334-335 site, 47 bases), 105-106
UserName, 12, 334-335 raMerge value, 279 TransactionDesc, 39
Values, 214 random-access redistributing
Visible, 242 navigation (client applications, 376
Width, 242 datasets), 114-116 CD-ROM-based, 378
Properties parameter, bookmarks, 114-115 fees, 378
300 record numbers, 115 Linux, 377-378
ProviderFlags property, Randomizer, 111 Windows, 376-377
292-293 RangeFilter Refresh button, 224,
providers, accessing on application, code 290
different form, 308 listings Refresh method, 290
providing FilterForm.pas, 133 refreshing data
data MainForm.pas, 130-132 (servers), 290-291
joins, 302-304, 307 RangeForm.pas, RefreshRecord method,
stored procedures, 134-135 291
302 RangeForm.pas relationships
values (calculated (RangeFilter master/detail, 74-76,
fields), 99-100 application), code 301
Proxy property, listing, 134-135 one-to-many, 74
334-335 ranges, 126-127 remote connections
pszTrace field, 48 raRefresh value, 279 (client applications),
pvContacts component, raSkip value, 279 creating, 330
353 ReadOnly property, remote data module
199, 242 context menu
RecErrorForm.pas, 352 commands (Add to
Q-R RecNo property, Interface), 324
115-116 remote data modules,
queries. See also
Reconcile Error Dialog 318-319
datasets
icon, 280 creating, 319
ordering data, 74
reconciliation errors, CORBA remote data
parameterized, 71-75
278-280, 289-290 modules, 322
query-level access
record numbers, 115 MTS remote data
(TSQLDataSet
records modules, 321-322
component), 61-62
deleting (client data- SOAP remote data
bases), 106 modules, 322-323
raAbort value, 279
determining position in standard remote data
raCancel value, 279
aggregate, 197 modules, 320-321
raCorrect value, 279
remote data modules
408

methods, adding, 324 index data, 382-383 runtime


populating with index information client datasets, creating,
components, 324 (client datasets), 122 101-103
RemoteDataModule.pa GetIndexNames creating indexes, 121
s (ConManServer method, 122 creating maintained
application), code TIndexDefs method, aggregates, 195
listing, 352-357, 123 deleting indexes, 121
359-361, 363-371, 373 index names, 19
removing indexes, 257
BLOB column, 70 parameters, 19-20, S
callback handlers, 49 25-27
RepositoryID schema information, Save1E XEcute method,
properties, 335 79-81, 87-88 112
Reset method, 225 stored procedures, 19 SavePoint property,
Reset property, 187-188 table/view/field names, 179-180
resized columns, 381 SaveToFile method, 50,
detecting, 260-261 tables, 18 106
ResolveToDataSet returned data, limiting, SaveToStream method,
property, 278 309 106
resolving, 274 BLOBs, fetching saving changes to
changes to BLOB fields, manually, 310 databases, 276-277
290 detail records, fetching Schema application
data manually, 310-311, (MainForm.pas), code
joins, 302-304, 307 314-315 listing, 81-87
stored procedures, returning schema columns
302 bookmarks, 114 stColumns value, 89
datasets, 278 result sets, 33 stIndexes value, 90-91
responding to dataset RevertRecord property, stProcedureParams
changes, 227 178-179 value, 89-90
result sets, returning, rolling back transac- stProcedures value, 88
33 tions, 40 stSystemTables value,
retrieving row (grids), determin- 88
database metadata, 18 ing stTables value, 88
field contents (datasets), current, 257 schema information,
64-65 mouse coordinate, retrieving, 79-81,
field metadata, 382 257-259 87-88
field names, 18-19 RowCount property, scientific notation
field objects, 257 267 (E+/–), 207
ScktSrvr, 331-332
Source property
409

scripting (dbExpress ServerDataModule.pas simple data-aware


Plus), 380-381 (MethodsServer appli- components, 211
Search application, cation), code listing, TDBCheckBox, 212
code listings 336-339 TDBComboBox, 213,
MainForm.pas, 141-143 ServerGUID property, 217-218
SearchForm.pas, 144 331-334 TDBEdit, 212
search methods, 136 ServerName property, TDBImage, 221
indexed, 138 331, 334 TDBListBox, 218-221
FindKey, 138-139 servers TDBLookupComboBox,
FindNearest, 139 application 223
GotoKey, 139-140 creating, 318 TDBLookupListBox,
GotoNearest, 140 testing, 328-329 223
nonindexed, 136 data, refreshing, TDBMemo, 212
Locate method, 290-291 TDBRadioGroup, 213
136-137 field data, changing, TDBText, 211
Lookup method, 297-298 simple SQL statements,
137-138 socket, 331-332 30
SearchForm.pas (Search stateless, 341-343 Single Instance value,
application), code setting 320
listing, 144 bookmarks, 114 single quotation mark
Second function, 129 database parameters, (‘), 207
SELECT statement, 11-12 Single value, 321
32-33 edit mode, 259-260 Skyline Tools Imaging
SelectedColor property, passwords (named (ImageLib Corporate
267 connections), 11 Suite), 165
SelectedField property, setting up Skyline Tools Imaging
257 connection to data Web site, 165
SelectedIndex property, source, 227 SOAP connections, 335
257 TFieldDataLink SOAP remote data
separate fields, storing component, 226-227 modules, creating,
images (BLOBs), 163 settings 322-323
separator character (;), DisplayFormat property, socket connections, 330
207, 210 208 socket servers, 331-332
sequential navigation EditMask property, SortFieldNames
(client datasets), 113 210-211 property, 61
server applications TableScope property, 18 sorting grids, 126,
callbacks ShowFocus property, 264-265
creating, 325-326 267 Source property, 120
limitations, 326
user interface, creating,
326-328
specifiers
410

specifiers stateless servers, stProcedures value, 80,


0, 209 341-343 88
9, 209 statements streamed data (BLOBs),
A, 209 DDL (Data Definition storing, 165-167
c, 209 Language), 27 streaming images
DisplayFormat property, DML, 27 (BLOBs), 163-164
207 SELECT, 32-33 streaming components
EditMask property, SQL (BLOBs), 167
209-210 parameterized, 30-32 string fields,
l, 209 result sets, returning, formatting/editing,
SQL operations 33 209-210
(feedback), 46-47 simple, 30 string functions
changing cursor, 47 static linking, 377 (filters), 128-129
multiple feedback, statically-linked driver stSysTables value, 80
50-51, 54 units, 377 stSystemTables value
trace callback events, Statistics1E XEcute (schema columns), 88
47-49 method, 112 stTables value, 80, 88
SQL statements StatusFilter property, SubString function, 128
parameterized, 30-32 181-182 Subtraction (–)
result sets, returning, 33 stColumns value, 80, operator, 128
simple, 30 89 Sum aggregate type,
SQLConnection stIndexes value, 80, 195
property, 61 90-91 support
sqlContacts stNoSchema value, 80 BLOB (Binary Large
component, 352 stored procedure Object), 69-70
SQLHourGlass access (TSQLDataSet transactions, 37-38
property, 47 component), 62 undo, 176-177
sqlID component, 352 stored procedures SupportCallbacks prop-
sqlTodos component, providing/resolving erty, 331
352 data, 302 switching between
standard calculated retrieving, 19 indexes (client
fields, 98 stored procedures, 60. datasets), 121-122
standard remote data See also datasets
modules, creating, storing
320-321 files, 168 T
starting transactions, images, 162-164
39-40 table names,
notes, 162
StartTransaction retrieving, 381-382
streamed data, 165-167
method, 39 table-level access
stProcedureParams
(TSQLDataSet
value, 80, 89-90
component), 61
third-party data-aware grids
411

tables, 59. See also TDataSource events, TDBLookupComboBox


datasets 205 component, 223
Contacts, 349 TDataSource TDBLookupListBox
creating (DDL properties, 204 component, 223
commands), 27-28 TDBCheckBox TDBMemo component,
ordering data, 73 component, 212 212
retrieving, 18 TDBComboBox TDBNavigator buttons,
Todos, 350 component, 213, 224
TableScope property, 217-218 TDBNavigator
9, 18 TDBCtrlGrid component, 223-225,
tabs component, 266 290
Dialogs, 280 events, 267-268 TDBRadioGroup
Multitier, 319 properties, 267 component, 213
WebServices, 323 TDBEdit component, TDBText component,
TClientDataSet 212 211
component, 94. TDBGrid component, TDCOMConnection
See also client 240-241 component, 333-334
datasets columns, customizing, testing application
TClientDataSetGrid 241-244 servers, 328-329
component, 261-264 custom drawing, TFieldDataLink class,
automatic sorting, 252-254 225-226
264-265 edit mode, setting, TFieldDataLink
columns, customizing, 259-260 component, 226-227
265 events, 245-246 TFieldDataLink events,
TColumn objects, grid options, 244-245 226
creating, 242 grid settings, persisting, TFieldDataLink
TColumn properties, 262-263 methods, 225
242-243 limitations, 263 TFieldDataLink
TConnectionBroker resized columns, properties, 225-226
component, 343 detecting, 260-261 TFieldDef object, 102
TCORBAConnection row/column/cell, TGridDrawState values,
component, 335 determining, 257-259 253
TCustomClientDataSet TDBGrid options, third-party data-aware
class, 94 244-245 grids, 271-272
TDataSetProvider TDBImage component, ExpressQuantumGrid,
component, 274, 221 271
293-297 TDBListBox InfoPower 2000, 271
TDataSource component, 218-219, Orpheus, 271
component, 204-205 221 TopGrid, 272
third-party imaging libraries
412

third-party imaging traceVENDOR value, 48 TSQLConnection


libraries, storing Trans application component
images (BLOBs), 164 (MainForm.pas), code events, 14, 17
thousands separator listing, 42-46 properties, 8-9
(,), 207 TransactionDesc Connected, 8
Threading Model record, 39 ConnectionName, 8
combo box, 321 TransactionID field, 39 DriverName, 8
Time function, 129 transactions, 37-38 GetDriverFunc, 8
time separator (:), 210 ACID (Atomic, KeepConnection, 8
TIndexDefs method, Consistent, Isolated, LibraryName, 8
123 Durable) properties, LoadParamsOnConn
Title property, 242-244 37 ect, 9
TitleSort property, 264 committing, 40 LoginPrompt, 9
TLocalConnection multiple, 40-42 Params, 9, 11-12
component, 329-330 nested, 40 TableScope, 9
TodoForm.pas, code overlapped, 40 VendorLib, 9
listings, 352, 372-373 rolling back, 40 TSQLDataPump
Todos table, 350 starting, 39-40 component, 383
toolbar buttons, New support, 37-38 TSQLDataSet
Method, 325 TransactionSupported component, 60
TopGrid, 272 property, 38 general-purpose data
trace callback events TReconciliationAction access, 62-63
(SQL operations value, 279 properties, 60-61
feedback), 47-49 Trim function, 128 query-level access,
traceBLOB value, 48 TrimLeft function, 129 61-62
traceDATAIN value, 48 TrimRight function, 129 stored procedure
traceDATAOUT TSchemaType access, 62
value, 48 values, 80 table-level access, 61
traceERROR value, 48 TSimpleObjectBroker TSQLMetaData
TraceList property, 50 component, 344 component, 381
traceMISC value, 48 TSOAPConnection methods, 383
traceQEXECUTE component, 335 retrieving
value, 48 TSocketConnection field metadata, 382
traceQPREPARE component, 330-331 index data, 382-383
value, 48 TSQLClientDataSet table/view/field
traceSTMT value, 48 component, 309 names, 381-382
traceTRANSACT TSQLMonitor
value, 48 component, 49, 54
values
413

TSQLQuery MergeChangeLog UserName property, 12,


component, 60 property, 181 334-335
TSQLScript component, RevertRecord usInserted value, 181
380-381 property, 178-179 usModified values, 181
TSQLStoredProc SavePoint property, usUnmodified value,
component, 60 179-180 181
TSQLTable StatusFilter property, uTotalMsgLen field, 48
component, 60 181-182
TUpdateKind values, UndoLastChange
279 property, 178 V
TUpdateMode values, viewing, 182, 186
UndoLastChange values
291-292
property, 178 Apartment, 321
TUpdateStatus value,
unnamed Both, 321
181
connections, 11 DataMoveMode, 384
TurboPower Software
update modes, 291-293 dmAlwaysInsert, 384
Company Web site,
UpdateAction method, DmAppend, 384
211
228 dmAppendUpdate, 384
TurboPower Software
UpdateMode property, DmDelete, 384
Web site, 299
291 DmUpdate, 384
TurboPower Web site,
Updates application Free, 321
271
(MainForm.pas), code gdFixed, 253
TWebConnection
listing, 159-161, gdFocused, 253
component, 334
281-289 gdSelected, 253
updating datasets, Internal, 320
Multiple Instance, 320
U 227-228
Neutral, 321
Upper function, 128
ukDelete value, 279 upWhereAll value, 291 pfHidden, 292
ukInsert value, 279 upWhereChanged pfInKey, 292
ukModify value, 279 value, 291 pfInUpdate, 292
underscore (_), 210 upWhereKeyOnly pfInWhere, 292
undo support, 176-177 value, 291 providing (calculated
Cancel method, 177 URL property, 334-335 fields), 99-100
change log, 177 usDeleted value, 181 raAbort, 279
CancelUpdates Use Callback check raCancel, 279
property, 181 box, 54 raCorrect, 279
ChangeCount user interfaces (server raMerge, 279
property, 181 applications), raRefresh, 279
LogChanges creating, 326-328 raSkip, 279
property, 177
values
414

Single, 321 xilDIRTYREAD, 39 X-Z


Single Instance, 320 xilREADCOMMITTED,
stColumns, 80, 89 39 xilCUSTOM value, 40
stIndexes, 80, 90-91 xilREPEATABLEREAD, xilDIRTYREAD value, 39
stNoSchema, 80 40 xilREADCOMMITTED
stProcedureParams, 80, Values property, 214 value, 39
89-90 VCL-only data-aware xilREPEATABLEREAD
stProcedures, 80, 88 controls, 222 value, 40
stSysTables, 80 VendorLib property, 9
stSystemTables, 88 view names, Year function, 129
stTables, 80, 88 retrieving, 381-382
TGridDrawState, 253 viewing change log, zero (0), 207
traceBLOB, 48 182, 186
traceDATAIN, 48 Visible property, 242
traceDATAOUT, 48
traceERROR, 48
traceMISC, 48 W
traceQEXECUTE, 48
traceQPREPARE, 48 Web sites
traceSTMT, 48 Borland, 378
traceTRANSACT, 48 Code Central, 264, 380
traceVENDOR, 48 Dan Miser, 333
TReconciliationAction, Developer Express, 272
279 Object-Insight, 272
TSchemaType, 80 Raize Software, 47
TUpdateKind, 279 Skyline Tools Imaging,
TUpdateMode, 291-292 165
TUpdateStatus, 181 TurboPower Software,
ukDelete, 279 211, 271, 299
ukInsert, 279 Woll2Woll, 271
ukModify, 279 WebServices tab, 323
upWhereAll, 291 Width property, 242
upWhereChanged, 291 Windows applications,
upWhereKeyOnly, 291 redistributing,
usDeleted, 181 376-377
usInserted, 181 Windows drivers, 376
usModified, 181 Woll2Woll Web site,
usUnmodified, 181 271
xilCUSTOM, 40 writing event
handlers, 50

You might also like