Sie sind auf Seite 1von 427

Delphi/Kylix Database

Development
Eric Harmon

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

Delphi/Kylix Database Development

ASSOCIATE PUBLISHER
Linda Engelman

Copyright 2002 by Sams Publishing


All rights reserved. No part of this book shall be reproduced, stored in a
retrieval system, or transmitted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the publisher. No patent liability is assumed with respect to the use of the information
contained herein. Although every precaution has been taken in the preparation
of this book, the publisher and author assume no responsibility for errors or
omissions. Nor is any liability assumed for damages resulting from the use of
the information contained herein.

ACQUISITIONS EDITOR
Karen Wachs

DEVELOPMENT EDITOR
Laurie McGuire

MANAGING EDITOR
Charlotte Clapp

PROJECT EDITOR

International Standard Book Number: 067232265x

Heather McNeill

Library of Congress Catalog Card Number: 2001093571

COPY EDITOR

Printed in the United States of America

INDEXER

First Printing: November 2001


03

02

01

00

Katie Robinson

Mary SeRine

PROOFREADER

Trademarks
All terms mentioned in this book that are known to be trademarks or service
marks have been appropriately capitalized. Sams Publishing cannot attest to
the accuracy of this information. Use of a term in this book should not be
regarded as affecting the validity of any trademark or service mark.

Bob LaRoche
Plan-it-Publishing

TECHNICAL EDITORS
Dan Miser
Ramesh Theivendran
Philippe Bruno

Warning and Disclaimer


Every effort has been made to make this book as complete and as accurate as
possible, but no warranty or fitness is implied. The information provided is on
an as is basis. The author and the publisher shall have neither liability nor
responsibility to any person or entity with respect to any loss or damages arising from the information contained in this book or from the use of the programs accompanying it.

TEAM COORDINATOR
Lynne Williams

MEDIA DEVELOPER
Dan Scherf

INTERIOR DESIGNER
Gary Adair

COVER DESIGNER
Gary Adair

PAGE LAYOUT
Ayanna Lacey

Contents at a Glance
Introduction 1
1

Establishing and Using Database Connections 7

dbExpress Datasets 57

Client Dataset Basics 93

Advanced Client Dataset Operations 147

Data-Aware Components 201

Data-Aware Grids 239

Dataset Providers 273

DataSnap 317

The ConMan Application 347


Appendixes

Redistributing dbExpress Applications 375

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

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

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

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

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

vii

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

DataSnap 317
What Is DataSnap? ..............................................................................318
Creating the Application Server ..........................................................318
Remote Data Modules ....................................................................318
Creating the Application Servers 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

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

dbExpress Plus 379


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

385

ix

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
Eric.Harmon@tpx.turbopower.com.
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. Dans 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 Developers Guide, acting as a technical editor, writing 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 headquartered in Montreal, Quebec, specializing in radio frequency identification (RFID) systems.
Scanpak is the creator of GETS (Galley Equipment Tracking System), an asset tracking system specifically targeted to the airline industry. He is also a part-time teacher for computerrelated 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 programmers in the TurboPower newsgroups.

Dedication
For my wife, Tina.

Acknowledgments
Writing a book isnt 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. Shes 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.
Id 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 couldnt have asked for better 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 dont know them individually. Thanks to all
those whose names I didnt 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 were doing right, what we could do better, what areas youd
like to see us publish in, and any other words of wisdom youre 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 didnt like about this bookas 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 books 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:

feedback@samspublishing.com

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 wont 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 dont 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 youll 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.

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 listings. The downloadable source code is provided in both CLX and VCL form, so if you dont
write cross-platform applications, you may want to experiment with the VCL code instead.
In case you arent 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


and so on.

TStringList, TObjectList,

DataCLX This wraps the CLX database functionality, such as dbExpress and dataaware 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).
Youll 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 shouldnt have any trouble
following along with the CLX code.

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 isnt 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

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 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.

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 6both 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 whats happening in a dbExpress


connection.

DELPHI/KYLIX DATABASE DEVELOPMENT

Chapter 2
Basic

Illustrates basic TSQLDataSet operation.


Shows more advanced TSQLDataSet methods and operations.

Advanced

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.


Illustrates how to create and use indexes on a TClientDataSet.

CDSIndex

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 TDBGrids default drawing to
provide visually exciting grids.
CtrlGrid

VCL-only sample that shows how to use the TDBCtrlGrid component.

Chapter 7
Updates
Joins

Shows the basic operation of dataset providers.

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.

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
COM+.

Shows how to create a stateless application server for use with MTS or

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 Borlands 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 engines API. For
this reason, it adds very little overhead to database operations.
High-performance Largely because of its low overhead, dbExpress is extremely highperformance. It is designed to work in conjunction with Delphis client dataset technology.

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 BDEs 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:
Its free. Anyone can download a free copy of InterBase from Borlands Web site and
work with any of the examples in this book.
Its 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.
Its 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 dont need to worry
about how Im going to redistribute a 30MB Oracle database to my readers.
Its 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
Eric.Harmon@tpx.turbopower.com. 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.

CHAPTER

Establishing and Using


Database Connections

IN THIS CHAPTER
Connecting to and Disconnecting from a
Database 8
Retrieving Database Metadata

18

Executing DDL and DML Statements


Transaction Support

37

Providing Feedback During SQL


Operations 46

27

Chapter 1

In this chapter, Ill show you how to connect to and disconnect from a database using
dbExpress, as well as how to manage the connection after youve 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 doesnt 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.
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 connection.
Indicates the type of database you are connecting to, such as
InterBase, Oracle, and so on.
Specifies the name of the function exported by the dbExpress driver that provides access to the driver.
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.
Specifies the name of the dbExpress database driver, such as
dbexpint.dll.

ConnectionName

DriverName
GetDriverFunc
KeepConnection

LibraryName

Establishing and Using Database Connections

TABLE 1.1

Continued

1
Description

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.
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.
Enables you to set database-specific parameters at design time or
at runtime. See the section Setting Database Parameters for
more information.
Specifies the types of information returned by GetTableNames.
This property is discussed in more detail in the section
Retrieving Database Metadata.
Refers to the database vendor client library used for connecting
to the database server. For example, InterBase supplies the
gds32.dll client-side library.

Params

TableScope

VendorLib

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:

ESTABLISHING AND
USING DATABSE
CONNECTIONS

Property

LoginPrompt

10

Chapter 1

[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 users 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

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 youre 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


TSQLConnections 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.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

The dbExpress Connection Editor displays the settings for the selected, named connection on
the right side in the Connection Settings section. You should set the appropriate settings for
your database engine. For example, with an InterBase connection, you want to at least set the
Database setting. You might also want to enter values for RoleName, SQLDialect, and User_Name.

11

12

Chapter 1

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 corresponds to User_Name. You usually do not want to set
this at design time, as it bypasses database security.
The username employed while establishing the connection. You
typically want the user to supply this information at runtime.

Password
User_Name

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 connections 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 connections OnLogin event, and setting the appropriate
parameters there. The following code snippet shows how:

Establishing and Using Database Connections

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

FIGURE 1.2
Delphis 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.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

procedure TForm1.SQLConnection1Login(Database: TSQLConnection;


LoginParams: TStrings);
var
TheUserName: string;
ThePassword: string;
begin
// Display a custom dialog to retrieve TheUserName and ThePassword

13

14

Chapter 1

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.


Fires after the database connection has been dropped.
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.
Fires immediately before the database connection is dropped. You
can raise an exception in this event handler to prevent the connection
from being dropped.
Fires before the connection is made so that you can provide a
username and password at runtime.

AfterDisconnect
BeforeConnect

BeforeDisconnect

OnLogin

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

EventsMainForm.pas

unit MainForm;
interface
uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DBXpress,
ExtCtrls, DB, SqlExpr, QStdCtrls, QExtCtrls;

Establishing and Using Database Connections

LISTING 1.1

Continued

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;

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

type
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;

15

16

Chapter 1

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

LISTING 1.1

Continued

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.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

lbEvents.Items.Add(E.Message);
end;
lbEvents.Items.Add(---End Close---);
lbEvents.Items.Add();
end;

17

18

Chapter 1

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
System tables
Normal, user-defined tables
Views

TsSysTable
TsTable
TsView

The following code snippet shows a typical call to GetTableNames:


GetTableNames(ListBox1.Items, False);

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:
GetFieldNames

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

Establishing and Using Database 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.
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.

List

The following code snippet shows how to correctly retrieve parameters for the stored procedure
named CONTACTSBYSTATE.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

19

20

Chapter 1

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 dont 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

MetaDataMainForm.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

LISTING 1.2

Continued

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]

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

btnConnect: TButton;
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;

21

22

Chapter 1

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

LISTING 1.2

Continued

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);

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

lvParameters.Items.Clear;

23

24

Chapter 1

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

LISTING 1.2

Continued

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.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

lblConnection.Caption := Not connected;

25

26

Chapter 1

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

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
youll 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, Ill 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 dont 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);

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

27

28

Chapter 1

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 doesnt allow direct execution of DDL or DML, so the dbExpress driver
attempts to prepare, and then to execute the CREATE DATABASE statement. Because InterBase
doesnt 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 database 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 cant 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

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 youre working on your application, if you change the database schema, you must remember 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 database 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.

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

29

30

Chapter 1

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 OToole, would you know how to quote it manually? The correct way is
OToole

Using QuotedStr, you dont 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

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

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:

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

31

32

Chapter 1

Simplicity. Given a long, complicated SQL statement, its 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 ODonnell. If you dont 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 ODonnell, 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 ODonnell, 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?
Its 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 dont discuss in detail until
the next chapter. For now, Ill 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 dont pass a third parameter to Execute, it defaults to nil, which means that the statement
doesnt 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

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

DDLSQLMainForm.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);

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

33

34

Chapter 1

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

LISTING 1.4

Continued

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;

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

procedure TfrmMain.btnDeleteClick(Sender: TObject);


begin
conn.ExecuteDirect(DELETE FROM TESTING WHERE NAME = Tina);

35

36

Chapter 1

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

Transaction Support

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 Ill 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;

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

Most SQL databases, with the notable exception of versions of MySQL prior to 3.23, provide
support for transactions. A transaction must satisfy four criteria, which are called the ACID
properties of transactions. Specifically, transactions are

37

38

Chapter 1

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 theres nothing wrong with this code, but lets
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 youre 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
youre writing a general-purpose application that can access many different database backends,
you dont know up front whether the selected database backend supports transactions or not.
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:
TSQLConnection

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

Starting a Transaction

procedure StartTransaction( TransDesc: TTransactionDesc);

is a record that describes the transaction in detail. Its definition is shown

here:
TTransactionDesc =
TransactionID
GlobalID
IsolationLevel
CustomIsolation
end;

packed record
: LongWord;
: LongWord;
: TTransIsolationLevel;
: LongWord;

{ Transaction id }
{ Global transaction id }
{Transaction Isolation level}
{ DB specific custom isolation }

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.
Used for Oracle transactions to define a transaction number that
must be unique across the entire Oracle database.
Used to specify how this transaction reacts to other transactions.
Valid values for this field are listed in Table 1.7.
Identifies the custom isolation level when IsolationLevel is set to
xilCUSTOM. No dbExpress drivers currently support this.

GlobalID
IsolationLevel
CustomIsolation

Table 1.7 shows the valid settings for TTransactionDescs 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.
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.

xilREADCOMMITTED

ESTABLISHING AND
USING DATABSE
CONNECTIONS

When youve determined that the database supports transactions, you can start a transaction.
To begin, call the method TSQLConnection.StartTransaction. StartTransaction is defined
like this:

TTransactionDesc

39

40

Chapter 1

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.
Database-specific isolation level. CustomIsolation specifies the
actual isolation level. No dbExpress drivers currently support this.

xilCUSTOM

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 dont 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

1
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 cant 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

42

Chapter 1

// 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

TransMainForm.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

LISTING 1.5

Continued

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

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

lbOutput: TListBox;
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;

43

44

Chapter 1

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

LISTING 1.5

Continued

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;

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

conn.StartTransaction(TransDesc1);
conn.ExecuteDirect(UPDATE CONTACTS SET PHONE = Trans 1 WHERE ID = 3);

45

46

Chapter 1

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

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, its 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 connections SQLHourGlass property to True. SQLHourGlass is not a
published property, so you cant 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 userdefined 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;

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

47

48

Chapter 1

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.
The category of the command just sent or received. Table 1.9 lists the
possible values of this field.

eTraceCat
ClientData
uTotalMsgLen

The user-defined value passed as the second parameter to


SetTraceCallbackEvent.
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.


A query was sent to the server to execute.
An error message was returned by the server.
An operation for the database to perform was sent to the server.
A connect-or disconnect-related operation was sent to the server.
A transaction-related operation was sent to the server.
A BLOB-related operation was sent to the server.
A vendor-specific API call was sent to the server.
Parameter data was sent to the server during an INSERT or UPDATE
command.
Data was retrieved from the server.
Any other command not falling under one of the previous categories.

traceQEXECUTE
traceERROR
traceSTMT
traceCONNECT
traceTRANSACT
traceBLOB
traceVENDOR
traceDATAIN
traceDATAOUT
traceMISC

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

The following code snippet sets a callback event:

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 youll 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
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 monitors internal buffer, and dump them to
a file (or other destination) in one fell swoop.

TSQLMonitor

To use a TSQLMonitor, drop it on a form or data module, along with your TSQLConnection
component. To begin logging events, set the monitors 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 TSQLMonitors AutoSave and FileName properties. Set FileName to

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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


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

49

50

Chapter 1

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 dont 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 components 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


is not designed to easily support sending database trace events to multiple
destinations at the same time. In other words, you cant use both a TSQLMonitor and a userdefined feedback event at the same time. So, if you want to assign a callback event, you should
TSQLConnection

Establishing and Using Database 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 youre 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

FeedbackMainForm.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);

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

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

51

52

Chapter 1

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

LISTING 1.6

Continued

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, dont 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);

1
ESTABLISHING AND
USING DATABSE
CONNECTIONS

procedure TfrmMain.btnExecSQLClick(Sender: TObject);


begin
conn.ExecuteDirect(SELECT * FROM CONTACTS);
end;

53

54

Chapter 1

LISTING 1.6

Continued

monitor.Active := True;
end;
end;
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
conn.Close;
end;
end.

Lets 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 weve handled the
message, theres no need to buffer it in the monitors 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

Summary

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.

ESTABLISHING AND
USING DATABSE
CONNECTIONS

This chapter introduced you to the TSQLConnection component, which is used to establish and
maintain a connection to an SQL database. Specifically, you learned:

55

CHAPTER

dbExpress Datasets

IN THIS CHAPTER
What Are dbExpress Datasets?
Types of Datasets
Data Manipulation
BLOB Support

58

59
63

69

Parameterized Queries

71

Ordering Data Returned from the Server


Master/Detail Relationships

74

Retrieving Schema Information

79

73

58

Chapter 2

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, Ill introduce dbExpress datasets, which enable you to retrieve data from the
database connection.
Ill 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 booksI apply them in Chapter 3, Client Dataset Basics. For these reasons, I wont
go into excruciating detail here. Instead, I will provide short code snippets to show how theyre
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.
Lets take a moment to discuss the characteristics of dbExpress datasets, and then well 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, youll 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 endone record at a time. If youre 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
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 youre 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 Ill
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).

DBEXPRESS
DATASETS

FIGURE 2.1

60

Chapter 2

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.
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.
TSQLDataSet

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.
The way in which CommandText is used depends on the value of
CommandType. See the following sections for more information on
CommandText.
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.
Used to establish a master/detail link between two datasets. See the
section titled Master/Detail Relationships for more information.

CommandText

CommandType

DataSource

dbExpress Datasets

TABLE 2.1

61

Continued

Description

MaxBlobSize

Sets the maximum amount of data returned from BLOB fields. See
the section titled BLOB Support for more information.
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.
When True, the dataset automatically generates parameters whenever
CommandText changes. If you want to create parameters manually,
set this to False. See the section titled Parameterized Queries for
more information.
Contains a list of input and output parameters for the current query
or stored procedure. See the section titled Parameterized Queries
for more information.
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.
The TSQLConnection component to which this dataset is connected.
You should set this property before setting any other properties in
the dataset.

ObjectView

ParamCheck

Params

SortFieldNames

SQLConnection

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;

2
DBEXPRESS
DATASETS

Property

62

Chapter 2

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 youre 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, its almost identical to the code required for TSQLTable. There is one
additional line, to tell the dataset that its 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:

Again, notice the similarities to the TSQLStoredProc component.


In many applications, you set these properties at design time rather than at runtime. Im 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 weve discussed the components necessary for data access, lets spend a few minutes
discussing the methods provided by those components. Because dbExpress datasets are a
unidirectional read-only technology, youll see that there isnt 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 youve 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;

DBEXPRESS
DATASETS

SQLDataset1.SQLConnection := conn;
SQLDataset1.CommandType := ctStoredProc;
SQLDataset1.CommandText := ContactsByState;
SQLDataset1.ParamByName(ASTATE).Value := FL;
SQLDataset1.Open;

64

Chapter 2

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

Its 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
adopt whichever method you prefer.

:= False,

so feel free to

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 youre 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 fields 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

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

BasicMainForm.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;

2
DBEXPRESS
DATASETS

Because dbExpress datasets are unidirectional, there isnt a lot of navigation that is supported.
The only two operations that you can perform are moving to the beginning of the dataset, and
moving to the next record in the result set. These operations are illustrated in the following
code snippet:

66

Chapter 2

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

LISTING 2.1

67

Continued

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

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;

2
DBEXPRESS
DATASETS

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

68

Chapter 2

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.
loops through the records in the dataset, loading the contacts first name, last
name, and phone number into a TListView.

LoadResults

LISTING 2.2

BasicDatasetTypeForm.pas

unit DatasetTypeForm;
interface
uses

dbExpress Datasets

LISTING 2.2

69

Continued

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


QDialogs, QExtCtrls, QStdCtrls;
type
TDatasetType = (dtTable, dtQuery, dtStoredProc, dtDataset);

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 thats 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

2
DBEXPRESS
DATASETS

TfrmDatasetType = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
btnOk: TButton;
btnCancel: TButton;
grpDatasetType: TRadioGroup;
procedure btnOkClick(Sender: TObject);
private
{ Private declarations }
FDatasetType: TDatasetType;
public
{ Public declarations }
property DatasetType: TDatasetType read FDatasetType;
end;

70

Chapter 2

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 youre 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 dont 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 TSQLDataSets 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 TSQLConnections 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:

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;

DBEXPRESS
DATASETS

SQLDataSet1.CommandType := ctQuery;
SQLDataSet1.CommandText := SELECT * FROM CONTACTS +
WHERE COUNTRY = United States;
SQLDataSet1.Open;
ProcessDataSet;
SQLDataSet1.Close;

72

Chapter 2

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 oncethe 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, lets 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

LAST
VARCHAR(30), +
) +
AS +
BEGIN +
FOR SELECT ID, FIRST, LAST +
FROM CONTACTS +
WHERE TITLE = :ATITLE +
INTO :ID, :FIRST, :LAST DO +
BEGIN +

SUSPEND; +
END +
END;;
SQLDataSet1.ExecSQL;

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.
Ill address the TSQLTable component first.

Ordering Data from a Table


If you are using a TSQLTable, you can set the components 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 dont 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).

2
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.

73

74

Chapter 2

Ordering Data from a Query


If youre 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.
* FROM ACTIVITIES WHERE ContactID =

Thats all thats required to establish a master/detail relationship in your program code. You
should note the following points:
The detail datasets 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.
Youll 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 datasets DataSource parameter is assigned, so it looks to the data sources 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.

DBEXPRESS
DATASETS

5. Set sqlActivities.CommandText to SELECT


:ContactID.

76

Chapter 2

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

AdvancedMainForm.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

LISTING 2.3

77

Continued

2
DBEXPRESS
DATASETS

Label11: TLabel;
cbCountry: TComboBox;
btnRetrieve: TButton;
conn: TSQLConnection;
sqlContacts: TSQLDataSet;
sqlContactsFIRST: TStringField;
sqlContactsLAST: TStringField;
sqlContactsDEAR: TStringField;
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;
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;

78

Chapter 2

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

LISTING 2.3

79

Continued

begin
sqlContacts.Next;
end;
procedure TfrmMain.sqlContactsAfterScroll(DataSet: TDataSet);
var
ListItem: TListItem;
begin
lvActivities.Items.BeginUpdate;
try
lvActivities.Items.Clear;

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
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:
TSQLDataSet.SetSchemaInfo,

procedure SetSchemaInfo(SchemaType: TSchemaType; SchemaObjectName,


SchemaPattern: string);

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;

80

Chapter 2

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.
Information about the tables in the database that match the object
name and pattern.
Information about the system tables in the database.
Information about the stored procedures in the database.
Information about the columns for a single table.
Information about the parameters for a single stored procedure.
Information about the indexes for a single table.

stTables
stSysTables
stProcedures
stColumns
stProcedureParams
stIndexes

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.
SchemaObjectName

dbExpress Datasets

81

SchemaPattern is an SQL pattern that is used to filter the data thats 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 dont 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 underscore (_) to match a single character. If you want to include a percent sign or underscore in the
pattern, double it up (%% or __).
After youve 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.

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

SchemaMainForm.pas

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

DBEXPRESS
DATASETS

Listing 2.4 shows the complete source code for an example program that can extract and display schema information for a dbExpress database. After the listing, Ill give just a quick
overview of the code because most of it should be self-explanatory at this point.

82

Chapter 2

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

LISTING 2.4

83

Continued

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

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))

2
DBEXPRESS
DATASETS

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

84

Chapter 2

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,
Check(eSQLView,
Check(eSQLSynonym,
Check(eSQLSystemTable,
Check(eSQLTempTable,
Check(eSQLLocal,

Table);
View);
Synonym);
System);
Temp);
Local);

if Result = then
Result := $ + IntToHex(TableType, 2);
end;

dbExpress Datasets

LISTING 2.4

85

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;

Check(eSQLProcedure,
Check(eSQLFunction,
Check(eSQLPackage,
Check(eSQLSysProcedure,

Procedure);
Function);
Package);
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,
Check(eSQLRowVersion,
Check(eSQLAutoIncr,
Check(eSQLDefault,

Row Id);
Row Version);
Auto Incr);
Default);

if Result = then
Result := $ + IntToHex(ColType, 2);

DBEXPRESS
DATASETS

begin
Result := ;

86

Chapter 2

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

LISTING 2.4

87

Continued
Result
Result
Result
Result
Result
Result
Result
Result
Result
Result
Result
Result
Result

:=
:=
:=
:=
:=
:=
:=
:=
:=
:=
:=
:=
:=

dBase OLE;
Typed Binary;
Access OLE;
CLOB;
BLOB;
BFILE;
Pasword;
Char;
Unicode;
AutoInc;
ADT Nest;
ADT Date;
$ + IntToHex(ColSubType, 2);

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.

2
DBEXPRESS
DATASETS

fldstDBSOLEOBJ:
fldstTYPEDBINARY:
fldstACCOLEOBJ:
fldstHMEMO:
fldstHBINARY:
fldstBFILE:
fldstPASSWORD:
fldstFIXED:
fldstUNICODE:
fldstAUTOINC:
fldstADTNestedTable:
fldstADTDATE:
else
end;
end;

88

Chapter 2

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.32.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.
The name of the catalog, or database, that contains the table.
The owner of the table.
The table name.
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.

CATALOG_NAME
SCHEMA_NAME
TABLE_NAME
TABLE_TYPE

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.
The name of the catalog, or database, that contains the stored procedure.
The owner of the stored procedure.
The name of the stored procedure.
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.
The number of input parameters to the stored procedure.
The number of output parameters from the stored procedure.

CATALOG_NAME
SCHEMA_NAME
PROC_NAME
PROC_TYPE

IN_PARAMS
OUT_PARAMS

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.
The name of the catalog, or database, that contains the table.
The owner of the column.

CATALOG_NAME
SCHEMA_NAME
TABLE_NAME

COLUMN_POSITION
COLUMN_TYPE

COLUMN_DATATYPE
COLUMN_TYPENAME
COLUMN_SUBTYPE
COLUMN_LENGTH
COLUMN_PRECISION

COLUMN_SCALE
COLUMN_NULLABLE

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.
The name of the catalog, or database, that contains the stored
procedure.

CATALOG_NAME

2
DBEXPRESS
DATASETS

COLUMN_NAME

The name of the table containing the column.


The name of the column.
The zero-based position of the column in the table definition.
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.
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.
The SQL column type (VARCHAR, BLOB, and the like).
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.
The size of the column in bytes.
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.
The numeric scale. It is the number of digits to the right of the
decimal point for BCD columns.
It is one if the column can contain NULL values, and zero if it cannot
contain NULL values.

90

Chapter 2

TABLE 2.6

Continued

Column

Description

SCHEMA_NAME

The owner of the procedure parameter.


The name of the procedure that contains the parameter.
The name of the parameter.
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.
It is one for an input parameter, two for an output parameter, three
for an input/output parameter, and four for a return value.

PROCEDURE_NAME
PARAM_NAME
PARAM_POSITION

PARAM_TYPE
PARAM_DATATYPE
PARAM_SUBTYPE

PARAM_TYPENAME
PARAM_LENGTH
PARAM_PRECISION

PARAM_SCALE
PARAM_NULLABLE

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.
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.
The SQL parameter type (VARCHAR, BLOB, and the like).
The size of the parameter in bytes.
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.
The numeric scale. It is the number of digits to the right of the decimal
point for BCD parameters.
It is one if the parameter can contain NULL values, and zero if it cannot 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.


The owner of the index.
The name of the table on which the index is defined.
The name of the index.
The name of the column that is part of the index.

SCHEMA_NAME
TABLE_NAME
INDEX_NAME
COLUMN_NAME

dbExpress Datasets

91

TABLE 2.7 Continued


Description

COLUMN_POSITION

The position of the column within the index.


If a primary key, this is the name of the primary key.
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.
This is A for an ascending column, and D for a descending column.
This represents the filter condition on a filtered/range index, or the
expression on an expression index. For example, (LAST + FIRST). It
is only supported by certain databases, such as Oracle.

PKEY_NAME
INDEX_TYPE

SORT_ORDER
FILTER

Summary
This chapter discussed the dbExpress dataset component TSQLDataSet, including the following
key concepts:

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.

TSQLDataSet

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.

supports BLOB information. To improve query performance, you can set


TSQLDataSets BlobSize property to specify the maximum amount of data to retrieve for
each BLOB field.
TSQLDataSet

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 oncethe first time that it is executed.
To order the data thats returned from the database, you can specify an ORDER BY
clause in the datasets 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.

2
DBEXPRESS
DATASETS

Column

CHAPTER

Client Dataset Basics

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
Client Dataset Indexes
Filters and Ranges
Searching

136

126

118

113

94

Chapter 3

In the preceding two chapters, I discussed dbExpressa 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
youll 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 dont
work with TCustomClientDataSet directly, but with its direct descendent, TClientDataSet.
(In Chapter 7, Dataset Providers, Ill introduce you to other descendents of
TCustomClientDataSet.) For readability and generalization, Ill 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, youll discover that these so-called disadvantages really
arent detrimental to your application at all. In particular, basing client datasets entirely in
RAM has both advantages and disadvantages.

On the flip side, you need to take steps to ensure that client datasets dont 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. (Ill 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, youll
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.

CLIENT DATASET
BASICS

Because they are kept entirely in your computers RAM, client datasets are extremely useful
for temporary tables, small lookup tables, and other nonpersistent database needs. Client
datasets also are fast because they are RAM based. Inserting, deleting, searching, sorting, and
traversing in client datasets are lightening fast.

96

Chapter 3

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
doesnt 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 youre familiar with the field editor, you notice a new field type available for client datasets,
called Aggregate fields. Ill 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 datasetjust 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

Lets 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. Youll see the new ID field listed in the field editor.

Similarly, add a 20-character FirstName field and an Integer Department field.Finally, lets
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.

CLIENT DATASET
BASICS

Now add a second field, called LastName. Right-click the field editor to display the New Field
dialog and enter LastName in the Name edit control. In the Type combo, select String. Then, set
Size to 30the size represents the maximum number of characters allowed for the field. Click
OK to add the LastName field to the dataset.

98

Chapter 3

Thats enough fields for this dataset. In the next section, Ill show you how to create a
calculated field.

Calculated Fields
Calculated fields, as indicated previously, dont 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,
well 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, well create a calculated field that computes an annual bonus, which well
assume to be five percent of an employees 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.
Thats all you need to do to create a calculated field. Now, lets 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 dialog, 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 recalculate automatically if the underlying fields that they are calculated from change.)
The datasets AutoCalcFields property determines exactly when calculated fields are recomputed. 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.
Lets 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).

Providing Values for Calculated Fields


At this point, weve 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.
Well 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;

Thats easywe just take the value of the Salary field, multiply it by five percent (0.05), and
store the value in the Bonus field.
Now, lets add the Name field calculation. A first (reasonable) attempt looks like this:
procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);
begin
cdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;

3
CLIENT DATASET
BASICS

In the Field Type radio group, select InternalCalc and click OK.

100

Chapter 3

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

This works, but it isnt efficient. The Name field calculates every time the Bonus field calculates.
However, recall that it isnt necessary to compute internal calculated fields as often as standard
calculated fields. Fortunately, we can check the datasets 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 its time to compute internal calculated fields.

Lookup Fields
Lookup fields are similar, in concept, to calculated fields because they arent 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. Lets look at an example.
Earlier, we created a Department field in our dataset. Lets 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 Field

Type radio group, select Lookup. Notice that two of the fields in the Lookup
group box are now enabled. In the Key Fields combo, select Department. In the
Dataset combo, select cdsDepartment.
definition

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.

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 youll see later in this chapter in the section titled Persisting Client
Datasets).

3
CLIENT DATASET
BASICS

Now that youve seen how to create a client dataset at design-time, lets see whats required to
create a client dataset at runtime.

102

Chapter 3

Adding Fields to a Client Dataset


To add fields to a client dataset at runtime, you use the client datasets 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
its 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, its 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 informationwhether its 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:

To retrieve the employees 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 doesnt mean you cant attempt to access it as a
string. The following code displays an employees 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 doesnt contain a value thats compatible with that
data type, Delphi raises an exception.

CLIENT DATASET
BASICS

ShowMessage(The employees last name is +


cdsEmployeeLastName.AsString);

104

Chapter 3

Nonpersistent Fields
If you create a dataset at design-time, you probably wont have any persistent field objects. In
that case, there are a few methods you can use to access a fields 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 employees last name
using FieldByName.
ShowMessage(The employees 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 datasets 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

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 its 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.

3
CLIENT DATASET
BASICS

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 datasets 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
modify and delete records.

106

Chapter 3

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 cant 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 Delphis 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);

accepts the name of the file that youre saving to. If the filename is blank, the data
is saved using the FileName property of the client dataset.

SaveToFile

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

TABLE 3.1

Data Packet Formats for Loading and Saving Client Datasets

Value

Description

dfBinary

Data is stored using a proprietary, binary format.


Data is stored in XML format. Extended characters are represented
using an escape sequence.
Data is stored in XML format. Extended characters are represented
using UTF8.

dfXML
dfXMLUTF8

107

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 youre 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).

if ClientDataSet1.DataSize > AvailableSpace then


ShowMessage(Not enough room to store the data);

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).

DataSize

NOTE
One way to determine the amount of space thats 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.

CLIENT DATASET
BASICS

Sometimes, you need to determine how many bytes are required to store the data contained in
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:

108

Chapter 3

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

CDSMainForm.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

LISTING 3.1

109

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;

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)] + +

CLIENT DATASET
BASICS

procedure TfrmMain.Populate1Execute(Sender: TObject);


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;

110

Chapter 3

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

LISTING 3.1

111

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;

There are five methods in this application and each one is worth investigating:

FormCreate

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.

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

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.

CLIENT DATASET
BASICS

end.

112

Chapter 3

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


operations and calculates the amount of space necessary to store the data on disk
(again, in binary format). Ill be discussing the Locate method later in this chapter.
Statistics1Execute
Locate

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.
Save1Execute

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.

loads the data from a file into the client dataset. If LoadFromFile fails
(presumably because the file doesnt 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.
Load1Execute

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.
Delphis datasets provide a large number of methods for traversing a dataset. The following
sections discuss Delphis 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 datasets 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
is True, Next will fail. If the call to Next reaches the end of the file, it sets the EOF
property to True.
EOF

moves to the last record in the dataset. Last always succeeds, even if the dataset is
empty. If it is empty, Last sets the datasets BOF (beginning of file) property to True.

Prior

Last

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;

CLIENT DATASET
BASICS

moves to the preceding record in the dataset (if the BOF property is not already
set). If BOF is True, Prior will fail. If the call to Prior reaches the beginning of the file,
it sets the BOF property to True.

114

Chapter 3

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
The following code shows this alternate method:

GetBookmark/GotoBookmark/FreeBookmark.

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 dont need to concern
yourself with freeing the string when youre 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.

You can determine the number of records in the dataset by inspecting the datasets
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, lets 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;

CLIENT DATASET
BASICS

NOTE

116

Chapter 3

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 isnt usually going to be your first line of attack for moving around in
a dataset, but its handy to remember that its 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

NavigateMainForm.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

LISTING 3.2

117

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

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);

CLIENT DATASET
BASICS

{$R *.xfm}

118

Chapter 3

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 havent created any indexes on the client dataset and you might be wondering if
(and why) theyre 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
Like field definitions, indexes can be created at design-time or at runtime. Unlike field
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 timesay, 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, its 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.

CLIENT DATASET
BASICS

Creating Indexes

120

Chapter 3

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).
Semicolon-delimited list of fields that make up the index. Example:
ID or Name;Salary.
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.
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.
Used for aggregation.
Sets additional options on the index. The options are discussed in
Table 3.3.
Not applicable to client datasets.
Not applicable to client datasets.

Fields
DescFields

CaseInsFields

GroupingLevel
Options
Expression
Source

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.


The index is unique.
The index is in descending order.
The index is not case sensitive.
Not applicable to client datasets.
Not applicable to client datasets.

IxUnique
IxDescending
IxCaseInsensitive
IxExpression
IxNonMaintained

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 datasets 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:

When you decide that you no longer need an index (remember, you can always re-create it if
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 doesnt 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 datasets
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;

3
CLIENT DATASET
BASICS

ClientDataSet1.AddIndex(byName, Last;First, [ixUnique]);

122

Chapter 3

// 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 its not likely to be noticeable, but
you should be aware that its 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 cant 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 following 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 havent defined any indexes,
youll 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 descending, and so on).
The following code snippet shows how to access index information directly through
TIndexDefs.

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, its possible 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.

3
CLIENT DATASET
BASICS

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

124

Chapter 3

FIGURE 3.7
CDSIndex

shows how to create indexes on-the-fly.

LISTING 3.3

CDSIndexMainForm.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

LISTING 3.3

125

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;

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;

3
CLIENT DATASET
BASICS

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


ClientDataSet1.IndexName := byUser;
end;

126

Chapter 3

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 doesnt 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 Kasters
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

TABLE 3.4

127

Sample Data for Ranges and Filters

ID

Name

Birthday

Salary

4
2
3
1
5

Bill Peterson
Frank Smith
Sarah Johnson
John Doe
Paula Wallace

3/28/1957
8/25/1963
7/5/1968
5/15/1970
1/15/1971

$60,000.00
$48,000.00
$52,000.00
$39,000.00
$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]);

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


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.53.10 list the various functions and operators available for use in
a filter.
TABLE 3.5

Filter Comparison Operators

Function

Description

Example

Equality test
Inequality test
Less than
Greater than
Less than or equal to
Greater than or equal to

Name = John Smith

<>
<
>
<=
>=

ID <> 100
Birthday < 1/1/1980
Birthday > 12/31/1960
Salary <= 80000
Salary >= 40000

3
CLIENT DATASET
BASICS

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,
1970 are included in the range.

128

Chapter 3

TABLE 3.5

Continued

Function

Description

Example

BLANK

Empty string field


(not used to test for
NULL values)
Test for NULL value
Test for non-NULL value

Name = BLANK

IS NULL
IS NOT NULL

TABLE 3.6
Function

Birthday IS NULL
Birthday IS NOT NULL

Filter Logical Operators

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


numbers, strings, or
dates/times.
Subtraction. Can be used
with numbers or dates/times.
Multiplication. Can be used
with numbers only.
Division. Can be used with
numbers only.

Birthday + 30 < 1/1/1960


Name + X = SmithX
Salary + 10000 = 100000

*
/

TABLE 3.8

Birthday - 30 > 1/1/1960


Salary - 10000 > 40000
Salary * 0.10 > 5000
Salary / 10 > 5000

Filter String Functions

Function

Description

Example

Upper

Uppercase
Lowercase
Return a portion of a string

Upper(Name) = JOHN SMITH

Trim leading and trailing


characters from a string

Trim(Name)
Trim(Name, .)

Lower
SubString
Trim

Lower(Name) = john smith


SubString(Name,6) = Smith
SubString(Name,1,4) = John

Client Dataset Basics

TABLE 3.8

Continued

Function

Description

Example

TrimLeft

Trim leading characters


from a string
Trim trailing characters
from a string

TrimLeft(Name)
TrimLeft(Name, .)

TrimRight

TABLE 3.9

TrimRight(Name)
TrimRight(Name, .)

Filter Date/Time Functions

Function

Description

Example

Year

Returns the year portion


of a date value.
Returns the month portion
of a date value.
Returns the day portion
of a date value.
Returns the hour portion
of a time value in 24-hour
format.
Returns the minute portion
of a time value.
Returns the second portion
of a time value.
Returns the current date
and time.
Returns the date portion
of a date/time value.
Returns the time portion
of a date/time value.

Year(Birthday) = 1970

Month
Day
Hour

Second
GetDate
Date
Time

TABLE 3.10

Month(Birthday) = 1
Day(Birthday) = 15
Hour(Appointment) = 18

Minute(Appointment) = 30
Second(Appointment) = 0
Appointment < GetDate
Date(Appointment)
Time(Appointment)

Other Filter Functions and Operators

Function

Description

Example

LIKE

Name LIKE %Smith%

IN

Partial string comparison.


Tests for multiple values.

Partial string comparison.

Name = John*

Year(Birthday) IN (1960,
1970, 1980)

3
CLIENT DATASET
BASICS

Minute

129

130

Chapter 3

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 dont 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 datasets 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 dont 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

RangeFilterMainForm.pas

unit MainForm;
interface
uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, DBClient, QExtCtrls, QGrids, QDBGrids;

Client Dataset Basics

LISTING 3.4

131

Continued

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);

3
CLIENT DATASET
BASICS

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 }
end;

132

Chapter 3

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

LISTING 3.5

133

RangeFilterFilterForm.pas

unit FilterForm;
interface
uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
QExtCtrls;

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.

3
CLIENT DATASET
BASICS

type
TfrmFilter = class(TForm)
pnlClient: TPanel;
pnlBottom: TPanel;
Label1: TLabel;
cbField: TComboBox;
Label2: TLabel;
cbRelationship: TComboBox;
Label3: TLabel;
ecValue: TEdit;
btnOk: TButton;
btnCancel: TButton;
private
function GetFilter: string;
{ Private declarations }
public
{ Public declarations }
property Filter: string read GetFilter;
end;

134

Chapter 3

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

RangeFilterRangeForm.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

LISTING 3.6

135

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);

end.

Figure 3.8 shows the RangeFilter application in operation.

FIGURE 3.8
RangeFilter

applies both ranges and filters to a dataset.

3
CLIENT DATASET
BASICS

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);
end;
end;

136

Chapter 3

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, Ill discuss the search techniques that dont 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.
Locate ignores case when searching for string fields.

loCaseInsensitive

Client Dataset Basics

137

Both options pertain to string fields only. They are ignored if you specify them for a nonstring
search.
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.

Locate

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
Johnny Jones, and JOHN ADAMS, but not Bill Johnson.

Smith,

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


[loPartialKey]);

Lookup
Lookup is similar in concept to Locate, except that it doesnt 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 cant 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;

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:
KeyFields

var
V: Variant;
begin
V := ClientDataSet1.Lookup(Name, John Doe, Birthday);
end;

3
CLIENT DATASET
BASICS

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
for that field only.

138

Chapter 3

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
dont 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
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.
FindKey

Client Dataset Basics

139

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.

FindKey

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:

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 doesnt 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;

3
CLIENT DATASET
BASICS

ClientDataSet1.IndexName := byID;
if ClientDataSet.FindKey([25]) then
ShowMessage(Found ID 25);
...
ClientDataSet1.IndexName := byName;
if ClientDataSet.FindKey([Doe, John]) then
ShowMessage(Found John Doe);

140

Chapter 3

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 doesnt 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

LISTING 3.7

141

SearchMainForm.pas

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

var
frmMain: TfrmMain;
implementation
uses SearchForm;
{$R *.xfm}
procedure TfrmMain.FormCreate(Sender: TObject);
begin

3
CLIENT DATASET
BASICS

type
TfrmMain = class(TForm)
DataSource1: TDataSource;
pnlClient: TPanel;
pnlBottom: TPanel;
btnSearch: TButton;
btnGotoBookmark: TButton;
btnGetBookmark: TButton;
btnLookup: TButton;
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
btnSetRecNo: TButton;
procedure FormCreate(Sender: TObject);
procedure btnGetBookmarkClick(Sender: TObject);
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;

142

Chapter 3

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

LISTING 3.7

143

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;

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.

3
CLIENT DATASET
BASICS

smGotoNearest: begin
ClientDataSet1.SetKey;
ClientDataSet1.FieldByName(Name).AsString :=
frmSearch.ecName.Text;
ClientDataSet1.GotoNearest;
end;
end;
end;
finally
frmSearch.Free;
end;
end;

144

Chapter 3

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

SearchSearchForm.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
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:
TClientDataSet

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.

and Lookup are nonindexed search techniques for locating a specific record in a
client dataset. FindKey, FindNearest, GotoKey, and GotoNearest are indexed search
techniques.

Locate

In the following chapter, Ill discuss more advanced client dataset functionality.

CLIENT DATASET
BASICS

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.

CHAPTER

Advanced Client Dataset


Operations

IN THIS CHAPTER
Dataset Events

148

Disabling Data-Aware Components


BLOBs

162

Nested Datasets
Undo Support

172
176

Cloning Data from Another Client


Dataset 186
Maintained Aggregates
Miscellaneous Properties

192
197

158

148

Chapter 4

The preceding chapter introduced you to TClientDataSet and discussed much of its basic
functionality in detail. In this chapter, Ill 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 were developing in this chapter), and some are only useful in
multitier applications (which well 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 performsactivities
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 doesnt 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, Ill present a sample application that illustrates this technique.
Table 4.1 lists the client datasets BeforeXxx events and their uses.
TABLE 4.1

Client Dataset BeforeXxx Events

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.
Called immediately before the dataset is closed.
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.
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).
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).
Called just before the dataset is opened.
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.
Triggered just before 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.

BeforeClose
BeforeDelete
BeforeEdit

BeforeInsert

BeforeOpen
BeforePost

BeforeScroll

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 handlers 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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

Event

150

Chapter 4

TABLE 4.2

Client Dataset AfterXxx Events

Event

Description

AfterCancel

Triggered after a Cancel method completes.


Occurs just after the dataset is closed.
Triggered immediately after a record in the dataset is deleted.
Called after the dataset is put into edit mode as a result of an Edit
method call.
Called after the dataset is put into insert mode as a result of an
Insert or Append method call.
Occurs just after the dataset is opened.
Triggered after a record is successfully posted to the dataset.
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.

AfterClose
AfterDelete
AfterEdit
AfterInsert
AfterOpen
AfterPost
AfterScroll

Table 4.3 lists the client datasets other notable event handlers.
TABLE 4.3

Other Event Handlers

Event Description
OnCalcFields
OnDeleteError
OnEditError
OnFilterRecord
OnNewRecord

OnPostError

As discussed in Chapter 6, Data-Aware Grids, this event is used to


provide values for calculated fields.
Fired if there is an error when deleting a record from the dataset
(for example, if the dataset is read-only).
Fired if there is an error when putting the dataset into edit mode
(for example, if the dataset is read-only).
As discussed in Chapter 6, this event is called to enable the user to
provide advanced filtering on a dataset.
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 shouldnt call TClientDataSet.Insert from within
this event handler.
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, Ive done just that. Listing 4.1
contains the complete source code for the EventLog application.
LISTING 4.1

EventLogMainForm.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);

4
ADVANCED CLIENT
DATASET
OPERATIONS

TfrmMain = class(TForm)
ClientDataSet1: TClientDataSet;
pnlClient: TPanel;
pnlLog: TPanel;
grid: TDBGrid;
lvLog: TListView;
DataSource1: TDataSource;
pnlBottom: TPanel;
btnConnect: TButton;
lblRecPos: TLabel;
btnDisconnect: TButton;
btnClearLog: TButton;
DBNavigator1: TDBNavigator;
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);

152

Chapter 4

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

LISTING 4.1

153

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.ClientDataSet1AfterOpen(DataSet: TDataSet);


begin
Log(logAfterOpen);
end;
procedure TfrmMain.ClientDataSet1AfterPost(DataSet: TDataSet);
begin
Log(logAfterPost);
end;

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.ClientDataSet1AfterInsert(DataSet: TDataSet);


begin
Log(logAfterInsert);
end;

154

Chapter 4

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

LISTING 4.1

155

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;

if frmOptions.ShowModal = mrOk then begin


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

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.btnOptionsClick(Sender: TObject);


var
frmOptions: TfrmOptions;
begin
frmOptions := TfrmOptions.Create(nil);
try
frmOptions.LogScrollEvents := FLogScrollEvents;
frmOptions.PromptOnDelete := FPromptOnDelete;

156

Chapter 4

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 bottom 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 applications 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

EventLogOptionsForm.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

LISTING 4.2

157

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;

procedure TfrmOptions.SetLogScrollEvents(const Value: Boolean);


begin
cbLogScrollEvents.Checked := Value;
end;
procedure TfrmOptions.SetPromptOnDelete(const Value: Boolean);
begin
cbPromptOnDelete.Checked := Value;
end;
end.

4
ADVANCED CLIENT
DATASET
OPERATIONS

function TfrmOptions.GetPromptOnDelete: Boolean;


begin
Result := cbPromptOnDelete.Checked;
end;

158

Chapter 4

Disabling Data-Aware Components


This topic actually applies to all Delphi datasets, but Im discussing it within the context of
client datasets because you typically dont connect data-aware components directly to a
dbExpress dataset. Usually, data-aware components are connected to a client dataset.
As youve learned in this chapter, data-aware components actively track the current record in
the dataset that theyre 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 dont 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 its
finished.
The problem with this code is that its slow. If you run it against the 10,000 record employee
dataset that we created in the preceding chapter, youll 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 youre
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;

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

UpdatesMainForm.pas

unit MainForm;
interface

4
ADVANCED CLIENT
DATASET
OPERATIONS

As this code snippet shows, you want to wrap the code between the calls to DisableControls
and EnableControls in a try/finally block. If you dont, and an exception occurs somewhere
in the code, the data-aware components cease to be updated.

160

Chapter 4

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

LISTING 4.3

161

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;

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

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.btnDisableEnableClick(Sender: TObject);


var
t1, t2: DWord;
begin
t1 := GetTickCount;
ClientDataSet1.DisableControls;
try
PerformWork;
finally
ClientDataSet1.EnableControls;
end;

162

Chapter 4

The results of this test are impressive. The baseline test (which doesnt 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, Ill show you how to effectively store BLOBs in a client dataset and how to
retrieve them later. In the pages to follow, Ill 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 youve 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.

With a little extra code, you can dispense with the additional field and store the image type in
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;

ADVANCED CLIENT
DATASET
OPERATIONS

Streaming the Image Type as Part of the BLOB Field

164

Chapter 4

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.
Fixed-Length Image Header
Image Data

File extension or other designation


to describe the image type
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 shouldnt
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 youre looking for a good imaging library, check out Skyline Tools Imagings
ImageLib Corporate Suite. ImageLib Corporate Suite has won numerous awards as the
best Delphi imaging library available (including Delphi Informant Magazines coveted
Readers 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));

procedure LoadListFromBlob(List: TList);


var
Stream: TStream;
Count: Integer;
Index: Integer;
Num: Integer;

4
ADVANCED CLIENT
DATASET
OPERATIONS

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


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

166

Chapter 4

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 datasets
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 Delphis
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, lets
create a component wrapper for the data.

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

TIntegerItem = class(TCollectionItem)
private
FNumber: Integer;
published
property Number: Integer read FNumber write FNumber;
end;

168

Chapter 4

NOTE
It is beyond the scope of this book to present a detailed discussion of Delphis streaming
support. For more information on streaming, please refer to the Component Writers
Guide or to the ultimate reference, the VCL/CLX source itself. If you are fortunate
enough to own a copy of Danny Thorpes Delphi Component Design, it also has an
informative chapter on Delphis 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 fields 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 cant (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 cant perform a locate or other search technique on a BLOB field.


The reconciliation features of DataSnap, which are discussed in Chapter 8, DataSnap,
dont work with BLOB fields. However, there is a workaround, as youll 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

BLOBsMainForm.pas

unit MainForm;
interface
uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,
DB, QDBCtrls, QExtCtrls, QComCtrls, DBClient, JPEG;

4
ADVANCED CLIENT
DATASET
OPERATIONS

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;
ClientDataSet1Notes: TBlobField;
ClientDataSet1ImageType: TStringField;
ClientDataSet1Image: TBlobField;
ClientDataSet1Attachment: TBlobField;
btnLoadAttachment: TButton;
btnSaveAttachment: TButton;
Label2: TLabel;
DBText2: TDBText;
ClientDataSet1AttachedFile: TStringField;
Bevel1: TBevel;
Image1: TImage;
btnLoadImage: TButton;
btnClearImage: TButton;
OpenPictureDialog1: TOpenDialog;

170

Chapter 4

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

LISTING 4.4

171

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.btnSaveAttachmentClick(Sender: TObject);


begin
if SaveDialog1.Execute then
ClientDataSet1Attachment.SaveToFile(SaveDialog1.FileName);
end;
end.

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.btnLoadAttachmentClick(Sender: TObject);


begin
if OpenDialog1.Execute then begin
ClientDataSet1.Edit;
ClientDataSet1AttachedFile.AsString := OpenDialog1.FileName;
ClientDataSet1Attachment.LoadFromFile(OpenDialog1.FileName);
end;
end;

172

Chapter 4

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 displayedThe program handles the data sources
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 fields LoadFromFile
method, and can be saved back to disk through the fields SaveToFile method.
The BLOBs application doesnt 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 programs.
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 TClientDataSets 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.

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.

ADVANCED CLIENT
DATASET
OPERATIONS

The only remaining piece of business is to link them together. To do this, click the detail
dataset and set the DataSetField property to the name of the DataSet field that you created on
the master. Thats all there is to it.

174

Chapter 4

Listing 4.5 contains the complete source code for the Nested application.
LISTING 4.5

NestedMainForm.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

LISTING 4.5

175

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;

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.btnLoadClick(Sender: TObject);


begin
if OpenDialog1.Execute then
cdsCustomer.LoadFromFile(OpenDialog1.FileName);
end;

176

Chapter 4

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, youll 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
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

TCustomClientDataSet

Advanced Client Dataset Operations

177

analysis of the data). If the user doesnt 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 Ill 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 thats 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.

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

LogChanges
In order for the change log to be active, the datasets LogChanges property must be set to True
(which is the default). If you dont intend to provide undo support in your application, you can
set LogChanges to False, slightly reducing memory requirements and increasing performance.

178

Chapter 4

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 demonstrationYou
dont 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.

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

For example, assume that the user made a change to a dataset. He then wants to experiment
with some other changes, but isnt sure that he wants to save the results. After the first change,
you could retrieve the current value of SavePoint, like this:

180

Chapter 4

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;
ClientDataSet1.Post;
ClientDataSet1.First;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(Last).AsString := Carter;
ClientDataSet1.Post;
Baseline := ClientDataSet1.SavePoint;

// Change 1

// Change 2

// 2 changes on the stack

ClientDataSet1.Next;
ClientDataSet1.Edit;
ClientDataSet1.FieldByName(First).AsString := Sam;
ClientDataSet1.Post;

// Change 3

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.

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.


The record has been newly inserted into the dataset.
The record was modified.
The record has been deleted.

usInserted
usModified
usDeleted

4
ADVANCED CLIENT
DATASET
OPERATIONS

StatusFilter
The StatusFilter property provides for a type of filter on the dataset, but I didnt discuss it
in the Ranges and Filters section because it relates directly to the change log.

182

Chapter 4

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 ClientDataSet1s 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

ChangeLogMainForm.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

LISTING 4.7

183

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.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;

4
ADVANCED CLIENT
DATASET
OPERATIONS

procedure TfrmMain.btnFilterClick(Sender: TObject);


begin
ClientDataSet1.StatusFilter := [usInserted];
end;

184

Chapter 4

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

ChangeLogChangeLogForm.pas

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

Advanced Client Dataset Operations

LISTING 4.8

185

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);

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

ClientDataSet1.Data := ADataSet.Delta;
end;

186

Chapter 4

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 datasets 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 datasets 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 datasets
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;

refers to the client dataset that you want to clone. You cant clone a nonclient dataset,
such as a BDE dataset or a dbExpress dataset.
Source

and KeepSettings work hand in hand, and determine how the clone handles the
following attributes:

Reset

Filter, Filtered, FilterOptions, OnFilterRecord

IndexName

MasterSource, MasterFields

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

188

Chapter 4

TABLE 4.5

Relationship between Reset and KeepSettings

Reset

KeepSettings

Behavior

False

False
True
N/A

Properties are copied from the original dataset to the clone.


Properties are not changed for the clone.
Properties are cleared on the clone.

False
True

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 clones 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 youll 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

CloneMainForm.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

LISTING 4.9

189

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);

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

ADVANCED CLIENT
DATASET
OPERATIONS

ClientDataSet1.AddIndex(byID, ID, [ixPrimary, ixUnique]);


ClientDataSet1.IndexName := byID;

190

Chapter 4

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

LISTING 4.9

191

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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

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 Ive 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.

192

Chapter 4

The Update button runs through the entire dataset and gives all employees a 10% raise. Notice
that even though the code doesnt call DisableControls (discussed near the beginning of this
chapter), the grid still doesnt scroll as the dataset is traversed. Thats 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 99999the 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 arent 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 youre 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 cant 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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

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).

194

Chapter 4

3. Set the Active property to True. By default, aggregates are not active, which means that
you arent 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 dont use
the AsFloat property to obtain an aggregates 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 floatingpoint 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 datasets 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

Description

Sum

Calculates the sum of a field.


Calculates the average value of a field.
Calculates the number of values for a field that are not blank.
Calculates the minimum value of a field.
Calculates the maximum value of a field.

Avg
Count
Min
Max

and Avg can only be used with numeric field types; but Count, Min, and Max can be used
with numbers, strings, or date values.

Sum

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 cant
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.

4
ADVANCED CLIENT
DATASET
OPERATIONS

Aggregate Type

196

Chapter 4

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, well 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 youve 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 sources 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 weve established our baseline, lets 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 datasets AggregatesActive property to False.
Disabling aggregates results in a slightly speedier application because Delphi doesnt 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 datasets
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, Ill refer you to the Aggregate
demo in Delphis DEMOS\MIDAS\AGGREGATE directory.

Miscellaneous Properties
This section lists several additional properties of the client dataset that dont logically fall into
one of the previously discussed categories.

Constraints provide a way of validating a records 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 records data might (nonsensically) be determined invalid if the Salary field is
less than 1000 times the employees 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.

ADVANCED CLIENT
DATASET
OPERATIONS

Constraints

198

Chapter 4

Back in the Object Inspector, enter the constraint into the CustomConstraint property. In the
ErrorMessage property, type the message that youd like to be displayed when the constraint is
not metfor example, The salary must be at least 1000 times the employees 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
doesnt 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 datasets 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 datasets
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.

TClientDataSets

undo support enables you to perform what-if scenarios in your

applications.

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.

ADVANCED CLIENT
DATASET
OPERATIONS

By cloning a client dataset, you can perform operations on a clone of the data without
disturbing the settings of the original dataset.

CHAPTER

Data-Aware Components

IN THIS CHAPTER
What Are Data-Aware Components?
TDataSource

204

Common Data-Aware Component


Characteristics 205
Simple Data-Aware Components
VCL-Only Data-Aware Controls
Lookup Data-Aware Controls
TDBNavigator

222
222

223

Creating Your Own Data-Aware


Components 225
Sample Application

232

211

202

202

Chapter 5

The preceding two chapters concentrated on client datasets, Delphis 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 NonData-Aware Equivalents

Data-Aware Component

NonData-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

(VCL-only component)
None. Allows for the display of multiple fields in a format
that is not row oriented (VCL-only component).

TDBCtrlGrid

TRichEdit

Data-Aware Components

TABLE 5.1

203

Continued

Data-Aware Component

NonData-Aware Equivalent

TDBNavigator

None. Provides a visual means of navigating and manipulating datasets without code.
Not a data-aware component per se. Provides a conduit
between a dataset and one or more data-aware components.

TDataSource

As you can see in Table 5.1, the similarities between the standard components and the dataaware components are self-explanatory (with the possible exception of the TDBLookupComboBox
and TDBLookupListBox components).
Except for TDataSource, youll 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, Ill spend the following chapter investigating
it. Rather than providing numerous small sample applications throughout this chapter, Ill defer
an example until the end.
For the most part, the data-aware components mirror their nondata-aware counterparts, so I
have not spent a lot of time and space here discussing each of their properties, events, and
methods. Instead, Ive concentrated on issues that are specific to the data-aware version of the
component. If you need basic information about the components properties or methods, please
refer to either the online help or to one of the excellent general-purpose Delphi books
available.

NOTE

5
DATA-AWARE
COMPONENTS

Some Delphi programmers shy away from data-aware componentsmostly 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
bad reputation in Delphis early days. Rest assured that data-aware components
exhibit good performance characteristics under Delphi, especially when used with
TClientDataSets (which they are in this chapter).

204

Chapter 5

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.

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 published properties and Table 5.3 lists the data sources events. OnDataChange and
OnStateChange (the most commonly used of the events) are applied in Listing 5.4, later in this
chapter.
TDataSource

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 specifically call the datasets Edit method before the user can type into
any of the connected data-aware controls.
Provides a link to the dataset from which the data-aware components retrieve data.
When Enabled is True, the data-aware components connected to
this data source display the data contained in the dataset. When its
False, data-aware controls are blank.

DataSet
Enabled

Data-Aware Components

TABLE 5.3

205

TDataSource Events

Event

Description

OnDataChange

Fires when the datasets current record data is changed; either


because the datasets cursor is moved to a new record, or because
one of the fields is modified.
Fires when the underlying datasets State property changes. For
example, when the dataset transitions from browse mode to edit
mode, or from insert mode to browse mode.
Fires immediately before the underlying dataset posts changes to the
database.

OnStateChange

OnUpdateData

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 application at the end of this chapter.

Common Data-Aware Component Characteristics


Before discussing the specifics of each individual data-aware component, there are some common 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 thats 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:

ClientDataSet1.Edit;
ClientDataSet1FirstName.AsString := John;

DATA-AWARE
COMPONENTS

206

Chapter 5

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 datasets Edit method before attempting to set
the field value. If the dataset is already in edit or insert mode, the redundant call to Edit doesnt have any adverse effects. If for some reason the dataset cant be edited (for example, if the
datasets 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 users ability to make edits from
within your code, you have four options:
Set the datasets ReadOnly property to True, preventing the user from changing any values in the dataset. This also prevents any changes being made to the data through code.
Set the underlying fields ReadOnly property to True. This prevents the user from modifying the field, and also prevents the field from being modified through code.
Set the data-aware components 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
components 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 component.
Set the data sources AutoEdit property to False, preventing the dataset from automatically 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 dont 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 display 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 fields 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

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.
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 formatted 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 stringit 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.
Scientific notation. If E+, E, e+, or e is present in the DisplayFormat,
the value is formatted using scientific notation. E+ indicates that all exponents should be preceded by a sign. E indicates that only negative exponents 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
exponent.
Separator character. Used between positive, negative, and zero portions of
the string.
Literal. Characters enclosed in single or double quotes are copied literally
to the output string, and are not interpreted as formatting characters.

E+/

;
or

5
DATA-AWARE
COMPONENTS

Character

208

Chapter 5

Table 5.5 lists some examples of DisplayFormat settings.


TABLE 5.5

DisplayFormat Settings

Value

DisplayFormat

Output

1.2
1.2
1
1

##0.00

1.20

000.##

001.2

#0.000

1.000

00.##

(The radix point is not displayed because it is


not needed.)

12.34

$##0.00

$12.34

1234.56
12.345

$,0.00

$1,234.56

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
15
0
10

#0.000E+00

10.000E+04

##0;(##0);zero

(15)

##0;(##0);zero

zero

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 semicolons, 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 positive 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 displayed 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 fields value. You can set a different format
to use when editing by setting the fields EditFormat property in addition to, or instead of the
DisplayFormat property. EditFormat works the same as DisplayFormat: It contains a semicolon-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 dont 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 fields 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, separated 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 thats 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

Description

Requires an alphabetic character.


Allows an alphabetic character, but does not require it.
Requires an alphanumeric character.
Allows an alphanumeric character, but does not require it.
Requires a character.
Allows a character, but does not require it.
Requires a numeric character.
Allows a numeric character, but does not require it.
Allows a numeric character, or a plus or minus sign, but does not
require it.

l
A
a
C
c
0
9
#

5
DATA-AWARE
COMPONENTS

Character

210

Chapter 5

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 its other than :.
Date separator. This character is replaced with the date separator that is
defined in the control panel under regional settings when its 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 characters 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, literally, and is not interpreted as a mask character.

/
_
;
!

>
<
<>
\

NOTE
Each character in the mask represents one byte in the stringnot 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 represent a single multibyte character. When inserting a literal into a mask, only singlebyte 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

123-456-7890

000-000-0000;1;_

123-456-7890

Phone number. Formatting


characters are not stored.
Social Security number.
Formatting characters
(hyphens) are stored.

Data-Aware Components

TABLE 5.7

211

Continued

Stored Value

EditMask

Displayed Value

Remarks

33467-0708

00000-0000;1;_

33467-0708

5/28/01

!99/99/00;1;_

5/28/01

ZIP code. Hyphen is


stored.
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 youre wanting to use
anything more than a simple edit mask. The standard data-aware components dont
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 TurboPowers Web site at http://www.turbopower.com.

Simple Data-Aware Components


Most data-aware components fall into a category that Ive defined as simple data-aware components. 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 values 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, Ill discuss the simple data-aware components, and following sections will
cover some that are more complex. Dont be too concerned at this point with the mechanics of
creating data-aware components. Near the end of this chapter, Ill show you how to take a
nondata-aware component and create a data-aware descendent from it.

TDBText

DATA-AWARE
COMPONENTS

is the simplest of all the data-aware components. It is a display-only component, similar 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.
TDBText

212

Chapter 5

TDBEdit
TDBEdit corresponds to the standard TEdit component. Its used to display and edit numeric,
string, or data/time data contained in a dataset.

Data is displayed according to the underlying fields DisplayFormat or EditMask property, and
is edited according to the EditFormat or EditMask property (depending on the field type).

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.

TDBMemo

TDBCheckBox
TDBCheckBox is used like a standard TCheckBoxto display and enter yes/no or true/false values. TDBCheckBox can be connected to a Logical or a Yes/No field in desktop databases, such
as Paradox or Access. Most SQL databases, however, dont directly support these field types.
In these cases, you connect the TDBCheckBox to a string field, which is frequently a single character.

To define the relationship between checked/unchecked and the underlying field data, you set
the components 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 displayed, and the underlying field contains a value that does not equal either of these two properties, the checkbox is initially grayed out.
By default, the values of these two properties are true and false, respectively. In my own applications, 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 dont 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 specify 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, Ive 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 components
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 underlying 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

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Tuesday

FIGURE 5.2
Relationship between TDBRadioGroups Items and Values properties.

5
works similarly to TDBRadioGroup because it enables the user to select an item
from a list and store it in a dataset. Unfortunately, it doesnt support the Values property, so
you cant use it to store the index of the selected item in a dataset. In my applications, this is
often a severe limitation, so Ive created a descendent component named TETHDBComboBox that
supports assigning a value to each string contained in the Items property.
TDBComboBox

DATA-AWARE
COMPONENTS

TDBComboBox

214

Chapter 5

NOTE
The Values property only comes into play when the components 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

LISTING 5.1

215

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;

DATA-AWARE
COMPONENTS

function TETHDBComboBox.GetComboValue(Index: Integer): string;


begin
if (Index < FValues.Count) and (FValues[Index] <> ) then
Result := FValues[Index]
else if Index < Items.Count then
Result := Items[Index]

216

Chapter 5

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

LISTING 5.1

217

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 Ill examine some of
the individual routines in more detail.
and Destroy simply create and free the new FValues property, and then pass control
onto TDBComboBoxs constructor and destructor.
Create

sends a CM_GETDATALINK message to the component to obtain a reference to the


components internal FDataLink field. Because TETHDBComboBox derives from TDBComboBox,
were actually retrieving TDBComboBoxs FDataLink. TDBComboBox.FDataLink is private and
TDBComboBox doesnt provide a property to access the value, so theres no way to directly get a
hold of the data link. Fortunately, TDBComboBox supports the CM_GETDATALINK message, which
accomplishes the same thing.
CreateWnd

5
DATA-AWARE
COMPONENTS

218

Chapter 5

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.
provides a handler for both of these methods, but the handlers dont take into
account our new FValues property. So, its necessary to override them.

TDBComboBox

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.
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.
GetComboValue

GetComboText returns the text for the currently displayed item in the combo box. If the combo
box allows text entryin other words, if the style is csDropDown or csSimplethe 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.
is used in a manner similar to the TDBRadioGroup component. If you leave the
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).
TDBComboBox
Values

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, Ive created my own version of TDBListBox.
TDBListBox

Data-Aware Components

219

Listing 5.2 contains the source code for TETHDBListBox (a descendent of TDBListBox that supports 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

{ TETHDBListBox }

5
DATA-AWARE
COMPONENTS

procedure Register;
begin
RegisterComponents(ETH, [TETHDBListBox]);
end;

220

Chapter 5

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

LISTING 5.2

221

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 wont go into it
in detail here.

TDBImage
is used to display bitmaps contained in a datasets BLOB field. Unfortunately,
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.
TDBImage
TDBImage

Listing 3.4 showed how to display image data from a dataset without using a
TDBImage data-aware component.

DATA-AWARE
COMPONENTS

NOTE

222

Chapter 5

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 Ill mention TDBRichEdit in
this chapter and TDBCtrlGrid in the next. Because date entry is something that many applications require, Ill present a data-aware implementation of the Win32 TDateTimePicker component later in this chapter.
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).
TDBRichEdit

Rich text enables the user to format paragraphs, words, or individual characters using different
font styles and formatting techniquessuch as bullets, numbering, tabs, and indentation.
Although TDBRichEdit and its nondata-aware counterpart, TRichEdit, support this functionality 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, Ill 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, lets assume that were 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 fordisplaying 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 additional 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

is a zero-based number that determines the field to be used for incremental searching in the component. For example, say that you set the ListField property 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 control, it performs automatic incremental searching on the LastName field.

determines the field whose value is to be stored in the dataset.

ListFieldIndex

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 components ListSource property.

NOTE
You can duplicate the functionality of the TETHDBComboBox and TETHDBListBox components 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
5
DATA-AWARE
COMPONENTS

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.
Visually, TDBNavigator looks like a toolbar because it contains a horizontal array of predefined buttons (actually, TSpeedButtons), which are listed in Table 5.8.

224

Chapter 5

TABLE 5.8
Button

TDBNavigator Buttons

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 corresponding 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 dont 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 isnt 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 navigators 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 isnt 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 section, I provide working code for a data-aware version of the Win32 TDateTimePicker component. Along the way, Ill 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
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 component. Tables 5.9, 5.10, and 5.11 list the most often-used methods, properties, and events
(respectively).
TFieldDataLink

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.
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).
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.

Modified

Reset

5
TABLE 5.10

TFieldDataLink Properties

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.

DATA-AWARE
COMPONENTS

Property

226

Chapter 5

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 automatically generated field object for a nonpersistent field.
The name of the field to which the data-aware component is bound.

FieldName

TABLE 5.11

TFieldDataLink Events

Event

Description

OnDataChange

Fires when there is a change to the underlying field.


Fires when the associated data source changes from an editing mode to
a browse mode, or vice versa.
Fires when the data contained in the data-aware component should be
written out to the dataset.
Fires when the underlying dataset changes from active to inactive, or
vice versa.

OnEditingChange
OnUpdateData
OnActiveChange

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.

also sets up event handlers for the TFieldDataLinks OnDataChange and OnUpdateData
events. You can also create handlers for the OnEditingChange and OnActiveChange events if
you want or need to, but I havent done that here.
Create

Destroy

method.

simply frees the TFieldDataLink component, and then calls the inherited Destroy

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 components 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 presented 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 components Date property to the value of
the associated field. If there is no associated field, the component displays todays 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

DATA-AWARE
COMPONENTS

Now that the component updates itself correctly when the underlying data changes, we need to
write the code that updates the data when the component changes. To do that, we need to write
the UpdateData event handler.

228

Chapter 5

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 handler. 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 components value.
In this case, Ive overridden TDateTimePickers Click and Change dynamic methods to add
calls to the data links Edit and Modified methods. The logic is this: First, call Edit to attempt
putting the underlying dataset into edit mode. Next, call the components 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 handlers 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

LISTING 5.3

229

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

{ TETHDBDateTimePicker }

5
DATA-AWARE
COMPONENTS

procedure Register;
begin
RegisterComponents(ETH, [TETHDBDateTimePicker]);
end;

230

Chapter 5

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

LISTING 5.3

231

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.UpdateData(Sender: TObject);

5
DATA-AWARE
COMPONENTS

procedure TETHDBDateTimePicker.DataChange(Sender: TObject);


begin
if FDataLink.Field <> nil then
Date := FDataLink.Field.AsDateTime
else
Date := Now;
end;

232

Chapter 5

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 components 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 everything needed to display and update datasets in your applications.
LISTING 5.4

DataAwareMainForm.pas

unit MainForm;
interface
uses
SysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DB, DBClient,
QStdCtrls, QExtCtrls, QButtons, Mask, QComCtrls, QDBCtrls, QMask;

Data-Aware Components

LISTING 5.4

233

Continued

5
DATA-AWARE
COMPONENTS

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;
cbWeekday: TDBComboBox;
lbWeekday: TDBListBox;
tabLookup: TTabSheet;
Label4: TLabel;
lbLookup: TDBLookupListBox;

234

Chapter 5

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

LISTING 5.4

235

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
dsBrowse:
lblState.Caption
dsEdit:
lblState.Caption
dsInsert:
lblState.Caption
end;
end;

:=
:=
:=
:=

Inactive;
Browse;
Edit;
Insert;

procedure TfrmMain.DataSource1UpdateData(Sender: TObject);


begin
lbDSEvents.Items.Add(Update Data)
end;

procedure TfrmMain.btnClearClick(Sender: TObject);

5
DATA-AWARE
COMPONENTS

procedure TfrmMain.btnLoadClick(Sender: TObject);


begin
if OpenDialog1.Execute then begin
ClientDataSet1.Edit;
ClientDataSet1Image.LoadFromFile(OpenDialog1.FileName);
end;
end;

236

Chapter 5

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 weve covered
are

TDataSource

TDBText

and TDBEdit are useful for displaying and editing simple field values.

TDBMemo

provides a means of displaying and editing unformatted multiline text.

TDBRichEdit

TDBCheckBox

TDBComboBox

provides a high-level conduit between data-aware components and datasets.

(a VCL-only data-aware component) can be used to display and edit formatted multiline text.
and TDBRadioGroup support the selection of one or more options from an
available list of options.

and TDBListBox enable the user to select a field value from a list of predefined 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

TDBLookupComboBox

TDBImage

TDBNavigator

237

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 thats being edited.
is used to display bitmaps that are stored in a datasets BLOB field.
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 components.
The following chapter continues this discussion of data-aware components with a look at dataaware grids.

5
DATA-AWARE
COMPONENTS

CHAPTER

Data-Aware Grids

IN THIS CHAPTER
TDBGrid

240

TClientDataSetGrid
TDBCtrlGrid

263

266

Third-Party Data-Aware Grids

271

240

Chapter 6

The preceding chapter introduced you to data-aware componentsin 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, Ill 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).
is supplied with Delphi, but not with Kylix. It allows for a nonlinear grid
layoutfor example, a grid where each record occupies several lines instead of a single line.

TDBCtrlGrid

TDBGrid
provides the cornerstone for Delphis grid-based, data-aware components. Using
you can create screens that look like the one shown in Figure 6.1 without a lot of
programming effort.
TDBGrid

TDBGrid,

FIGURE 6.1
A sample screen created using a TDBGrid.

Later in this chapter, well investigate the code required to produce this screen. In this section,
Ill 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

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 grids properties and events, you can create a grid that looks much
more pleasing to the eye. In the following sections, Ill 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 datasets 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 datasets persistent field
objects), you use the columns editor.

6
DATA-AWARE
GRIDS

The simplest way to use a TDBGrid is to drop it on a form, connect the data source, open the
dataset, and then run the application. If you do this, youll see a fairly mundane grid using all
default settings, as shown in Figure 6.2.

241

242

Chapter 6

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 Delphipress 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 columns 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.
Sets the background color of the individual column.
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).
Customizes the font used to display the column data.
When True, the column data cannot be edited, even if the underlying
field and dataset allow editing.
Enables customization of the columns title cell. This property is
discussed later in the Column Titles section.
When False, the column is not displayed.
Sets the width of the column in screen pixels.

Color
FieldName

Font
ReadOnly
Title
Visible
Width

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 doesnt automatically set the titles
alignment to taRightJustify. So, you need to set the titles 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 columns Alignment and Width properties manually, and you must use
the grids 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

Column Types

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.
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).
Specifies the maximum number of items to display in the columns
combo box when it is dropped down.
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 columns ButtonStyle property is set to bsNone).

DropDownRows
PickList

For columns with a ButtonStyle of bsEllipsis, the grids 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
grids TitleFont property accordingly.
However, for control over each column title, you should resort to the individual columns
Title property. The Title property expands to enable the following properties to be set for the
column title.

DATA-AWARE
GRIDS

Most columns are displayed and edited as a simple string. For cases in which you want the
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 columns
active cell:

TABLE 6.2

243

244

Chapter 6

TABLE 6.3

TColumn Title Properties

Property

Description

Alignment

Sets the alignment of the column title to left-, center-, or right-justified.


Specifies the text to be displayed in the column title.
Sets the background color of the column title.
Sets the font for the text displayed in the column title.

Caption
Color
Font

As mentioned earlier, Delphi does not automatically set the titles Alignment property to
taRightJustify for numeric fields. So, you should make sure that you check the titles
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.
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.
When this option is set, column titles are displayed.
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).
Setting this option enables individual columns to be moved or
resized at runtime.
When set, vertical lines are drawn between columns.
When set, horizontal lines are drawn between rows.
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.

dgAlwaysShowEditor

dgTitles
dgIndicator

dgColumnResize
dgColLines
dgRowLines
dgTabs

Data-Aware Grids

TABLE 6.4

Continued

6
Description

dgRowSelect

When set, clicking a row highlights the entire row rather than
selecting an individual cell. Row highlighting can also be
accomplished manually by custom drawing the grid, as explained
later in this chapter.
Set this option to highlight the current cell even when the grid does
not have focus.
If the grids 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.
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.
When set, the user can select multiple rows in the grid by pressing
Ctrl and clicking individual rows.

dgConfirmDelete

dgCancelOnExit

dgMultiSelect

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 grids 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 youll 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 grids 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.
Fires immediately after focus enters the current column.

OnColEnter

DATA-AWARE
GRIDS

Option

dgAlwaysShow
Selection

245

246

Chapter 6

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.
Fires after the user moves (but not after the user resizes) a column
at runtime.
Fires when a cell is about to be drawn. Its used to implement
custom drawing, which is explained later in this chapter.

OnColumnMoved
OnDrawColumnCell
OnDrawDataCell
OnEditButtonClick
OnTitleClick

Obsolete and included for backward compatibility only.


Fires when the user clicks the ellipsis button in a cell.
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

OptionsMainForm.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

LISTING 6.1

Continued

6
DATA-AWARE
GRIDS

cbColumnResize: TCheckBox;
cbColLines: TCheckBox;
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;

247

248

Chapter 6

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

LISTING 6.1

Continued

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;

6
DATA-AWARE
GRIDS

begin
UpdateOption(dgEditing, cbEditing.Checked);
end;

249

250

Chapter 6

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

LISTING 6.1

Continued

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.

6
DATA-AWARE
GRIDS

procedure TfrmMain.gridCellClick(Column: TColumn);


begin
lbEvents.Items.Add(OnCellClick - Col + IntToStr(grid.SelectedIndex) +
, Field + grid.SelectedField.FieldName + ));
end;

251

252

Chapter 6

Figure 6.3 shows the Options application at runtime.

FIGURE 6.3
The Options application lets you experiment with the TDBGrid components options.

Custom Drawing
As you can see from Figure 6.2, the grids 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 grids 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

TABLE 6.6

TGridDrawState Values

Description

gdSelected

The cell is selected.


The cell has the focus.
The cell is fixed (that is, its the indicator cell).

gdFocused
gdFixed

The difference between gdSelected and gdFocused can get confusing (especially because
these values change meaning slightly as the Options property changes), so Ill 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 grids 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 grids 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, Delphis 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 manuallyeven
those that you dont need any special drawing for.
Fortunately, this isnt the case. The solution is to set DefaultDrawing to False, and then inside
the OnCustomDrawColumn event handler, call the grids DefaultDrawColumnCell, like this:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);

6
DATA-AWARE
GRIDS

Value

253

254

Chapter 6

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 Delphis 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

CustomDrawMainForm.pas

unit MainForm;
interface

Data-Aware Grids

LISTING 6.2

Continued

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;

6
DATA-AWARE
GRIDS

uses
SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, DB,
DBClient, QGrids, QDBGrids, QExtCtrls, DateUtils;

255

256

Chapter 6

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 grids 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 doesnt check the DataCol parameter to see if its 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 doesnt 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

Solutions to Common Grid Questions

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 datasets 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 grids 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 grids SelectedField
property. SelectedField returns the underlying datasets 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 something 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 dont need to do this because you can use the datasets current record and the
grids SelectedIndex or SelectedField properties to determine the current cell. However,
you might want to know about a cell that isnt current.

6
DATA-AWARE
GRIDS

In this section, Ill attempt to answer a number of commonly asked questions about the
TDBGrid component. The solutions to these problems are not overly difficult, but in most cases
they involve more than simply calling a method or setting a property.

257

258

Chapter 6

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 grids 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.
TGridCoords

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 grids 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 TGridCoords 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

// To move the dataset to the corresponding record, clone the dataset,


// and set the clones 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 cells contents.) However, what if (for
compatibility with another software package) you want to enter edit mode when the user
presses a different keyperhaps 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 grids OnKeyDown event. Within that handler, set the grids 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).

6
DATA-AWARE
GRIDS

- Indicator
else
Label1.Caption := IntToStr(GC.X) + , + IntToStr(GC.Y) + - +
DBGrid1.Columns[GC.X - IndicatorOffset].FieldName;

259

260

Chapter 6

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 isnt, the
grid doesnt enter edit mode even when the code sets EditorMode to True.

Detecting When a Column Is Resized


provides an OnColumnMoved event, but no OnColumnSized event. This means that you
can easily tell when a column is moved, but its 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.

TDBGrid

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

LISTING 6.3

Continued

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 doesnt 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 handler, 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.

6
DATA-AWARE
GRIDS

property OnColumnSized: TNotifyEvent


read FOnColumnSized write FOnColumnSized;
end;

261

262

Chapter 6

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 isnt 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 Ill cover it first. TDBGrids 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 grids 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

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 doesnt
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
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.

TClientDataSetGrid

In addition, TClientDataSetGrid can automatically persist column information to and from a


separate configuration file. It doesnt support saving and loading column information to and

6
DATA-AWARE
GRIDS

This code first creates a memory stream and saves the column configuration to that stream.
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.

263

264

Chapter 6

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 components 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

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 grids ConfigureColumns method, like this:
ClientDataSetGrid1.ConfigureColumns;

FIGURE 6.5
TClientDataSetGrid

enables the user to hide columns that he doesnt 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 doesnt support
saving multiple configurations in a single file.

6
DATA-AWARE
GRIDS

This is actually easier done than said. If the previous explanation sounds complicated, you
might want to play with clicking, and pressing shift and clicking, column titles to see the effect
for yourself.

265

266

Chapter 6

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 personal 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 addition 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
Im not going to spend a lot of time on TDBCtrlGrid because it isnt CLX-compatible, and
because there isnt any new development going on in terms of 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.

TDBCtrlGrid

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 components constructor). The following is a snippet
from TDBEdits 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

Properties

TDBCtrlGrid Properties

Property

Description

AllowInsert

When True, the user can scroll past the last record in the grid to
insert a new record.
When True, the user can delete the current record by pressing
Ctrl+Delete.
Determines the number of columns displayed in the grid.
Determines the direction in which the grid scrolls to display more data.
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.
Refers to the height, in pixels, of a single panel (cell).
Refers to the width, in pixels, of a single panel (cell).
Determines the number of rows displayed at a time in the grid.
Determines the background color of the current cell.
When True, a focus rectangle is drawn around the current cell.

AllowDelete
ColCount
Orientation
PanelBorder

PanelHeight
PanelWidth
RowCount
SelectedColor
ShowFocus

The ColCount, PanelWidth, and Width properties are directly related. ColCountPanelWidth 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;

DATA-AWARE
GRIDS

introduces a handful of properties that you can use to customize its look and feel.
Table 6.7 lists these properties.

TDBCtrlGrid

TABLE 6.7

267

268

Chapter 6

Index refers to the zero-based index of the panel about to be drawn, and is a number between
zero and RowCountone, inclusive.

You might notice that there is no Rect parameter passed to this function, so at first glance it
isnt 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 were 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 TDBCtrlGrids properties.
LISTING 6.4

CtrlGridMainForm.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

LISTING 6.4

Continued

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;

6
DATA-AWARE
GRIDS

Label3: TLabel;
Label4: TLabel;
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;

269

270

Chapter 6

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

6
DATA-AWARE
GRIDS

FIGURE 6.6
CtrlGrid demonstrates TDBCtrlGrids behavior.

Third-Party Data-Aware Grids


Though TDBGrid and TClientDataSetGrid are useful grid components, they dont come close
to the flexibility and power thats 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
youre 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 Companys 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.
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.
An extremely powerful TDBGrid replacement that offers numerous
advanced grid featurestoo many to list here. Some of the
notable features include multiline data display, runtime sorting

InfoPower 2000

ExpressQuantumGrid

272

Chapter 6

TABLE 6.8

Continued

Product

TopGrid

Others

Description
and grouping of data, and extensive customization (both at
design time and at runtime). For more information, visit
www.devexpress.com.
Provides both data-aware and nondata-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.
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.
is a free component that you can use for automatic column sorting
and additional column customization.

TClientDataSetGrid

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.

CHAPTER

Dataset Providers

IN THIS CHAPTER
What Is a Dataset Provider?
Connecting to a Dataset

275

Resolving Changes to Data


Provider Options
Provider Events

274

276

293
295

Changing Field Values on the Server


Intercepting Data

297

298

Optional Parameters

300

Master/Detail Relationships

301

Providing and Resolving Data from Stored


Procedures and Joins 302
Connecting to a Local Database

308

274

Chapter 7

Earlier in this book, you learned about dbExpressa high-performance, low-overhead database 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 inherently 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 programming 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 storetypically 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 ita 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

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 Ill 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 component to the data source.
5. Create an event handler for the forms 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 displayed in the grid. Notice that you can scroll forward and backward through the data, and you

7
DATASET
PROVIDERS

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);
procedure PSStartTransaction;
function PSUpdateRecord(UpdateKind: TUpdateKind;
Delta: TDataSet): Boolean;
end;

275

276

Chapter 7

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. Well 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 dont
support editing or bidirectional scrolling on their ownthey 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; well
embellish on it in the following sections.

Resolving Changes to Data


If youll 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
grids data in the preceding section, those changes didnt get reflected in the underlying database.

Applying Updates
To save changes to the database permanently, you need to call the client datasets
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 application and rerun it, youll 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, Ive 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
committed to the underlying database.

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 dont 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 maximum number of errors that you will allow to ApplyUpdates. If you dont 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 datasets
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, Ill cover some of the various techniques
you can use.

DATASET
PROVIDERS

NOTE

278

Chapter 7

Resolving to a Dataset
publishes a property named ResolveToDataSet. By default it is False,
indicating that the provider resolves data directly to the database server associated with the
providers DataSet. This is generally the most efficient way to resolve data.

TDataSetProvider

In some cases, you must set ResolveToDataSet to True. The most common reasons are listed
next:
The providers DataSet is not connected to a databasefor example, it is a
TClientDataSet.
The providers DataSet does not provide the necessary implementation of the
IProviderSupport interface.
In these cases, updates will be applied to the dataset referenced by the providers DataSet
property. You can then handle the providers AfterApplyUpdates method to make those
changes persistent (in the case of a TClientDataSet, you could call the datasets 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 doesnt give you any control over how to handle reconciliation errors.
For greatest control over reconciliation errors, you should provide an event handler for the
client datasets 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 exception 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 whats 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.


The record refers to a deleted record.

ukDelete

TReconcileAction Values

Value

Description

raSkip

Dont apply updates to this record. Leave the unapplied changes in the
client datasets change log. This record will be counted in the return
value from ApplyUpdates.
Abort the whole operation. No updates are made to the underlying
database at all, and all changes are left in the client datasets change
log. All records are counted in the return value from ApplyUpdates.
Merge the record with the record in the underlying database. This
works only if the fields that are changed in the record dont 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.
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.
Cancel changes to this record, reverting to the original record data.
This record will not be counted in the return value from ApplyUpdates.
Cancel changes to this record and reread the record data from the database. This record will not be counted in the return value from
ApplyUpdates. You must set the pfInKey flag for all fields in the primary key for this option to be available.

raAbort

raMerge

raCorrect

raCancel
raRefresh

DATASET
PROVIDERS

TABLE 7.2

280

Chapter 7

Fortunately, in most situations you dont have to worry about writing a complicated event handler 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 doesnt
matter).
5. Add the new unit to the uses clause of the form that contains the client dataset (in this
example, its the main form).
6. Add the following code to the OnReconcileError event for the client dataset:
Action := HandleReconcileError(DataSet, UpdateKind, E);

I wont go into detail about the inner working of HandleReconcileError here. You should take
a look at the units 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

LISTING 7.1

281

UpdatesMainForm.pas

unit MainForm;
interface
uses
SysUtils, Variants, Classes, QGraphics, QControls, QForms, QStdCtrls,
QDialogs, QExtCtrls, DBXpress, FMTBcd, QGrids, QDBGrids, DB, Provider,
DBClient, SqlExpr, QDBCtrls, QTypes;

7
DATASET
PROVIDERS

type
TfrmMain = class(TForm)
pnlClient: TPanel;
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;

282

Chapter 7

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

LISTING 7.1

283

Continued

var
frmMain: TfrmMain;
implementation
uses RecErrorForm;
{$R *.xfm}
procedure TfrmMain.FormCreate(Sender: TObject);

7
DATASET
PROVIDERS

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;
var OwnerData: OleVariant);
procedure ClientDataSet1AfterRefresh(DataSet: TDataSet);
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;

284

Chapter 7

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

LISTING 7.1

285

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;

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);

DATASET
PROVIDERS

procedure TfrmMain.DataSetProvider1AfterExecute(Sender: TObject;


var OwnerData: OleVariant);
begin
Log(TDataSetProvider.AfterExecute);
end;

286

Chapter 7

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

LISTING 7.1

287

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);
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);

DATASET
PROVIDERS

procedure TfrmMain.DataSetProvider1UpdateError(Sender: TObject;


DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind;
var Response: TResolverResponse);
begin
Log(TDataSetProvider.OnUpdateError);
end;

288

Chapter 7

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

LISTING 7.1

289

Continued

procedure TfrmMain.btnClearEventLogClick(Sender: TObject);


begin
lbEvents.Items.Clear;
end;
end.

FIGURE 7.3
Delphis reconcile error handler allows the user to decide how to deal with record conflicts.

By default, this dialog shows conflicting values onlyin other words, fields that were changed
by another user. Uncheck the Show Conflicting Fields Only check box to display all field values. If you do, youll 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.

7
DATASET
PROVIDERS

Run two occurrences of this application on your computer. In the first occurrence, change
Johns first name to Eddie and apply updates. In the second occurrence, notice that the record
still shows a first name of John. Change Johns last name to Smith and apply updates. The
OnReconcileError event fires, indicating that the record has been changed by someone else in
the meantime (see Figure 7.3).

290

Chapter 7

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


doesnt automatically check for conflicts on BLOB fields, including
memos. If two users change the contents of a BLOB field or memo, the second users changes
will overwrite the first users changes without warning.

TDataSetProvider

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 column 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.
TDBNavigators 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 datasets 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 datasets 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 discussed in the following section). In addition, you should usually call RefreshRecord
only when the UpdateStatus of the current record is usUnmodified.

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 statement is
UPDATE CONTACTS SET LAST = Smith WHERE ID = 5

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.
TDataSetProvider

TABLE 7.3

TUpdateMode Values

Value

Description

upWhereAll

All designated fields are included in the WHERE clause.


Only key fields as well as modified fields are included in the WHERE
clause.
Only key fields are included in the WHERE clause.

upWhereChanged
upWhereKeyOnly

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 Smiths SALARY field was changed from $40,000 to
$45,000.

DATASET
PROVIDERS

if ClientDataSet1.UpdateStatus = usUnmodified then


ClientDataSet1.RefreshRecord;

292

Chapter 7

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 dont 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.


The field is included in the WHERE clause when UpdateMode is set to
upWhereAll or upWhereChanged.
The field is part of the primary key and controls such features as
refreshing records through a call to RefreshRecord, as well as reconciliation options such as merging.
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 cant see
or modify the field.

pfInWhere
pfInKey

pfHidden

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

TABLE 7.5

ID

293

ProviderFlags Settings for the Fictitious EMPLOYEES Table Fields

Field

ProviderFlags

ID

[pfInUpdate, pfInWhere, pfInKey]

NAME

[pfInUpdate, pfInWhere]

BIRTHDAY

[pfInUpdate, pfInWhere]

SALARY

[pfInUpdate, pfInWhere]

is the primary key for the table.

NAME, BIRTHDAY,

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 relationship. 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.
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.

poIncFieldProps

DATASET
PROVIDERS

and SALARY can all be updated, and they should all be included in the WHERE
clause of an update SQL statement when the providers UpdateMode property is set to
upWhereAll.

294

Chapter 7

TABLE 7.6

Continued

Option

Description

poCascadeDeletes

Used when the provider is part of a master/detail relationship. When True, the server deletes detail records automatically when the master record is deleted.
Used when the provider is part of a master/detail relationship. When True, the server automatically updates detail
records when the key value(s) of the master record
changes.
When True, you cant edit the data in the client dataset.
When True, allows updates that affect multiple records.
When False, updates that affect more than one record raise
an exception.
When True, client datasets will not be able to Insert or
Append new records.
When True, client datasets will not be able to Edit existing
records.
When True, client datasets will not be able to Delete existing records.
When True, calls to AS_GetRecords ignore the reset flag.
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 implemented in Delphi as of version 6.
When True, any changes made to data during the
BeforeUpdateRecord or AfterUpdateRecord event handlers are sent back to the client.
When True, the client dataset can override the
CommandText property of the providers dataset. When
False, attempting to set the client datasets CommandText
property raises an exception.
When True, alerts the client dataset that it should not
attempt to sort the data returned from the server.

poCascadeUpdates

poReadOnly
poAllowMultiRecordUpdates

poDisableInserts
poDisableEdits
poDisableDeletes
poNoReset
poAutoRefresh

poPropogateChanges

poAllowCommandText

poRetainServerOrder

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 tables 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.

Provider Events
BeforeXxx/AfterXxx

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.


Fired after the server executes the query or stored procedure
that will ultimately return data to the client.
Fired after the server returns output parameters from the
dataset to the client dataset.
Fired after the provider creates the data packet to send to the
client.
Fired after the provider refreshes the current record because of
a call to TClientDataSet.RefreshRecord or any other
method that fetches data.
Fired after a record is successfully updated.
Fired before updates are applied to the database.
Fired before the server executes a query or stored procedure.
Fired before the server returns output parameters from the
dataset to the client dataset.
Fired before the provider creates the data packet to send to the
client.

AfterExecute
AfterGetParams
AfterGetRecords
AfterRowRequest

AfterUpdateRecord
BeforeApplyUpdates
BeforeExecute
BeforeGetParams
BeforeGetRecords

DATASET
PROVIDERS

publishes two types of eventsOnXxx and BeforeXxx/AfterXxx. The


events fire before and after interesting things happen in the provider,
such as when updates are applied to the underlying database.
TDataSetProvider

296

Chapter 7

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.
Fired before each records updates are applied to the database.

BeforeUpdateRecord

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 integeror something more complex, such as an array of values.
As flow passes to the provider, the OwnerData parameter is passed to the providers BeforeXxx
event. The value of OwnerData may be changed in the BeforeXxx event, if needed.
Next, flow continues to the providers AfterXxx event, where OwnerData may again be
changed if necessary.
Finally, flow passes to the client datasets AfterXxx event, when you may inspect the (possibly
modified) value of OwnerData. The client datasets AfterXxx event is the end of the line, so
there is no reason to modify the value of OwnerData in the client datasets 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, compress 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

TABLE 7.8

297

Continued

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.
Used when the provider returns data from a join or stored
procedure. You can handle this event to instruct the
provider which table to apply updates to. This option is
discussed in the section titled Providing and Resolving
Data from a Join, later in this chapter.
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.
Fired when an error occurs while reconciling data. If you
dont 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 sending them back to the client.

OnGetTableName

OnUpdateData

OnUpdateError

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 individual 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 TDataSetProviders Options property.
3. Include the pfInKey setting in the ID fields ProviderFlags.
4. Provide an event handler for the providers BeforeUpdateRecord event.

7
DATASET
PROVIDERS

Event

298

Chapter 7

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 implementation 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 wont provide a program example using this technique at this time, but youll see this technique 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, were 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, well 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 anything 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.
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.
OnGetData

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 Softwares LockBox
product at www.tpx.turbopower.com/products/LockBox.

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.


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.
ClientDataSet1.FieldByName(AccountNumber).AsString

7
DATASET
PROVIDERS

procedure TForm1.ProviderGetData(Sender: TObject;


DataSet: TCustomClientDataSet);
begin
while not DataSet.EOF do begin
DataSet.Edit;
DataSet.FieldByName(AccountNumber).AsString :=
EncryptData(DataSet.FieldByName(AccountNumber).AsString);
DataSet.Post;
DataSet.Next;
end;
end;

300

Chapter 7

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 provided, 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 TDataSetProviders
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;

is a variant array of variant arrays. This code snippet creates an array of two variants. 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.
Properties

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 relationship 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.

Using providers, you will establish the master/detail relationship on the server-side data module, 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 module and connect it to the master datasets provider. This will automatically create a nested
dataset on the client side.

DATASET
PROVIDERS

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 section 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 havent included a listing here, because almost no code is requiredeverything 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.

302

Chapter 7

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 simple 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 following 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 statein 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.
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 providers OnGetTableName
event. In the event handler, specify the name of the table to be updated, like this:
TDataSetProvider

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 previously, 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 hes 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 situation, you will need to write some code. Again, we turn to TDataSetProviders
BeforeUpdateRecord event handler. The trick is to apply the necessary updates to the individual 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.

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);

7
DATASET
PROVIDERS

procedure TForm1.SQLClientDataSet1BeforeUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied Boolean);
var
SQL: string;
Connection: TSQLConnection;
begin
// Obtain a pointer to the connection from the source dataset
Connection := (SourceDS as TCustomSQLDataSet).SQLConnection;

304

Chapter 7

// 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 operation 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 youve 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

JoinsMainForm.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

LISTING 7.2

305

Continued

private
{ Private declarations }
FNextID: Integer;
function GetNextID: Integer;
public
{ Public declarations }
end;
var
frmMain: TfrmMain;

implementation

DATASET
PROVIDERS

{$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;

306

Chapter 7

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

LISTING 7.2

307

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;

// 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.

DATASET
PROVIDERS

ukDelete: begin
ID := DeltaDS.FieldByName(CONTACTID).OldValue;

308

Chapter 7

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 arrangement, 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 necessary 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 wont be able to directly connect to the dataset provider. In other words, the
TClientDataSets ProviderName property wont 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, discussed 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 dont want to plan ahead for easy
migration to a separate application server, you may find that the TSQLClientDataSet component simplifies your application somewhat.
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.
TSQLClientDataSet

It doesnt 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 dont
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 TClientDataSets
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 serverno 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.

7
DATASET
PROVIDERS

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 components 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.

310

Chapter 7

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 dont always need BLOB information, you
can turn on the TDataSetProviders 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
TSQLDatasets CommandText property so that the SELECT statement doesnt 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 datasets
FetchBlobs method, like this:
ClientDataSet1.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.
FetchBlobs

Note that if the client datasets 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 datasets 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 dont always need detail
information, you can turn on the TDataSetProviders 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 datasets
FetchDetails method, like this:
ClientDataSet1.FetchDetails;

Dataset Providers

311

As with fetching BLOBs, you must set the client datasets 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.

LISTING 7.3

DataFetchMainForm.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;

7
DATASET
PROVIDERS

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 application, you should create separate server-side and client-side data modules for the data access
components.

312

Chapter 7

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

LISTING 7.3

313

Continued

procedure TfrmMain.btnFetchBlobsClick(Sender: TObject);


begin
cdsContacts.FetchBlobs;
ShowTodoCount;
end;
procedure TfrmMain.cdsContactsAfterScroll(DataSet: TDataSet);
begin
ShowTodoCount;
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. Lets take a look at the methods one
at a time.
First, however, lets examine the structure of the application.
Figure 7.5 shows the main form of the DataFetch application at design time.

DATASET
PROVIDERS

procedure TfrmMain.ShowTodoCount;
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;

314

Chapter 7

FIGURE 7.5
DataFetch

main form showing relationships between components.

Two dbExpress datasetssqlContacts and sqlTodosare linked in a master/detail relationship. 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 datasetscdsContacts and cdsTodosrepresent 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
and btnFetchBlobsClick events, respectively. These two events, in
turn, call the client datasets FetchDetails and FetchBlobs methods. They then call the
ShowTodoCount method, which well examine shortly.
btnFetchDetailsClick

is fired whenever the current record changes on the cdsContacts


dataset. It also makes a call to ShowTodoCount.
cdsContactsAfterScroll

is a method that is connected to cdsContacts AfterDelete and


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.
cdsContactsApplyUpdates
AfterPost

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.

7
DATASET
PROVIDERS

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 datasets ApplyUpdates method to save changes to a database
permanently.
For greatest control over reconciliation errors, you should provide an event handler for
the client datasets 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 providers 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.

316

Chapter 7

By handling the providers OnGetTableName event, you can update data returned from a
stored procedure.
By handling the providers 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 providers
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.

CHAPTER

DataSnap

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

318

Chapter 8

The previous chapter gave an overview of providers and explained how to use them in a multitier application where the client and application server are physically located in a single executable.
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, including 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 application server. DataSnap is implemented by a number of components that can be used to connect 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 youll 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 applicationperhaps a normal user and an administrator. 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.

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 modulesRemote Data Modules, Transactional Data Modules, and CORBA Data
Modules.

8
DATASNAP

From Delphis main menu, select File, New, Other. Delphis New Items dialog is displayed. Select the Multitier tab. The New Items dialog should look like the image shown
in Figure 8.1.

320

Chapter 8

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 meaningful 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 values 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.
Each client that attempts to connect to the server will cause a
separate instance of the server executable to run.
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.

Single Instance
Multiple Instance

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 possible values for the Threading Model combo.

DataSnap

TABLE 8.2

321

Threading Model Values

Value

Description

Single

The data module will receive requests from only a single client at a
time. Because of this, you dont need to deal with threading issues.
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 multithreading issues on global data.
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.
The same as Free with the exception that any callbacks made to client
interfaces are serialized.
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.

Apartment

Free

Both
Neutral

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 librarynot 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.

DATASNAP

After you have filled in the appropriate values (the default values for Instancing and
Threading Model are fine in most cases) you can click OK to create the remote data module.

322

Chapter 8

Enter a CoClass name for the data module, such as ContactDataServer, or some other meaningful name.
Select the threading model to use for the remote data module. Transactional data modules support the Single, Apartment, and Both threading models from Table 8.2.
Finally, select the transaction model to use for the data module. Because we wont be creating
in this book, Ill refer you to the Delphi help for an explanation of the different transactional models.
TMTSDataModules

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 meaningful name.
Select the instancing type and threading models to use for the remote data module. Because
we wont be creating TCORBADataModules in this book, Ill 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

service.

is used to provide data to clients that are set up to access data from a Web

DataSnap

323

To create a remote data module for a SOAP application server, perform the following steps.
From Delphis main menu, select File, New, Other. Delphis New Items dialog is displayed. 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.

Again, select File, New, Other from Delphis 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.

8
DATASNAP

Select the type of SOAP server application to create. If youre creating a Web App
Debugger executable, enter a CoClass name to use for the COM object that the Web App
Debugger uses to call your Web module. Click OK to create the SOAP Server
Application.

324

Chapter 8

Placing Components on the Remote Data Module


Regardless of which remote data module you create, it is now time to populate it with components.
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 7namely, the database connection component, datasets, and providers.
Because were using dbExpress for these examples, this means that youll 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 machines 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, youll 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.

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 wont take any parameters or return a
result.
Close the type library editor.
Now that the server application knows about ITestCallback, youll 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:

326

Chapter 8

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 dont
support callbacks at all, so keep that in mind if you intend to use HTTP as the communication protocol between the client and server applications.

Creating the Application Servers 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 wont have much of a
user interface.
However, it is often useful to provide a minimal user interface for the application server to display information such as how many users are logged in, database statistics, or other pertinent
information. Even if you dont 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 connections 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 modules 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

MethodsServerMainForm.pas

unit MainForm;
interface

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

DATASNAP

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

328

Chapter 8

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 forms Connect and Disconnect methods on creation 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. Youll frequently need to modify, recompile, and redeploy the application
server. Its 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. Its 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 registering the server with MTS or CORBA.

Creating the Client Application


Now that the application server has been created and registered, its time to create the client
application.
The client application is functionally identical to the applications created in the previous chapter, except that it doesnt 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.

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.

DATASNAP

To get the client application started, select File, New from the Delphi main menu, and then
select Application (at this time, CLX applications dont support DataSnap).

330

Chapter 8

3. In the client-side data module, set the client datasets RemoteServer property to the
TLocalConnection component.
4. In the client datasets 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 isnt generated by Delphi for you, I havent 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
clients 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
uses sockets as a transport between the application server and client application. It is the simplest of the connection protocols to set up and use and is the one that is
used in this books 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.

TSocketConnection

The disadvantage of a socket connection is that sockets dont 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 cant 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

TABLE 8.3

331

TSocketConnection Properties

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.
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.
Specifies the GUID of a COM object that intercepts data packets
sent between the client and the application server. You may specify the ProgID of the COM object rather than the GUID by setting the InterceptName property instead of InterceptGUID. Data
interceptors are discussed in the section titled Intercepting Data
Packets, later in this chapter.
Specifies the ProgID of a COM object that intercepts data packets sent between the client and the application server. You may
specify the GUID of the COM object rather than the ProgID by
setting the InterceptGUID property instead of InterceptName.
Data interceptors are discussed in the section titled Intercepting
Data Packets, later in this chapter.
Specifies the port that the socket server uses to listen to socket
requests. By default, this is set to 211.
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.
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.
Set to True if your server supports callbacks to the client application. 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 doesnt support
callbacks.

Host

InterceptGUID

InterceptName

Port
ServerGUID

ServerName

SupportCallbacks

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.

8
DATASNAP

Property

332

Chapter 8

You must run the socket server on the remote machine before attempting to connect to an
application server using sockets. If you dont run the socket server first, the attempted connection 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 computer 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 doubleclick 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 compressed 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 wont duplicate that example in this book.
2. Register the COM object on both the server machine and all client machines.
3. Set the TSocketConnections 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.

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 application server isnt running on a Windows domain server.
I assume at this point that if youre 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 Misers 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.
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.

ServerGUID

DATASNAP

You should debug your application without the interceptor to make sure it works correctly
before adding the interceptor into the mix.

334

Chapter 8

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 applications that run over the Internet. HTTP offers some additional features over a socket connection 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.
Semicolon-delimited list of proxy servers that can be used to resolve
the IP address of the host machine.
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.
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.
The URL used to locate httpsrvr on the host machine.
Valid username used for authentication on the host machine. This may
be left blank if the host does not require authentication.

Proxy
ServerGUID

ServerName

URL
UserName

DataSnap

335

SOAP Connections
TSOAPConnection
TSOAPConnection

uses SOAP to communicate between the client and server applications.


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.
Semicolon-delimited list of proxy servers that can be used to resolve
the IP address of the host machine.
The URL used to locate the application server on the host machine.
Valid username used for authentication on the host machine. This may
be left blank if the host does not require authentication.

Proxy
URL
UserName

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.
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.
Specifies the CORBA data module to connect to. May take one of two
forms:

ObjectName

RepositoryID

IDL:ProjectName/CorbaDataModuleName:1.0

or
ProjectName/CorbaDataModuleName

DATASNAP

CORBA Connections

336

Chapter 8

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 servers remote data module.
LISTING 8.2

MethodsServerServerDataModule.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

LISTING 8.2

337

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;

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;

DATASNAP

function TMethodsDM.GetServerTime: TDateTime;


begin
Result := Now;
end;

338

Chapter 8

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

MethodsClientMainForm.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

LISTING 8.3

339

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));
end;

DM.SocketConnection1.AppServer.SetCallback(FTest as IDispatch);
DM.SocketConnection1.AppServer.TestCallbacks;
end;
procedure TTest.Test;
begin
ShowMessage(In callback);
end;
end.

DATASNAP

procedure TfrmMain.btnTestCallbacksClick(Sender: TObject);


var
typelib: ITypeLib;
begin
OleCheck(LoadRegTypeLib(LIBID_MethodsServer, 1, 0, 0, typelib));
FTest := TTest.Create(typelib, ITestCallback);

340

Chapter 8

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 users
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 applications 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 wayonly the client application contains code to support the briefcase model.
Because Delphi ships with a useful briefcase demo application, I wont duplicate its functionality 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 datasets 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 serverside database components dont 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 isnt 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
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. Lets 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 providers OnGetData event. In this event, youll 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;

DATASNAP

The server retrieves the next batch of records that occurs after the last record previously
retrieved.

342

Chapter 8

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 application. 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 providers AfterGetRecords event handler. In this handler,
youll 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 providers BeforeGetRecords event handler. In
this event, youll 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;

Thats all you need to do on the server. In the client application, you need to first provide an
event handler for the client datasets AfterGetRecords event. In this event handler, youll 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 datasets Tag property. You could also set up a form variable named FLastContactID
to save the ID.
Finally, provide an event handler for the client datasets BeforeGetRecords event. In this event
handler, youll 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 wont
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 connection. In this way, the client application lets the user make the decision as to which connection
type is most appropriate for a particular situation.

In small sample applications, this isnt 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 clients data
modules and set the ConnectionBroker property for each TClientDataSet component to the
TConnectionBroker. Then, set TConnectionBrokers RemoteServer property to the correct
connection component. Switching connections is then a trivial matter of changing the connection brokers 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;

8
DATASNAP

To provide support for multiple connection types, you need to place multiple connection components on the clients data module. For example, you may drop a TSocketConnection, a
TDCOMConnection, and a TWebConnection component on the data module. Switching from one
connection to another means that you need to set the RemoteServer property for each
TClientDataSet component to the correct connection at runtime.

344

Chapter 8

You dont 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 component onto your clients data module. In the Servers property, enter a list of servers that the
client application can attempt to connect to. Set the connection objects ObjectBroker property
to the name of the TSimpleObjectBroker.
When the TSimpleObjectBrokers 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 different 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 clients 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, Delphis 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 modules OnCreate and OnDestroy event handlers.

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.

DATASNAP

You can connect to the application server from a client application, using a variety of
connection protocols, including TSocketConnection, TDCOMConnection,
TWebConnection, TSOAPConnection, and TCORBAConnection.

The ConMan Application

IN THIS CHAPTER
What Is ConMan?
Database Structure

348
349

Overview of the Code

352

The Server Application

352

The Client Application

358

Room for Improvement

373

CHAPTER

348

Chapter 9

In this book, Ive discussed quite a number of techniques for writing database applications in
Delphi and Kylix. Along the way, Ive presented a number of small sample applications to
solidify many of the important points discussed in each chapter.
In this chapter, Ill 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 isnt 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, including 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 wouldnt 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 Kylixsuch as
DataSnapit 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 programs provided with earlier chapters. Listing 9.1 contains the partial SQL script used to create
a new, empty version of the CONMAN database.

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.

9
THE CONMAN
APPLICATION

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 company, the company data is stored with each contact.

350

Chapter 9

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 tables 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
wont 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

LISTING 9.1

351

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 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 ; ^

9
THE CONMAN
APPLICATION

CREATE TRIGGER CONTACTS_INSERT FOR CONTACTS


ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
IF (New.CONTACTID IS NULL) THEN
New.CONTACTID = GEN_ID(ID_GENERATOR, 1);
END ^

352

Chapter 9

Overview of the Code


ConMan is separated into an application server and a client, providing true multitier functionality.
The entire source code for ConMan is contained in seven unitstwo of which Delphi automatically 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.

Type library import file for the application server. Delphi automatically creates and maintains this unit.
ConManServer_TLB.pas

The client application is composed of the following units:

Client-side data module, including DataSnap components and

DataModule.pas
TClientDataSets.

Form to handle reconciliation errors. This form is automatically


generated by Delphi (see Chapter 7 for more information on handling reconciliation
errors).

RecErrorForm.pas

MainForm.pas

The applications 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 servers remote data module. As you
can see from Figure 9.2, the remote data module contains six components. These are

conn

sqlContacts

sqlTodos

sqlID A TSQLDataSet that calls the stored procedure ID_GEN in the database to retrieve
a unique ID for each inserted record.

A TSQLConnection component for connecting to the database.


A TSQLDataSet component for retrieving data from the Contacts table.

A TSQLDataSet component for retrieving data from the Todos table.

The ConMan Application

dsContacts

pvContacts A TDataSetProvider component used to provide contacts and todo


records to the client application. Because the contacts and todos are connected in a master/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).

353

A TDataSource component used to establish a server-side master/detail


relationship between sqlContacts and sqlTodos.

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

ConManServerRemoteDataModule.pas

unit RemoteDataModule;
{$WARN SYMBOL_PLATFORM OFF}
interface

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;

9
THE CONMAN
APPLICATION

uses
Windows, Messages, SysUtils, Classes, ComServ, ComObj, VCLCom, DataBkr,
DBClient, ConManServer_TLB, StdVcl, DBXpress, FMTBcd, DB, SqlExpr,
Provider;

354

Chapter 9

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

LISTING 9.2

355

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;

function TConManDataServer.GetNextID: Integer;


begin
sqlID.ExecSQL;
Result := sqlID.ParamByName(AValue).AsInteger;

9
THE CONMAN
APPLICATION

procedure TConManDataServer.pvContactsBeforeUpdateRecord(Sender: TObject;


SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean);
begin
if UpdateKind = ukInsert then
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;

356

Chapter 9

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. Youll see in Listing 9.3 how the main form responds
to that message to update its user interface accordingly.
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 (youll see on the client side that
primary keys are generated with a negative value, which Ill explain when I discuss the clientside code). If so, the code calls the GetNextID method, which executes the GEN_ID stored procedure to retrieve the next available ID for the record.
pvContactsBeforeUpdateRecord

Listing 9.3 contains the source code for the application servers main form. As discussed in
Chapter 8, application servers often dont display a main form, but Ive found that it is often
useful to display a small main form that shows the number of active connections.
LISTING 9.3

ConManServerMainForm.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

LISTING 9.3

357

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;

procedure TfrmMain.Timer1Timer(Sender: TObject);


var
HS: THeapStatus;
begin
HS := GetHeapStatus;

THE CONMAN
APPLICATION

StatusBar1.SimpleText := Format(SHeapAllocated,
[FloatToStrF(HS.TotalAllocated, ffNumber, 10, 0)]);
end;
end.

358

Chapter 9

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 display 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 application. The first unit that well look at is the data module. Listing 9.4 contains the source code
for the client-side data module.
LISTING 9.4

ConManDataModule.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

LISTING 9.4

359

Continued

var
DM: TDM;
implementation
uses RecErrorForm;

9
THE CONMAN
APPLICATION

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 }
public
{ Public declarations }
function GetNextID(DataSet: TCustomClientDataSet;
const PrimaryKey: string): Integer;
end;

360

Chapter 9

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

LISTING 9.4

361

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.

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.
DataModuleDestroy

THE CONMAN
APPLICATION

The first two methods of interest are the DataModuleCreate and DataModuleDestroy methods.
DataModuleCreate opens the cdsContacts table, which attempts to load local data from the
file CONMAN.CDS because the components FileName property is set to CONMAN.CDS. If
the file does not exist, the program will display a blank screenit doesnt attempt to connect
to the application server automatically.

362

Chapter 9

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 database have a negative ID. The server-side data module checks for a negative ID and then calls
the databases 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 simply decrement the variable to obtain the next ID. The reason I dont do this is because
in real-world applications I sometimes have 40 or 50 datasets in my data module. I
dont like to set up 40 or 50 variablesone for each dataset. Instead, I use the common 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 clients main form, which is where the bulk of the
code for this application lies.
LISTING 9.5

ConManMainForm.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

LISTING 9.5

363

Continued

9
THE CONMAN
APPLICATION

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;
ToolButton1: TToolButton;
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;

364

Chapter 9

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

LISTING 9.5

365

Continued

9
THE CONMAN
APPLICATION

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;
ecCountry: TDBEdit;
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);

366

Chapter 9

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
SUpdates

= 1 update pending;
= %d updates pending;

SOnline
SOffline

= Online;
= Offline;

SChangesPending = The current contact has been changed.


Do you want to save changes to this record?;

The ConMan Application

LISTING 9.5

367

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

9
THE CONMAN
APPLICATION

procedure TfrmMain.StatusBar1DrawPanel(StatusBar: TStatusBar;


Panel: TStatusPanel; const Rect: TRect);
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;

368

Chapter 9

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

LISTING 9.5

369

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;

// Todo popup menu


procedure TfrmMain.TodoAddExecute(Sender: TObject);
var
frmTodo: TfrmTodo;
DT: TDateTime;

9
THE CONMAN
APPLICATION

procedure TfrmMain.DataSetNumUpdatesUpdate(Sender: TObject);


begin
if DM.cdsContacts.ChangeCount = 1 then
StatusBar1.Panels[1].Text := SOneUpdate
else
StatusBar1.Panels[1].Text := Format(SUpdates,
[DM.cdsContacts.ChangeCount]);
end;

370

Chapter 9

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

LISTING 9.5

371

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 forms OnCreate event handler sets the data modules TSocketConnection.AfterConnect
and AfterDisconnect event handlers to point to the main forms 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 invalidates the status bar, which will then be updated to show the new connection status.

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.

and ImageClear1Execute fire when the user loads a new image for the
current contact or clears the contacts 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 displayed image to update.
ImageLoad1Execute

9
THE CONMAN
APPLICATION

These event handlers are assigned at runtime rather than design time because the event handler
is not located in the same unit as the data module. Because the data module doesnt use the
main form, the event handlers cant be assigned at design time.

372

Chapter 9

The only interesting action on the File menu is the Connect/Disconnect menu item.
FileConnect1Execute simply toggles the socket connections Connected property to connect
to or disconnect from the application server. FileConnect1Update takes care of setting the
actions 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 wont 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

ConManTodoForm.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

LISTING 9.6

373

Continued

implementation
{$R *.dfm}
procedure TfrmTodo.FormCreate(Sender: TObject);
begin
dtDate.Date := Date;
dtTime.Time := StrToTime(12:00 pm);
end;
end.

The todo forms 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 considerable amount of time to load all the data onto a client machine.

Summary
This chapter pulled together many of the concepts discussed in prior chapters to create a complete 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.

THE CONMAN
APPLICATION

Ill leave it to you to implement these suggestions if you see fit. That will give you a springboard to learning multitier development techniques on your own.

374

Chapter 9

The application server contains a minimal user interface that displays the number of current connections along with the amount of RAM in use by the server, as shown in
Chapter 8.
The servers 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 applications dsContactsDataChange event handler takes care of displaying an
image from the dataset in a non data-aware TImage component. This concept was discussed in Chapter 4.
The client applications cdsContactsReconcileError event handler takes advantage of
Delphis built-in reconciliation form to handle reconciliation errors, as discussed in
Chapter 7.

Redistributing dbExpress
Applications

IN THIS APPENDIX
Redistributable Files
Licensing Issues

376

378

CD-ROM-Based Applications

378

APPENDIX

376

Appendix A

This appendix tells you what you need to know to redistribute your database applications after
youve written them. Because this book assumes that youre using dbExpress as the data access
technology behind your applications, I wont explain how to redistribute the BDE or ADO.
Instead, Ill concentrate specifically on dbExpress and DataSnap.

Redistributable Files
Redistributing an application built with dbExpress and DataSnap is incredibly simpleespecially so if youre 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 application.

NOTE
Well, thats 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
Oracle
DB2
MySQL

Dbexpint.dll
dbexpora.dll
dbexpdb2.dll
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 projects uses clause and rebuilding. If you do this, you dont need to redistribute MIDAS.DLL with the client application (it must be redistributed with the server application, 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
Oracle
DB2
MySQL

Dbexpint
Dbexpora
dbexpdb2
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 isnt
completely compatible with all DataSnap code. For example, attempting to call
TClientDataSet.SaveToFile to save a client dataset to an XML file doesnt 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

Linux Driver

InterBase
Oracle
DB2
MySQL

libsqlib.so
libsqlor.so
libsqldb2.so
libsqlmy.so

A
REDISTRIBUTING
DBEXPRESS
APPLICATIONS

Database

378

Appendix A

In addition to the dbExpress driver, you need to redistribute LIBMIDAS.SO. The redistributable files should be placed in /home/<usrname>/delphidir/bin.

Licensing Issues
If youre writing database applications that work with local data (that is, data does not travel
from one machine to another), you dont need to worry about any licensing fees for your applications. 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 Borlands 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 doesnt need to update
the datathe 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.

APPENDIX

dbExpress Plus

IN THIS APPENDIX
What Is dbExpress Plus?
For More Information

380

384

380

Appendix B

This appendix takes a quick look at dbExpress Plus, Thomas Millers 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
provides a means of executing multiple SQL statements at once through what is
known as a script. Youre 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.

TSQLScript

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

CREATE UNIQUE INDEX IX_CONNAME ON CONTACTS (LAST, FIRST);

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


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.
TSQLMetaData

SQLMetaData1.GetTableNames(ListBox1.Items);

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:
TSQLMetaData.GetFieldNames

procedure GetFieldNames(const ATableName: string; AList: TStrings;


ASortOrder: TMetaDataSortOrder = soPosition); overload;

PLUS

Ordinarily, youd need to execute these three statements (CREATE TABLE, CREATE INDEX, and
CREATE TABLE) separately by calling TSQLConnection.ExecuteDirect three times, once for
each statement.

B
DBEXPRESS

CREATE TABLE TODOS


(
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
);

381

382

Appendix B

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);

returns a string list composed of the fields that make up the index
AIndexName on table ATableName.

GetIndexFieldNames

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

procedure GetPrimaryKeyFieldNames(const ATableName: string;


AList: TStrings);
function GetPrimaryKeyFields(const ATableName: string): string;

Additional Methods
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:
TSQLMetaData

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 BDEs TBatchMove component, allowing you to easily move data from one table to another.

To do this, TSQLDataPump publishes SQLMetaDataSource and SQLMetaDataDestination properties, 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,
JOB_CODE, JOB_GRADE, JOB_COUNTRY, SALARY FROM EMPLOYEE

DEPT_NO,

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.

PLUS

These two methods perform the same service as GetIndexFieldNames and GetIndexFields,
except that they always work on the primary key.

B
DBEXPRESS

know the name of the primary key. In that case, you can call either GetPrimaryKeyFieldNames
or GetPrimaryKeyFields.

383

384

Appendix B

TABLE B.1

Valid DataMoveMode Values

Value

Description

dmAlwaysInsert

Inserts all selected records into the destination table.


Appends selected records to the destination table.
Updates the record if it already exists in the destination table.
Otherwise, appends the record to the destination table.
Deletes matching records from the destination table.
Updates matching records in the destination table.

DmAppend
dmAppendUpdate
DmDelete
DmUpdate

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
+ (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

INDEX

386

A specifier

A
A specifier, 209
accessing
fields (client datasets),
103
nonpersistent fields,
104-105
persistent fields, 103
Fields objects, 64
providers on different
form, 308
ACID (Atomic,
Consistent, Isolated,
Durable)
properties, 37
action handlers, 228
activating indexes
(client datasets), 121
Active property, 60
Add Connection
button, 10
Add method, 102
Add to Interface
command (remote
data module context
menu), 324
Add To Interface dialog
box, 324
AddFieldDef method,
102
AddIndex method, 121
adding
fields (client datasets),
102
methods (remote data
modules), 324
Addition (+) operator,
128

Address property, 331


Advanced application
(MainForm.pas), code
listing, 76-79
advantages
client datasets, 94-95
ConMan, 373
AfterApplyUpdates
event, 295
AfterCancel event, 150
AfterClose event, 150
AfterConnect event, 14
AfterDelete event, 150
AfterDisconnect
event, 14
AfterEdit event, 150
AfterExecute event,
295
AfterGetParams event,
295
AfterGetRecords event,
295, 342
AfterInsert event, 150
AfterOpen event, 150
AfterPost event, 150
AfterRowRequest
event, 295
AfterScroll event, 150
AfterUpdateRecord
event, 295
aggregate expressions,
195
Aggregate fields, 96
aggregate types, 195
aggregates
across datasets, 196-197
determining position of
record, 197
enabling/disabling, 197

grouped, 196
maintained, 192-193
creating at design
time, 193-195
creating at runtime,
195
Aggregates property,
194-195
Alignment property,
242-244
AllowDelete property,
267
AllowInsert property,
267
And operator, 128
Apartment value, 321
Append method, 105
application servers
ConMan, 352-353
creating, 318
testing, 328-329
applications. See also
individual
applications
client
ConMan, 358,
361-362, 371-372
CORBA connections,
335
creating, 329-330
DCOM connections,
333
HTTP connections,
334
intercepting data
packets, 333
multiple connection
types, 343-344

ButtonStyle property

SOAP connections,
335
socket connections,
330
socket servers,
331-332
RangeFilter, 135
redistributing, 376
CD-ROM-based, 378
fees, 378
Linux, 377-378
Windows, 376-377
server, creating user
interface, 326-328
ApplyUpdates method,
276-277
arithmetic operators
(filters)), 128
AsString method, 162
AsString property, 103
asterisk (*) operator,
129
AutoCalcFields property, 98
AutoEdit property, 204
automatic sorting
(grids), 264-265
AutoSave property, 49
Avg aggregate type,
195

B
backslash (\), 210
Basic application, code
listings
DatasetTypeForm.pas,
68-69
MainForm.pas, 65-68

BDE (Borland Database


Engine), 59
BeforeApplyUpdates
event, 295
BeforeCancel event,
149
BeforeClose event, 149
BeforeConnect
event, 14
BeforeDelete event,
149
BeforeDisconnect
event, 14
BeforeEdit event, 149
BeforeExecute event,
295
BeforeGetParams
event, 295
BeforeGetRecords
event, 295, 342
BeforeInsert event, 149
BeforeOpen event, 149
BeforePost event, 149
BeforeRowRequest
event, 296
BeforeScroll event, 149
BeforeUpdateRecord
event, 296
Binary Large Objects.
See BLOBs
BLANK operator, 128
BLOB column,
removing, 70
BLOBs (Binary Large
Objects), 162
fetching manually, 310
fields
limitations, 168-169,
172
resolving changes,
290

storing
files, 168
images, 162-164
notes, 162
streamed data,
165-167
streaming components,
167
support, 69-70
BLOBs application
(MainForm.pas), code
listing, 169-171
BlobSize property, 70
bookmarks, 114-115
Borland Database
Engine (BDE), 59
Borland Web site, 378
Both value, 321
briefcase model, 340
btnRetrieveClick
method, 87
buffering messages, 50
buttons
Add Connection, 10
Cancel, 224
Delete, 224
Edit, 224
First, 224
Insert, 224
Last, 224
New Interface, 325
Next, 224
Post, 224
Prior, 224
Refresh, 224, 290
TDBNavigator, 224
ButtonStyle property,
243

387

388

c specifier

C
c specifier, 209
calculated fields, 98
internal, 98-99
providing values,
99-100
standard, 98
callback handlers,
removing, 49
callback events, trace
(SQL operations
feedback), 47-49
callbacks, 325-326
Cancel button, 224
Cancel method, 177
CancelUpdates
property, 181
CanModify property,
225
Caption property, 244
CaseInsFields property,
120
cbProcedureClick
method, 25
cbUseCallbackClick
event handler, 54
CD-ROM-based
applications,
redistributing, 378
CDS (ClientDataset)
application
(MainForm.pas), code
listing, 108-111
CDSIndex application
(MainForm.pas), code
listing, 124-126
cell (grids), 257-259

change log, 177


properties
CancelUpdates, 181
ChangeCount, 181
LogChanges, 177
MergeChangeLog,
181
RevertRecord,
178-179
SavePoint, 179-180
StatusFilter, 181-182
UndoLastChange,
178
viewing, 182, 186
ChangeCount property,
181
CHANGEINDEX index,
123
ChangeLog application, code listings
ChangeLogForm.pas,
184-185
MainForm.pas, 182-185
ChangeLogForm.pas
(ChangeLog
application), code
listing, 184-185
changing
cursor (SQL operations
feedback), 47
field data (servers),
297-298
check boxes
Log Trace, 54
Use Callback, 54
classes
TCustomClientDataSet,
94
TFieldDataLink,
225-226

client applications
ConMan, 358, 361-362,
371-372
CORBA connections,
335
creating, 329
local connections,
329-330
remote connections,
330
DCOM connections,
333
HTTP connections, 334
intercepting data
packets, 333
multiple connection
types, 343-344
SOAP connections, 335
socket connections, 330
socket servers, 331-332
client datasets, 94
advantages/
disadvantages, 94-95
cloning, 186-188,
191-192
creating, 95
design-time, 96
example, 108
runtime, 101-103
field definitions,
creating, 96
fields
accessing, 103-105
adding, 102
calculated, 98-100
data, 96-97
lookup, 100-101
file formats, 107

code listings

indexes, 118-119
activating, 121
creating, 119-121
retrieving
information,
122-123
switching between,
121-122
manipulating
deleting records, 106
example, 108
modifying records,
105-106
navigating, 113
code listing, 116-118
random-access navigation, 114-116
sequential navigation, 113
populating, 105
example, 108
from a file, 106-107
from a stream,
106-107
from another database, 106
manually, 105
ClientData field, 48
Clone application
(MainForm.pas), code
listing, 188-191
CloneCursor method,
187
cloning client datasets,
186-188, 191-192
closing
database connections,
64
datasets, 64

CMExit method, 228


Code Central Web site,
264, 380
code listings
Advanced application
(MainForm.pas),
76-79
Basic application
DatasetTypeForm.pas,
68-69
MainForm.pas,
65-68
BLOBs application
(MainForm.pas),
169-171
CDS (ClientDataset)
application
(MainForm.pas),
108-111
CDSIndex application
(MainForm.pas),
124-126
ChangeLog application
ChangeLogForm.pas,
184-185
MainForm.pas,
182-185
Clone application
(MainForm.pas),
188-191
ConMan
DataModule.pas,
358-361
MainForm.pas,
362-371
TodoForm.pas,
372-373
ConMan.SQL, 350-351

ConManServer
MainForm.pas,
356-357
RemoteDataModule.
pas, 353-357,
359-361, 363-371,
373
creating empty databases from a resource,
29
CtrlGrid application
(MainForm.pas),
268-270
CustomDraw
(MainForm.pas),
254-256
DataAware application
(MainForm.pas),
232-236
DataFetch application
(MainForm.pas),
311-313
DDLSQL
(MainForm.pas), 33-36
ETHDBComboBox.pas,
214-217, 220-221
ETHDBDateTimePicker
.pas, 228-236
ETHDBGrid.pas,
260-261
ETHDBListBox.pas,
219-221
EventLog application
MainForm.pas,
151-155
OptionsForm.pas,
156-157
Events application
(MainForm.pas), 14-17

389

390

code listings

feedback
(MainForm.pas),
51-54
Joins application
(MainForm.pas),
304-307, 312-313
MetaData
(MainForm.pas),
20-25
MethodsClient
application
(MainForm.pas),
338-339
MethodsServer
application
MainForm.pas,
327-328
ServerDataModule.
pas, 336-339
navigating client
datasets, 116-118
Nested application
(MainForm.pas),
174-175
options
(MainForm.pas),
246-251
RangeFilter
FilterForm.pas, 133
MainForm.pas,
130-132
RangeForm.pas,
134-135
SavePoint property, 180
Schema application
(MainForm.pas),
81-87

Search application
MainForm.pas,
141-143
SearchForm.pas, 144
Trans application
(MainForm.pas),
42-46
Updates application
(MainForm.pas),
159-161, 281-289
ColCount property, 267
colon (:), 210
Color property, 242-244
column (grid)
current, determining,
257
mouse coordinate,
determining, 257-259
column titles, 243-244
column types, 243
columns
BLOB, removing, 70
customizing, 241-242,
265
column titles,
243-244
column types, 243
resized, detecting,
260-261
schema
stColumns value, 89
stIndexes value,
90-91
stProcedureParams
value, 89-90
stProcedures value,
88
stSystemTables
value, 88
stTables value, 88

columns editor, 242


Columns property,
240-241
combo boxes
Driver Name, 10
Instancing, 320
Threading Model, 321
comma (,), 207
commands
DDL, 27
databases, creating,
28-29
executing, 27
tables, creating,
27-28
DML, 29
parameterized SQL
statements, 30-32
SELECT statement,
32-33
simple SQL
statements, 30
File menu (New), 319
gfix, 378
New menu (Other), 280,
319
remote data module
context menu (Add to
Interface), 324
CommandText
property, 60, 310
CommandType
property, 60
committing
transactions, 40
comparison operators
(filters), 127-128

components

components
conn, 352
data-aware, 202-203
controlling user
editing, 206
creating, 225
disabling, 158-159,
162
formatting/editing
field values, 206
numeric fields, 207-209
string fields, 209-210
modifying data from
code, 205-206
non-data-aware
equivalents,
202-203
simple. See simple
data-aware
components
data-aware. See
individual components
dsContacts, 353
populating remote data
modules, 324
pvContacts, 353
sqlContacts, 352
sqlID, 352
sqlTodos, 352
streaming (BLOBs), 167
TClientDataSet, 94. See
also client datasets
TClientDataSetGrid,
261-264
automatic sorting,
264-265
columns,
customizing, 265

TConnectionBroker,
343
TCORBAConnection,
335
TDataSetProvider, 274,
293-295
TDBCheckBox, 212
TDBComboBox, 213,
217-218
TDBCtrlGrid, 266
events, 267-268
properties, 267
TDBEdit, 212
TDBGrid, 240-241
custom drawing,
252-254
customizing columns,
241-244
edit mode, setting,
259-260
events, 245-246
grid options,
244-245
grid settings, persisting, 262-263
limitations, 263
resized columns,
detecting, 260-261
row/column/cell,
determining,
257-259
TDBImage, 221
TDBListBox, 218-221
TDBLookupComboBox
, 223
TDBLookupListBox,
223
TDBMemo, 212

TDBNavigator,
223-225, 290
TDBRadioGroup, 213
TDBText, 211
TDCOMConnection,
333-334
TFieldDataLink,
226-227
TLocalConnection,
329-330
TSimpleObjectBroker,
344
TSOAP, 335
TSocketConnection,
330-331
TSQLClientDataSet,
309
TSQLDataPump, 383
TSQLDataSet, 60
general-purpose data
access, 62-63
properties, 60-61
query-level access,
61-62
stored procedure
access, 62
table-level access, 61
TSQLMetaData, 381
field metadata, 382
index data, 382-383
methods, 383
table/view/field
names, 381-382
TSQLMonitor, 49, 54
TSQLQuery, 60
TSQLScript, 380-381
TSQLStoredProc, 60
TSQLTable, 60
TWebConnection, 334

391

392

ComputerName property

ComputerName
property, 333
ConfigFile property,
265
ConfigureColumns
method, 265
ConMan, 348
advantages/
disadvantages, 373
application server,
352-353
client application, 358,
361-362, 371-372
database structure,
349-350
code listings
DataModule.pas,
358-361
MainForm.pas,
362-371
TodoForm.pas,
372-373
ConMan.SQL, code
listing, 350-351
ConManServer_TLB.
pas, 352
conn component, 352
connect events, 14, 17
Connected property, 8
connecting to databases, 8-9
controlling login, 12-13
dbExpress, 8
local, 308
named connections,
9-11
setting database
parameters, 11-12
unnamed
connections, 11

connecting to datasets,
275-276
connection brokering,
344-345
Connection Editor,
10-11
Connection Name list
box, 10
ConnectionName
property, 8
connections
database, closing, 64
DCOM (Distributed
COM), 333
HTTP, 334
creating
local, 329-330
remote, 330
named, 9-11
SOAP, 335
sockets, 330
unnamed, 11
constraints, 197-198
Constraints property,
197
Contacts table, 349
ContactsByState
procedure, 350
Control property, 225
controlling login, 12-13
controls (data-aware)
lookup, 222-223
VCL-only, 222
CORBA remote data
modules, creating,
322
Count aggregate type,
195
CreateBlobStream
method, 166

creating
application servers, 318
callbacks, 325-326
client application, 329
local connections,
329-330
remote connections,
330
client datasets, 95
design-time, 96
example, 108
runtime, 101-103
data-aware components,
225
databases (DDL
commands), 28-29
DataChange event
handler, 227
empty databases from a
resource, code listing,
29
field definitions (client
datasets), 96
grouped aggregates, 196
indexes (client datasets),
119
at design-time,
119-120
at runtime, 121
maintained aggregates
design time, 193-195
runtime, 195
master/detail
relationships, 74-76
remote data modules,
319
CORBA remote data
modules, 322
MTS remote data
modules, 321-322

data-aware grids

SOAP remote data


modules, 322-323
standard remote data
modules, 320-321
tables (DDL commands), 27-28
TColumn object, 242
user interfaces (server
applications), 326-328
CtrlGrid application
(MainForm.pas), code
listing, 268-270
cursor, changing (SQL
operations
feedback), 47
custom drawing,
252-254
CustomContraint
property, 198
CustomDraw
(MainForm.pas), code
listing, 254-256
CustomIsolation
field, 39
customizing columns,
241-242, 265
column titles, 243-244
column types, 243

D
Dan Miser Web site,
333
data
fields (servers), changing, 297-298
intercepting, 298-299
ordering, 73
queries, 74
tables, 73

returned, limiting,
309-311, 314-315
server, refreshing,
290-291
streamed, storing
(BLOBs), 165-168
data clashes, 277
Data Definition
Language (DDL)
statements, 27
data fields, 96-97
Data Manipulation
Language (DML)
statements, 27
data modules, remote,
318-319
adding methods, 324
creating, 319-322
populating with
components, 324
data packets, intercepting, 333
data pumping
(dbExpress Plus),
383-384
data sources, setting
up connection to, 227
data-aware components, 202-203
controlling user editing,
206
creating, 225
disabling, 158-159, 162
formatting/editing field
values, 206
numeric fields,
207-209
string fields, 209-210

modifying data from


code, 205-206
non-data-aware
equivalents, 202-203
simple, 211
TDBCheckBox, 212
TDBComboBox, 213,
217-218
TDBEdit, 212
TDBImage, 221
TDBListBox,
218-221
TDBLookupComboB
ox, 223
TDBLookupListBox,
223
TDBMemo, 212
TDBRadioGroup,
213
TDBText, 211
TDataSource, 204-205
TDBNavigator, 223-225
data-aware controls
lookup, 222-223
VCL-only, 222
data-aware grids
TClientDataSetGrid,
261, 263-264
automatic sorting,
264-265
columns, customizing, 265
TDBCtrlGrid, 266
events, 267-268
properties, 267
TDBGrid, 240-241
custom drawing,
252-254
customizing columns,
241-244

393

394

data-aware grids

edit mode, setting,


259-260
events, 245-246
grid options, 244-245
grid settings,
persisting, 262-263
limitations, 263
resized columns,
detecting, 260-261
row/column/cell,
determining,
257-259
third party, 271-272
ExpressQuantumGrid,
271
InfoPower 2000, 271
Orpheus, 271
TopGrid, 272
DataAware application
(MainForm.pas), code
listing, 232-236
database connections,
closing, 64
database events,
monitoring, 49
buffering messages, 50
logging messages,
49-50
Database Login dialog
box, 12
database metadata,
retrieving, 18
database parameters,
11-12
Database property, 12
database structure
(ConMan), 349-350

databases
connecting to, 8-9
controlling login,
12-13
dbExpress, 8
named connections,
9-11
setting database
parameters, 11-12
unnamed
connections, 11
creating (DDL
commands), 28-29
creating empty
databases from a
resource, code
listing, 29
disconnecting from, 13
automatically, 13-14
manually, 13
local, connecting to,
308
saving changes,
276-277
DataChange event
handler, creating, 227
DataFetch application
(MainForm.pas), code
listing, 311-313
DataModule.pas, 352,
358-361
DataModuleCreate
method, 361
DataModuleDestroy
method, 361
DataMoveMode
values, 384

dataset events, 148


AfterXxx events,
149-150
BeforeXxx events, 149
event handlers, 150
DataSet property, 204
dataset providers,
274-275
datasets. See also
queries; stored
procedures; tables
aggregates, 196-197
client, 94
advantages/disadvantages, 94-95
calculated fields,
98-100
cloning, 186-188,
191-192
creating, 95-96,
101-103, 108
data fields, 96-97
field definitions, creating, 96
fields, accessing,
103-105
fields, adding, 102
file formats, 107
indexes. See client
datasets, indexes
lookup fields,
100-101
manipulating,
105-108
navigating, 113-118
populating, 105-108
closing, 64
connecting to, 275-276

dgRowSelect option

constraints, 197-198
dbExpress, 58-59
field contents,
retrieving, 64-65
navigating, 65, 68
nested, 172-176
opening, 63-64
resolving to, 278
responding to changes,
227
updating, 227-228
DatasetTypeForm.pas
(Basic application),
code listing, 68-69
DataSize property, 107
DataSnap, 318
DataSource property,
60, 240
Date function, 129
date separator (/), 210
date/time functions
(filters), 129
Day function, 129
dbExpress
connecting to
databases, 8
Linux drivers, 377
statically-linked driver
unites, 377
dbExpress Connection
Editor, 10-11
dbExpress datasets,
58-59
dbExpress Plus, 380
data pumping, 383-384
metadata, 381-383
scripting, 380-381
dbExpress Windows
drivers, 376

DCOM (Distributed
COM) connections,
333
DDL (Data Definition
Language)
statements, 27
DDL commands, 27
creating tables, 28
databases, creating,
28-29
executing, 27
tables, creating, 27
DDLSQL
(MainForm.pas), code
listing, 33-36
decimal point (.), 207
Declaration edit box,
324
DefaultDrawing
property, 253-254
DEFAULT_ORDER
index, 123
Delete button, 224
Delete method, 106
DeleteIndex method,
121
deleting
indexes (client datasets),
121
records (client
databases), 106
Delphi Randomizer,
111
DescFields property,
120
design-time
client datasets,
creating, 96

indexes, creating,
119-120
maintained aggregates,
creating, 193-195
detail records, fetching
manually, 310-311,
314-315
detecting
resized columns,
260-261
transaction support, 38
determining
position of record in
aggregate, 197
row/cell
current, 257
mouse coordinate,
257-259
Developer Express Web
site, 272
dgAlwaysShowEditor
option, 244, 259
dgAlwaysShowSelectio
n option, 245
dgCancelOnExit option,
245
dgColLines option, 244
dgColumnResize
option, 244
dgConfirm option, 245
dgEditing option, 244,
260
dgIndicator option, 244
dgMultiSelect option,
245
dgRowLines option,
244
dgRowSelect option,
245, 253

395

396

dgTabs option

dgTabs option, 244


dgTitles option, 244
dialog boxes
Add to Interface, 324
Database Login, 12
New Field, 97
New Items, 280, 319
Dialogs tab, 280
digit placeholders, 207
DisableControls
method, 159
DisableStringTrim
property, 198-199
disabling
aggregates, 197
data-aware components,
158-159, 162
disadvantages
client datasets, 95
ConMan, 373
disconnect events,
14, 17
disconnecting from
databases, 13
automatically, 13-14
manually, 13
DisplayFormat
property, 207-209
settings, 208
specifiers, 207
Distributed COM
connections, 333
Division ([db])
operator, 128
dmAlwaysInsert value,
384
DmAppend value, 384
dmAppendUpdate
value, 384

DmDelete value, 384


DML (Data
Manipulation
Language)
statements, 27
DML commands, 29
SELECT statement,
32-33
SQL statements
parameterized, 30-32
simple, 30
DmUpdate value, 384
double quotation mark
(), 207
drawing, 252-254
Driver Name combo
box, 10
DriverName property, 8
drivers
Linux, 377
statistically-linked, 377
Windows, 376
DropDownRows
property, 243
dsContacts component,
353

E
E+/, 207
edit boxes,
Declaration, 324
Edit button, 224
Edit method, 105, 225
edit mode, setting,
259-260
editing
data-aware component
data, 206

field values, 206


numeric fields,
207-209
string fields, 209-210
EditKey method, 140
EditMask property, 209
settings, 210-211
specifiers, 209-210
EditorMode property,
259
editors (columns), 242
empty databases, creating from a resource
(code listing), 29
EmptyDataSet method,
106
EnableControls
method, 159
Enabled property, 204
enabling aggregates,
197
encryption libraries,
299
equality test (=)
operator, 127
ETHDBComboBox.pas,
code listing, 214-217,
220-221
ETHDBDateTimePicker.
pas, code listing,
228-236
ETHDBGrid.pas, code
listing, 260-261
ETHDBListBox.pas,
code listing, 219-221
eTraceCat field, 48
event handlers
cbUseCallbackClick, 54
DataChange, 227

feedback

stateless servers,
341-343
writing, 50
EventLog application,
code listings
MainForm.pas, 151-155
OptionsForm.pas,
156-157
events
AfterApplyUpdates, 295
AfterCancel, 150
AfterClose, 150
AfterConnect, 14
AfterDelete, 150
AfterDisconnect, 14
AfterEdit, 150
AfterExecute, 295
AfterGetParams, 295
AfterGetRecords, 295,
342
AfterInsert, 150
AfterOpen, 150
AfterPost, 150
AfterRowRequest, 295
AfterScroll, 150
AfterUpdateRecord, 295
BeforeApplyUpdates,
295
BeforeCancel, 149
BeforeClose, 149
BeforeConnect, 14
BeforeDelete, 149
BeforeDisconnect, 14
BeforeEdit, 149
BeforeExecute, 295
BeforeGetParams, 295
BeforeGetRecords, 295,
342

BeforeInsert, 149
BeforeOpen, 149
BeforePost, 149
BeforeRowRequest, 296
BeforeScroll, 149
BeforeUpdateRecord,
296
connect/disconnect,
14, 17
database, monitoring,
49-50
dataset, 148-150
OnActiveChange, 226
OnCalcFields, 150
OnCellClick, 245
OnColEnter, 245
OnColExit, 246
OnColumnMoved, 246
OnDataChange, 205,
226
OnDeleteError, 150
OnDrawColumnCell,
246, 252
OnDrawDataCell, 246,
252
OnEditButtonClick, 246
OnEditError, 150
OnEditingChange, 226
OnFilterRecord, 130,
150
OnGetData, 296-298,
341
OnGetDataSetProperties,
297
OnGetTableName, 297
OnLogin, 12, 14
OnLogTrace, 50
OnNewRecord, 150

OnPaintPanel, 267-268
OnPostError, 150
OnReconcileError, 278
OnStateChange, 205
OnTitleClick, 246
OnTrace, 50
OnUpdateData, 205,
226, 297-298
OnUpdateError, 297
TDataSetProvider
component, 295-297
TDataSource, 205
TDBCtrlGrid
component, 267-268
TDBGrid, 245-246
TFieldDataLink, 226
Events application
(MainForm.pas), code
listing, 14-17
exclamation point (!),
210
ExecuteAction method,
228
ExecuteDirect method,
27-28
executing DDL
commands, 27
Expression property,
120
expressions
(aggregates), 195
ExpressQuantumGrid,
271

F
feedback
MainForm.pas, code
listing, 51-54

397

398

feedback

SQL operations, 46-47


changing cursor, 47
multiple feedback,
50-51, 54
trace callback events,
47-49
fees, redistributing
applications, 378
FetchBlobs method,
310
FetchDetails method,
310
fetching
BLOBs, 310
detail records, 310-311,
314-315
FetchOnDemand
property, 310-311
field data (servers),
changing, 297-298
field definitions (client
datasets), creating, 96
field names, retrieving,
18-19, 381-382
field objects,
retrieving, 257
Field property, 226
field values,
formatting/editing,
206
numeric fields, 207-209
string fields, 209-210
FieldByName method,
104
FieldCount property, 65
FieldDefs property, 102
FieldName property,
226, 242

fields
Aggregate, 96
BLOBs
limitations, 168-169,
172
resolving changes,
290
calculated, 98
internal, 98-99
providing values,
99-100
standard, 98
client dataset
accessing, 103-105
adding, 102
ClientData, 48
CustomIsolation, 39
data, 96-97
eTraceCat, 48
FSupportsMultiTrans,
41
GlobalID, 39
IsolationLevel, 39-40
lookup, 100-101
metadata, retrieving,
382
nonpersistent, 104-105
persistent, 103
pszTrace, 48
retrieving contents from
datasets, 64-65
separate, storing images
(BLOBs), 163
TransactionID, 39
uTotalMsgLen, 48
Fields object, accessing, 64
Fields property, 104,
120

file formats (client


datasets), 107
File menu commands
(New), 319
FileName property, 49
files
BLOBs, storing, 168
MyBase, 107
Filter property, 130
FilterForm.pas
(RangeFilter
application), code
listing, 133
filters, 126-127, 130
arithmetic operators,
128
comparison operators,
127-128
date/time functions, 129
functions, 129
logical operators, 128
operators, 129
string functions,
128-129
FindField method, 104
FindKey method,
138-139
FindNearest method,
139
First button, 224
First method, 113
Font property, 242-244
formats, files (client
datasets), 107
formatting field values,
206
numeric fields, 207-209
string fields, 209-210

indexes

FormCreate method,
111, 175
Free value, 321
freeing bookmarks, 114
FSupportsMultiTrans
field, 41
functions
Date, 129
date/time (filters), 129
Day, 129
filters, 129
GetDate, 129
Hour, 129
Lower, 128
Minute, 129
Month, 129
Second, 129
string (filters), 128-129
SubString, 128
Time, 129
Trim, 128
TrimLeft, 129
TrimRight, 129
Upper, 128
Year, 129

G
gdFixed value, 253
gdFocused value, 253
gdSelected value, 253
general-purpose data
access (TSQLDataSet
component), 62-63
GetDate function, 129
GetDriverFunc
property, 8

GetFieldNames
property, 18-19
GetGroupState
method, 197
GetIndexNames
method, 122
GetIndexNames
property, 19
GetProcedureNames
property, 19
GetProcedureParams
property, 19-20, 25-27
GetTableNames
property, 18
gfix command, 378
GlobalID field, 39
GotoKey method,
139-140
GotoNearest method,
140
greater than (>)
operator, 127
greater than or equal
to (>=) operator, 127
greater than sign (>),
210
greater than/less than
sign (< >), 210
grid options (TDBGrid),
244-245
grid settings, persisting, 262-263
grids
data-aware. See individual data-aware grids
sorting, 126, 264-265
grouped aggregates,
creating, 196
GroupingLevel property, 120

H-I
handlers (callback),
removing, 49
Host property, 331
HostName property,
335
Hour function, 129
HTTP connections, 334
icons, Reconcile Error
Dialog, 280
ImageLib Corporate
Suite, 165
images (BLOBs),
storing, 162-164
IN operator, 129
index names,
retrieving, 19
IndexDefs property,
119
indexed search
methods, 138
FindKey, 138-139
FindNearest, 139
GotoKey, 139-140
GotoNearest, 140
indexes
CHANGEINDEX, 123
client datasets, 118-119
activating, 121
creating, 119-121
deleting, 121
retrieving
information,
122-123
switching between,
121-122
data, retrieving, 382-383

399

400

indexes

DEFAULT_ORDER,
123
retrieving, 257
IndexFieldNames
property, 73, 122
IndexName property,
73
inequality test (< >)
operator, 127
InfoPower 2000, 271
Insert button, 224
Insert method, 105
Instancing combo box,
320
InterceptGUID
property, 331
intercepting
data, 298-299
data packets, 333
InterceptName
property, 331
interfaces (callbacks)
creating, 325-326
limitations, 326
internal calculated
fields, 98-99
Internal value, 320
InTransaction
property, 41
IS NOT NULL operator,
128
IS NULL operator, 128
IsolationLevel field,
39-40
IxCaseInsensitive
option, 120
IxDescending option,
120

IxExpression option,
120
IxNonMaintained
option, 120
IxPrimary option, 120
IxUnique option, 120

J-K
joins, providing/
resolving data,
302-304, 307
Joins application
(MainForm.pas), code
listing, 304-307,
312-313
KeepConnection
property, 8
KeepSettings property,
187-188
KeyFields parameter,
136
KeyValues parameter,
136

L
L specifier, 209
Last button, 224
Last method, 113
less than (<) operator,
127
less than or equal to
(<=) operator, 127
less than sign (<), 210
LibraryName
property, 8

licensing, 378
LIKE operator, 129
limitations
callbacks, 326
fields (BLOBs),
168-169, 172
TDBGrid component,
263
limiting returned data,
309
BLOBs, fetching manually, 310
detail records, fetching
manually, 310-311,
314-315
Linux
applications,
redistributing, 377-378
drivers, 377
list boxes, Connection
Name, 10
Load1E XEcute
method, 112
LoadBalanced property,
344
LoadFromFile method,
168
LoadParamsOnConnect
property, 9
local connections
(client applications),
creating, 329-330
local databases,
connecting to, 308
loCaseInsensitve
option, 136
Locate method,
136-137

methods

Log Trace check box, 54


LogChanges property,
177
logging messages,
49-50
logical operators
(filters), 128
login, controlling,
12-13
LoginPrompt
property, 9
lookup data-aware
controls, 222-223
lookup fields, 100-101
Lookup method,
137-138
loPartialKey option,
136
Lower function, 128

M
MainForm.pas code
listings, 352
Advanced application,
76-79
Basic application, 65-68
BLOBs application,
169-171
CDS (ClientDataset)
application, 108-111
CDSIndex, 124-126
ChangeLog application,
182-185
Clone application,
188-191
ConMan application,
362-371

ConManServer
application, 356-357
CtrlGrid application,
268-270
CustomDraw application, 254-256
DataAware application,
232-236
DataFetch application,
311-313
DDLSQL application,
33-36
EventLog application,
151-155
Events application,
14-17
feedback, 51-54
Joins application,
304-307, 312-313
MetaData, 20-25
MethodsClient
application, 338-339
MethodsServer
application, 327-328
navigating client
datasets, 116-118
Nested application,
174-175
options, 246-251
RangeFilter application,
130-132
Schema application,
81-87
Search application,
141-143
Trans, 42-46
Updates application,
159-161, 281-289

maintained aggregates,
192-193
creating at design time,
193
nonpersistent
aggregates,
194-195
persistent aggregates,
193-194
creating at runtime, 195
manipulating client
datasets
deleting records, 106
example, 108
modifying records,
105-106
manually populating
client datasets, 105
master/detail
relationships, 74-76,
301
Max aggregate type,
195
MaxBlobSize
property, 61
MergeChangeLog
property, 181
message handlers, 228
messages
buffering, 50
logging, 49-50
metadata (dbExpress
Plus), 381-383
MetaData application
(MainForm.pas), code
listing, 20-25
methods
Add, 102
AddFieldDef, 102
AddIndex, 121

401

402

methods

adding to remote data


modules, 324
Append, 105
ApplyUpdates, 276-277
AsString, 162
btnRetrieveClick, 87
Cancel, 177
cbProcedureClick, 25
CloneCursor, 187
CMExit, 228
ConfigureColumns, 265
CreateBlobStream, 166
DataModuleCreate, 361
DataModuleDestroy,
361
Delete, 106
DeleteIndex, 121
DisableControls, 159
Edit, 105, 225
EditKey, 140
EmptyDataSet, 106
EnableControls, 159
Execute Direct, 27-28
ExecuteAction, 228
FetchBlobs, 310
FetchDetails, 310
FieldByName, 104
FindField, 104
FindKey, 138-139
FindNearest, 139
First, 113
FormCreate, 111, 175
GetGroupState, 197
GetIndexNames, 122
GotoKey, 139-140
GotoNearest, 140
Insert, 105
Last, 113

Load1E XEcute, 112


LoadFromFile, 168
Locate, 136-137
Lookup, 137-138
Modified, 225
MonitorTrace, 54
Next, 113
OnCalcFields, 99
Populate1E XEcute,
111
Prior, 113
Refresh, 290
RefreshRecord, 291
Reset, 225
Save1E XEcute, 112
SaveToFile, 50, 106
SaveToStream, 106
StartTransaction, 39
Statistics1E XEcute,
112
TFieldDataLink, 225
TIndexDefs, 123
TSQLMetaData
component, 383
UpdateAction, 228
MethodsClient
application
(MainForm.pas), code
listing, 338-339
MethodsServer
application, code
listings
MainForm.pas, 327-328
ServerDataModule.pas,
336-339
Microsoft ADO,
incompatibility with
MyBase, 107
MIDAS, 318

Min aggregate type,


195
Minute function, 129
modes, update,
291-293
Modified method, 225
modifying
data-aware component
data from code,
205-206
records (client
databases), 105-106
monitoring database
events, 49
buffering messages, 50
logging messages,
49-50
MonitorTrace
method, 54
Month function, 129
MTS remote data
modules, creating,
321-322
multiple connection
types (client
applications), 343-344
multiple feedback (SQL
operations), 50-51, 54
Multiple Instance
value, 320
multiple transactions,
40-42
Multiplication ([ts])
operator, 128
Multitier tab, 319
MyBase,
incompatibility with
Microsoft ADO, 107
MyBase file, 107

OnTrace event

N
Name property, 120
named connections,
9-11
navigating
client datasets, 113
random-access
navigation, 114-116
sequential
navigation, 113
datasets, 65, 68
Nested application
(MainForm.pas), code
listing, 174-175
nested datasets,
172-176
nested transactions, 40
Neutral value, 321
New command (File
menu), 319
New Field dialog box,
97
New Interface button,
325
New Items dialog box,
280, 319
New menu commands
(Other), 280, 319
New Method toolbar
button, 325
Next button, 224
Next method, 113
nonindexed search
methods, 136
Locate method, 136-137
Lookup method,
137-138

nonpersistent
aggregates, creating
at design time,
194-195
nonpersistent fields,
104-105
Not operator, 128
notes (BLOBs), storing,
162
numeric fields,
formatting/editing,
207-209

O
Object-Insight Web
site, 272
ObjectBroker property,
344
ObjectName property,
335
objects
field, retrieving, 257
Fields, accessing, 64
TColumn, creating, 242
TFieldDef, 102
ObjectView
property, 61
OnActiveChange
event, 226
OnCalcFields event,
150
OnCalcFields
method, 99
OnCellClick event, 245
OnColEnter event, 245
OnColExit event, 246

OnColumnMoved
event, 246
OnDataChange event,
205, 226
OnDeleteError event,
150
OnDrawColumnCell
event, 246, 252
OnDrawDataCell event,
246, 252
one-to-many
relationships, 74
OnEditButtonClick
event, 246
OnEditError event, 150
OnEditingChange
event, 226
OnFilterRecord event,
130, 150
OnGetData event,
296-298, 341
OnGetDataSetPropertie
s event, 297
OnGetTableName
event, 297
OnLogin event, 12, 14
OnLogTrace event, 50
OnNewRecord event,
150
OnPaintPanel event,
267-268
OnPostError event, 150
OnReconcileError
event, 278
OnStateChange event,
205
OnTitleClick event, 246
OnTrace event, 50

403

404

OnUpdateData event

OnUpdateData event,
205, 226, 297-298
OnUpdateError event,
297
opening datasets,
63-64
operators
Addition (+), 128
And, 128
arithmetic (filters), 128
asterisk (*), 129
BLANK, 128
comparison (filters),
127-128
Division ([db]), 128
equality test (=), 127
filters, 129
greater than (>), 127
greater than or equal to
(>=), 127
IN, 129
inequality test (< >),
127
IS NOT NULL, 128
IS NULL, 128
less than (<), 127
less than or equal to
(<=), 127
LIKE, 129
logical (filters), 128
Multiplication ([ts]),
128
Not, 128
Or, 128
Subtraction (), 128
optional parameters,
300-301

options
dgAlwaysShowEditor,
244, 259
dgAlwaysShowSelectio
n, 245
dgCancelOnExit, 245
dgColLines, 244
dgColumnResize, 244
dgConfirm, 245
dgEditing, 244, 260
dgIndicator, 244
dgMultiSelect, 245
dgRowLines, 244
dgRowSelect, 245, 253
dgTabs, 244
dgTitles, 244
grid (TDBGrid),
244-245
IxCaseInsensitive, 120
IxDescending, 120
IxExpression, 120
IxNonMaintained, 120
IxPrimary, 120
IxUnique, 120
loCaseInsensitive, 136
loPartialKey, 136
MainForm.pas, code
listing, 246-251
poAllowCommandText,
294
poAllowMultiRecordUp
dates, 294
poAutorRefresh, 294
poCascadeUpdates, 294
poDisableDeletes, 294
poDisableEdits, 294
poDisableInserts, 294
poFetchBlobsOnDemand,
293, 310

poFetchDetailsOnDema
nd, 293, 310
poIncFieldProps, 293
poNoReset, 294
poPropogateChanges,
294
poReadOnly, 294
poRetainServerOrder,
294
TDataSetProvider
component, 293-295
Options parameter, 136
Options property, 120,
293-294
OptionsForm.pas
(EventLog
application), code
listing, 156-157
Or operator, 128
ordering data, 73
queries, 74
tables, 73
Orientation property,
267
Orpheus, 271
Other command (New
menu), 280, 319
overlapped
transactions, 40

P
PacketRecords
property, 309
PanelBorder property,
267
PanelHeight property,
267

properties

PanelWidth property,
267
ParamCheck property,
61, 72-73
parameterized queries,
71-73, 75
parameterized SQL
statements, 30-32
parameters
database, 11-12
KeyFields, 136
KeyValues, 136
optional, 300-301
Options, 136
Properties, 300
retrieving, 19-20, 25-27
Params property,
9-12, 61
Password property, 12,
334-335
passwords, setting
(named
connections), 11
period (.), 207
persistent aggregates,
creating at design
time, 193-194
persistent fields, 103
persisting grid settings,
262-263
pfHidden value, 292
pfInKey value, 292
pfInUpdate value, 292
pfInWhere value, 292
PickList property, 243
poAllowCommandText
option, 294
poAllowMultiRecordUp
dates option, 294

poAutoRefresh option,
294
poCascadeUpdates
option, 294
poDisableDeletes
option, 294
poDisableEdits option,
294
poDisableInserts
option, 294
poFetchBlobsOnDeman
d option, 293, 310
poFetchDetailsOnDema
nd option, 293, 310
poIncFieldProps option,
293
poNoReset option, 294
poPropogateChanges
option, 294
Populate1E XEcute
method, 111
populating
client datasets, 105-108
remote data modules,
324
poReadOnly option,
294
poRetainServerOrder
option, 294
Port property, 331
Post button, 224
pound sign (#),
207-209
Prior button, 224
Prior method, 113
procedures
ContactsByState, 350
stored,
providing/resolving
data, 302. See also
datasets

properties
Active, 60
Address, 331
Aggregates, 194-195
Alignment, 242-244
AllowDelete, 267
AllowInsert, 267
AsString, 103
AutoCalcFields, 98
AutoEdit, 204
AutoSave, 49
BlobSize, 70
ButtonStyle, 243
CancelUpdates, 181
CanModify, 225
Caption, 244
CaseInsFields, 120
ChangeCount, 181
ColCount, 267
Color, 242-244
Columns, 240-241
CommandText, 60, 310
CommandType, 60
ComputerName, 333
ConfigFile, 265
Constraints, 197
Control, 225
CustomContraint, 198
Database, 12
DataSet, 204
DataSize, 107
DataSource, 60, 240
DefaultDrawing,
253-254
DescFields, 120
DisableStringTrim,
198-199
DisplayFormat, 207-209

405

406

properties

settings, 208
specifiers, 207
DropDownRows, 243
EditMask, 209
settings, 210-211
specifiers, 209-210
EditMode, 259
Enabled, 204
Expression, 120
FetchOnDemand,
310-311
Field, 226
FieldCount, 65
FieldDefs, 102
FieldName, 226, 242
Fields, 104, 120
FileName, 49
Filter, 130
Font, 242, 244
GetFieldNames, 18-19
GetIndexNames, 19
GetProcedureNames, 19
GetProcedureParams,
19-20, 25-27
GetTableNames, 18
GroupingLevel, 120
Host, 331
HostName, 335
IndexDefs, 119
IndexFieldNames, 73,
122
IndexName, 73
InterceptGUID, 331
InterceptName, 331
InTransaction, 41
KeepSettings, 187-188
LoadBalanced, 344
LogChanges, 177

MaxBlobSize, 61
MergeChangeLog, 181
Name, 120
ObjectBroker, 344
ObjectName, 335
ObjectView, 61
Options, 120, 293-294
Orientation, 267
PacketRecord, 309
PanelBorder, 267
PanelHeight, 267
PanelWidth, 267
ParamCheck, 61, 72-73
Params, 61
Password, 12, 334-335
PickList, 243
Port, 331
ProviderFlags, 292-293
Proxy, 334-335
ReadOnly, 199, 242
RecNo, 115-116
RepositoryID, 335
Reset, 187-188
ResolveToDataSet, 278
RevertRecord, 178-179
RowCount, 267
SavePoint, 179-180
SelectedColor, 267
SelectedField, 257
SelectedIndex, 257
ServerGUID, 331-334
ServerName, 331, 334
ShowFocus, 267
SortFieldNames, 61
Source, 120
SQLConnection, 61
SQLHourGlass, 47
StatusFilter, 181-182

SupportCallbacks, 331
TableScope, 18
TColumn, 242-243
TCORBAConnection
component, 335
TDataSource, 204
TDBCtrlGrid
component, 267
TDCOMConnection,
333-334
TFieldDataLink,
225-226
Title, 242-244
TitleSort, 264
TraceList, 50
TransactionSupported,
38
TSOAPConnection, 335
TSocketConnection
component, 331
TSQLConnection, 8-9
Connected, 8
ConnectionName, 8
DriverName, 8
GetDriverFunc, 8
KeepConnection, 8
LibraryName, 8
LoadParamsOnConnect, 9
LoginPrompt, 9
Params, 9, 11-12
TableScope, 9
VendorLib, 9
TSQLDataSet
component, 60-61
TWebConnection
component, 334
UndoLastChange, 178

remote data modules

UpdateMode, 291
URL, 334-335
UserName, 12, 334-335
Values, 214
Visible, 242
Width, 242
Properties parameter,
300
ProviderFlags property,
292-293
providers, accessing on
different form, 308
providing
data
joins, 302-304, 307
stored procedures,
302
values (calculated
fields), 99-100
Proxy property,
334-335
pszTrace field, 48
pvContacts component,
353

Q-R
queries. See also
datasets
ordering data, 74
parameterized, 71-75
query-level access
(TSQLDataSet
component), 61-62
raAbort value, 279
raCancel value, 279
raCorrect value, 279

Raize Software Web


site, 47
raMerge value, 279
random-access
navigation (client
datasets), 114-116
bookmarks, 114-115
record numbers, 115
Randomizer, 111
RangeFilter
application, code
listings
FilterForm.pas, 133
MainForm.pas, 130-132
RangeForm.pas,
134-135
RangeForm.pas
(RangeFilter
application), code
listing, 134-135
ranges, 126-127
raRefresh value, 279
raSkip value, 279
ReadOnly property,
199, 242
RecErrorForm.pas, 352
RecNo property,
115-116
Reconcile Error Dialog
icon, 280
reconciliation errors,
278-280, 289-290
record numbers, 115
records
deleting (client databases), 106
determining position in
aggregate, 197

modifying (client databases), 105-106


TransactionDesc, 39
redistributing
applications, 376
CD-ROM-based, 378
fees, 378
Linux, 377-378
Windows, 376-377
Refresh button, 224,
290
Refresh method, 290
refreshing data
(servers), 290-291
RefreshRecord method,
291
relationships
master/detail, 74-76,
301
one-to-many, 74
remote connections
(client applications),
creating, 330
remote data module
context menu
commands (Add to
Interface), 324
remote data modules,
318-319
creating, 319
CORBA remote data
modules, 322
MTS remote data
modules, 321-322
SOAP remote data
modules, 322-323
standard remote data
modules, 320-321

407

408

remote data modules

methods, adding, 324


populating with
components, 324
RemoteDataModule.pa
s (ConManServer
application), code
listing, 352-357,
359-361, 363-371, 373
removing
BLOB column, 70
callback handlers, 49
RepositoryID
properties, 335
Reset method, 225
Reset property, 187-188
resized columns,
detecting, 260-261
ResolveToDataSet
property, 278
resolving, 274
changes to BLOB fields,
290
data
joins, 302-304, 307
stored procedures,
302
datasets, 278
responding to dataset
changes, 227
result sets, returning,
33
retrieving
database metadata, 18
field contents (datasets),
64-65
field metadata, 382
field names, 18-19
field objects, 257

index data, 382-383


index information
(client datasets), 122
GetIndexNames
method, 122
TIndexDefs method,
123
index names, 19
indexes, 257
parameters, 19-20,
25-27
schema information,
79-81, 87-88
stored procedures, 19
table/view/field names,
381
tables, 18
returned data, limiting,
309
BLOBs, fetching
manually, 310
detail records, fetching
manually, 310-311,
314-315
returning
bookmarks, 114
result sets, 33
RevertRecord property,
178-179
rolling back transactions, 40
row (grids), determining
current, 257
mouse coordinate,
257-259
RowCount property,
267

runtime
client datasets, creating,
101-103
creating indexes, 121
creating maintained
aggregates, 195
deleting indexes, 121

S
Save1E XEcute method,
112
SavePoint property,
179-180
SaveToFile method, 50,
106
SaveToStream method,
106
saving changes to
databases, 276-277
Schema application
(MainForm.pas), code
listing, 81-87
schema columns
stColumns value, 89
stIndexes value, 90-91
stProcedureParams
value, 89-90
stProcedures value, 88
stSystemTables value,
88
stTables value, 88
schema information,
retrieving, 79-81,
87-88
scientific notation
(E+/), 207
ScktSrvr, 331-332

Source property

scripting (dbExpress
Plus), 380-381
Search application,
code listings
MainForm.pas, 141-143
SearchForm.pas, 144
search methods, 136
indexed, 138
FindKey, 138-139
FindNearest, 139
GotoKey, 139-140
GotoNearest, 140
nonindexed, 136
Locate method,
136-137
Lookup method,
137-138
SearchForm.pas (Search
application), code
listing, 144
Second function, 129
SELECT statement,
32-33
SelectedColor property,
267
SelectedField property,
257
SelectedIndex property,
257
separate fields, storing
images (BLOBs), 163
separator character (;),
207, 210
sequential navigation
(client datasets), 113
server applications
callbacks
creating, 325-326
limitations, 326
user interface, creating,
326-328

ServerDataModule.pas
(MethodsServer application), code listing,
336-339
ServerGUID property,
331-334
ServerName property,
331, 334
servers
application
creating, 318
testing, 328-329
data, refreshing,
290-291
field data, changing,
297-298
socket, 331-332
stateless, 341-343
setting
bookmarks, 114
database parameters,
11-12
edit mode, 259-260
passwords (named
connections), 11
setting up
connection to data
source, 227
TFieldDataLink
component, 226-227
settings
DisplayFormat property,
208
EditMask property,
210-211
TableScope property, 18
ShowFocus property,
267

simple data-aware
components, 211
TDBCheckBox, 212
TDBComboBox, 213,
217-218
TDBEdit, 212
TDBImage, 221
TDBListBox, 218-221
TDBLookupComboBox,
223
TDBLookupListBox,
223
TDBMemo, 212
TDBRadioGroup, 213
TDBText, 211
simple SQL statements,
30
Single Instance value,
320
single quotation mark
(), 207
Single value, 321
Skyline Tools Imaging
(ImageLib Corporate
Suite), 165
Skyline Tools Imaging
Web site, 165
SOAP connections, 335
SOAP remote data
modules, creating,
322-323
socket connections, 330
socket servers, 331-332
SortFieldNames
property, 61
sorting grids, 126,
264-265
Source property, 120

409

410

specifiers

specifiers
0, 209
9, 209
A, 209
c, 209
DisplayFormat property,
207
EditMask property,
209-210
l, 209
SQL operations
(feedback), 46-47
changing cursor, 47
multiple feedback,
50-51, 54
trace callback events,
47-49
SQL statements
parameterized, 30-32
result sets, returning, 33
simple, 30
SQLConnection
property, 61
sqlContacts
component, 352
SQLHourGlass
property, 47
sqlID component, 352
sqlTodos component,
352
standard calculated
fields, 98
standard remote data
modules, creating,
320-321
starting transactions,
39-40
StartTransaction
method, 39

stateless servers,
341-343
statements
DDL (Data Definition
Language), 27
DML, 27
SELECT, 32-33
SQL
parameterized, 30-32
result sets, returning,
33
simple, 30
static linking, 377
statically-linked driver
units, 377
Statistics1E XEcute
method, 112
StatusFilter property,
181-182
stColumns value, 80,
89
stIndexes value, 80,
90-91
stNoSchema value, 80
stored procedure
access (TSQLDataSet
component), 62
stored procedures
providing/resolving
data, 302
retrieving, 19
stored procedures, 60.
See also datasets
storing
files, 168
images, 162-164
notes, 162
streamed data, 165-167
stProcedureParams
value, 80, 89-90

stProcedures value, 80,


88
streamed data (BLOBs),
storing, 165-167
streaming images
(BLOBs), 163-164
streaming components
(BLOBs), 167
string fields,
formatting/editing,
209-210
string functions
(filters), 128-129
stSysTables value, 80
stSystemTables value
(schema columns), 88
stTables value, 80, 88
SubString function, 128
Subtraction ()
operator, 128
Sum aggregate type,
195
support
BLOB (Binary Large
Object), 69-70
transactions, 37-38
undo, 176-177
SupportCallbacks property, 331
switching between
indexes (client
datasets), 121-122

T
table names,
retrieving, 381-382
table-level access
(TSQLDataSet
component), 61

third-party data-aware grids

tables, 59. See also


datasets
Contacts, 349
creating (DDL
commands), 27-28
ordering data, 73
retrieving, 18
Todos, 350
TableScope property,
9, 18
tabs
Dialogs, 280
Multitier, 319
WebServices, 323
TClientDataSet
component, 94.
See also client
datasets
TClientDataSetGrid
component, 261-264
automatic sorting,
264-265
columns, customizing,
265
TColumn objects,
creating, 242
TColumn properties,
242-243
TConnectionBroker
component, 343
TCORBAConnection
component, 335
TCustomClientDataSet
class, 94
TDataSetProvider
component, 274,
293-297
TDataSource
component, 204-205

TDataSource events,
205
TDataSource
properties, 204
TDBCheckBox
component, 212
TDBComboBox
component, 213,
217-218
TDBCtrlGrid
component, 266
events, 267-268
properties, 267
TDBEdit component,
212
TDBGrid component,
240-241
columns, customizing,
241-244
custom drawing,
252-254
edit mode, setting,
259-260
events, 245-246
grid options, 244-245
grid settings, persisting,
262-263
limitations, 263
resized columns,
detecting, 260-261
row/column/cell,
determining, 257-259
TDBGrid options,
244-245
TDBImage component,
221
TDBListBox
component, 218-219,
221

TDBLookupComboBox
component, 223
TDBLookupListBox
component, 223
TDBMemo component,
212
TDBNavigator buttons,
224
TDBNavigator
component, 223-225,
290
TDBRadioGroup
component, 213
TDBText component,
211
TDCOMConnection
component, 333-334
testing application
servers, 328-329
TFieldDataLink class,
225-226
TFieldDataLink
component, 226-227
TFieldDataLink events,
226
TFieldDataLink
methods, 225
TFieldDataLink
properties, 225-226
TFieldDef object, 102
TGridDrawState values,
253
third-party data-aware
grids, 271-272
ExpressQuantumGrid,
271
InfoPower 2000, 271
Orpheus, 271
TopGrid, 272

411

412

third-party imaging libraries

third-party imaging
libraries, storing
images (BLOBs), 164
thousands separator
(,), 207
Threading Model
combo box, 321
Time function, 129
time separator (:), 210
TIndexDefs method,
123
Title property, 242-244
TitleSort property, 264
TLocalConnection
component, 329-330
TodoForm.pas, code
listings, 352, 372-373
Todos table, 350
toolbar buttons, New
Method, 325
TopGrid, 272
trace callback events
(SQL operations
feedback), 47-49
traceBLOB value, 48
traceDATAIN value, 48
traceDATAOUT
value, 48
traceERROR value, 48
TraceList property, 50
traceMISC value, 48
traceQEXECUTE
value, 48
traceQPREPARE
value, 48
traceSTMT value, 48
traceTRANSACT
value, 48

traceVENDOR value, 48
Trans application
(MainForm.pas), code
listing, 42-46
TransactionDesc
record, 39
TransactionID field, 39
transactions, 37-38
ACID (Atomic,
Consistent, Isolated,
Durable) properties,
37
committing, 40
multiple, 40-42
nested, 40
overlapped, 40
rolling back, 40
starting, 39-40
support, 37-38
TransactionSupported
property, 38
TReconciliationAction
value, 279
Trim function, 128
TrimLeft function, 129
TrimRight function, 129
TSchemaType
values, 80
TSimpleObjectBroker
component, 344
TSOAPConnection
component, 335
TSocketConnection
component, 330-331
TSQLClientDataSet
component, 309

TSQLConnection
component
events, 14, 17
properties, 8-9
Connected, 8
ConnectionName, 8
DriverName, 8
GetDriverFunc, 8
KeepConnection, 8
LibraryName, 8
LoadParamsOnConn
ect, 9
LoginPrompt, 9
Params, 9, 11-12
TableScope, 9
VendorLib, 9
TSQLDataPump
component, 383
TSQLDataSet
component, 60
general-purpose data
access, 62-63
properties, 60-61
query-level access,
61-62
stored procedure
access, 62
table-level access, 61
TSQLMetaData
component, 381
methods, 383
retrieving
field metadata, 382
index data, 382-383
table/view/field
names, 381-382
TSQLMonitor
component, 49, 54

values

TSQLQuery
component, 60
TSQLScript component,
380-381
TSQLStoredProc
component, 60
TSQLTable
component, 60
TUpdateKind values,
279
TUpdateMode values,
291-292
TUpdateStatus value,
181
TurboPower Software
Company Web site,
211
TurboPower Software
Web site, 299
TurboPower Web site,
271
TWebConnection
component, 334

U
ukDelete value, 279
ukInsert value, 279
ukModify value, 279
underscore (_), 210
undo support, 176-177
Cancel method, 177
change log, 177
CancelUpdates
property, 181
ChangeCount
property, 181
LogChanges
property, 177

MergeChangeLog
property, 181
RevertRecord
property, 178-179
SavePoint property,
179-180
StatusFilter property,
181-182
UndoLastChange
property, 178
viewing, 182, 186
UndoLastChange
property, 178
unnamed
connections, 11
update modes, 291-293
UpdateAction method,
228
UpdateMode property,
291
Updates application
(MainForm.pas), code
listing, 159-161,
281-289
updating datasets,
227-228
Upper function, 128
upWhereAll value, 291
upWhereChanged
value, 291
upWhereKeyOnly
value, 291
URL property, 334-335
usDeleted value, 181
Use Callback check
box, 54
user interfaces (server
applications),
creating, 326-328

UserName property, 12,


334-335
usInserted value, 181
usModified values, 181
usUnmodified value,
181
uTotalMsgLen field, 48

V
values
Apartment, 321
Both, 321
DataMoveMode, 384
dmAlwaysInsert, 384
DmAppend, 384
dmAppendUpdate, 384
DmDelete, 384
DmUpdate, 384
Free, 321
gdFixed, 253
gdFocused, 253
gdSelected, 253
Internal, 320
Multiple Instance, 320
Neutral, 321
pfHidden, 292
pfInKey, 292
pfInUpdate, 292
pfInWhere, 292
providing (calculated
fields), 99-100
raAbort, 279
raCancel, 279
raCorrect, 279
raMerge, 279
raRefresh, 279
raSkip, 279

413

414

values

Single, 321
Single Instance, 320
stColumns, 80, 89
stIndexes, 80, 90-91
stNoSchema, 80
stProcedureParams, 80,
89-90
stProcedures, 80, 88
stSysTables, 80
stSystemTables, 88
stTables, 80, 88
TGridDrawState, 253
traceBLOB, 48
traceDATAIN, 48
traceDATAOUT, 48
traceERROR, 48
traceMISC, 48
traceQEXECUTE, 48
traceQPREPARE, 48
traceSTMT, 48
traceTRANSACT, 48
traceVENDOR, 48
TReconciliationAction,
279
TSchemaType, 80
TUpdateKind, 279
TUpdateMode, 291-292
TUpdateStatus, 181
ukDelete, 279
ukInsert, 279
ukModify, 279
upWhereAll, 291
upWhereChanged, 291
upWhereKeyOnly, 291
usDeleted, 181
usInserted, 181
usModified, 181
usUnmodified, 181
xilCUSTOM, 40

xilDIRTYREAD, 39
xilREADCOMMITTED,
39
xilREPEATABLEREAD,
40
Values property, 214
VCL-only data-aware
controls, 222
VendorLib property, 9
view names,
retrieving, 381-382
viewing change log,
182, 186
Visible property, 242

W
Web sites
Borland, 378
Code Central, 264, 380
Dan Miser, 333
Developer Express, 272
Object-Insight, 272
Raize Software, 47
Skyline Tools Imaging,
165
TurboPower Software,
211, 271, 299
Woll2Woll, 271
WebServices tab, 323
Width property, 242
Windows applications,
redistributing,
376-377
Windows drivers, 376
Woll2Woll Web site,
271
writing event
handlers, 50

X-Z
xilCUSTOM value, 40
xilDIRTYREAD value, 39
xilREADCOMMITTED
value, 39
xilREPEATABLEREAD
value, 40
Year function, 129
zero (0), 207

Das könnte Ihnen auch gefallen