427

Delphi Kylix Database Development

Embed Size (px)

Citation preview

Page 1: Delphi Kylix Database Development
Page 2: Delphi Kylix Database Development

Eric Harmon

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

Delphi/Kylix DatabaseDevelopment

Page 3: Delphi Kylix Database Development

Delphi/Kylix Database DevelopmentCopyright © 2002 by Sams Publishing

All rights reserved. No part of this book shall be reproduced, stored in aretrieval system, or transmitted by any means, electronic, mechanical, photo-copying, recording, or otherwise, without written permission from the pub-lisher. No patent liability is assumed with respect to the use of the informationcontained herein. Although every precaution has been taken in the preparationof this book, the publisher and author assume no responsibility for errors oromissions. Nor is any liability assumed for damages resulting from the use ofthe information contained herein.

International Standard Book Number: 067232265x

Library of Congress Catalog Card Number: 2001093571

Printed in the United States of America

First Printing: November 2001

03 02 01 00 4 3 2 1

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

Warning and DisclaimerEvery effort has been made to make this book as complete and as accurate aspossible, but no warranty or fitness is implied. The information provided is onan “as is” basis. The author and the publisher shall have neither liability norresponsibility to any person or entity with respect to any loss or damages aris-ing from the information contained in this book or from the use of the pro-grams accompanying it.

ASSOCIATE PUBLISHER

Linda Engelman

ACQUISITIONS EDITOR

Karen Wachs

DEVELOPMENT EDITOR

Laurie McGuire

MANAGING EDITOR

Charlotte Clapp

PROJECT EDITOR

Heather McNeill

COPY EDITOR

Katie Robinson

INDEXER

Mary SeRine

PROOFREADER

Bob LaRochePlan-it-Publishing

TECHNICAL EDITORS

Dan Miser

Ramesh Theivendran

Philippe Bruno

TEAM COORDINATOR

Lynne Williams

MEDIA DEVELOPER

Dan Scherf

INTERIOR DESIGNER

Gary Adair

COVER DESIGNER

Gary Adair

PAGE LAYOUT

Ayanna Lacey

Page 4: Delphi Kylix Database Development

Contents at a GlanceIntroduction 1

1 Establishing and Using Database Connections 7

2 dbExpress Datasets 57

3 Client Dataset Basics 93

4 Advanced Client Dataset Operations 147

5 Data-Aware Components 201

6 Data-Aware Grids 239

7 Dataset Providers 273

8 DataSnap 317

9 The ConMan Application 347

Appendixes

A Redistributing dbExpress Applications 375

B dbExpress Plus 379

Index 385

Page 5: Delphi Kylix Database Development

ContentsIntroduction 1

Who This Book Is For ............................................................................1How This Book Is Organized ..................................................................1

VCL or CLX? ....................................................................................2Components Developed in This Book................................................3Sample Applications ..........................................................................3dbExpress............................................................................................5Databases Used in This Book ............................................................6

Conventions Used in This Book ..............................................................6Contacting the Author..............................................................................6

1 Establishing and Using Database Connections 7Connecting to and Disconnecting from a Database ................................8

Establishing the Connection ..............................................................9Disconnecting from the Database ....................................................13Connect and Disconnect Events ......................................................14

Retrieving Database Metadata ..............................................................18GetTableNames ..................................................................................18GetFieldNames ..................................................................................18GetIndexNames ..................................................................................19GetProcedureNames ..........................................................................19GetProcedureParams ........................................................................19

Executing DDL and DML Statements ..................................................27DDL Commands ..............................................................................27DML Commands ..............................................................................29

Transaction Support ..............................................................................37Checking for Transaction Support....................................................38Starting a Transaction ......................................................................39Committing a Transaction ................................................................40Rolling Back a Transaction ..............................................................40Multiple Transactions ......................................................................40

Providing Feedback During SQL Operations........................................46Changing the Cursor While Executing SQL Statements ................47Creating a Callback Event to Monitor SQL Commands ................47TSQLMonitor......................................................................................49Using Multiple Feedback Mechanisms ............................................50

Summary ................................................................................................55

Page 6: Delphi Kylix Database Development

2 dbExpress Datasets 57What Are dbExpress Datasets?..............................................................58

dbExpress Datasets Are Unidirectional............................................58dbExpress Datasets Are Read-Only ................................................59dbExpress Datasets Are Lightweight ..............................................59

Types of Datasets ..................................................................................59Tables................................................................................................59Queries..............................................................................................60Stored Procedures ............................................................................60General-Purpose Datasets ................................................................60

Data Manipulation ................................................................................63Opening a Dataset ............................................................................63Closing a Dataset..............................................................................64Retrieving Field Contents from a Dataset ........................................64Navigating a Dataset ........................................................................65

BLOB Support ......................................................................................69Parameterized Queries ..........................................................................71Ordering Data Returned from the Server ..............................................73

Ordering Data from a Table..............................................................73Ordering Data from a Query ............................................................74

Master/Detail Relationships ..................................................................74Retrieving Schema Information ............................................................79Summary ................................................................................................91

3 Client Dataset Basics 93What Is a Client Dataset? ......................................................................94Advantages and Disadvantages of Client Datasets................................94Creating Client Datasets ........................................................................95

Creating a Client Dataset at Design-Time ......................................96Creating a Client Dataset at Runtime ............................................101Accessing Fields ............................................................................103

Populating and Manipulating Client Datasets ....................................105Populating Manually ......................................................................105Populating from Another Dataset ..................................................106Populating from a File or Stream: Persisting Client Datasets ......106Example: Creating, Populating, and Manipulating a

Client Dataset ..............................................................................108Navigating Client Datasets ..................................................................113

Sequential Navigation ....................................................................113Random-Access Navigation ..........................................................114

Client Dataset Indexes ........................................................................118Creating Indexes ............................................................................119Using Indexes ................................................................................121Retrieving Index Information ........................................................122

Page 7: Delphi Kylix Database Development

DELPHI/KYLIX DATABASE DEVELOPMENTvi

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

Searching..............................................................................................136Nonindexed Search Techniques......................................................136Indexed Search Techniques ............................................................138

Summary ..............................................................................................145

4 Advanced Client Dataset Operations 147Dataset Events......................................................................................148Disabling Data-Aware Components ....................................................158BLOBs ................................................................................................162

Notes ..............................................................................................162Images ............................................................................................162Streamed Data ................................................................................165Streamed Components....................................................................167File BLOBs ....................................................................................168Limitations of BLOB Fields ..........................................................168

Nested Datasets....................................................................................172Undo Support ......................................................................................176

Cancel ............................................................................................177The Change Log ............................................................................177Viewing the Change Log................................................................182

Cloning Data from Another Client Dataset ........................................186Maintained Aggregates ........................................................................192

Creating a Maintained Aggregate at Design Time ........................193Creating a Maintained Aggregate at Runtime................................195Aggregate Expressions ..................................................................195Aggregates Across a Group of Records ........................................196Enabling and Disabling Aggregates ..............................................197GetGroupState ................................................................................197

Miscellaneous Properties ....................................................................197Constraints ......................................................................................197DisableStringTrim ........................................................................198ReadOnly ........................................................................................199

Summary ..............................................................................................199

5 Data-Aware Components 201What Are Data-Aware Components? ..................................................202TDataSource ........................................................................................204Common Data-Aware Component Characteristics..............................205

Modifying Component Data from Code ........................................205Controlling When the User Is Allowed to Edit Data ....................206Formatting and Editing Field Values..............................................206

Page 8: Delphi Kylix Database Development

CONTENTSvii

Simple Data-Aware Components ........................................................211TDBText ..........................................................................................211TDBEdit ..........................................................................................212TDBMemo ..........................................................................................212TDBCheckBox....................................................................................212TDBRadioGroup ................................................................................213TDBComboBox....................................................................................213TDBListBox ....................................................................................218TDBImage ........................................................................................221

VCL-Only Data-Aware Controls ........................................................222Lookup Data-Aware Controls ..............................................................222TDBNavigator ......................................................................................223Creating Your Own Data-Aware Components ....................................225

TFieldDataLink ..............................................................................225Setting Up the TFieldDataLink......................................................226Setting Up a Connection to the Data Source ................................227Responding to Changes in the Dataset ..........................................227Updating the Dataset ......................................................................227Message Handlers ..........................................................................228Action Handlers..............................................................................228Data-Aware TDateTimePicker ........................................................228

Sample Application..............................................................................232Summary ..............................................................................................236

6 Data-Aware Grids 239TDBGrid ................................................................................................240

TDBGrid Basic Operation ................................................................240Customizing Columns ....................................................................241Grid Options ..................................................................................244Events ............................................................................................245Custom Drawing ............................................................................252Solutions to Common Grid Questions ..........................................257Limitations......................................................................................263

TClientDataSetGrid............................................................................263Automatic Sorting ..........................................................................264Column Customization ..................................................................265

TDBCtrlGrid ........................................................................................266Properties ........................................................................................267Events ............................................................................................267

Third-Party Data-Aware Grids ............................................................271Summary ..............................................................................................272

Page 9: Delphi Kylix Database Development

DELPHI/KYLIX DATABASE DEVELOPMENTviii

7 Dataset Providers 273What Is a Dataset Provider? ................................................................274Connecting to a Dataset ......................................................................275Resolving Changes to Data..................................................................276

Applying Updates ..........................................................................276Resolving to a Dataset....................................................................278Reconciliation Errors......................................................................278Resolving Changes to BLOB Fields ..............................................290Refreshing Data from the Server....................................................290Update Modes ................................................................................291

Provider Options ..................................................................................293Provider Events....................................................................................295Changing Field Values on the Server ..................................................297Intercepting Data..................................................................................298Optional Parameters ............................................................................300Master/Detail Relationships ................................................................301Providing and Resolving Data from Stored Procedures and Joins ....302

Providing and Resolving Data from a Stored Procedure ..............302Providing and Resolving Data from a Join ....................................302

Connecting to a Local Database ..........................................................308Using Providers Located on a Different Form ..............................308One-Stop Shopping: TSQLClientDataSet ......................................309Limiting the Amount of Data Returned by the Server ..................309

Summary ..............................................................................................315

8 DataSnap 317What Is DataSnap? ..............................................................................318Creating the Application Server ..........................................................318

Remote Data Modules ....................................................................318Creating the Application Server’s User Interface ..........................326Preparing the Application Server for Testing ................................328

Creating the Client Application ..........................................................329Connecting to a Local Database Connection ................................329Connecting to a Remote Database Connection..............................330

A Complete Example ..........................................................................336The Briefcase Model............................................................................340Stateless Servers ..................................................................................341Sharing a Connection Between Multiple Client DataSets ..................343Brokering Connections Between Multiple Servers ............................344Summary ..............................................................................................345

Page 10: Delphi Kylix Database Development

CONTENTSix

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

Appendixes

A Redistributing dbExpress Applications 375Redistributable Files ............................................................................376

Redistributing a Windows Application ..........................................376Redistributing a Linux Application ................................................377

Licensing Issues ..................................................................................378CD-ROM-Based Applications ............................................................378

B dbExpress Plus 379What Is dbExpress Plus? ....................................................................380

Scripting..........................................................................................380Enhanced Metadata ........................................................................381Data Pumping ................................................................................383

For More Information ..........................................................................384

Index 385

Page 11: Delphi Kylix Database Development

About the AuthorEric Harmon is Director of Software Development at Advanced Estimating Systems, Inc.,located in Delray Beach, Florida. Advanced Estimating Systems is the developer of TheEDGE, the industry standard in construction-estimating software. Eric is also a member ofTPX (TurboPower experts), a volunteer group of programmers that assists the TurboPowerSoftware company in providing support for its newsgroups. TurboPower is one of the premierproviders of tools coded in Delphi for Delphi programmers. Eric was recruited by TurboPoweras 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 [email protected].

Dan Miser is a Research and Development Project Manager for the DSP group at Borland,where he spends most of his time researching emerging technologies. Dan also worked on theDelphi R&D team where his responsibilities included DataSnap development. Dan’s majorfocus is finding ways to allow information to be shared across boundaries, and this hasallowed 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 promotingDelphi by contributing to the “Delphi x Developer’s Guide,” acting as a technical editor, writ-ing magazine articles, participating on the Borland newsgroups as a member of TeamB, andspeaking at BorCon on topics such as COM and MIDAS.

Ramesh Theivendran has been a member of the SQL Links research and development teamsince October 1995. Prior to joining Borland, Ramesh was employed as a Programmer at theIndian 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 andserves as an architect for dbExpress. Ramesh lives in Santa Cruz, California with his wife,Aruna, and their little one, Vineha.

Philippe Bruno is the Director of Research and Development at Scanpak Inc., a firm head-quartered in Montreal, Quebec, specializing in radio frequency identification (RFID) systems.Scanpak is the creator of GETS (Galley Equipment Tracking System), an asset tracking sys-tem specifically targeted to the airline industry. He is also a part-time teacher for computer-related courses in various universities and colleges in the Montreal area. Philippe hasprogrammed in several computer languages since 1987, but Pascal and Delphi have alwaysbeen his favorites. He is also a member of TPX (TurboPower experts), where he volunteers hisexpertise in serial communications, networks, and protocols to the service of fellow program-mers in the TurboPower newsgroups.

Page 12: Delphi Kylix Database Development

DedicationFor my wife, Tina.

AcknowledgmentsWriting a book isn’t a one-man (or woman) operation, and I would like to thank the peoplewho 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. Shepatiently led me through the process of writing my first book and was back to assist on thisone, also. She’s a pleasure to work with. Thanks, Karen! I also want to thank Katie Robinsonand 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 andhelped to make sure that things ran smoothly; and to Laurie McGuire, who suggested ways toimprove the flow of the text and otherwise ensured that the overall quality of the book was upto par.

I’d like to say a special thanks to my technical reviewers, Dan Miser and Ramesh Theivendran,both Borland employees, who provided large quantities of extremely helpful feedback andpointed 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 valuableand timely technical review of the final chapter and appendixes. I couldn’t have asked for bet-ter tech reviewers.

With all these people assisting me, I have made every attempt to fix all errors, both technicaland typographical, that may have originally appeared in the manuscript. Writing a book is avery complex process, and inevitably, some errors will have survived. Any errors that remainare, of course, my own fault.

My apologies to anyone who I may have inadvertently omitted. A number of people worked onthis book that I never had direct contact with, so I don’t know them individually. Thanks to allthose whose names I didn’t specifically mention.

Page 13: Delphi Kylix Database Development

Tell Us What You Think!As the reader of this book, you are our most important critic and commentator. We value youropinion and want to know what we’re doing right, what we could do better, what areas you’dlike to see us publish in, and any other words of wisdom you’re willing to pass our way.

As an associate publisher for Sams, I welcome your comments. You can e-mail or writeme directly to let me know what you did or didn’t like about this book—as well as what wecan 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 everymessage.

When you write, please be sure to include this book’s title and author as well as your nameand phone or fax number. I will carefully review your comments and share them with theauthor and editors who worked on the book.

E-mail: [email protected]

Mail: Sams Publishing800 East 96th Street StreetIndianapolis, IN 46240 USA

Page 14: Delphi Kylix Database Development

IntroductionThis 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 havemade no effort to test it.

Who This Book Is ForThis book targets Delphi 6 and Kylix database programmers. I assume that you already havean understanding of the Object Pascal language and that you know how to create a Delphi orKylix application, drop components on a form, create and connect event handlers, and performthe various and sundry tasks required to produce a working application.

I further assume that you have some basic knowledge of databases and their terminology. Forthat reason, I won’t explain what table, view, column, and other database-related terms mean inthis book.

This book also uses some of the standard components in its sample applications. Apart fromdata-aware components, I don’t explain how to use the standard components used in thesesamples, such as action lists, buttons, list boxes, and the like. If you need additional informationon those components, please refer to the Delphi or Kylix help or to a general-purpose,third-party Delphi book.

How This Book Is OrganizedIf 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 aboutdbExpress (for example), you can jump directly to the appropriate chapter(s) and read those.Whether you read sequentially or not, the following is a quick overview of what you’ll find ineach of the chapters.

• Chapter 1, “Establishing and Using Database Connections,” introduces dbExpress, thenew 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 discussesthe dataset components specific to dbExpress.

• Chapter 3, “Client Dataset Basics,” introduces client datasets and the TClientDataSetcomponent, which provides for high-speed, in-memory datasets.

• Chapter 4, “Advanced Client Dataset Operations,” continues with the discussion of clientdatasets and goes into detail about a number of more advanced client dataset operations.

Page 15: Delphi Kylix Database Development

DELPHI/KYLIX DATABASE DEVELOPMENT

• Chapter 5, “Data-Aware Components,” introduces data-aware components, which providea bridge between the data and user interface of an application, automatically displayinginformation from a dataset and allowing the user to enter new data.

• Chapter 6, “Data-Aware Grids,” continues with the data-aware component discussion toshow you how to display and edit data in a grid format.

• Chapter 7, “Dataset Providers,” provides the foundation for multitier database developmentby 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 illustratemultitier database development techniques in a real-world (albeit simplistic) application.

VCL or CLX?Because the technology discussed in this book applies equally well to Delphi 6 and Kylix(with the exception of Chapter 8, “DataSnap”), all the code listings in this book are CLX list-ings. The downloadable source code is provided in both CLX and VCL form, so if you don’twrite cross-platform applications, you may want to experiment with the VCL code instead.

In case you aren’t familiar with these terms, VCL stands for Visual Component Library; it isthe original, Windows-specific class library supported by Delphi. CLX stands for ComponentLibrary Cross-Platform (the X stands for Cross-Platform) and is the new, cross-platform classlibrary 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 asTStringList, TObjectList, and so on.

• DataCLX This wraps the CLX database functionality, such as dbExpress and data-aware components.

• VisualCLX This includes visual components such as menu bars, toolbars, buttons, listboxes, and so on.

• NetCLX This includes Internet-related components.

The only part of CLX specifically discussed in this book is DataCLX (although bits and piecesof BaseCLX and VisualCLX are used to create the sample CLX applications).

You’ll see from the code listings that apart from the uses clause at the top of each unit, there isalmost no difference between the VCL code and CLX code, so you shouldn’t have any troublefollowing along with the CLX code.

2

Page 16: Delphi Kylix Database Development

INTRODUCTION

To avoid clumsy constructs such as “Delphi 6 or Kylix” or “Delphi/Kylix” throughout thebook, I use the generic term “Delphi,” which will serve to mean either Delphi 6 or Kylix. Inthe few cases where a statement applies only to Delphi 6 (VCL), I specifically point that out.

Components Developed in This BookAlthough this isn’t a book about component development, I have included four VCL-specificdescendents of data-aware components that I think you will find useful. These components arediscussed in Chapters 5 and 6. The source code for this book includes the Delphi packageETH, which includes the following components:

• TETHDBComboBox A descendent of the data-aware component TDBComboBox that allowsyou to select an item from a combo box and store its index in an integer field.

• TETHDBListBox A descendent of the data-aware component TDBListBox that allowsyou to select an item from a list box and store its index in an integer field.

• TETHDBDateTimePicker A data-aware version of the Win32 componentTDateTimePicker.

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

Sample ApplicationsEach chapter in this book includes a number of sample applications to help you understand theconcepts being discussed. The samples were all compiled and tested using Delphi 6—both theVCL 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 Booksand 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 inthis 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 TSQLConnectioncomponent.

• Trans Illustrates how transaction support works in dbExpress.

• Feedback Shows how to provide feedback about what’s happening in a dbExpress connection.

3

Page 17: Delphi Kylix Database Development

DELPHI/KYLIX DATABASE DEVELOPMENT

Chapter 2• Basic Illustrates basic TSQLDataSet operation.

• Advanced Shows more advanced TSQLDataSet methods and operations.

• Schema Shows how to retrieve more advanced metadata information from a dbExpressconnection using TSQLDataSet.

Chapter 3• CDS Shows the basics of client dataset support.

• Navigate Shows how to navigate through a TClientDataSet.

• CDSIndex Illustrates how to create and use indexes on a TClientDataSet.

• RangeFilter Shows how to limit the amount of data in a TClientDataSet by applyingranges 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 speeddataset 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 aTClientDataSet.

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

Chapter 6• Options Shows how the various options for a TDBGrid work.

• CustomDraw Illustrates the correct way to override the TDBGrid’s default drawing toprovide visually exciting grids.

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

Chapter 7• Updates Shows the basic operation of dataset providers.

• Joins Shows how to correctly resolve data that was retrieved through an SQL JOIN.

• DataFetch Illustrates how to limit the amount of BLOB and detail data returned from adataset to speed application performance.

4

Page 18: Delphi Kylix Database Development

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

• Stateless Shows how to create a stateless application server for use with MTS orCOM+.

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 CLXsubdirectories under it. In turn, the VCL and CLX subdirectories have a separate subdirectoryfor each sample application.

In addition to a subdirectory for each chapter, there is a separate subdirectory namedComponents, 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 runfine 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 mightwant to modify them to provide a complete path to the data, such as D:\Data\conman.gdb.

dbExpressdbExpress is Borland’s newest database-access technology, supported both by Delphi andKylix. 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 needa 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 thatDelphi/Kylix support). If Borland ever decides to support another platform, such as Mac,BE, or what have you, dbExpress will be there also.

• Low overhead dbExpress is a thin layer over the underlying database engine’s API. Forthis reason, it adds very little overhead to database operations.

• High-performance Largely because of its low overhead, dbExpress is extremely high-performance. It is designed to work in conjunction with Delphi’s client dataset technology.

5

Page 19: Delphi Kylix Database Development

DELPHI/KYLIX DATABASE DEVELOPMENT

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

Databases Used in This BookdbExpress 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 thisbook. I chose InterBase, for four reasons:

• It’s free. Anyone can download a free copy of InterBase from Borland’s Web site andwork with any of the examples in this book.

• It’s universally accessible. InterBase ships on the Delphi and Kylix CDs, so if you have acopy of either Delphi 6 or Kylix, you should already have a copy of InterBase.

• It’s manageable. I can easily provide a small InterBase database for download. You cancopy the database onto your local machine and be off and running. I don’t need to worryabout how I’m going to redistribute a 30MB Oracle database to my readers.

• It’s the only one of the four database engines that I have.

Conventions Used in This BookSeveral typographic conventions are used through Delphi/Kylix Database Development. Thesehave 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 asTClientDataSet. Filenames are written with lowercase letters.

Contacting the AuthorIf you would like to contact me regarding any questions, comments, praise, or criticism youmight have concerning this book, please feel free to email me [email protected]. I will do my best to respond to you as quickly as possible.Please understand, though, that I receive a large amount of e-mail on a daily basis, so it cansometimes take a little while.

6

Page 20: Delphi Kylix Database Development

CHAPTER

1Establishing and UsingDatabase Connections

IN THIS CHAPTER• Connecting to and Disconnecting from a

Database 8

• Retrieving Database Metadata 18

• Executing DDL and DML Statements 27

• Transaction Support 37

• Providing Feedback During SQL Operations 46

Page 21: Delphi Kylix Database Development

Chapter 18

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

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

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

NOTE

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 connectionparameters 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 toa database, or set to False to disconnect.

ConnectionName Used to establish a named connection. Setting ConnectionNameautomatically sets DriverName, GetDriverFunc, LibraryName,Params, and VendorLib. See the section titled “NamedConnections” for more information on setting up a named con-nection.

DriverName Indicates the type of database you are connecting to, such asInterBase, Oracle, and so on.

GetDriverFunc Specifies the name of the function exported by the dbExpress dri-ver that provides access to the driver.

KeepConnection When True, keeps the connection to the database, even whenthere are no active datasets for the connection. When False,drops the connection as soon as all active datasets are closed.

LibraryName Specifies the name of the dbExpress database driver, such as dbexpint.dll.

Page 22: Delphi Kylix Database Development

LoadParamsOnConnect When True, dbExpress loads the DriverName, GetDriverFunc,LibraryName, Params, and VendorLib from thedbxconnections.ini configuration file at runtime. When False, allproperties must be set at design time.

LoginPrompt When True, dbExpress prompts the user for username and password information when connecting to the database. WhenFalse, the application must supply username and passwordinformation directly, through the Params property.

Params Enables you to set database-specific parameters at design time orat runtime. See the section “Setting Database Parameters” formore information.

TableScope Specifies the types of information returned by GetTableNames.This property is discussed in more detail in the section“Retrieving Database Metadata.”

VendorLib Refers to the database vendor client library used for connectingto the database server. For example, InterBase supplies thegds32.dll client-side library.

Establishing the ConnectionTo connect to a database, set the appropriate connection properties and issue one of the followingtwo 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 toset 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 unnamedconnection. Both connection types are discussed in the following two sections.

Named ConnectionsNamed 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 Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

9

TABLE 1.1 Continued

Property Description

Page 23: Delphi Kylix Database Development

[Accounting]BlobSize=-1CommitRetain=FalseDatabase=D:\InterbaseData\Accounting.gdbDriverName=InterbaseLocaleCode=0Password=masterkeyRoleName=AdminServerCharSet=ASCIISQLDialect=1Interbase TransIsolation=ReadCommitedUser_Name=sysdbaWaitOnLocks=True

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

Chapter 110

FIGURE 1.1The 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 aConnection Name list box. By default, the list box shows all existing named connections onyour 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 clickthe Add Connection button (which looks like a plus sign) on the toolbar to create a new namedconnection.

Page 24: Delphi Kylix Database Development

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

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

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

11

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

CAUTION

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 thedbExpress Connection Editor that assists your end users in creating named connections.

Unnamed ConnectionsNamed connections are useful in applications that support a number of different databaseservers. 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 InterBasedatabase, such as a parts listing or customer list. In these cases, your end users have no need tocreate 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 ParametersTSQLConnection’s Params property contains settings specific to the database server you areconnecting to. Several properties, listed in Table 1.2, are common to all databases. You shouldrefer to your database server documentation for information on other database-specific properties.

Page 25: Delphi Kylix Database Development

TABLE 1.2 Common Database Parameters

Property Description

Database Specifies the database to connect to. For an InterBase database, thisrefers to the actual filename, such as \\SERVER\D:\RemoteData.gdb.For an Oracle database, this refers to the entry in TNSNames.ora thatuniquely identifies the database.

Password Password corresponds to User_Name. You usually do not want to setthis at design time, as it bypasses database security.

User_Name The username employed while establishing the connection. You typically want the user to supply this information at runtime.

You can also access the Params property at runtime to set database parameters, like this:

SQLConnection1.Params.Values[‘Database’] := ‘D:\Interbase\LocalDatabase.gdb’;

For an InterBase connection, you can specify the database in one of three ways: as a local path, as a UNC path, or as a TCP path. The following are examples of these three constructs,respectively:

D:\Interbase\LocalDatabase.gdb\\SERVER\D:\Data\RemoteDatabase.gdb192.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 databasebecause InterBase has difficulties connecting to a database using a local path under certainconditions (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 LoginAs indicated previously, the connection’s LoginPrompt property determines whetherVCL/DataCLX automatically prompts the user for a username and password at runtime. If youset LoginPrompt to True, the default connection dialog is displayed at runtime, as shown inFigure 1.2.

You can override the default login dialog and provide your own means of retrieving the username and password by handling the connection’s OnLogin event, and setting the appropriateparameters there. The following code snippet shows how:

Chapter 112

Page 26: Delphi Kylix Database Development

FIGURE 1.2Delphi’s default Database Login dialog.

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

Manually Disconnecting from a DatabaseManually disconnecting from a database is straightforward. Simply issue one of the followingtwo 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 DatabaseIf you want to enable dbExpress to automatically disconnect from the database when there areno open datasets, you should set TSQLConnection.KeepConnection := False. VCL/DataCLXmonitors the number of open datasets on the connection, and when the last dataset is closed,the connection is automatically dropped.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

13

procedure TForm1.SQLConnection1Login(Database: TSQLConnection;LoginParams: TStrings);

varTheUserName: string;ThePassword: string;

begin// Display a custom dialog to retrieve TheUserName and ThePassword

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

end;

Page 27: Delphi Kylix Database Development

Connect and Disconnect EventsTSQLConnection surfaces a handful of events that fire at opportune times during theconnect/disconnect process. The events and their usage are listed in Table 1.3.

TABLE 1.3 TSQLConnection Events

Event Description

AfterConnect Fires after the database connection has been successfully established.

AfterDisconnect Fires after the database connection has been dropped.

BeforeConnect Fires immediately before the connection to the database isattempted. You can raise an exception in this event handler to preventthe connection from being established.

BeforeDisconnect Fires immediately before the database connection is dropped. Youcan raise an exception in this event handler to prevent the connectionfrom being dropped.

OnLogin Fires before the connection is made so that you can provide a username and password at runtime.

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

LISTING 1.1 Events—MainForm.pas

unit MainForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DBXpress,ExtCtrls, DB, SqlExpr, QStdCtrls, QExtCtrls;

Chapter 114

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

CAUTION

Page 28: Delphi Kylix Database Development

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.connAfterConnect(Sender: TObject);beginlbEvents.Items.Add(‘AfterConnect’);

end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);beginlbEvents.Items.Add(‘AfterDisconnect’);

end;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

15

LISTING 1.1 Continued

Page 29: Delphi Kylix Database Development

procedure TfrmMain.connBeforeConnect(Sender: TObject);beginlbEvents.Items.Add(‘BeforeConnect’);if not cbAllowConnect.Checked thenAbort;

end;

procedure TfrmMain.connBeforeDisconnect(Sender: TObject);beginlbEvents.Items.Add(‘BeforeDisconnect’);if not cbAllowDisconnect.Checked thenAbort;

end;

procedure TfrmMain.connLogin(Database: TSQLConnection;LoginParams: TStrings);

beginlbEvents.Items.Add(‘OnLogin’);

end;

procedure TfrmMain.btnConnectClick(Sender: TObject);beginlbEvents.Items.Add(‘---Begin Open---’);tryconn.Open;

excepton EAbort dolbEvents.Items.Add(‘Connect aborted’);

on E: Exception dolbEvents.Items.Add(E.Message);

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

end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginlbEvents.Items.Add(‘---Begin Close---’);tryconn.Close;

excepton EAbort dolbEvents.Items.Add(‘Disconnect aborted’);

on E: Exception do

Chapter 116

LISTING 1.1 Continued

Page 30: Delphi Kylix Database Development

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

end;

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

end;

end.

Notice in the code that the BeforeConnect and BeforeDisconnect event handlers call Abort ifthe appropriate check box is not selected. btnConnectClick and btnDisconnectClick checkfor an EAbort (or other exception), and display an appropriate message in the list box if theconnect or disconnect attempt fails for any reason.

Figure 1.3 shows what the application looks like at runtime. The list box is filled with informativetext that illustrates exactly when, and in what order, the connection events fire.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

17

LISTING 1.1 Continued

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

Page 31: Delphi Kylix Database Development

Retrieving Database MetadataTSQLConnection surfaces a handful of properties that enable you to retrieve basic schemainformation from the database, including table, field, index, stored procedure, and stored procedure parameter attributes. The following sections describe how to obtain each of thesetypes of information from the database connection.

GetTableNamesYou can call GetTableNames to retrieve a list of the tables in the database, including usertables, 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 existingstrings in the list are cleared. The second parameter indicates whether to return only systemtables.

If the SystemTables parameter is set to True, only system tables are added to the list, regardlessof the current setting for the TableScope property (shown in Table 1.4). If SystemTables isFalse, TableScope controls the types of tables that are added to the list.

TABLE 1.4 TableScope Settings

Property Description

TsSynonym Synonyms

TsSysTable System tables

TsTable Normal, user-defined tables

TsView Views

The following code snippet shows a typical call to GetTableNames:

GetTableNames(ListBox1.Items, False);

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

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

Chapter 118

Page 32: Delphi Kylix Database Development

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. Anyexisting strings in the list are deleted.

GetFieldNames(‘CONTACTS’, ListBox1.Items);

GetIndexNamesSimilar to GetFieldNames, GetIndexNames is used to retrieve the names of all indexes definedon 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 stringsin the list are deleted.

GetIndexNames(‘CONTACTS’, ListBox1.Items);

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

GetProcedureParamsGetProcedureParams 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 wantto retrieve parameter names for. The second parameter refers to a precreated TList in whichthe procedure parameters are returned. Upon return from the procedure, List contains a list ofparameters for the stored procedure.

List itself is not directly usable. You should call the helper function, LoadParamListItems, toconvert the TList to a TParams object, which is a much easier structure to inspect.

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

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

19

Page 33: Delphi Kylix Database Development

varlistParams: TList;Params: TParams;

beginlistParams := TList.Create;trySQLConnection1.GetProcedureParams(‘CONTACTSBYSTATE’, listParams);Params := TParams.Create;tryLoadParamListItems(Params, listParams);

// Do something with Params here...finallyParams.Free;

end;finallyFreeProcParams(listParams);

end;

Note the call to FreeProcParams at the end of this code snippet. FreeProcParams frees andnils the listParams TList, so you don’t need to explicitly free the list.

Listing 1.2 shows the complete source code for a sample application that uses TSQLConnectionto retrieve table, field, index, procedure, and parameter names from an InterBase database.

LISTING 1.2 MetaData—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = class(TForm)conn: TSQLConnection;pnlBottom: TPanel;lblConnection: TLabel;pnlClient: TPanel;grpTableScope: TGroupBox;btnTable: TCheckBox;btnView: TCheckBox;btnSynonym: TCheckBox;btnSystemTable: TCheckBox;

Chapter 120

Page 34: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);

procedure CheckScope(Value: Boolean; TableScope: TTableScope);beginif Value thenconn.TableScope := conn.TableScope + [TableScope]

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

21

LISTING 1.2 Continued

Page 35: Delphi Kylix Database Development

elseconn.TableScope := conn.TableScope - [TableScope];

end;

beginif OpenDialog1.Execute then beginconn.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);beginconn.Close;

end;

procedure TfrmMain.cbTableClick(Sender: TObject);beginconn.GetFieldNames(cbTable.Items[cbTable.ItemIndex], lbFields.Items);conn.GetIndexNames(cbTable.Items[cbTable.ItemIndex], lbIndexes.Items);

end;

procedure TfrmMain.cbProcedureClick(Sender: TObject);varlistParams: TList;Params: TParams;Index: Integer;Param: TParam;ListItem: TListItem;

beginlistParams := TList.Create;tryconn.GetProcedureParams(cbProcedure.Items[cbProcedure.ItemIndex],listParams);

Params := TParams.Create;tryLoadParamListItems(Params, listParams);

lvParameters.Items.BeginUpdate;try

Chapter 122

LISTING 1.2 Continued

Page 36: Delphi Kylix Database Development

lvParameters.Items.Clear;

for Index := 0 to Params.Count - 1 do beginParam := Params[Index];ListItem := lvParameters.Items.Add;ListItem.Caption := Param.Name;case Param.DataType offtUnknown: 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’);

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

23

LISTING 1.2 Continued

Page 37: Delphi Kylix Database Development

ftFMTBcd: ListItem.SubItems.Add(‘FMTBcd’);end;

end;finallylvParameters.Items.EndUpdate;

end;finallyParams.Free;

end;finallyFreeProcParams(listParams);

end;end;

procedure TfrmMain.connAfterConnect(Sender: TObject);beginbtnTable.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);beginbtnTable.Enabled := True;btnView.Enabled := True;btnSynonym.Enabled := True;btnSystemTable.Enabled := True;

lblConnection.Font.Color := clRed;

Chapter 124

LISTING 1.2 Continued

Page 38: Delphi Kylix Database Development

lblConnection.Caption := ‘Not connected’;

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

end;

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

end;

end.

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

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

25

LISTING 1.2 Continued

FIGURE 1.4Column 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 astored procedure to determine their name, type, and other attributes.

Page 39: Delphi Kylix Database Development

FIGURE 1.5Parameter 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 thesame 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:

varSL: TStringList;

beginSL := TStringList.Create;trySQLConnection1.TableScope := [tsTable, tsView];SQLConnection1.GetTableNames(ListBox1.Items, False);SQLConnection1.GetTableNames(SL, True);ListBox1.Items.AddStrings(SL);

finallySL.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, itadds 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 isextremely basic. Other than for stored procedure parameters, the only data that

Chapter 126

Page 40: Delphi Kylix Database Development

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

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

DDL CommandsDDL commands are statements that operate on the database schema, rather than on the dataitself. In the previous section, I showed you how to retrieve information about the databaseschema. In this section, I’ll show you how to change the database schema.

TSQLConnection provides a method named ExecuteDirect, which you use to execute DDLcommands. 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 befound 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 sentdirectly 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.)

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

27

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

NOTE

Creating a TableOne of the simplest and most useful DDL commands you can issue is the command to create anew table in the database. Assume that you want to create a table named EMPLOYEE with thestructure 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’);

Page 41: Delphi Kylix Database Development

TABLE 1.5 Sample EMPLOYEE Table Definition

Column Definition

EMPNO INTEGER

NAME VARCHAR(30)

HIREDATE DATE

SALARY DOUBLE PRECISION

Creating a DatabaseYou cannot use ExecuteDirect to create a new InterBase database. The following line of codedoes not work:

SQLConnection1.ExecuteDirect(‘CREATE DATABASE ‘’C:\NewData.gdb’’’);

If you try to execute this code, you receive the following exception:

Cannot prepare a CREATE DATABASE/SCHEMA statement.

The InterBase client doesn’t allow direct execution of DDL or DML, so the dbExpress driverattempts to prepare, and then to execute the CREATE DATABASE statement. Because InterBasedoesn’t allow a CREATE DATABASE statement to be prepared, an exception is raised.

If you want your applications to be capable of creating new InterBase databases, you need tofind another way to do it. One way is to keep an empty copy of the database in your programdirectory and copy it when the user creates a new database.

I have found the following to be useful in my own experience: Save a copy of the empty data-base as a resource in your application. When the user creates a new database, save the resourceto disk under the filename that the user selects. The following explanation shows how this canbe done in a Windows environment. Note that this is Windows specific and can’t be used for aLinux 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, thefollowing 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 thefollowing command:

BRCC32 EMPTYDB.RC

Chapter 128

Page 42: Delphi Kylix Database Development

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

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);varHRsrc: THandle;Stream: TResourceStream;

beginHRsrc := FindResource(HInstance, PChar(‘EMPTYDB’), RT_RCDATA);if HRsrc <> 0 then beginStream := TResourceStream.Create(HInstance, ‘EMPTYDB’, RT_RCDATA);tryStream.SaveToFile(DatabaseName);

finallyStream.Free;

end;end elseraise Exception.Create(‘Internal error: unable to create database.’);

end;

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

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

As you’re working on your application, if you change the database schema, you must remem-ber to reissue the BRCC32 command (shown previously) to re-create the EMPTYDB.RES file.Otherwise, you wind up with an incorrect, empty database inside your application.

Another way that you could handle this situation is to make direct, low-level calls to the data-base API from within your application. This approach has the benefit of being cross-platform,but you must learn the appropriate API commands for the database backend in question.

DML CommandsWhereas DDL commands are used to define the database schema, DML commands are used tomanipulate (read, write, and update) the data in the database.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

29

Page 43: Delphi Kylix Database Development

Simple SQL StatementsThe 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 issuethe 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 toquoting string constants manually is to use the RTL function, QuotedStr. QuotedStr takes astring parameter and returns the string within quotes. One of the main reasons for usingQuotedStr is that it correctly handles string constants that contain quotes. For example, giventhe name O’Toole, would you know how to quote it manually? The correct way is

‘O’’Toole’

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

SQLConnection1.ExecuteDirect(‘DELETE FROM EMPLOYEE WHERE Active = ‘ + QuotedStr(‘N’));

Parameterized SQL StatementsMany times, you want to execute the same basic SQL statement more than once, changingonly the values that are passed to the parameters in the statement. For example, say you wantto insert a number of records into the previously created EMPLOYEE table. To insert anemployee, 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 youcan easily enter the appropriate values for each statement before executing. For the EMPLOYEEtable, 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 easilydetectable because they start with a colon. Internally, the dbExpress components parse the SQLstatement, 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.

Chapter 130

Page 44: Delphi Kylix Database Development

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

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

The dbExpress driver may replace the question marks with some other construct that is specificto the database server.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

31

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

NOTE

After you create the SQL statement, you need to fill in the value of each parameter beforeexecuting. The following code snippet shows how this is done:

Params := TParams.Create(nil);try// Create the parametersParams.CreateParam(ftInteger, ‘EmpNumber’, ptInput);Params.CreateParam(ftString, ‘Name’, ptInput);Params.CreateParam(ftSQLDateTime, ‘Hired’, ptInput);Params.CreateParam(ftFloat, ‘Salary’, ptInput);

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

// Execute the statementSQLConnection1.Execute(‘INSERT INTO EMPLOYEE VALUES (:EmpNumber, :Name, ‘ +‘:Hired, :Salary)’, Params);

finallyParams.Free;

end;

You might be wondering why you would ever want to do this rather than simply executingeach statement directly. After all, creating the parameters and assigning them takes a lot moreeffort. In addition, time tests on my development machine show that the second method takesabout 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:

Page 45: Delphi Kylix Database Development

• Simplicity. Given a long, complicated SQL statement, it’s often difficult to form the SQLstatement manually. Getting the quotes lined up correctly can be prone to errors, consideringthat 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 init, such as Frank O’Donnell. If you don’t use parameters, the quote looks as if it were theterminating quote on the name. The parameterless SQL statement would look like this:

INSERT INTO EMPLOYEE VALUES (123, ‘Frank O’Donnell’, ‘5/15/1994’, 35000)

In this case, the name appears to be Frank O, and the following character (D) in thestatement is in error. You can solve this problem by double quoting the name, like this:

INSERT INTO EMPLOYEE VALUES (123, ‘Frank O’’Donnell’, ‘5/15/1994’, 35000)

• When using parameters, a true relational database can compile the prepared statement,and then use that compiled version for all subsequent calls to the SQL statement. Whenrepeatedly issuing the same SQL statement, this more than compensates for the additionaltime 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 CursorIn 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 themost commonly used DML statement. So, why am I shying away from it?

It’s because SELECT statements return data, and you need to have a place to put the resultingdata. For dbExpress, that place is a TCustomSQLDataSet, which I don’t discuss in detail untilthe next chapter. For now, I’ll just give a quick overview of how to retrieve data from aTSQLConnection.

The Execute method, which I discussed in the previous section, actually takes a third parameter:a pointer to a result set.

function Execute(const SQL: string; Params: TParams;ResultSet: Pointer = nil): Integer;

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

Chapter 132

If you pass a nil value for the ResultSet and the SQL command actually does returna result set, the result set is simply discarded. No exception is raised, and no error isreturned.

NOTE

Page 46: Delphi Kylix Database Development

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

SQLDataSet1 := TSQLDataSet.Create(nil);trySQLConnection1.Execute(‘SELECT * FROM EMPLOYEE’, nil, SQLDataSet1);

// Do something with SQLDataSet1 here...finallySQLDataSet1.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 tableusing the Execute method. Listing 1.4 shows the complete code for DDLSQL.

LISTING 1.4 DDLSQL—MainForm.pas

unit MainForm;

interface

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

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

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

33

Page 47: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);beginconn.Open;

end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginconn.Close;

end;

procedure TfrmMain.btnCreateClick(Sender: TObject);beginconn.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);beginconn.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;

Chapter 134

LISTING 1.4 Continued

Page 48: Delphi Kylix Database Development

procedure TfrmMain.btnDeleteClick(Sender: TObject);beginconn.ExecuteDirect(‘DELETE FROM TESTING WHERE NAME = “Tina”’);

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

procedure TfrmMain.btnParametersClick(Sender: TObject);constSQL = ‘INSERT INTO TESTING VALUES (:Name, :Age)’;

varParams: TParams;

beginParams := TParams.Create(nil);tryParams.CreateParam(ftString, ‘PName’, ptInput);Params.CreateParam(ftInteger, ‘PAge’, ptInput);

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

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

end;end;

procedure TfrmMain.btnDropClick(Sender: TObject);beginconn.ExecuteDirect(‘DROP TABLE TESTING’);

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

procedure TfrmMain.connAfterConnect(Sender: TObject);beginbtnConnect.Enabled := False;btnCreate.Enabled := True;btnPopulate.Enabled := True;btnDelete.Enabled := True;btnParameters.Enabled := True;btnDrop.Enabled := True;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

35

LISTING 1.4 Continued

Page 49: Delphi Kylix Database Development

btnDisconnect.Enabled := True;

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

procedure TfrmMain.connAfterDisconnect(Sender: TObject);beginbtnConnect.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);beginconn.Close;

end;

end.

Figure 1.6 shows the DDLSQL program at runtime.

Chapter 136

LISTING 1.4 Continued

FIGURE 1.6DDLSQL after running the gamut of buttons.

Page 50: Delphi Kylix Database Development

Transaction SupportMost SQL databases, with the notable exception of versions of MySQL prior to 3.23, providesupport for transactions. A transaction must satisfy four criteria, which are called the ACIDproperties of transactions. Specifically, transactions are

• Atomic. The transaction must either succeed or fail as a whole. It is not acceptable forpart 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. Alldata must adhere to current referential integrity constraints.

• Isolated. Changes made in one transaction must not be visible to another transaction untilthe transaction is committed.

• Durable. Once a transaction is committed, its changes must be permanent. Nothing,including a system crash, must alter the effects of the committed transaction.

Transactions are most easily described with an example, so I’ll go through the most often-citedexample 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 tohis 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 withdrawalfrom the savings account, and a second INSERT statement to record the deposit into the checkingaccount. Assuming the ATM software is written using Delphi and dbExpress, it might containsome code similar to the following:

procedure TransferFunds(FromAccountID: Integer; ToAccountID: Integer; Amount: Double);

varSQL: string;Params: TParams;

beginParams := TParams.Create;trySQL := ‘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;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

37

Page 51: Delphi Kylix Database Development

SQLConnection1.Execute(SQL, Params);finallyParams.Free;

end;end;

Conceptually, this code snippet creates a withdrawal for the originating account, and a depositfor the target account. At first, it might seem like there’s nothing wrong with this code, but let’stake a look at the possibilities.

What happens if a power outage, connection failure, or some other catastrophic failure occursbetween the time the withdrawal is recorded and the time the deposit is recorded? The moneywould be deducted from the savings account, but it would never get added to the checkingaccount (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 veryhappy and $100 richer, but the bank is shorted).

Clearly, the ATM software needs to have some assurance that either the withdrawal and thedeposit 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 SupportIf you’re writing an application that only works with a single database backend (such as Oracleor InterBase), you know before you start coding that the database supports transactions. Ifyou’re writing a general-purpose application that can access many different database backends,you don’t know up front whether the selected database backend supports transactions or not.

TSQLConnection provides a way to detect whether the underlying database engine supportstransactions: the TransactionsSupported property. Before you checkTransactionsSupported, you need to establish a connection to the database, like this:

SQLConnection1.Open;...if SQLConnection1.TransactionsSupported then begin// Go ahead with transaction code

end else begin// Transactions not supported - proceed with alternate code or bail out

end;

Chapter 138

Page 52: Delphi Kylix Database Development

Starting a TransactionWhen you’ve determined that the database supports transactions, you can start a transaction.To begin, call the method TSQLConnection.StartTransaction. StartTransaction is definedlike this:

procedure StartTransaction( TransDesc: TTransactionDesc);

TTransactionDesc is a record that describes the transaction in detail. Its definition is shownhere:

TTransactionDesc = packed recordTransactionID : LongWord; { Transaction id }GlobalID : LongWord; { Global transaction id }IsolationLevel : TTransIsolationLevel; {Transaction Isolation level}CustomIsolation : LongWord; { DB specific custom isolation }

end;

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

TABLE 1.6 TTransactionDesc Fields

Field Definition

TransactionID User-defined, local transaction number that uniquely identifies thetransaction for purposes of this application.

GlobalID Used for Oracle transactions to define a transaction number thatmust be unique across the entire Oracle database.

IsolationLevel Used to specify how this transaction reacts to other transactions.Valid values for this field are listed in Table 1.7.

CustomIsolation Identifies the custom isolation level when IsolationLevel is set toxilCUSTOM. No dbExpress drivers currently support this.

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

TABLE 1.7 Valid IsolationLevel Values

Field Definition

xilDIRTYREAD The transaction sees all changes made by other transactions, even ifthose changes have not yet been committed. Oracle does not supportthis level of transaction isolation.

xilREADCOMMITTED The transaction sees only those changes made by other transactionsthat have been committed both before this transaction was started,and after this transaction was started.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

39

Page 53: Delphi Kylix Database Development

xilREPEATABLEREAD The transaction sees only those changes made by other transactions,but only if they were committed before this transaction started.

xilCUSTOM Database-specific isolation level. CustomIsolation specifies theactual isolation level. No dbExpress drivers currently support this.

The following code snippet shows how to start a transaction.

varTransDesc: TTransactionDesc;

beginTransDesc.TransactionID := 1;TransDesc.IsolationLevel := xilREADCOMMITTED;SQLConnection1.StartTransaction(TransDesc);

end;

Committing a TransactionAfter you have issued the appropriate SQL commands inside a transaction, you want to committhe 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 followingline of code illustrates:

SQLConnection.Commit(TransDesc);

Rolling Back a TransactionAt times, you start a transaction only to find out later that you don’t want to save the changesmade during that transaction. If that should occur, you can roll back the transaction rather thancommitting 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 TransactionsYou 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.

Chapter 140

TABLE 1.7 Continued

Field Definition

Page 54: Delphi Kylix Database Development

FIGURE 1.7Transactions may be nested or overlapped.

Not all databases support multiple transactions. Unfortunately, to determine whether a databasesupports multiple transactions, you can’t check a simple property. TSQLConnection contains aprivate 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;varSupported: LongBool;PropSize: SmallInt;

beginconn.MetaData.GetOption(eMetaSupportsTransactions, @Supported, SizeOf(Integer), PropSize);

Result := Supported;end;

If the database does not support multiple transactions, you want to refrain from callingStartTransaction while a transaction is active. You can test to see whether a transaction iscurrently 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 snippetshows:

varTransOuter: TTransactionDesc;TransInner: TTransactionDesc;

beginTransOuter.TransactionID := 1;TransOuter.IsolationLevel := xilREADCOMMITTED;SQLConnection1.StartTransaction(TransOuter);try

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

41

Page 55: Delphi Kylix Database Development

// Execute some SQL statements here

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

SQLConnection1.Commit(TransInner);exceptSQLConnection1.Rollback(TransInner);raise;

end;

// Even more SQL statements here

SQLConnection1.Commit(TransOuter);exceptSQLConnection1.Rollback(TransOuter);raise;

end;end;

Note in the preceding code that the transactions are enclosed in try/except blocks. If anexception occurs while executing the code, the transaction is rolled back.

Listing 1.5 contains the complete source code for an application that demonstrates dbExpresstransaction support (including transaction isolation, committing and rolling back transactions,and nested transactions).

LISTING 1.5 Trans—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = class(TForm)pnlClient: TPanel;conn: TSQLConnection;btnConnect: TButton;btnDisconnect: TButton;

Chapter 142

Page 56: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

function SupportsMultiTrans(conn: TSQLConnection): Boolean;varSupported: LongBool;PropSize: SmallInt;

beginconn.MetaData.GetOption(eMetaSupportsTransactions, @Supported,SizeOf(Integer), PropSize);

Result := Supported;end;

procedure TfrmMain.btnConnectClick(Sender: TObject);beginconn.Open;

if conn.TransactionsSupported thenlbOutput.Items.Add(‘Connection supports transactions’)

else

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

43

LISTING 1.5 Continued

Page 57: Delphi Kylix Database Development

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

if SupportsMultiTrans(conn) thenlbOutput.Items.Add(‘Connection supports multiple transactions’)

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

end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginconn.Close;

end;

procedure TfrmMain.btnCommitClick(Sender: TObject);varTransDesc: TTransactionDesc;

beginTransDesc.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);varTransDesc: TTransactionDesc;

beginTransDesc.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);varTransDesc1: TTransactionDesc;TransDesc2: TTransactionDesc;

beginTransDesc1.TransactionID := 1;TransDesc1.IsolationLevel := xilREADCOMMITTED;

Chapter 144

LISTING 1.5 Continued

Page 58: Delphi Kylix Database Development

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

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

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

end;

procedure TfrmMain.btnOverlappingClick(Sender: TObject);varTransDesc3: TTransactionDesc;TransDesc4: TTransactionDesc;

beginTransDesc3.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);beginbtnConnect.Enabled := False;btnDisconnect.Enabled := True;btnCommit.Enabled := True;btnRollback.Enabled := True;btnMultiLevel.Enabled := True;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

45

LISTING 1.5 Continued

Page 59: Delphi Kylix Database Development

btnOverlapping.Enabled := True;end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);beginbtnConnect.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);beginconn.Close;

end;

end.

Figure 1.8 shows the Trans application at runtime.

Chapter 146

LISTING 1.5 Continued

FIGURE 1.8Trans shows how to perform nested transactions.

Providing Feedback During SQL OperationsThere are several good reasons for providing feedback as SQL operations execute. Considerthe following two:

Page 60: Delphi Kylix Database Development

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

• Especially when using TSQLConnection in conjunction with datasets (discussed inChapter 3), the connection might run some SQL commands on your behalf. It can beuseful for both learning and debugging purposes to intercept all SQL commands sent tothe database.

In either of these situations, it’s helpful to be able to provide feedback or logging facilities tothe 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 StatementsThe simplest form of feedback that you can provide is to change the cursor to an hourglasswhen an SQL statement is executing. To provide this functionality in your application, all youneed to do is set the connection’s SQLHourGlass property to True. SQLHourGlass is not a published property, so you can’t set it at design time.

SQLConnection1.SQLHourGlass := True;

Creating a Callback Event to Monitor SQL CommandsIf you want to intercept every SQL command that passes from TSQLConnection to the database,you can set up what is called a trace callback event.

You set up a trace callback event by calling TSQLConnection.SetTraceCallbackEvent.SetTraceCallbackEvent takes two parameters: the event to call for SQL activity, and a user-defined integer value that is passed to the callback event.

The format of the callback event is defined as follows:

TSQLCallbackEvent = function(CallType: TRACECat; CBInfo: Pointer): CBRType; stdcall;

CallType is always set to cbTRACE on entry to the callback function. CBInfo is a pointer to an SQLTRACEDesc record, which is defined like this:

SQLTRACEDesc = packed record { trace callback info }pszTrace : array [0..1023] of Char;eTraceCat : TRACECat;ClientData : Integer;uTotalMsgLen : Word;

end;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

47

Page 61: Delphi Kylix Database Development

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

eTraceCat The category of the command just sent or received. Table 1.9 lists thepossible values of this field.

ClientData The user-defined value passed as the second parameter toSetTraceCallbackEvent.

uTotalMsgLen The length, in characters, of the string contained in pszTrace.

Table 1.9 describes the possible values for the eTraceCat field.

TABLE 1.9 eTraceCat Values

Value Definition

traceQPREPARE A query was sent to the server to prepare.

traceQEXECUTE A query was sent to the server to execute.

traceERROR An error message was returned by the server.

traceSTMT An operation for the database to perform was sent to the server.

traceCONNECT A connect-or disconnect-related operation was sent to the server.

traceTRANSACT A transaction-related operation was sent to the server.

traceBLOB A BLOB-related operation was sent to the server.

traceVENDOR A vendor-specific API call was sent to the server.

traceDATAIN Parameter data was sent to the server during an INSERT or UPDATEcommand.

traceDATAOUT Data was retrieved from the server.

traceMISC Any other command not falling under one of the previous categories.

Chapter 148

Not all of these options are currently supported. Many of them are there for futureexpansion. Currently, all vendor calls and executed SQL commands are traced.

NOTE

Page 62: Delphi Kylix Database Development

The following code snippet sets a callback event:

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

beginCBI := pSQLTRACEDesc(CBInfo);ShowMessage(CBI.pszTrace);

end;

beginSQLConnection1.SetTraceCallbackEvent(SQLCallback, 1);

end;

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

49

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

CAUTION

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

SQLConnection1.SetTraceCallbackEvent(nil, 0);

TSQLMonitorTSQLMonitor provides a ready-made, easy-to-use mechanism for capturing database events.You can log database messages to a list box, a log file, or another destination as they occur.You can also allow messages to accumulate in the monitor’s internal buffer, and dump them toa file (or other destination) in one fell swoop.

To use a TSQLMonitor, drop it on a form or data module, along with your TSQLConnectioncomponent. To begin logging events, set the monitor’s SQLConnection property to theTSQLConnection component and set the Active property to True.

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

Logging Messages as They OccurThere are actually two different ways that you can log database messages as they occur. Thefirst is through the use of TSQLMonitor’s AutoSave and FileName properties. Set FileName to

Page 63: Delphi Kylix Database Development

the name of the log file that you want to create, such as C:\TRACE.LOG. Set AutoSave toTrue, and the component automatically logs all database messages to the specified filename. Ifthe 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 OnTraceor OnLogTrace event. OnTrace is fired as soon as the component receives an indication that amessage has passed between the application and the database server. The event handler lookslike this:

procedure TForm1.SQLMonitor1Trace(Sender: TObject; CBInfo: pSQLTRACEDesc;var LogTrace: Boolean);

begin

end;

Inside the event handler, you can set LogTrace to False if, for some reason, you don’t want themessage 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 lookslike 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 MessagesRather than dealing with each individual message as it arrives, you can allow them to bebuffered in an internal list by the TSQLMonitor component. You can then save them to a file orto a string list at a later time.

To save the list to a file, call the component’s SaveToFile method, like this:

SQLMonitor1.SaveToFile(‘C:\EventList.LOG’);

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

Using Multiple Feedback MechanismsTSQLConnection is not designed to easily support sending database trace events to multipledestinations at the same time. In other words, you can’t use both a TSQLMonitor and a user-defined feedback event at the same time. So, if you want to assign a callback event, you should

Chapter 150

Page 64: Delphi Kylix Database Development

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

Another thing you should avoid is accidentally overwriting an existing callback handler byassigning a new one. To check for an existing callback event (including the presence of aTSQLMonitor component), you should check the value of the read-onlyTSQLConnection.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 installyour own event handler, and reinstate the existing callback event when you’re finished.

• Save a pointer to the existing callback event, call SetTraceCallbackEvent to install yourown 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 operationseither through a TSQLMonitor component, or through a programmer-defined callback event.

LISTING 1.6 Feedback—MainForm.pas

unit MainForm;

interface

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

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

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

51

Page 65: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

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

beginResult := cbrUSEDEF;if CBInfo <> nil then beginCBI := pSQLTRACEDesc(CBInfo);frmMain.lbTrace.Items.Add(‘Callback: ‘ + CBI.pszTrace);

end;end;

procedure TfrmMain.btnConnectClick(Sender: TObject);beginconn.Open;

cbUseCallbackClick(cbUseCallback);end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginconn.Close;

end;

Chapter 152

LISTING 1.6 Continued

Page 66: Delphi Kylix Database Development

procedure TfrmMain.btnExecSQLClick(Sender: TObject);beginconn.ExecuteDirect(‘SELECT * FROM CONTACTS’);

end;

procedure TfrmMain.btnDumpClick(Sender: TObject);beginlbTrace.Items.Assign(monitor.TraceList);

end;

procedure TfrmMain.monitorTrace(Sender: TObject; CBInfo: pSQLTRACEDesc;var LogTrace: Boolean);

beginif btnLogTrace.Checked then beginlbTrace.Items.Add(CBInfo.pszTrace);

// Since we handled the message ourselves, don’t log it.LogTrace := False;

end;end;

procedure TfrmMain.connAfterConnect(Sender: TObject);beginbtnConnect.Enabled := False;btnDisconnect.Enabled := True;btnExecSQL.Enabled := True;btnDump.Enabled := True;

end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);beginbtnConnect.Enabled := True;btnDisconnect.Enabled := False;btnExecSQL.Enabled := False;btnDump.Enabled := False;

end;

procedure TfrmMain.cbUseCallbackClick(Sender: TObject);beginif cbUseCallback.Checked then beginmonitor.Active := False;conn.SetTraceCallbackEvent(MySQLCallback, 1);

end else beginconn.SetTraceCallbackEvent(nil, 0);

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

53

LISTING 1.6 Continued

Page 67: Delphi Kylix Database Development

monitor.Active := True;end;

end;

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

end;

end.

Let’s take a look at the cbUseCallbackClick event handler. If you check the Use Callbackcheck box, the code disables the monitor. It then calls SetTraceCallbackEvent to set up acallback procedure to monitor database messages.

When you uncheck the Use Callback check box, the code sets the monitor to active. This re-establishes the TSQLMonitor component as the feedback mechanism for database messages.

Another point of interest is the MonitorTrace method. If the Log Trace check box is checked,we capture the message immediately and send it to the list box. Because we’ve handled themessage, there’s no need to buffer it in the monitor’s internal list. For that reason, the code setsLogTrace to False.

Figure 1.9 shows the Feedback application at runtime.

Chapter 154

LISTING 1.6 Continued

FIGURE 1.9Feedback logs messages sent to and from the database server.

Page 68: Delphi Kylix Database Development

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

• You can create either named or unnamed database connections.

• TSQLConnection surfaces a small number of events that are useful in allowing or preventing connections to, and disconnections from, a database.

• You can easily retrieve schema information (also called metadata) from a database connection, including table, field, index, and procedure data.

• TSQLConnections can be used to execute both DDL and DML commands against a database.

• If a database server supports transactions, you can control those transactions through theconnection component.

• Several mechanisms are available for you to report feedback while performing operationsagainst a database.

The next chapter introduces unidirectional datasets, which enable you to retrieve result setsfrom an SQL connection.

Establishing and Using Database Connections

1

ESTA

BLISH

ING

AN

DU

SING

DA

TAB

SEC

ON

NEC

TION

S

55

Page 69: Delphi Kylix Database Development
Page 70: Delphi Kylix Database Development

CHAPTER

2dbExpress Datasets

IN THIS CHAPTER• What Are dbExpress Datasets? 58

• Types of Datasets 59

• Data Manipulation 63

• BLOB Support 69

• Parameterized Queries 71

• Ordering Data Returned from the Server 73

• Master/Detail Relationships 74

• Retrieving Schema Information 79

Page 71: Delphi Kylix Database Development

Chapter 258

The preceding chapter presented an overview of dbExpress connections and theTSQLConnection component. You learned how to connect to and disconnect from a database,how to set connection parameters, and how to retrieve schema information from a database. In this chapter, I’ll introduce dbExpress datasets, which enable you to retrieve data from thedatabase connection.

I’ll make references to a number of dataset methods, such as First, Next, FieldByName, and so on. These methods are actually defined at the TDataSet level, which means that they areapplicable 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-purposeDelphi books—I apply them in Chapter 3, “Client Dataset Basics.” For these reasons, I won’tgo into excruciating detail here. Instead, I will provide short code snippets to show how they’reused in context.

What Are dbExpress Datasets?Datasets are the means by which dbExpress retrieves data from a database. For example, giventhe following SQL SELECT statement:

SELECT * FROM EMPLOYEES

The employee rows are returned in a dataset. You saw in the preceding chapter thatTSQLConnection could directly process SQL statements that did not return a result set. Forqueries that return a result set, TSQLDataSet (and its derivatives) should be used.

Let’s take a moment to discuss the characteristics of dbExpress datasets, and then we’ll moveinto the concrete dbExpress classes that implement them.

dbExpress Datasets Are UnidirectionalThe most important thing to know about dbExpress datasets is that they are unidirectional. Atfirst this might seem like a huge disadvantage, but in the rest of this book, you’ll see how thedbExpress architecture provides for an extremely lightweight, flexible, and powerful means ofaccessing and updating data.

Because dbExpress datasets are unidirectional, the only navigating that you can do is movingfrom the beginning of the dataset to the end—one record at a time. If you’re familiar withother Delphi datasets (such as TTable, TADODataSet, or TIBDataSet), you might be wonderinghow to search for records or move backward through the dataset. Again, the solution to theseissues will become crystal clear in later chapters.

Page 72: Delphi Kylix Database Development

dbExpress Datasets Are Read-OnlydbExpress 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 dbExpressrecord results in a Delphi exception, as shown in Figure 2.1.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS59

FIGURE 2.1dbExpress datasets are read-only.

dbExpress Datasets Are LightweightBecause dbExpress datasets are unidirectional and read-only, they are extremely lightweight.There is no overhead involved for the fundamental (but expensive) tasks of bidirectional cursorsupport, record buffering, and the like.

If you’re familiar with the Borland Database Engine (BDE), you know that the BDE supportsfull editing capabilities, forward and backward navigation of datasets, and drivers for multipledatabase backends. However, the BDE install is approximately 10MB, and the BDE itself ismemory-intensive. Consider that dbExpress (when coupled with client datasets, which I’lldiscuss in future chapters) consists of a mere two library files totaling some 400K. Considerfurther 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 DatasetsAs with other Delphi database technologies (such as ADO and IBX), dbExpress supports threedifferent types of datasets: tables, queries, and stored procedures. These are discussed in thefollowing sections.

TablesA table is a direct view of the underlying database table. It consists of all columns for all rowsin the table. You cannot limit the rows returned from the table, and you cannot select a subsetof columns (or join columns from another table).

Page 73: Delphi Kylix Database Development

QueriesA 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 enablesyou to execute any SQL SELECT statement and return the results.

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

General-Purpose DatasetsThe dbExpress components that provide table-level, query-level, and stored procedure accessare TSQLTable, TSQLQuery, and TSQLStoredProc, respectively. These components are providedsolely for ease of conversion from BDE applications, and correspond to the BDE data-accesscomponents TTable, TQuery, and TStoredProc.

For all new development, it is strongly recommended that you use the general-purpose componentTSQLDataSet. TSQLDataSet allows access to tables, queries, and stored procedures alike, and ismore flexible than any of the other special-purpose components mentioned previously.

TSQLDataSet implements almost all of the dbExpress dataset functionality. TSQLTable,TSQLQuery, and TSQLStoredProc descend from TSQLDataSet and add behavior specific totables, queries, and stored procedures. In turn, TSQLDataSet descends from TDataSet, which isthe root class for all Delphi datasets.

Table 2.1 lists the relevant properties defined by TSQLDataSet.

TABLE 2.1 TSQLDataSet Properties

Property Description

Active Set to True to open the dataset, or to False to close the dataset. You can also read this property to determine whether the dataset iscurrently open or closed.

CommandText The way in which CommandText is used depends on the value ofCommandType. See the following sections for more information onCommandText.

CommandType Set CommandType to ctQuery to execute a query, to ctStoredProc toexecute a stored procedure, or to ctTable to open a table. See thefollowing sections for more information on CommandType.

DataSource Used to establish a master/detail link between two datasets. See thesection titled “Master/Detail Relationships” for more information.

Chapter 260

Page 74: Delphi Kylix Database Development

MaxBlobSize Sets the maximum amount of data returned from BLOB fields. Seethe section titled “BLOB Support” for more information.

ObjectView When True, the dataset provides additional support for ADT fields,array fields, and master/detail relationships. See the section titled“Master/Detail Relationships” for more information.

ParamCheck When True, the dataset automatically generates parameters wheneverCommandText changes. If you want to create parameters manually,set this to False. See the section titled “Parameterized Queries” formore information.

Params Contains a list of input and output parameters for the current queryor stored procedure. See the section titled “Parameterized Queries”for more information.

SortFieldNames Only used when ComandType = ctTable. Defines the order in whichdata is returned from the server. See the section titled “OrderingData Returned from the Server” for more information.

SQLConnection The TSQLConnection component to which this dataset is connected.You should set this property before setting any other properties inthe dataset.

Table-Level AccessTo access an underlying database table, you can use the TSQLTable component. The fundamentalproperties are SQLConnection and TableName. You can also set IndexName if you want to selectan index for record-ordering purposes. The following code snippet illustrates how to set tableproperties and open a table-based dataset:

SQLTable1.SQLConnection := conn;SQLTable1.TableName := ‘CONTACTS’;SQLTable1.IndexName := ‘IX_CONNAME’;SQLTable1.Open;

Query-Level AccessYou 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;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS61

TABLE 2.1 Continued

Property Description

Page 75: Delphi Kylix Database Development

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 “ParameterizedQueries.”

Stored Procedure AccessWhen you want to execute a stored procedure on the server, you can use the TSQLStoredProccomponent, 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 theParamByName method to set the parameters before executing the query.

General-Purpose Data AccessAs the preceding sections show, there are several different components that you can use fordbExpress data access (depending on whether you’re accessing a table, query, or stored procedure). However, there is a single, multipurpose component that provides all the functionalityof the three separate components: TSQLDataSet.

You should use TSQLDataSet in all new code that you write, and in my opinion, you shouldalso use it when converting existing applications. Borland provides the separate, special-purposecomponents 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 TSQLDataSetcomponent.

The following code snippet shows how to use TSQLDataSet to access a table:

SQLDataset1.SQLConnection := conn;SQLDataset1.CommandType := ctTable;SQLDataset1.CommandText := ‘CONTACTS’;SQLDataset1.IndexName := ‘IX_CONNAME’;SQLDataset1.Open;

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

Chapter 262

Page 76: Delphi Kylix Database Development

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:

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

Again, notice the similarities to the TSQLStoredProc component.

In many applications, you set these properties at design time rather than at runtime. I’m showingthe 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 isthe Navigate example (shown later in this chapter), which serves as the single example of howto use the different dbExpress dataset components.

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

Opening a DatasetAfter you’ve set the appropriate properties on the dataset, you need to open the dataset toretrieve data from the database connection. There are actually two ways to do this, both ofwhich achieve exactly the same result:

SQLDataSet1.Open;

or

SQLDataSet1.Active := True;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS63

Page 77: Delphi Kylix Database Development

If you look at the VCL/CLX source code, you’ll see that TSQLDataSet.Open resolves to a callto TDataSet.Open, which looks like this:

procedure TDataSet.Open;beginActive := True;

end;

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

Closing a DatasetClosing a dataset is as easy as opening one:

SQLDataSet1.Close;

or

SQLDataSet1.Active := False;

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

Chapter 264

Remember that closing the database connection also closes all open datasets. So, ifyou 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 opendatasets manually.

NOTE

Retrieving Field Contents from a DatasetWhen the dataset is open, you normally want to access the individual columns (or fields) in theresult 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 precedingcode snippet). However, if you’re writing a general-purpose database utility application thatworks with any dbExpress-supported database or table, you might not know in advance whatcolumns are in the table that you are accessing. In those cases, you can access the Fieldsobject directly, like this:

ShowMessage(‘The first field’’s contents are: ‘ +

Page 78: Delphi Kylix Database Development

SQLDataset1.Fields[0].AsString);

You can also use the FieldCount property to determine the number of fields in the result setand 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 DatasetBecause dbExpress datasets are unidirectional, there isn’t a lot of navigation that is supported.The only two operations that you can perform are moving to the beginning of the dataset, andmoving to the next record in the result set. These operations are illustrated in the followingcode snippet:

while not SQLDataset1.EOF do beginfor 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. Itillustrates how to use the TSQLTable, TSQLQuery, TSQLStoredProc, and TSQLDataSet componentsto 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, itsimply loads a TListView component with some of the field contents.)

Listing 2.1 shows the complete source code for the main form of the application.

LISTING 2.1 Basic—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = class(TForm)pnlClient: TPanel;pnlBottom: TPanel;btnConnect: TButton;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS65

Page 79: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

uses DatasetTypeForm;

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);varfrmDatasetType: TfrmDatasetType;

begin// Ask the user whether to open the table, query, stored procedure,// or general-purpose datasetfrmDatasetType := TfrmDatasetType.Create(nil);tryif frmDatasetType.ShowModal = mrOk then begincase frmDatasetType.DatasetType ofdtTable: OpenTable;dtQuery: OpenQuery;dtStoredProc: OpenStoredProcedure;dtDataset: OpenDataset;

end;

Chapter 266

LISTING 2.1 Continued

Page 80: Delphi Kylix Database Development

conn.Close;end;

finallyfrmDatasetType.Free;

end;end;

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

LoadResults(SQLTable1);end;

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

SQLQuery1.Open;

LoadResults(SQLQuery1);end;

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

LoadResults(SQLStoredProc1);end;

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

SQLDataset1.Open;

LoadResults(SQLDataset1);end;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS67

LISTING 2.1 Continued

Page 81: Delphi Kylix Database Development

procedure TfrmMain.LoadResults(DataSet: TDataSet);varListItem: TListItem;

beginlvResults.Items.BeginUpdate;trylvResults.Items.Clear;

while not DataSet.EOF do beginListItem := 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;finallylvResults.Items.EndUpdate;

end;end;

end.

As you can see from the code, when the user clicks the Connect button, the program creates aninstance of TfrmDatasetType (shown in Listing 2.2), which asks the user to select the type ofdataset to open: table, query, stored procedure, or generic dataset. After the user selects thedataset type, the program calls one of four methods: OpenTable, OpenQuery,OpenStoredProcedure, or OpenDataset. These methods each set the properties of the selecteddataset and make a call to LoadResults.

Notice that LoadResults takes a TDataSet as a parameter, which I indicated earlier is the rootclass of all datasets. What this means is that LoadResults would actually work with anydataset, whether it is a dbExpress dataset, BDE dataset, or some third-party dataset.

LoadResults loops through the records in the dataset, loading the contact’s first name, lastname, and phone number into a TListView.

LISTING 2.2 Basic—DatasetTypeForm.pas

unit DatasetTypeForm;

interface

uses

Chapter 268

LISTING 2.1 Continued

Page 82: Delphi Kylix Database Development

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

typeTDatasetType = (dtTable, dtQuery, dtStoredProc, dtDataset);

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;

implementation

{$R *.xfm}

procedure TfrmDatasetType.btnOkClick(Sender: TObject);beginFDatasetType := TDatasetType(grpDatasetType.ItemIndex);

end;

end.

Figure 2.2 shows the Basic demo application at runtime.

BLOB SupportLike most datasets, dbExpress datasets support BLOB data. BLOB stands for Binary LargeObject, 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 BLOBdata that is retrieved from the database to improve query performance. For example, say thatyou have a BLOB field that’s used to store an image of a contact. Executing a query thatreturns a large result set (such as SELECT * FROM CONTACTS) could potentially retrieve

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS69

LISTING 2.2 Continued

Page 83: Delphi Kylix Database Development

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

Chapter 270

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

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

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

SELECT * FROM CONTACTS

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

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 thefirst 100 bytes of one BLOB field and the entire contents of another BLOB field.

NOTE

Page 84: Delphi Kylix Database Development

Parameterized QueriesIn most of the examples shown so far in this chapter, when querying the database, the entirequery 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 querymultiple times. For example, say that you want to first retrieve all the contacts in the UnitedStates, and then all the contacts in Canada, and finally all the contacts in Mexico. You couldwrite something like the following:

SQLDataSet1.CommandType := ctQuery;SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS ‘ +‘WHERE COUNTRY = “United States”’;

SQLDataSet1.Open;ProcessDataSet;SQLDataSet1.Close;

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

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

In this example, ProcessDataSet is some fictitious method that would operate on the results ofthe query in some manner.

Although the preceding code works correctly, it is far from optimal. Each timeSQLDataSet1.CommandText is set, the backend database engine must parse and prepare theSQL statement. A better way to accomplish the same result is to parameterize the query, likethis:

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 differentcountries, like this:

SQLDataSet1.CommandType := ctQuery;SQLDataSet1.CommandText := ‘SELECT * FROM CONTACTS WHERE COUNTRY = :Country’;SQLDataSet1.ParamByName(‘Country’).AsString := ‘United States’;SQLDataSet1.Open;ProcessDataSet;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS71

Page 85: Delphi Kylix Database Development

SQLDataSet1.Close;

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

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

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

Chapter 272

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

NOTE

There is one additional property that governs how parameters are treated: the ParamCheckproperty. 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 statementthat creates a stored procedure which accepts parameters as part of the stored procedure.Because that sounds confusing, let’s take a look at an example:

SQLDataSet1.CommandType := ctQuery;SQLDataSet1.ParamCheck := False;SQLDataSet1.CommandText :=‘CREATE PROCEDURE CONTACTSBYTITLE(ATITLE VARCHAR(20)) ‘ +‘RETURNS ( ‘ +‘ID INTEGER, ‘ +‘FIRST VARCHAR(20), ‘ +[DM] Fix typo: should be FIRST

Page 86: Delphi Kylix Database Development

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

In this case, the parameters ATITLE, ID, FIRST, and LAST used in the body of the stored procedureare 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 beforesetting CommandText.

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

Ordering Data from a TableIf you are using a TSQLTable, you can set the component’s IndexName or IndexFieldNamesproperty before opening the dataset. IndexName refers to the name of an index as it is stored inthe underlying database. For example, in the ConMan database, I have defined an index,named IX_CONNAME, which is composed of the LAST and FIRST columns (in that order).

SQLTable1.IndexName := ‘IX_CONNAME’;

If you don’t know the name of the underlying index, you can set the IndexFieldNames propertyinstead. IndexFieldNames is a semicolon-delimited list of fields that make up the index.

SQLTable1.IndexFieldNames := ‘LAST;FIRST’;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS73

You do not need to ensure that there is an index defined on the fields that you set inthe IndexFieldNames property. Behind the scenes, TSQLTable creates an ORDER BYclause for the SQL statement that it passes to the database (based on the fields listedin IndexFieldNames).

NOTE

Page 87: Delphi Kylix Database Development

Ordering Data from a QueryIf you’re using a TSQLQuery or TSQLDataSet component, the method used to order the resultset 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 underlyingdatabase engine uses the index for increased speed. If not, the database engine sorts the data inthe requested order before it is returned to the application.

Master/Detail RelationshipsAlthough standalone datasets are useful, datasets are commonly related to other datasets inmaster/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 anotherdataset.

The most commonly cited example of a master/detail relationship uses customers, orders, anditems as its datasets. A single customer can place more than one order with a given vendor. Inturn, a single order can contain more than one item. Figure 2.3 shows a graphic representationof typical customer, order, and item datasets.

Chapter 274

Customers

ID

More fields… Orders

ID

CustomerID

More fields…

Items

ID

OrderID

More fields…

FIGURE 2.3Master/Detail relationships can have multiple levels.

In the ConMan database, the CONTACT and ACTIVITIES tables are joined in a master/detailrelationship on the ContactID field.

To create this master/detail link in an application, you would perform the following steps:

Page 88: Delphi Kylix Database Development

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 theTSQLConnection component that you created in step 1. Set the Name property tosqlContacts and the CommandText property to SELECT * FROM CONTACTS. This is themaster dataset.

3. Drop a TDataSource on the form, set its Name property to dsContacts, and set itsDataSet property to sqlContacts.

4. Drop another TSQLDataSet on the form. Set the Name property to sqlActivities and theDataSource property to dsContacts. This is the detail dataset.

5. Set sqlActivities.CommandText to SELECT * FROM ACTIVITIES WHERE ContactID =

:ContactID.

That’s all that’s required to establish a master/detail relationship in your program code. Youshould note the following points:

• The detail dataset’s CommandText property is always a parameterized query. The parameternames actually refer to column names in the master dataset.

• Whenever the master dataset changes records, the detail dataset automatically retrievesthe records that are associated with the current master record.

The first point deserves a little more explanation.

You’ll recall from the section titled “Parameterized Queries” that when using parameterizedqueries, 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 thatthe detail dataset’s DataSource parameter is assigned, so it looks to the data source’s dataset asthe source of parameter values.

For example, in the CONTACT/ACTIVITIES example, sqlActivities.CommandText containsa single parameter: ContactID. When Delphi assigns the value of the ContactID parameter, itlooks 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.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS75

You can create a detail dataset that gets only some of its parameter values from themaster 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 thevalue for the ContactID parameter from the master dataset. You would make a callto sqlActivities.ParamByName to set the Earliest parameter.

NOTE

Page 89: Delphi Kylix Database Development

As you scroll through the master dataset, the detail dataset automatically updates itself to stayin sync with the master.

Listing 2.3 shows the complete source code for an application that makes use of parameterizedqueries, master/detail relationships, and BLOB fields.

LISTING 2.3 Advanced—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = 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;

Chapter 276

Page 90: Delphi Kylix Database Development

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;

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS77

LISTING 2.3 Continued

Page 91: Delphi Kylix Database Development

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnConnectClick(Sender: TObject);beginconn.Open;

end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginsqlContacts.Close;conn.Close;

end;

procedure TfrmMain.connAfterConnect(Sender: TObject);beginbtnConnect.Enabled := False;btnDisconnect.Enabled := True;

end;

procedure TfrmMain.connAfterDisconnect(Sender: TObject);beginbtnConnect.Enabled := True;btnDisconnect.Enabled := False;

end;

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

end;

procedure TfrmMain.btnRetrieveClick(Sender: TObject);beginsqlContacts.Close;sqlContacts.ParamByName(‘CountryName’).Value := cbCountry.Text;sqlContacts.Open;

end;

procedure TfrmMain.btnNextClick(Sender: TObject);

Chapter 278

LISTING 2.3 Continued

Page 92: Delphi Kylix Database Development

beginsqlContacts.Next;

end;

procedure TfrmMain.sqlContactsAfterScroll(DataSet: TDataSet);varListItem: TListItem;

beginlvActivities.Items.BeginUpdate;trylvActivities.Items.Clear;

while not sqlActivities.EOF do beginListItem := lvActivities.Items.Add;ListItem.Caption := DateTimeToStr(sqlActivitiesSCHEDULED.AsDateTime);ListItem.SubItems.Add(sqlActivitiesDESCRIPTION.AsString);if not sqlActivitiesCOMPLETED.IsNull thenListItem.Data := Pointer(1);

sqlActivities.Next;end;

finallylvActivities.Items.EndUpdate;

end;end;

end.

Figure 2.4 shows the Advanced program in action.

Retrieving Schema InformationThe preceding chapter showed how to retrieve basic schema information for a database (suchas table names, index names, and stored procedure names). This section explains how to useTSQLDataSet to retrieve more detailed information about tables and columns.

To retrieve comprehensive schema information from a database, you callTSQLDataSet.SetSchemaInfo, which specifies the object whose schema you want to retrieve,and what type of schema data to return. SetSchemaInfo takes three parameters, and is definedlike this:

procedure SetSchemaInfo(SchemaType: TSchemaType; SchemaObjectName, SchemaPattern: string);

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS79

LISTING 2.3 Continued

Page 93: Delphi Kylix Database Development

FIGURE 2.4Advanced displays contact data and related activities.

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

TABLE 2.2 TSchemaType Values

Value Schema Information Returned

stNoSchema No schema information. The dataset returns the results of the queryor stored procedure rather than schema information for that object.

stTables Information about the tables in the database that match the objectname and pattern.

stSysTables Information about the system tables in the database.

stProcedures Information about the stored procedures in the database.

stColumns Information about the columns for a single table.

stProcedureParams Information about the parameters for a single stored procedure.

stIndexes Information about the indexes for a single table.

SchemaObjectName specifies the name of the table or stored procedure to return data for. It isonly used for schema types of stColumns, stProcedureParams, and stIndexes. If the schematype is stColumns or stIndexes, SchemaObjectName specifies the table to return column orindex information for. If the schema type is stProcedureParams, SchemaObjectName specifiesthe name of the stored procedure to return parameter information for.

Chapter 280

Page 94: Delphi Kylix Database Development

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

Within SchemaPattern, use a percent sign (%) to match a string of any length and an under-score (_) to match a single character. If you want to include a percent sign or underscore in thepattern, double it up (%% or __).

After you’ve retrieved schema data for a database, you can use the same dataset to run normalqueries against the database by calling SetSchemaInfo with a SchemaType of stNoSchema.

Listing 2.4 shows the complete source code for an example program that can extract and dis-play schema information for a dbExpress database. After the listing, I’ll give just a quickoverview of the code because most of it should be self-explanatory at this point.

Figure 2.5 shows the Schema program as it displays column information from the CONTACTStable in the ConMan.gdb database.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS81

FIGURE 2.5The Schema application decodes and displays column information.

LISTING 2.4 Schema—MainForm.pas

unit MainForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms,QDialogs, DBXpress, FMTBcd, DB, SqlExpr, QExtCtrls, QStdCtrls, QComCtrls;

Page 95: Delphi Kylix Database Development

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.btnRetrieveClick(Sender: TObject);varSchemaType: TSchemaType;ListColumn: TListColumn;ListItem: TListItem;Index: Integer;

beginSchemaType := TSchemaType(grpSchemaType.ItemIndex + 1);

// Columns, Indexes, and Procedure Params must have a schema object namecase SchemaType of

Chapter 282

LISTING 2.4 Continued

Page 96: Delphi Kylix Database Development

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

end;

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

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

end;

while not dataset.EOF do beginListItem := lvColumns.Items.Add;

ListItem.Caption := dataset.Fields[0].AsString;for Index := 1 to dataset.FieldCount - 1 do beginif dataset.Fields[Index].FieldName = ‘TABLE_TYPE’ thenListItem.SubItems.Add(GetTableTypeString(dataset.Fields[Index].AsInteger))

else if dataset.Fields[Index].FieldName = ‘PROC_TYPE’ thenListItem.SubItems.Add(GetProcTypeString(dataset.Fields[Index].AsInteger))

else if dataset.Fields[Index].FieldName = ‘COLUMN_TYPE’ thenListItem.SubItems.Add(GetColTypeString(dataset.Fields[Index].AsInteger))

else if dataset.Fields[Index].FieldName = ‘COLUMN_DATATYPE’ thenListItem.SubItems.Add(GetColDataTypeString(dataset.Fields[Index].AsInteger))

else if dataset.Fields[Index].FieldName = ‘COLUMN_SUBTYPE’ thenListItem.SubItems.Add(GetColSubTypeString(dataset.Fields[Index].AsInteger))

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS83

LISTING 2.4 Continued

Page 97: Delphi Kylix Database Development

else if dataset.Fields[Index].FieldName = ‘INDEX_TYPE’ thenListItem.SubItems.Add(GetIndexTypeString(dataset.Fields[Index].AsInteger))

elseListItem.SubItems.Add(dataset.Fields[Index].AsString);

end;

dataset.Next;end;

finallylvColumns.Columns.EndUpdate;

end;finallylvColumns.Items.EndUpdate;

end;finallyconn.Close;

end;end;

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

procedure Check(SQLTableType: Integer; const Desc: string);beginif (TableType and SQLTableType) <> 0 then beginif Result <> ‘’ thenResult := Result + ‘, ‘;

Result := Result + Desc;end;

end;

beginResult := ‘’;

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

if Result = ‘’ thenResult := ‘$’ + IntToHex(TableType, 2);

end;

Chapter 284

LISTING 2.4 Continued

Page 98: Delphi Kylix Database Development

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

procedure Check(SQLProcType: Integer; const Desc: string);beginif (ProcType and SQLProcType) <> 0 then beginif Result <> ‘’ thenResult := Result + ‘, ‘;

Result := Result + Desc;end;

end;

beginResult := ‘’;

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

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

end;

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

procedure Check(SQLColType: Integer; const Desc: string);beginif (ColType and SQLColType) <> 0 then beginif Result <> ‘’ thenResult := Result + ‘, ‘;

Result := Result + Desc;end;

end;

beginResult := ‘’;

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

if Result = ‘’ thenResult := ‘$’ + IntToHex(ColType, 2);

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS85

LISTING 2.4 Continued

Page 99: Delphi Kylix Database Development

end;

function TfrmMain.GetColDataTypeString(ColDataType: Integer): string;begincase ColDataType offldUNKNOWN: 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;begincase ColSubType offldstMONEY: Result := ‘Money’;fldstMEMO: Result := ‘Memo’;fldstBINARY: Result := ‘Binary’;fldstFMTMEMO: Result := ‘Fmt Memo’;fldstOLEOBJ: Result := ‘Pdox OLE’;fldstGRAPHIC: Result := ‘Graphic’;

Chapter 286

LISTING 2.4 Continued

Page 100: Delphi Kylix Database Development

fldstDBSOLEOBJ: Result := ‘dBase OLE’;fldstTYPEDBINARY: Result := ‘Typed Binary’;fldstACCOLEOBJ: Result := ‘Access OLE’;fldstHMEMO: Result := ‘CLOB’;fldstHBINARY: Result := ‘BLOB’;fldstBFILE: Result := ‘BFILE’;fldstPASSWORD: Result := ‘Pasword’;fldstFIXED: Result := ‘Char’;fldstUNICODE: Result := ‘Unicode’;fldstAUTOINC: Result := ‘AutoInc’;fldstADTNestedTable: Result := ‘ADT Nest’;fldstADTDATE: Result := ‘ADT Date’;else Result := ‘$’ + IntToHex(ColSubType, 2);

end;end;

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

procedure Check(SQLIndexType: Integer; const Desc: string);beginif (IndexType and SQLIndexType) <> 0 then beginif Result <> ‘’ thenResult := Result + ‘, ‘;

Result := Result + Desc;end;

end;

beginResult := ‘’;

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

if Result = ‘’ thenResult := ‘$’ + 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 userenters an object name if the requested schema type is columns, indexes, or procedure parameters.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS87

LISTING 2.4 Continued

Page 101: Delphi Kylix Database Development

When the schema type is known, it is a simple matter to make the appropriate call toSetSchemaInfo, open the dataset, and load the list view with the schema information. Certaincolumns (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 ofhelper routines to display a textual representation of these values. The rest of the columns aredisplayed as is.

You will notice when you run this application that different schema types return different datafields. Tables 2.3–2.7 explain the columns that are returned for each of the schema types.

Table 2.3 lists the columns that are returned for a schema type of stTables orstSystemTables.

TABLE 2.3 stTables and stSystemTables Schema Columns

Column Description

RECNO The absolute record number. It is one for the first record, two for thesecond, and so on.

CATALOG_NAME The name of the catalog, or database, that contains the table.

SCHEMA_NAME The owner of the table.

TABLE_NAME The table name.

TABLE_TYPE A bitmapped value that represents the type of table. See Listing 2.4, orthe source code for DBXpress.pas, for an explanation of the possible values for this field.

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

TABLE 2.4 stProcedures Schema Columns

Column Description

RECNO The absolute record number. It is one for the first record, two for thesecond, and so on.

CATALOG_NAME The name of the catalog, or database, that contains the stored procedure.

SCHEMA_NAME The owner of the stored procedure.

PROC_NAME The name of the stored procedure.

PROC_TYPE A bitmapped value that represents the type of stored procedure. SeeListing 2.4, or the source code for DBXpress.pas, for an explanation ofthe possible values for this field.

IN_PARAMS The number of input parameters to the stored procedure.

OUT_PARAMS The number of output parameters from the stored procedure.

Chapter 288

Page 102: Delphi Kylix Database Development

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

TABLE 2.5 stColumns Schema Columns

Column Description

RECNO The absolute record number. It is one for the first record, two for the second, and so on.

CATALOG_NAME The name of the catalog, or database, that contains the table.

SCHEMA_NAME The owner of the column.

TABLE_NAME The name of the table containing the column.

COLUMN_NAME The name of the column.

COLUMN_POSITION The zero-based position of the column in the table definition.

COLUMN_TYPE A bitmapped value that represents the type of column. See Listing2.4, or the source code for DBXpress.pas, for an explanation of thepossible values for this field.

COLUMN_DATATYPE The logical field type. See Listing 2.4, or the source code forDBXpress.pas, for an explanation of the possible values for this field.

COLUMN_TYPENAME The SQL column type (VARCHAR, BLOB, and the like).

COLUMN_SUBTYPE The logical field subtype. See Listing 2.4, or the source code forDBXpress.pas, for an explanation of the possible values for this field.

COLUMN_LENGTH The size of the column in bytes.

COLUMN_PRECISION The precision of the column. It varies by column type. For example,it is the number of characters for strings, and it is the number of significant digits for BCD values.

COLUMN_SCALE The numeric scale. It is the number of digits to the right of the decimal point for BCD columns.

COLUMN_NULLABLE It is one if the column can contain NULL values, and zero if it cannotcontain NULL values.

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

TABLE 2.6 stProcedureParams Schema Columns

Column Description

RECNO The absolute record number. It is one for the first record, two for thesecond, and so on.

CATALOG_NAME The name of the catalog, or database, that contains the stored procedure.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS89

Page 103: Delphi Kylix Database Development

SCHEMA_NAME The owner of the procedure parameter.

PROCEDURE_NAME The name of the procedure that contains the parameter.

PARAM_NAME The name of the parameter.

PARAM_POSITION The zero-based position of the parameter. Note that input and outputparameters each have their own list, so the first input parameter isposition zero, and the first output parameter is also position zero.

PARAM_TYPE It is one for an input parameter, two for an output parameter, threefor an input/output parameter, and four for a return value.

PARAM_DATATYPE The logical parameter type. See Listing 2.4, or the source code forDBXpress.pas, for an explanation of the possible values for this field.

PARAM_SUBTYPE The logical parameter subtype. See Listing 2.4, or the source codefor DBXpress.pas, for an explanation of the possible values for thisfield.

PARAM_TYPENAME The SQL parameter type (VARCHAR, BLOB, and the like).

PARAM_LENGTH The size of the parameter in bytes.

PARAM_PRECISION The precision of the parameter. It varies by parameter type. Forexample, it is the number of characters for strings, and it is the numberof significant digits for BCD values.

PARAM_SCALE The numeric scale. It is the number of digits to the right of the decimalpoint for BCD parameters.

PARAM_NULLABLE It is one if the parameter can contain NULL values, and zero if it can-not contain NULL values.

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

TABLE 2.7 stIndexes Schema Columns

Column Description

RECNO The absolute record number. It is one for the first record, two for the second, and so on.

CATALOG_NAME The name of the catalog, or database, that contains the index.

SCHEMA_NAME The owner of the index.

TABLE_NAME The name of the table on which the index is defined.

INDEX_NAME The name of the index.

COLUMN_NAME The name of the column that is part of the index.

Chapter 290

TABLE 2.6 Continued

Column Description

Page 104: Delphi Kylix Database Development

COLUMN_POSITION The position of the column within the index.

PKEY_NAME If a primary key, this is the name of the primary key.

INDEX_TYPE A bitmapped value that represents the type of column. See Listing2.4, or the source code for DBXpress.pas, for an explanation of thepossible values for this field.

SORT_ORDER This is A for an ascending column, and D for a descending column.

FILTER This represents the filter condition on a filtered/range index, or theexpression on an expression index. For example, (LAST + FIRST). Itis only supported by certain databases, such as Oracle.

SummaryThis chapter discussed the dbExpress dataset component TSQLDataSet, including the followingkey concepts:

• TSQLDataSet is a unidirectional, read-only, lightweight data access mechanism. It can beused to retrieve data from a table, query, or stored procedure in the database.

• dbExpress datasets support two navigation options: moving to the beginning of thedataset and moving to the next record in the result set.

• To retrieve data from a TSQLDataSet, you typically call the FieldByName method.

• TSQLDataSet supports BLOB information. To improve query performance, you can setTSQLDataSet’s BlobSize property to specify the maximum amount of data to retrieve foreach BLOB field.

• To make repeated queries more efficient, you can parameterize them by creating a placeholder in the SQL statement. Then, you can set different values for this parameter.The SQL statement is prepared only once—the first time that it is executed.

• To order the data that’s returned from the database, you can specify an ORDER BYclause in the dataset’s CommandText property.

• You can easily set up master/detail relationships between tables and queries.

• It is possible to use dbExpress datasets to retrieve detailed schema information for tables,queries, and stored procedures in a database.

The following chapter begins a two-chapter exploration of client datasets.

dbExpress Datasets

2

DBE

XPR

ESSD

ATA

SETS91

TABLE 2.7 Continued

Column Description

Page 105: Delphi Kylix Database Development
Page 106: Delphi Kylix Database Development

CHAPTER

3Client Dataset Basics

IN THIS CHAPTER• What Is a Client Dataset? 94

• Advantages and Disadvantages of ClientDatasets 94

• Creating Client Datasets 95

• Populating and Manipulating Client Datasets105

• Navigating Client Datasets 113

• Client Dataset Indexes 118

• Filters and Ranges 126

• Searching 136

Page 107: Delphi Kylix Database Development

Chapter 394

In the preceding two chapters, I discussed dbExpress—a unidirectional database technology. Inthe real world, most applications support bidirectional scrolling through a dataset. As notedpreviously, Borland has addressed bidirectional datasets through a technology known as clientdatasets. This chapter introduces you to the basic operations of client datasets, including howthey are a useful standalone tool. Subsequent chapters focus on more advanced client datasetcapabilities, including how you can hook a client dataset up to a dbExpress (or other) databaseconnection 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 (asopposed to an application server). The name is a bit of a misnomer, because it seems to indicatethat client datasets have no use outside a client/server or multitier application. However, asyou’ll see in this chapter, client datasets are useful in other types of applications, especiallysingle-tier database applications.

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

NOTE

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

Advantages and Disadvantages of Client DatasetsClient datasets have a number of advantages, and a couple of perceived disadvantages. Theadvantages include

• Memory based. Client datasets reside completely in memory, making them useful fortemporary tables.

• Fast. Because client datasets are RAM based, they are extremely fast.

• Efficient. Client datasets store their data in a very efficient manner, making themresource friendly.

Page 108: Delphi Kylix Database Development

• 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 easyto 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 inChapter 4.

The perceived disadvantages include

• Memory based. This client dataset advantage can also be a disadvantage. Because clientdatasets 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 inRAM.

When you understand client datasets, you’ll discover that these so-called disadvantages reallyaren’t detrimental to your application at all. In particular, basing client datasets entirely inRAM has both advantages and disadvantages.

Because they are kept entirely in your computer’s RAM, client datasets are extremely usefulfor temporary tables, small lookup tables, and other nonpersistent database needs. Clientdatasets also are fast because they are RAM based. Inserting, deleting, searching, sorting, andtraversing in client datasets are lightening fast.

On the flip side, you need to take steps to ensure that client datasets don’t grow too largebecause you waste precious RAM if you attempt to store huge databases in in-memorydatasets. Fortunately, client datasets store their data in a very compact form. (I’ll discuss this in more detail in the “Undo Support” section of Chapter 7.)

Because they are memory based, client datasets are inherently single user. Remote machinesdo not have access to a client dataset on a local machine. In Chapter 8, “DataSnap,” you’lllearn how to connect a client dataset to an application server in a three-tier configuration thatsupports true multiuser operation.

Creating Client DatasetsUsing client datasets in your application is similar to using any other type of dataset becausethey derive from TDataSet.

You can create client datasets either at design-time or at runtime, as the following sectionsexplain.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S95

Page 109: Delphi Kylix Database Development

Creating a Client Dataset at Design-TimeTypically, 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, butdoesn’t set up any field or index definitions. Name the component cdsEmployee.

To create the field definitions for the client dataset, double-click the TClientDataSet componentin 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 inFigure 3.1 appears.

Chapter 396

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

If you’re familiar with the field editor, you notice a new field type available for client datasets,called Aggregate fields. I’ll discuss Aggregate fields in detail in the following chapter. For now,you should understand that you can add data, lookup, calculated, and internally calculatedfields to a client dataset—just as you can for any dataset.

The difference between client datasets and other datasets is that when you create a data fieldfor a typical dataset, all you are doing is creating a persistent field object that maps to a field inthe underlying database. For a client dataset, you are physically creating the field in the datasetalong with a persistent field object. At design-time, there is no way to create a field in a clientdataset without also creating a persistent field object.

Data FieldsMost of the fields in your client datasets will be data fields. A data field represents a field thatis physically part of the dataset, as opposed to a calculated or lookup field (which are discussedin the following sections). You can think of calculated and lookup fields as virtual fieldsbecause they appear to exist in the dataset, but their data actually comes from another location.

Page 110: Delphi Kylix Database Development

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

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S97

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

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

Now add a second field, called LastName. Right-click the field editor to display the New Fielddialog and enter LastName in the Name edit control. In the Type combo, select String. Then, setSize to 30—the size represents the maximum number of characters allowed for the field. ClickOK to add the LastName field to the dataset.

Similarly, add a 20-character FirstName field and an Integer Department field.Finally, let’sadd a Salary field. Open the New Field dialog. In the Name edit control, type Salary. Set theType 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.3The field editor after adding five fields.

Page 111: Delphi Kylix Database Development

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

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

Chapter 398

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

NOTE

Standard Calculated FieldsIn this section, we’ll create a calculated field that computes an annual bonus, which we’llassume to be five percent of an employee’s salary.

To create a standard calculated field, open the New Field dialog (as you did in the precedingsection). Enter a Name of Bonus and a Type of Currency.

In the Field Type radio group, select Calculated. This instructs Delphi to create a calculatedfield, rather than a data field. Click OK.

That’s all you need to do to create a calculated field. Now, let’s look at internal calculatedfields.

Internal Calculated FieldsCreating an internal calculated field is almost identical to creating a standard calculated field.The only difference is that you select InternalCalc as the Field Type in the New Field dia-log, instead of Calculated.

Another difference between the two types of calculated fields is that standard calculated fieldsare calculated on-the-fly every time their value is required, but internal calculated fields arecalculated once and their value is stored in RAM. (Of course, internal calculated fields recal-culate automatically if the underlying fields that they are calculated from change.)

The dataset’s AutoCalcFields property determines exactly when calculated fields are recom-puted. If AutoCalcFields is True (the default value), calculated fields are computed when thedataset is opened, when the dataset enters edit mode, and whenever focus in a form moves

Page 112: Delphi Kylix Database Development

from one data-aware control to another and the current record has been modified. IfAutoCalcFields is False, calculated fields are computed when the dataset is opened, when thedataset enters edit mode, and when a record is retrieved from an underlying database into thedataset.

There are two reasons that you might want to use an internal calculated field instead of a standardcalculated field. If you want to index the dataset on a calculated field, you must use an internalcalculated field. (Indexes are discussed in detail later in this chapter.) Also, you might elect touse 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 tobe computed as often as standard calculated fields.

Let’s add an internal calculated field to our dataset. The field will be called Name, and it willconcatenate the FirstName and LastName fields together. We probably will want an index onthis 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 thefirst name, plus a comma and a space to separate the two).

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

Providing Values for Calculated FieldsAt this point, we’ve created our calculated fields. Now we need to provide the code to calculate the values. TClientDataSet, like all Delphi datasets, supports a method namedOnCalcFields that we need to provide a body for.

Click the client dataset again, and in the Object Inspector, click the Events tab. Double-clickthe OnCalcFields event to create an event handler.

We’ll calculate the value of the Bonus field first. Flesh out the event handler so that it lookslike this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);begincdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;

end;

That’s easy—we just take the value of the Salary field, multiply it by five percent (0.05), andstore the value in the Bonus field.

Now, let’s add the Name field calculation. A first (reasonable) attempt looks like this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);begincdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S99

Page 113: Delphi Kylix Database Development

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

end;

This works, but it isn’t efficient. The Name field calculates every time the Bonus field calculates.However, recall that it isn’t necessary to compute internal calculated fields as often as standardcalculated fields. Fortunately, we can check the dataset’s State property to determine whetherwe need to compute internal calculated fields or not, like this:

procedure TForm1.cdsEmployeeCalcFields(DataSet: TDataSet);begincdsEmployeeBonus.AsFloat := cdsEmployeeSalary.AsFloat * 0.05;

if cdsEmployee.State = dsInternalCalc thencdsEmployeeName.AsString := cdsEmployeeLastName.AsString + ‘, ‘ +cdsEmployeeFirstName.AsString;

end;

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

Lookup FieldsLookup fields are similar, in concept, to calculated fields because they aren’t physically storedin the dataset. However, instead of requiring you to calculate the value of a lookup field,Delphi gets the value from another dataset. Let’s look at an example.

Earlier, we created a Department field in our dataset. Let’s create a new Department dataset tohold department information.

Drop a new TClientDataSet component on your form and name it cdsDepartment. Add twofields: Dept (an integer) and Description (a 30-character string).

Show the field editor for the cdsEmployee dataset by double-clicking the dataset. Open theNew Field dialog. Name the field DepartmentName, and give it a Type of String and a Size of30.

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

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

Chapter 3100

Page 114: Delphi Kylix Database Development

FIGURE 3.4Adding a lookup field to a dataset.

The important thing to remember about lookup fields is that the Key field represents the fieldin the base dataset that references the lookup dataset. Dataset refers to the lookup dataset. TheLookup Keys combo box represents the Key field in the lookup dataset. The Result field isthe 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 andselect Create DataSet from the pop-up menu.

Now that you’ve seen how to create a client dataset at design-time, let’s see what’s required tocreate a client dataset at runtime.

Creating a Client Dataset at RuntimeTo create a client dataset at runtime, you start with the following skeletal code:

varCDS: TClientDataSet;

beginCDS := TClientDataSet.Create(nil);try// Do something with the client dataset here

finallyCDS.Free;

end;end;

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

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S101

Page 115: Delphi Kylix Database Development

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

AddFieldDef

TFieldDefs.AddFieldDef is defined like this:

function AddFieldDef: TFieldDef;

As you can see, AddFieldDef takes no parameters and returns a TFieldDef object. When youhave the TFieldDef object, you can set its properties, as the following code snippet shows.

varFieldDef: TFieldDef;

beginFieldDef := 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 isdefined 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 snippetbecomes 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 thatTFieldDef contains several more-advanced properties (such as field precision, whether or notit’s read-only, and a few other attributes) in addition to the four supported by Add. If you wantto set these properties for a field, you need to go through the TFieldDef. You should refer tothe Delphi documentation for TFieldDef for more details.

Creating the DatasetAfter you create the field definitions, you need to create the empty dataset in memory. To dothis, call TClientDataSet.CreateDataSet, like this:

ClientDataSet1.CreateDataSet;

Chapter 3102

Page 116: Delphi Kylix Database Development

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

Accessing FieldsRegardless of how you create the client dataset, at some point you need to access field infor-mation—whether it’s for display, to calculate some values, or to add or modify a new record.

There are several ways to access field information in Delphi. The easiest is to use persistent fields.

Persistent FieldsEarlier in this chapter, when we used the field editor to create fields, we were also creatingpersistent 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 fieldby using the AsXxx family of methods. For example, to access a field as a string, you wouldreference the AsString property, like this:

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

To retrieve the employee’s salary as a floating-point number, you would reference the AsFloatproperty:

Bonus := cdsEmployeeSalary.AsFloat * 0.05;

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

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S103

You are not limited to accessing a field value in its native format. For example, justbecause Salary is a currency field doesn’t mean you can’t attempt to access it as astring. The following code displays an employee’s salary as a formatted currency:

ShowMessage(‘Your salary is ‘ + cdsEmployeeSalary.AsString);

You could access a string field as an integer, for example, if you knew that the fieldcontained an integer value. However, if you try to access a field as an integer (orother data type) and the field doesn’t contain a value that’s compatible with thatdata type, Delphi raises an exception.

NOTE

Page 117: Delphi Kylix Database Development

Nonpersistent FieldsIf you create a dataset at design-time, you probably won’t have any persistent field objects. Inthat case, there are a few methods you can use to access a field’s value.

The first is the FieldByName method. FieldByName takes the field name as a parameter andreturns a temporary field object. The following code snippet displays an employee’s last nameusing FieldByName.

ShowMessage(‘The employee’’s last name is ‘ + ClientDataSet1.FieldByName(‘LastName’).AsString);

Chapter 3104

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

CAUTION

Another way to access the fields in a dataset is through the FindField method, like this:

if ClientDataSet1.FindField(‘LastName’) <> nil thenShowMessage(‘Dataset contains a LastName field’);

Using this technique, you can create persistent fields for datasets created at runtime.

varfldLastName: TField;fldFirstName: TField;

begin...fldLastName := cds.FindField(‘LastName’);fldFirstName := cds.FindField(‘FirstName’);...ShowMessage(‘The last name is ‘ + fldLastName.AsString);

end;

Finally, you can access the dataset’s Fields property. Fields contains a list of TField objectsfor the dataset, as the following code illustrates:

varIndex: Integer;

beginfor Index := 0 to ClientDataSet1.Fields.Count - 1 doShowMessage(ClientDataSet1.Fields[Index].AsString);

end;

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

Page 118: Delphi Kylix Database Development

times when the Fields list comes in handy. For example, if you have two client datasets withthe same structure, you could add a record from one dataset to the other using the following code:

varIndex: Integer;

beginClientDataSet2.Append;for Index := 0 to ClientDataSet1.Fields.Count - 1 doClientDataSet2.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 DatasetsAfter you create a client dataset (either at design-time or at runtime), you want to populate itwith data. There are several ways to populate a client dataset: You can populate it manuallythrough code, you can load the dataset’s records from another dataset, or you can load thedataset from a file or a stream. The following sections discuss these methods, as well as how tomodify and delete records.

Populating ManuallyThe 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 newrecord at the end of the dataset, but Insert places the new record immediately before the current record.

I always use Append to insert new records because it’s slightly faster than Insert. If the datasetis 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 wellcdsEmployee.FieldByName(‘ID’).AsInteger := 5;cdsEmployee.FieldByName(‘FirstName’).AsString := ‘Eric’;cdsEmployee.Post;

Modifying RecordsModifying an existing record is almost identical to adding a new record. Rather than callingAppend or Insert to create the new record, you call Edit to put the dataset into edit mode. Thefollowing code changes the first name of the current record to Fred.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S105

Page 119: Delphi Kylix Database Development

cdsEmployee.Edit; // Edit the current recordcdsEmployee.FieldByName(‘FirstName’).AsString := ‘Fred’;cdsEmployee.Post;

Deleting RecordsTo 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 DatasetdbExpress datasets are unidirectional and you can’t scroll backward through them. This makesthem incompatible with bidirectional, data-aware controls such as TDBGrid. However,TClientDataSet can load its data from another dataset (including dbExpress datasets, BDEdatasets, or other client datasets) through a provider. Using this feature, you can load a clientdataset from a unidirectional dbExpress dataset, and then connect a TDBGrid to the clientdataset, providing bidirectional support.

Indeed, this capability is so powerful and important that it forms the basis for Delphi’s multitierdatabase support.

Populating from a File or Stream: Persisting ClientDatasetsThough client datasets are located in RAM, you can save them to a file or a stream and reloadthem at a later point in time, making them persistent. This is the third method of populating aclient dataset.

To save the dataset to a file, use the SaveToFile method, which is defined like this:

procedure SaveToFile(const FileName: string = ‘’; Format: TDataPacketFormat = dfBinary);

Similarly, to save the dataset to a stream, you call SaveToStream, which is defined as follows:

procedure SaveToStream(Stream: TStream; Format: TDataPacketFormat = dfBinary);

SaveToFile accepts the name of the file that you’re saving to. If the filename is blank, the datais saved using the FileName property of the client dataset.

Both SaveToFile and SaveToStream take a parameter that indicates the format to use whensaving data. Client datasets can be stored in one of three file formats: binary, or either flavor ofXML. Table 3.1 lists the possible formats.

Chapter 3106

Page 120: Delphi Kylix Database Development

TABLE 3.1 Data Packet Formats for Loading and Saving Client Datasets

Value Description

dfBinary Data is stored using a proprietary, binary format.

dfXML Data is stored in XML format. Extended characters are representedusing an escape sequence.

dfXMLUTF8 Data is stored in XML format. Extended characters are representedusing UTF8.

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

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S107

If you’re familiar with Microsoft ADO, you recall that ADO enables you to persistdatasets 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).

NOTE

Sometimes, you need to determine how many bytes are required to store the data contained inthe client dataset. For example, you might want to check to see if there is enough room on afloppy disk before saving the data there, or you might need to preallocate the memory for astream. In these cases, you can check the DataSize property, like this:

if ClientDataSet1.DataSize > AvailableSpace thenShowMessage(‘Not enough room to store the data’);

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

One way to determine the amount of space that’s required to save the dataset inXML format is to save the dataset to a memory stream, and then obtain the size ofthe resulting stream.

NOTE

Page 121: Delphi Kylix Database Development

Example: Creating, Populating, and Manipulating a ClientDatasetThe following example illustrates how to create, populate, and manipulate a client dataset atruntime. Code is also provided to save the dataset to disk and to load it.

Listing 3.1 shows the complete source code for the CDS (ClientDataset) application.

LISTING 3.1 CDS—MainForm.pas

unit MainForm;

interface

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

constMAX_RECS = 10000;

typeTfrmMain = 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

Chapter 3108

Page 122: Delphi Kylix Database Development

{ Public declarations }end;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginFCDS := 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;

procedure TfrmMain.Populate1Execute(Sender: TObject);constFirstNames: 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’);

varIndex: Integer;t1, t2: DWord;

beginRandSeed := 0;

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

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S109

LISTING 3.1 Continued

Page 123: Delphi Kylix Database Development

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;

finallyFCDS.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);vart1, t2: DWord;msLocateID: DWord;msLocateName: DWord;

beginFCDS.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);vart1, t2: DWord;

begint1 := GetTickCount;

Chapter 3110

LISTING 3.1 Continued

Page 124: Delphi Kylix Database Development

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

end;

procedure TfrmMain.Load1Execute(Sender: TObject);vart1, t2: DWord;

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

exceptFCDS.Open;raise;

end;end;

end.

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

• FormCreate creates the client dataset and its schema at runtime. It would actually be easierto create the dataset at design-time, but I wanted to show you the code required to do thisat runtime. The code creates four fields: Employee ID, Name, Birthday, and Salary.

• Populate1Execute loads the client dataset with 10,000 employees made up of randomdata. At the beginning of the method, I manually set RandSeed to 0 to ensure that multipleexecutions of the application would generate the same data.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S111

LISTING 3.1 Continued

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 randomnumbers generated are consistent every time we run the program.

NOTE

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

Page 125: Delphi Kylix Database Development

• Statistics1Execute simply measures the length of time required to perform a couple ofLocate operations and calculates the amount of space necessary to store the data on disk(again, in binary format). I’ll be discussing the Locate method later in this chapter.

• Save1Execute saves the data to disk under the filename C:\Employee.cds. The .cdsextension is standard, although not mandatory, for client datasets that are saved in abinary format. Client datasets stored in XML format generally have the extension .xml.

Chapter 3112

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, aswell as some of the examples in the following chapter.

NOTE

• Load1Execute loads the data from a file into the client dataset. If LoadFromFile fails(presumably because the file doesn’t exist or is not a valid file format), the client datasetis left in a closed state. For this reason, I reopen the client dataset when an exception israised.

Figure 3.5 shows the CDS application running on my computer. Note the impressive timesposted to locate a record. Even when searching through almost the entire dataset to find ID9763, it only takes approximately 10 ms on my computer.

FIGURE 3.5The CDS application at runtime.

Page 126: Delphi Kylix Database Development

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

Sequential NavigationThe most basic way to navigate through a dataset is sequentially in either forward or reverseorder. For example, you might want to iterate through a dataset when printing a report, or forsome 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 datasetis empty. If it is empty, First sets the dataset’s EOF (end of file) property to True.

• Next moves to the next record in the dataset (if the EOF property is not already set). IfEOF is True, Next will fail. If the call to Next reaches the end of the file, it sets the EOFproperty to True.

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

• Prior moves to the preceding record in the dataset (if the BOF property is not alreadyset). 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.

The following code snippet shows how you can use these methods to iterate through a dataset:

if not ClientDataSet1.IsEmpty then beginClientDataSet1.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

3

CLIEN

TD

ATA

SETB

ASIC

S113

Page 127: Delphi Kylix Database Development

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

BookmarksA bookmark used with a client dataset is very similar to a bookmark used with a paper-basedbook: 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 abookmark, and free a bookmark. The following code snippet shows how to do all three:

varBookmark: TBookmark;

beginBookmark := ClientDataSet1.GetBookmark;try// Do something with ClientDataSet1 here that changes the current record...ClientDataSet1.GotoBookmark(Bookmark);

finallyClientDataSet1.FreeBookmark(Bookmark);

end;end;

You can create as many bookmarks as you want for a dataset. However, keep in mind that abookmark allocates a small amount of memory, so you should be sure to free all bookmarksusing FreeBookmark or your application will leak memory.

There is a second set of operations that you can use for bookmarks instead ofGetBookmark/GotoBookmark/FreeBookmark. The following code shows this alternate method:

varBookmarkStr: string;

beginBookmarkStr := ClientDataSet1.Bookmark;try// Do something with ClientDataSet1 here that changes the current record...

finallyClientDataSet1.Bookmark := BookmarkStr;

end;end;

Chapter 3114

Page 128: Delphi Kylix Database Development

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

Record NumbersClient datasets support a second way of moving directly to a given record in the dataset: settingthe 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 writethe RecNo property to set the current record. There are two important things to keep in mindwith respect to RecNo:

• Attempting to set RecNo to a number less than one, or to a number greater than the numberof records in the dataset results in an At beginning of table, or an At end of tableexception, 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 thedataset.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S115

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

NOTE

However, when used discriminately, RecNo has its uses. For example, let’s say the user of yourapplication wants to delete all records between the John Smith record and the Fred Jonesrecord. The following code shows how you can accomplish this:

varRecNoJohn: Integer;RecNoFred: Integer;Index: Integer;

beginif not ClientDataSet1.Locate(‘Name’, ‘John Smith’, []) thenraise Exception.Create(‘Cannot locate John Smith’);

RecNoJohn := ClientDataSet1.RecNo;

if not ClientDataSet1.Locate(‘Name’, ‘Fred Jones’, []) thenraise Exception.Create(‘Cannot locate Fred Jones’);

RecNoFred := ClientDataSet1.RecNo;

Page 129: Delphi Kylix Database Development

if RecNoJohn < RecNoFred then// Locate John againClientDataSet1.RecNo := RecNoJohn;

for Index := 1 to Abs(RecNoJohn - RecNoFred) + 1 doClientDataSet1.Delete;

end;

This code snippet first locates the two bounding records and remembers their absolute recordnumbers. 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 addone) to determine the number of records to delete. Deleting a record makes the next record current, so a simple for loop handles the deletion of the records.

Keep in mind that RecNo isn’t usually going to be your first line of attack for moving around ina dataset, but it’s handy to remember that it’s available if you ever need it.

Listing 3.2 contains the complete source code for an application that demonstrates the differentnavigational methods of client datasets.

LISTING 3.2 Navigate—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = 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;

Chapter 3116

Page 130: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

end;

procedure TfrmMain.btnFirstClick(Sender: TObject);beginClientDataSet1.First;

end;

procedure TfrmMain.btnPriorClick(Sender: TObject);beginClientDataSet1.Prior;

end;

procedure TfrmMain.btnNextClick(Sender: TObject);beginClientDataSet1.Next;

end;

procedure TfrmMain.btnLastClick(Sender: TObject);

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S117

LISTING 3.2 Continued

Page 131: Delphi Kylix Database Development

beginClientDataSet1.Last;

end;

procedure TfrmMain.btnSetRecNoClick(Sender: TObject);varValue: string;

beginValue := ‘1’;if InputQuery(‘RecNo’, ‘Enter Record Number’, Value) thenClientDataSet1.RecNo := StrToInt(Value);

end;

procedure TfrmMain.btnGetBookmarkClick(Sender: TObject);beginif Assigned(FBookmark) thenClientDataSet1.FreeBookmark(FBookmark);

FBookmark := ClientDataSet1.GetBookmark;end;

procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject);beginif Assigned(FBookmark) thenClientDataSet1.GotoBookmark(FBookmark)

elseShowMessage(‘No bookmark set!’);

end;

end.

Figure 3.6 shows this program at runtime.

Client Dataset IndexesSo far, we haven’t created any indexes on the client dataset and you might be wondering if(and why) they’re even necessary when sequential searches through the dataset (using Locate)are so fast.

Indexes are used on client datasets for at least three reasons:

• To provide faster access to data. A single Locate operation executes very quickly, but ifyou need to perform thousands of Locate operations, there is a noticeable performancegain when using indexes.

Chapter 3118

LISTING 3.2 Continued

Page 132: Delphi Kylix Database Development

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

• To implement maintained aggregates.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S119

FIGURE 3.6The Navigate application demonstrates various navigational techniques.

Creating IndexesLike 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 destroyindexes at runtime. For example, some indexes are only used for a short time—say, to create areport in a certain order. In this case, you might want to create the index, use it, and thendestroy it. If you constantly need an index, it’s better to create it at design-time (or to create itthe first time you need it and not destroy it afterward).

Creating Indexes at Design-TimeTo create an index at design-time, click the TClientDataSet component located on the formor data module. In the Object Inspector, double-click the IndexDefs property. The index editorappears.

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. Table3.2 shows the index properties.

Page 133: Delphi Kylix Database Development

TABLE 3.2 Index Properties

Property Description

Name The name of the index. I recommend prefixing index names withthe letters by (as in byName, byState, and so on).

Fields Semicolon-delimited list of fields that make up the index. Example:‘ID’ or ‘Name;Salary’.

DescFields A list of the fields contained in the Fields property that should beindexed in descending order. For example, to sort ascending byname, and then descending by salary, set Fields to ‘Name;Salary’

and DescFields to ‘Salary’.

CaseInsFields A list of the fields contained in the Fields property that should beindexed in a manner which is not case sensitive. For example, if theindex is on the last and first name, and neither is case sensitive, setFields to ‘Last;First’ and CaseInsFields to ‘Last;First’.

GroupingLevel Used for aggregation.

Options Sets additional options on the index. The options are discussed in Table 3.3.

Expression Not applicable to client datasets.

Source Not applicable to client datasets.

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

TABLE 3.3 Index Options

Option Description

IxPrimary The index is the primary index on the dataset.

IxUnique The index is unique.

IxDescending The index is in descending order.

IxCaseInsensitive The index is not case sensitive.

IxExpression Not applicable to client datasets.

IxNonMaintained Not applicable to client datasets.

You can create multiple indexes on a single dataset. So, you can easily have both an ascendingand a descending index on EmployeeName, for example.

Chapter 3120

Page 134: Delphi Kylix Database Development

Creating and Deleting Indexes at RuntimeIn contrast to field definitions (which you usually create at design-time), index definitions aresomething 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 indexfor a short period of time (to print a report in a certain order, for example), creating anddestroying 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 indexesin your code.

To create an index, you use the client dataset’s AddIndex method. AddIndex takes three mandatoryparameters, 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 codesnippet shows how to create a unique index by last and first names:

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

When you decide that you no longer need an index (remember, you can always re-create it ifyou 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 indexcreated in the preceding code snippet:

ClientDataSet1.DeleteIndex(‘byName’);

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

ClientDataSet1.IndexName := ‘byName’;

If you have two or more indexes defined on a dataset, you can quickly switch back and forthby changing the value of the IndexName property. If you want to discontinue the use of anindex and revert to the default record order, you can set the IndexName property to an emptystring, as the following code snippet illustrates:

// Do something in name orderClientDataSet1.IndexName := ‘byName’;

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S121

Page 135: Delphi Kylix Database Development

// Do something in salary orderClientDataSet1.IndexName := ‘bySalary’;

// Switch back to the default orderingClientDataSet1.IndexName := ‘’;

There is a second way to specify indexes on-the-fly at runtime. Instead of creating an indexand setting the IndexName property, you can simply set the IndexFieldNames property.IndexFieldNames accepts a semicolon-delimited list of fields to index on. The following codeshows how to use it:

ClientDataSet1.IndexFieldNames := ‘Last;First’;

Though IndexFieldNames is quicker and easier to use than AddIndex/IndexName, its simplicitydoes 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 ofIndexFieldNames), the old index is automatically dropped. If you switch back at a latertime, the index is re-created. This happens so fast that it’s not likely to be noticeable, butyou should be aware that it’s happening, nonetheless. When you create indexes usingAddIndex, the index is maintained until you specifically delete it using DeleteIndex.

Chapter 3122

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

NOTE

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

GetIndexNamesThe simplest method for retrieving index information is GetIndexNames. GetIndexNames takesa single parameter, a TStrings object, in which to store the resultant index names. The follow-ing code snippet shows how to load a list box with the names of all indexes defined for adataset.

ClientDataSet1.GetIndexNames(ListBox1.Items);

Page 136: Delphi Kylix Database Development

TIndexDefsIf you want to obtain more detailed information about an index, you can go directly to thesource: TIndexDefs. TIndexDefs contains a list of all indexes, along with the information associated with each one (such as the fields that make up the index, which fields are descend-ing, and so on).

The following code snippet shows how to access index information directly throughTIndexDefs.

varIndex: Integer;IndexDef: TIndexDef;

beginClientDataSet1.IndexDefs.Update;

for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do beginIndexDef := ClientDataSet1.IndexDefs[Index];ListBox1.Items.Add(IndexDef.Name);

end;end;

Notice the call to IndexDefs.Update before the code that loops through the index definitions.This call is required to ensure that the internal IndexDefs list is up-to-date. Without it, it’s pos-sible that IndexDefs might not contain any information about recently added indexes.

The following application demonstrates how to provide on-the-fly indexing in a TDBGrid. Italso contains code for retrieving detailed information about all the indexes defined on adataset.

Figure 3.7 shows the CDSIndex application at runtime, as it displays index information for theemployee client dataset.

Listing 3.3 contains the complete source code for the CDSIndex application.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S123

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

CAUTION

Page 137: Delphi Kylix Database Development

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

LISTING 3.3 CDSIndex—MainForm.pas

unit MainForm;

interface

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

typeTfrmMain = 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;

Chapter 3124

Page 138: Delphi Kylix Database Development

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

end;

procedure TfrmMain.DBGrid1TitleClick(Column: TColumn);begintryClientDataSet1.DeleteIndex(‘byUser’);

exceptend;

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

end;

procedure TfrmMain.btnDefaultOrderClick(Sender: TObject);begin// Deleting the current index will revert to the default ordertryClientDataSet1.DeleteIndex(‘byUser’);

exceptend;

ClientDataSet1.IndexFieldNames := ‘’;end;

procedure TfrmMain.btnIndexListClick(Sender: TObject);varIndex: Integer;IndexDef: TIndexDef;

beginClientDataSet1.IndexDefs.Update;

ListBox1.Items.BeginUpdate;tryListBox1.Items.Clear;

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S125

LISTING 3.3 Continued

Page 139: Delphi Kylix Database Development

for Index := 0 to ClientDataSet1.IndexDefs.Count - 1 do beginIndexDef := ClientDataSet1.IndexDefs[Index];ListBox1.Items.Add(IndexDef.Name);

end;finallyListBox1.Items.EndUpdate;

end;end;

end.

The code to dynamically sort the grid at runtime is contained in the methodDBGrid1TitleClick. First, it attempts to delete the temporary index named byUser, if it exists.If it doesn’t exist, an exception is raised, which the code simply eats. A real application shouldnot mask exceptions willy-nilly. Instead, it should trap for the specific exceptions that might bethrown 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.

Chapter 3126

LISTING 3.3 Continued

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

NOTE

Filters and RangesFilters 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, andthe WHERE clause is that when you apply a filter or a range, it does not physically change whichdata is contained in the dataset. It only limits the amount of data that you can see at any giventime.

RangesRanges are useful when the data that you want to limit yourself to is stored in a consecutivesequence of records. For example, say a dataset contains the data shown in Table 3.4.

Page 140: Delphi Kylix Database Development

TABLE 3.4 Sample Data for Ranges and Filters

ID Name Birthday Salary

4 Bill Peterson 3/28/1957 $60,000.00

2 Frank Smith 8/25/1963 $48,000.00

3 Sarah Johnson 7/5/1968 $52,000.00

1 John Doe 5/15/1970 $39,000.00

5 Paula Wallace 1/15/1971 $36,500.00

The data in this much-abbreviated table is indexed by birthday. Ranges can only be used whenthere is an active index on the dataset.

Assume that you want to see all employees who were born between 1960 and 1970. Becausethe data is indexed by birthday, you could apply a range to the dataset, like this:

ClientDataSet1.SetRange([‘1/1/1960’], [‘12/31/1970’]);

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

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

ClientDataSet1.CancelRange;

FiltersUnlike 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 evensupported by SQL. Tables 3.5–3.10 list the various functions and operators available for use ina filter.

TABLE 3.5 Filter Comparison Operators

Function Description Example

= Equality test Name = ‘John Smith’

<> Inequality test ID <> 100

< Less than Birthday < ‘1/1/1980’

> Greater than Birthday > ‘12/31/1960’

<= Less than or equal to Salary <= 80000

>= Greater than or equal to Salary >= 40000

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S127

Page 141: Delphi Kylix Database Development

BLANK Empty string field Name = BLANK

(not used to test forNULL values)

IS NULL Test for NULL value Birthday IS NULL

IS NOT NULL Test for non-NULL value Birthday IS NOT NULL

TABLE 3.6 Filter Logical Operators

Function Example

And (Name = ‘John Smith’) and (Birthday = ‘5/16/1964’)

Or (Name = ‘John Smith’) or (Name = ‘Julie Mason’)

Not Not (Name = ‘John Smith’)

TABLE 3.7 Filter Arithmetic Operators

Function Description Example

+ Addition. Can be used with Birthday + 30 < ‘1/1/1960’

numbers, strings, or Name + ‘X’ = ‘SmithX’

dates/times. Salary + 10000 = 100000

– Subtraction. Can be used Birthday - 30 > ‘1/1/1960’

with numbers or dates/times. Salary - 10000 > 40000

* Multiplication. Can be used Salary * 0.10 > 5000

with numbers only.

/ Division. Can be used with Salary / 10 > 5000

numbers only.

TABLE 3.8 Filter String Functions

Function Description Example

Upper Uppercase Upper(Name) = ‘JOHN SMITH’

Lower Lowercase Lower(Name) = ‘john smith’

SubString Return a portion of a string SubString(Name,6) = ‘Smith’

SubString(Name,1,4) = ‘John’

Trim Trim leading and trailing Trim(Name)

characters from a string Trim(Name, ‘.’)

Chapter 3128

TABLE 3.5 Continued

Function Description Example

Page 142: Delphi Kylix Database Development

TrimLeft Trim leading characters TrimLeft(Name)

from a string TrimLeft(Name, ‘.’)

TrimRight Trim trailing characters TrimRight(Name)

from a string TrimRight(Name, ‘.’)

TABLE 3.9 Filter Date/Time Functions

Function Description Example

Year Returns the year portion Year(Birthday) = 1970

of a date value.

Month Returns the month portion Month(Birthday) = 1

of a date value.

Day Returns the day portion Day(Birthday) = 15

of a date value.

Hour Returns the hour portion Hour(Appointment) = 18

of a time value in 24-hour format.

Minute Returns the minute portion Minute(Appointment) = 30

of a time value.

Second Returns the second portion Second(Appointment) = 0

of a time value.

GetDate Returns the current date Appointment < GetDate

and time.

Date Returns the date portion Date(Appointment)

of a date/time value.

Time Returns the time portion Time(Appointment)

of a date/time value.

TABLE 3.10 Other Filter Functions and Operators

Function Description Example

LIKE Partial string comparison. Name LIKE ‘%Smith%’

IN Tests for multiple values. Year(Birthday) IN (1960,

1970, 1980)

* Partial string comparison. Name = ‘John*’

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S129

TABLE 3.8 Continued

Function Description Example

Page 143: Delphi Kylix Database Development

To filter a dataset, set its Filter property to the string used for filtering, and then set theFiltered property to True. For example, the following code snippet filters out all employeeswhose 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 thefilter, like this:

ClientDataSet1.Filter := ‘Name LIKE ‘ + QuotedStr(‘P%’);

To remove the filter, set the Filtered property to False. You don’t have to set the Filterproperty to an empty string to remove the filter (which means that you can toggle the mostrecent filter on and off by switching the value of Filtered from True to False).

You can apply more advanced filter criteria by handling the dataset’s OnFilterRecord event(instead of setting the Filter property). For example, say that you want to filter out allemployees whose last names sound like Smith. This would include Smith, Smythe, and possiblyothers. Assuming that you have a Soundex function available, you could write a filter methodlike the following:

procedure TForm1.ClientDataSet1FilterRecord(DataSet: TDataSet;var Accept: Boolean);

beginAccept := 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 Acceptto False, the record is hidden.

After you set up an OnFilterRecord event handler, you can simply setTClientDataSet.Filtered to True. You don’t need to set the Filter property at all.

The following example demonstrates different filter and range techniques.

Listing 3.4 contains the source code for the main form.

LISTING 3.4 RangeFilter—MainForm.pas

unit MainForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,DB, DBClient, QExtCtrls, QGrids, QDBGrids;

Chapter 3130

Page 144: Delphi Kylix Database Development

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

uses FilterForm, RangeForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);

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

end;

procedure TfrmMain.btnFilterClick(Sender: TObject);varfrmFilter: TfrmFilter;

beginfrmFilter := TfrmFilter.Create(nil);

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S131

LISTING 3.4 Continued

Page 145: Delphi Kylix Database Development

tryif frmFilter.ShowModal = mrOk then beginClientDataSet1.Filter := frmFilter.Filter;ClientDataSet1.Filtered := True;

end;finallyfrmFilter.Free;

end;end;

procedure TfrmMain.btnClearFilterClick(Sender: TObject);beginClientDataSet1.Filtered := False;

end;

procedure TfrmMain.btnRangeClick(Sender: TObject);varfrmRange: TfrmRange;

beginfrmRange := TfrmRange.Create(nil);tryif frmRange.ShowModal = mrOk thenClientDataSet1.SetRange([frmRange.LowValue], [frmRange.HighValue]);

finallyfrmRange.Free;

end;end;

procedure TfrmMain.btnClearRangeClick(Sender: TObject);beginClientDataSet1.CancelRange;

end;

end.

As you can see, the main form loads the employee dataset from a disk, creates an index on theSalary field, and makes the index active. It then enables the user to apply a range, a filter, orboth to the dataset.

Listing 3.5 contains the source code for the filter form. The filter form is a simple form thatenables the user to select the field on which to filter, and to enter a value on which to filter.

Chapter 3132

LISTING 3.4 Continued

Page 146: Delphi Kylix Database Development

LISTING 3.5 RangeFilter—FilterForm.pas

unit FilterForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,QExtCtrls;

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

privatefunction GetFilter: string;{ Private declarations }

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

end;

implementation

{$R *.xfm}

{ TfrmFilter }

function TfrmFilter.GetFilter: string;beginResult := 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 thevalues of the three input controls into a filter string and returns it to the main application.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S133

Page 147: Delphi Kylix Database Development

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

LISTING 3.6 RangeFilter—RangeForm.pas

unit RangeForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,QStdCtrls;

typeTfrmRange = class(TForm)pnlClient: TPanel;pnlBottom: TPanel;Label1: TLabel;Label2: TLabel;ecLower: TEdit;ecUpper: TEdit;btnOk: TButton;btnCancel: TButton;procedure btnOkClick(Sender: TObject);

privatefunction 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;beginResult := StrToFloat(ecUpper.Text);

end;

function TfrmRange.GetLowValue: Double;

Chapter 3134

Page 148: Delphi Kylix Database Development

beginResult := StrToFloat(ecLower.Text);

end;

procedure TfrmRange.btnOkClick(Sender: TObject);varLowValue: Double;HighValue: Double;

begintryLowValue := StrToFloat(ecLower.Text);HighValue := StrToFloat(ecUpper.Text);

if LowValue > HighValue then beginModalResult := mrNone;ShowMessage(‘The upper salary must be >= the lower salary’);

end;exceptModalResult := mrNone;ShowMessage(‘Both values must be a valid number’);

end;end;

end.

Figure 3.8 shows the RangeFilter application in operation.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S135

LISTING 3.6 Continued

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

Page 149: Delphi Kylix Database Development

SearchingIn addition to filtering out uninteresting records from a client dataset, TClientDataSet providesa number of methods for quickly locating a specific record. Some of these methods require anindex to be active on the dataset, and others do not. The search methods are described in detailin the following sections.

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

LocateLocate is perhaps the most general purpose of the TClientDataSet search methods. You canuse Locate to search for a record based on any given field or combination of fields. Locate canalso 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 multiplefields, separate them by semicolons (for example, ‘Name;Birthday’).

The second parameter, KeyValues, represents the values to search for. The number of valuesmust match the number of key fields exactly. If there is only one search field, you can simplypass the value to search for here. To search for multiple values, you must pass the values as avariant 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. Table3.11 lists the available options.

TABLE 3.11 Locate Options

Value Description

loPartialKey KeyValues do not necessarily represent an exact match. Locatefinds the first record whose field value starts with the value specifiedin KeyValues.

loCaseInsensitive Locate ignores case when searching for string fields.

Chapter 3136

Page 150: Delphi Kylix Database Development

Both options pertain to string fields only. They are ignored if you specify them for a nonstringsearch.

Locate returns True if a matching record is found, and False if no match is found. In case of a match, the record is made current.

The following examples help illustrate the options:

ClientDataSet1.Locate(‘Name’, ‘John Smith’, []);

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

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

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

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

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

LookupLookup is similar in concept to Locate, except that it doesn’t change the current record pointer.Instead, Lookup returns the values of one or more fields in the record. Also, Lookup does notaccept an Options parameter, so you can’t perform a lookup that is based on a partial key orthat is not case sensitive.

Lookup is defined like this:

function Lookup(const KeyFields: string; const KeyValues: Variant;const ResultFields: string): Variant; override;

KeyFields and KeyValues specify the fields to search and the values to search for, just as withthe Locate method. ResultFields specifies the fields for which you want to return data. Forexample, to return the birthday of the employee named John Doe, you could write the following code:

varV: Variant;

beginV := ClientDataSet1.Lookup(‘Name’, ‘John Doe’, ‘Birthday’);

end;

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S137

Page 151: Delphi Kylix Database Development

The following code returns the name and birthday of the employee with ID number 100.

varV: Variant;

beginV := ClientDataSet1.Lookup(‘ID’, 100, ‘Name;Birthday’);

end;

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

Chapter 3138

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

NOTE

The following code snippet shows how you can access the results that are returned from Lookup.

varV: Variant;

beginV := ClientDataSet1.Lookup(‘ID’, 100, ‘Name’);if not VarIsNull(V) thenShowMessage(‘ID 100 refers to ‘ + V);

V := ClientDataSet1.Lookup(‘ID’, 200, ‘Name;Birthday’);if not VarIsNull(V) thenShowMessage(‘ID 200 refers to ‘ + V[0] + ‘, born on ‘ + DateToStr(V[1]));

end;

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

FindKeyFindKey searches for an exact match on the key fields of the current index. For example, if thedataset is currently indexed by ID, FindKey searches for an exact match on the ID field. If thedataset is indexed by last and first name, FindKey searches for an exact match on both the lastand the first name.

Page 152: Delphi Kylix Database Development

FindKey takes a single parameter, which specifies the value(s) to search for. It returns aBoolean value that indicates whether a matching record was found. If no match was found, thecurrent record pointer is unchanged. If a matching record is found, it is made current.

The parameter to FindKey is actually an array of values, so you need to put the values inbrackets, as the following examples show:

if ClientDataSet.FindKey([25]) thenShowMessage(‘Found ID 25’);

...if ClientDataSet.FindKey([‘Doe’, ‘John’]) thenShowMessage(‘Found John Doe’);

You need to ensure that the values you search for match the current index. For that reason, youmight want to set the index before making the call to FindKey. The following code snippetillustrates this:

ClientDataSet1.IndexName := ‘byID’;if ClientDataSet.FindKey([25]) thenShowMessage(‘Found ID 25’);

...ClientDataSet1.IndexName := ‘byName’;if ClientDataSet.FindKey([‘Doe’, ‘John’]) thenShowMessage(‘Found John Doe’);

FindNearestFindNearest works similarly to FindKey, except that it finds the first record that is greaterthan or equal to the value(s) passed to it. This depends on the current value of theKeyExclusive property.

If KeyExclusive is False (the default), FindNearest finds the first record that is greater thanor equal to the passed-in values. If KeyExclusive is True, FindNearest finds the first recordthat is greater than the passed-in values.

If FindNearest doesn’t find a matching record, it moves the current record pointer to the endof the dataset.

GotoKeyGotoKey performs the same function as FindKey, except that you set the values of the searchfield(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;

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S139

Page 153: Delphi Kylix Database Development

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 thesearch. For example, the following code snippet shows how to search for John Doe, and thenlater 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 SetKeyClientDataSet1.EditKey;ClientDataSet1.FieldByName(‘Last’).AsString := ‘Smith’;ClientDataSet1.GotoKey;

GotoNearestGotoNearest works similarly to GotoKey, except that it finds the first record that is greater thanor equal to the value(s) passed to it. This depends on the current value of the KeyExclusiveproperty.

If KeyExclusive is False (the default), GotoNearest finds the first record that is greater thanor equal to the field values set after a call to either SetKey or EditKey. If KeyExclusive isTrue, GotoNearest finds the first record that is greater than the field values set after callingSetKey or EditKey.

If GotoNearest doesn’t find a matching record, it moves the current record pointer to the endof 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 illustratesthe various indexed and nonindexed searching techniques supported by TClientDataSet.

Chapter 3140

Page 154: Delphi Kylix Database Development

unit MainForm;

interface

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

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

uses SearchForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);begin

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S141

LISTING 3.7 Search—MainForm.pas

Page 155: Delphi Kylix Database Development

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

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

end;

procedure TfrmMain.btnGetBookmarkClick(Sender: TObject);beginif Assigned(FBookmark) thenClientDataSet1.FreeBookmark(FBookmark);

FBookmark := ClientDataSet1.GetBookmark;end;

procedure TfrmMain.btnGotoBookmarkClick(Sender: TObject);beginif Assigned(FBookmark) thenClientDataSet1.GotoBookmark(FBookmark)

elseShowMessage(‘No bookmark assigned’);

end;

procedure TfrmMain.btnSetRecNoClick(Sender: TObject);varValue: string;

beginValue := ‘1’;if InputQuery(‘RecNo’, ‘Enter Record Number’, Value) thenClientDataSet1.RecNo := StrToInt(Value);

end;

procedure TfrmMain.btnSearchClick(Sender: TObject);varfrmSearch: TfrmSearch;

beginfrmSearch := TfrmSearch.Create(nil);tryif frmSearch.ShowModal = mrOk then begincase TSearchMethod(frmSearch.grpMethod.ItemIndex) ofsmLocate:ClientDataSet1.Locate(‘Name’, frmSearch.ecName.Text,[loPartialKey, loCaseInsensitive]);

Chapter 3142

LISTING 3.7 Continued

Page 156: Delphi Kylix Database Development

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

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

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

ClientDataSet1.GotoKey;end;

smGotoNearest: beginClientDataSet1.SetKey;ClientDataSet1.FieldByName(‘Name’).AsString :=frmSearch.ecName.Text;

ClientDataSet1.GotoNearest;end;

end;end;

finallyfrmSearch.Free;

end;end;

procedure TfrmMain.btnLookupClick(Sender: TObject);varValue: string;V: Variant;

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

end;end;

end.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S143

LISTING 3.7 Continued

Page 157: Delphi Kylix Database Development

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

LISTING 3.8 Search—SearchForm.pas

unit SearchForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QExtCtrls,QStdCtrls;

typeTSearchMethod = (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.

Chapter 3144

Page 158: Delphi Kylix Database Development

FIGURE 3.9Search demonstrates indexed and nonindexed searches.

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

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

• 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 ina given order, and to locate records that match a certain criteria.

• Filters and ranges can be used to limit the amount of data that is visible in the dataset.Ranges are useful when the relevant data is stored in a consecutive sequence of records.Unlike ranges, filters do not require an index to be set before applying them.

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

In the following chapter, I’ll discuss more advanced client dataset functionality.

Client Dataset Basics

3

CLIEN

TD

ATA

SETB

ASIC

S145

Page 159: Delphi Kylix Database Development
Page 160: Delphi Kylix Database Development

CHAPTER

4Advanced Client DatasetOperations

IN THIS CHAPTER• Dataset Events 148

• Disabling Data-Aware Components 158

• BLOBs 162

• Nested Datasets 172

• Undo Support 176

• Cloning Data from Another Client Dataset 186

• Maintained Aggregates 192

• Miscellaneous Properties 197

Page 161: Delphi Kylix Database Development

Chapter 4148

The preceding chapter introduced you to TClientDataSet and discussed much of its basicfunctionality in detail. In this chapter, I’ll explore a number of more advanced client datasetcapabilities, including:

• Dataset Events

• Disabling Data-Aware Components

• BLOBs

• Nested Datasets

• Undo Support

• Cloning Data from Another Client Dataset

• Maintained Aggregates

• Miscellaneous Properties

Dataset EventsClient datasets support a large number of events. Some of these events are useful in single-tierapplications (such as the ones we’re developing in this chapter), and some are only useful inmultitier applications (which we’ll be developing in future chapters).

This chapter discusses dataset events that are useful in all applications, including single-tierand multitier. Broadly speaking, these events fall into three categories: BeforeXxx notificationevents, AfterXxx notification events, and other events. BeforeXxx and AfterXxx notificationevents are fired by Delphi before and after interesting activities occur. For purposes of this discussion, interesting refers to normal, everyday activities that the dataset performs—activitiesfor 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 deletionsfrom a dataset. One way to do this is to display a confirmation message to the user at everypoint 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 toimplement the confirmation altogether.

• It doesn’t work in cases where VCL/CLX implicitly deletes a record. If you pressCtrl+Delete while in a data-aware grid, VCL/CLX handles the deletion for you with nocoding on your part.

Page 162: Delphi Kylix Database Development

A better way to code for this situation is to handle the BeforeDelete event and display a messagethere asking the user if he is sure he wants to delete the record. As with most BeforeXxx eventhandlers, raising an exception (usually Abort) inside the event handler prevents the operationfrom taking place.

Later in this section, I’ll present a sample application that illustrates this technique.

Table 4.1 lists the client dataset’s BeforeXxx events and their uses.

TABLE 4.1 Client Dataset BeforeXxx Events

Event Description

BeforeCancel Triggered by the Cancel method just before the edits to the currentrecord are canceled. You might take advantage of this event to confirmthat the user does indeed want to cancel any changes that he has made.

BeforeClose Called immediately before the dataset is closed.

BeforeDelete Called just before the current record in the dataset is deleted. This is a good place to confirm that the user really wants to delete the record.

BeforeEdit Triggered by the Edit method immediately before the dataset is putinto edit mode. You could use this event as a handy place to restrictediting (by raising an Abort exception).

BeforeInsert Triggered by the Append and Insert methods immediately beforethe dataset is put into insert mode. You could use this event as a wayto restrict editing (by raising an Abort exception).

BeforeOpen Called just before the dataset is opened.

BeforePost Occurs just before the data in a newly inserted or edited record isposted to the dataset. This is a good place to perform validation onthe data.

BeforeScroll Triggered just before the dataset moves to a new record. This occurswhen the dataset is opened during a First, Next, Prior, or Lastoperation; during searches; and when a range or filter is applied to the dataset.

In contrast to the BeforeXxx event handlers, which are triggered before an event actuallyoccurs (and therefore enable you to prevent the event from occurring), AfterXxx event han-dlers are triggered after the event has occurred to let you know that the operation in questionhas occurred successfully.

Table 4.2 lists the AfterXxx event handlers, which mirror the BeforeXxx event handlers.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

149

Page 163: Delphi Kylix Database Development

TABLE 4.2 Client Dataset AfterXxx Events

Event Description

AfterCancel Triggered after a Cancel method completes.

AfterClose Occurs just after the dataset is closed.

AfterDelete Triggered immediately after a record in the dataset is deleted.

AfterEdit Called after the dataset is put into edit mode as a result of an Editmethod call.

AfterInsert Called after the dataset is put into insert mode as a result of anInsert or Append method call.

AfterOpen Occurs just after the dataset is opened.

AfterPost Triggered after a record is successfully posted to the dataset.

AfterScroll Triggered just after the dataset moves to a new record. This occurswhen the dataset is opened during a First, Next, Prior, or Lastoperation; during searches; and when a range or filter is applied tothe dataset.

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

TABLE 4.3 Other Event Handlers

EventDescription

OnCalcFields As discussed in Chapter 6, “Data-Aware Grids,” this event is used toprovide values for calculated fields.

OnDeleteError Fired if there is an error when deleting a record from the dataset (for example, if the dataset is read-only).

OnEditError Fired if there is an error when putting the dataset into edit mode (for example, if the dataset is read-only).

OnFilterRecord As discussed in Chapter 6, this event is called to enable the user toprovide advanced filtering on a dataset.

OnNewRecord Triggered whenever a new record is created, but before it is editedor posted to the dataset. This is a good place to set default values forthe record. Inside this event handler, the dataset is already in insertmode, so you shouldn’t call TClientDataSet.Insert from withinthis event handler.

OnPostError Fired if an error occurs when attempting to post a record to thedataset (such as a key conflict).

Chapter 4150

Page 164: Delphi Kylix Database Development

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

LISTING 4.1 EventLog—MainForm.pas

unit MainForm;

interface

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

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

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

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

151

Page 165: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

uses OptionsForm;

{$R *.xfm}

procedure TfrmMain.Log(EventType: TLogEventType);constEventText: array[TLogEventType] of string = (‘BeforeCancel’, ‘BeforeClose’, ‘BeforeDelete’, ‘BeforeEdit’,‘BeforeInsert’, ‘BeforeOpen’, ‘BeforePost’, ‘BeforeScroll’,‘AfterCancel’, ‘AfterClose’, ‘AfterDelete’, ‘AfterEdit’,‘AfterInsert’, ‘AfterOpen’, ‘AfterPost’, ‘AfterScroll’);

varListItem: TListItem;

beginListItem := lvLog.Items.Add;ListItem.Caption := EventText[EventType];

end;

Chapter 4152

LISTING 4.1 Continued

Page 166: Delphi Kylix Database Development

procedure TfrmMain.FormCreate(Sender: TObject);beginFLogScrollEvents := True;FPromptOnDelete := True;

end;

procedure TfrmMain.ClientDataSet1AfterCancel(DataSet: TDataSet);beginLog(logAfterCancel);

end;

procedure TfrmMain.ClientDataSet1AfterClose(DataSet: TDataSet);beginLog(logAfterClose);

end;

procedure TfrmMain.ClientDataSet1AfterDelete(DataSet: TDataSet);beginLog(logAfterDelete);

end;

procedure TfrmMain.ClientDataSet1AfterEdit(DataSet: TDataSet);beginLog(logAfterEdit);

end;

procedure TfrmMain.ClientDataSet1AfterInsert(DataSet: TDataSet);beginLog(logAfterInsert);

end;

procedure TfrmMain.ClientDataSet1AfterOpen(DataSet: TDataSet);beginLog(logAfterOpen);

end;

procedure TfrmMain.ClientDataSet1AfterPost(DataSet: TDataSet);beginLog(logAfterPost);

end;

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

153

LISTING 4.1 Continued

Page 167: Delphi Kylix Database Development

procedure TfrmMain.ClientDataSet1AfterScroll(DataSet: TDataSet);beginif FLogScrollEvents thenLog(logAfterScroll);

lblRecPos.Caption := Format(‘Record %d of %d’,[DataSet.RecNo, DataSet.RecordCount]);

end;

procedure TfrmMain.ClientDataSet1BeforeCancel(DataSet: TDataSet);beginLog(logBeforeCancel);

end;

procedure TfrmMain.ClientDataSet1BeforeClose(DataSet: TDataSet);beginLog(logBeforeClose);

end;

procedure TfrmMain.ClientDataSet1BeforeDelete(DataSet: TDataSet);beginLog(logBeforeDelete);

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

end;end;

procedure TfrmMain.ClientDataSet1BeforeEdit(DataSet: TDataSet);beginLog(logBeforeEdit);

end;

procedure TfrmMain.ClientDataSet1BeforeInsert(DataSet: TDataSet);beginLog(logBeforeInsert);

end;

procedure TfrmMain.ClientDataSet1BeforeOpen(DataSet: TDataSet);beginLog(logBeforeOpen);

end;

Chapter 4154

LISTING 4.1 Continued

Page 168: Delphi Kylix Database Development

procedure TfrmMain.ClientDataSet1BeforePost(DataSet: TDataSet);beginLog(logBeforePost);

end;

procedure TfrmMain.ClientDataSet1BeforeScroll(DataSet: TDataSet);beginif FLogScrollEvents thenLog(logBeforeScroll);

end;

procedure TfrmMain.btnConnectClick(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

end;

procedure TfrmMain.btnDisconnectClick(Sender: TObject);beginClientDataSet1.Close;

end;

procedure TfrmMain.btnClearLogClick(Sender: TObject);beginlvLog.Items.Clear;

end;

procedure TfrmMain.btnOptionsClick(Sender: TObject);varfrmOptions: TfrmOptions;

beginfrmOptions := TfrmOptions.Create(nil);tryfrmOptions.LogScrollEvents := FLogScrollEvents;frmOptions.PromptOnDelete := FPromptOnDelete;

if frmOptions.ShowModal = mrOk then beginFLogScrollEvents := frmOptions.LogScrollEvents;FPromptOnDelete := frmOptions.PromptOnDelete;

end;finallyfrmOptions.Free;

end;end;

end.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

155

LISTING 4.1 Continued

Page 169: Delphi Kylix Database Development

Figure 4.1 shows the application at runtime. A data-aware grid occupies the top half of themain form enabling the user to insert, edit, and delete records. The middle section of the formcontains a list view, which records a log of all activities performed on the dataset. On the bot-tom of the form are buttons to open and close the dataset, clear the log, and set options for thedemo.

The user can select whether to log scroll events and whether to verify deletions by clicking theOptions… button at the bottom of the main form, as Figure 4.1 illustrates.

Chapter 4156

FIGURE 4.1The EventLog application demonstrates client dataset events.

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

LISTING 4.2 EventLog—OptionsForm.pas

unit OptionsForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms,QDialogs, QStdCtrls, QExtCtrls;

typeTfrmOptions = class(TForm)pnlClient: TPanel;pnlBottom: TPanel;

Page 170: Delphi Kylix Database Development

btnOk: TButton;btnCancel: TButton;cbLogScrollEvents: TCheckBox;cbPromptOnDelete: TCheckBox;

privateprocedure SetLogScrollEvents(const Value: Boolean);function GetLogScrollEvents: Boolean;function GetPromptOnDelete: Boolean;procedure SetPromptOnDelete(const Value: Boolean);{ Private declarations }

public{ Public declarations }property LogScrollEvents: Booleanread GetLogScrollEvents write SetLogScrollEvents;

property PromptOnDelete: Booleanread GetPromptOnDelete write SetPromptOnDelete;

end;

implementation

{$R *.xfm}

{ TfrmOptions }

function TfrmOptions.GetLogScrollEvents: Boolean;beginResult := cbLogScrollEvents.Checked;

end;

function TfrmOptions.GetPromptOnDelete: Boolean;beginResult := cbPromptOnDelete.Checked;

end;

procedure TfrmOptions.SetLogScrollEvents(const Value: Boolean);begincbLogScrollEvents.Checked := Value;

end;

procedure TfrmOptions.SetPromptOnDelete(const Value: Boolean);begincbPromptOnDelete.Checked := Value;

end;

end.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

157

LISTING 4.2 Continued

Page 171: Delphi Kylix Database Development

Disabling Data-Aware ComponentsThis topic actually applies to all Delphi datasets, but I’m discussing it within the context ofclient datasets because you typically don’t connect data-aware components directly to adbExpress dataset. Usually, data-aware components are connected to a client dataset.

As you’ve learned in this chapter, data-aware components actively track the current record inthe dataset that they’re connected to. Although this is usually a good thing, at times you wantto prevent data-aware components from updating. This happens most often when you arescrolling through a dataset to perform an operation on the records, and you don’t want to seeall the data-aware components rapidly updating as you do. For example, take a look at the following code snippet:

varBookmark: TBookmarkStr;

beginBookmark := ClientDataSet1.Bookmark;tryClientDataSet1.First;while not ClientDataSet1.EOF do beginif ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then beginClientDataSet1.Edit;ClientDataSet1.FieldByName(‘Salary’).AsFloat :=ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;

ClientDataSet1.Post;end;ClientDataSet1.Next;

end;finallyClientDataSet1.Bookmark := Bookmark);

end;end;

This code loops through all records in the dataset, giving anyone who makes less than $30,000per year a 5% raise.

There is nothing wrong with this code from the standpoint that it does what it is intended todo. It even remembers the current record so that it can reposition the dataset correctly when it’sfinished.

The problem with this code is that it’s slow. If you run it against the 10,000 record employeedataset that we created in the preceding chapter, you’ll see the grid scroll through all therecords in the dataset as they are updated. (The example application at the end of this sectionshows this effect.)

Chapter 4158

Page 172: Delphi Kylix Database Development

The solution is to disable all data-aware components (namely, the TDBGrid) attached to thedataset before beginning this operation. To do that, you simply call the DisableControlsmethod before performing the lengthy operation, and then call EnableControls when you’refinished. The following code snippet shows the updated procedure:

varBookmark: TBookmarkStr;

beginClientDataSet1.DisableControls;tryBookmark := ClientDataSet1.Bookmark;tryClientDataSet1.First;while not ClientDataSet1.EOF do beginif ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then beginClientDataSet1.Edit;ClientDataSet1.FieldByName(‘Salary’).AsFloat :=ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;

ClientDataSet1.Post;end;ClientDataSet1.Next;

end;finallyClientDataSet1.Bookmark := Bookmark;

end;finallyClientDataSet1.EnableControls;

end;end;

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

Note that the calls to DisableControls and EnableControls are reference counted. If you callDisableControls three times in your code, then you will need to call EnableControls threetimes before data-aware controls are updated again.

Listing 4.3 contains the complete source code for the Updates application.

LISTING 4.3 Updates—MainForm.pas

unit MainForm;

interface

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

159

Page 173: Delphi Kylix Database Development

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

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

end;

procedure TfrmMain.PerformWork;varBookmark: TBookmark;

beginBookmark := ClientDataSet1.GetBookmark;tryClientDataSet1.First;while not ClientDataSet1.EOF do begin

Chapter 4160

LISTING 4.3 Continued

Page 174: Delphi Kylix Database Development

if ClientDataSet1.FieldByName(‘Salary’).AsFloat < 30000.0 then beginClientDataSet1.Edit;ClientDataSet1.FieldByName(‘Salary’).AsFloat :=ClientDataSet1.FieldByName(‘Salary’).AsFloat * 1.05;

ClientDataSet1.Post;end;ClientDataSet1.Next;

end;

ClientDataSet1.GotoBookmark(Bookmark);finallyClientDataSet1.FreeBookmark(Bookmark);

end;end;

procedure TfrmMain.btnBaselineClick(Sender: TObject);vart1, t2: DWord;

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

end;

procedure TfrmMain.btnDisableEnableClick(Sender: TObject);vart1, t2: DWord;

begint1 := GetTickCount;ClientDataSet1.DisableControls;tryPerformWork;

finallyClientDataSet1.EnableControls;

end;

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

end;

end.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

161

LISTING 4.3 Continued

Page 175: Delphi Kylix Database Development

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

BLOBsBLOBs, 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, orany other chunk of bytes; BLOBs are an essential part of your database-programming repertoire.

In this section, I’ll show you how to effectively store BLOBs in a client dataset and how toretrieve them later. In the pages to follow, I’ll focus specifically on notes, images, streamedcomponents, and generic BLOB storage.

As with other field types, you can create BLOB fields either at design time or at runtime. Thefollowing 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.

NotesOne of the most common ways to use a BLOB field is to store notes, or free-format text. Forsmall 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 methodon 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;

ImagesAnother common use of BLOB fields is to store images. You might want to write a Delphiapplication to catalog the pictures you’ve taken on your digital camera, or you might want totrack scanned documents in a paperless office. Either way, a BLOB field provides the necessarysupport to store these images in a database.

Chapter 4162

Page 176: Delphi Kylix Database Development

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 imagetype 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 todetermine how to store/load the image.

• Writing a value to the BLOB field indicating the image type, immediately followed bythe 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 TypeOne way to track the type of image stored in a dataset is to add a separate field, perhaps namedImageType, to track the type of image. Say, for the sake of argument, that your application canstore BMPs and JPGs. You would set up constants for each image type, like this:

constIMAGE_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 wheneverthe user loads an image, and then reset it to IMAGE_NONE if the user clears the image.

Streaming the Image Type as Part of the BLOB FieldWith a little extra code, you can dispense with the additional field and store the image type inthe BLOB field along with the image itself. Figure 4.2 shows conceptually what is involvedwith this method.

The following pseudo-code shows how you might implement this method:

procedure SaveImage;beginOpenOutputStream;WriteImageType;WriteImageData;

end;

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

163

Page 177: Delphi Kylix Database Development

procedure LoadImage;varImageType: Integer;

beginOpenInputStream;ImageType := ReadImageType;case ImageType ofIMAGE_BMP: ReadBitmap;IMAGE_JPG: ReadJPEG;

end;end;

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

Chapter 4164

Fixed-Length Image Header

Image Data

File extension or other designationto describe the image type

Image data in a format suitable forreading into an TBitmap, TJPEG,or other image class.

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

Third-Party Imaging LibrariesThird-party imaging libraries generally follow the steps outlined in the preceding section (thatis, 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 thatafter you decide on an imaging library and implement it in your applications, you shouldn’texpect that you can arbitrarily swap out the library with a different one at a later date. Switchingimage libraries might require you to write a data conversion program for your BLOB data.

Page 178: Delphi Kylix Database Development

Streamed DataIn 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 savedata to a BLOB, and then read it back in later.

procedure SaveListToBlob(List: TList);varStream: TStream;Num: Integer;Index: Integer;

beginStream := ClientDataSet1.CreateBlobStream(ClientDataSet1.FieldByName(‘DATA’), bmWrite);

try// Write out the number of integersNum := List.Count;Stream.Write(Num, sizeof(Num));

for Index := 0 to List.Count - 1 do beginNum := Integer(List[Index]);Stream.Write(Num, sizeof(Num));

end;finallyStream.Free;

end;end;

procedure LoadListFromBlob(List: TList);varStream: TStream;Count: Integer;Index: Integer;Num: Integer;

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

165

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

NOTE

Page 179: Delphi Kylix Database Development

beginStream := ClientDataSet1.CreateBlobStream(ClientDataSet1.FieldByName(‘DATA’), bmRead);

tryList.Clear;

Stream.Read(Count, sizeof(Count));for Index := 0 to Count - 1 do beginStream.Read(Num, sizeof(Num));List.Add(Pointer(Num));

end;finallyStream.Free;

end;end;

Note the use of Stream.Write and Stream.Read in the previous procedures. Both of thesemethods take a reference to the data to be written as the first parameter, and the number ofbytes to write as the second parameter. Saving a block of data is as straightforward as makingrepeated calls to TStream.Write. You must make sure to read the data in exactly the sameorder as it was written in, or you will end up with an exception at best and corrupted data atworst.

Note also the use of the TDataSet method CreateBlobStream to create a blob stream suitablefor the dataset. Many beginning Delphi database programmers attempt to callTBlobStream.Create, like this:

varStream: TBlobStream;

beginStream := TBlobStream.Create(...);try// Read from or write to the stream here.

finallyStream.Free;

end;end;

The problem with this approach is that TBlobStream is specific to the BDE. Creating aninstance of TBlobStream will not work with non-BDE datasets, such as TClientDataSet. Tocreate a blob stream that will work with the current dataset, always call the dataset’sCreateBlobStream method, like this:

Chapter 4166

Page 180: Delphi Kylix Database Development

varStream: TStream;

beginStream := TheDataSet.CreateBlobStream(...);try// Read from or write to the stream here.

finallyStream.Free;

end;end;

Notice that Stream is defined as a TStream, which is the ancestor class for streams. The actualstream 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 ComponentsAlthough the concept of streaming data relies on you (the programmer) to make sure to readand write data in the same order, by creating and streaming a component, you can let Delphi’sbuilt-in streaming mechanism do the work for you.

Delphi provides streaming support for components derived from TPersistent, as well ashelper functions for the TCollection family of classes.

To make streaming the list of integers that were described previously more automatic, let’screate a component wrapper for the data.

TIntegerItem = class(TCollectionItem)privateFNumber: Integer;

publishedproperty Number: Integer read FNumber write FNumber;

end;

TIntegerList = class(TComponent)privateFIntList: TCollection;

publishedproperty IntList: TCollection read FIntList;

end;

Granted, this might be overkill for a data structure as simple as a list of integers, but the sameconcept works for a component containing multiple fields or complex subdata.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

167

Page 181: Delphi Kylix Database Development

File BLOBsAnother common use of BLOB fields is storing an entire file inside a BLOB. For example, saythat you have a large number of PDF documents that you want to catalog and allow your usersto read.

By storing the PDF files in a BLOB field, you can fairly easily create an application that enablesthe user to search for the PDF by category or keyword, and then view the file on his computer.

You can load an external file into a BLOB field by using the field’s LoadFromFile method, asthe following code snippet shows:

varB: TBlobField;

beginB := 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 AcrobatReader), you would write the following:

varB: TBlobField;

beginB := ClientDataSet1.FieldByName(‘AttachedFile’);B.SaveToFile(‘C:\TEMP.PDF’);

end;

Limitations of BLOB FieldsFor all their usefulness, BLOBs do have a couple of limitations. Namely:

• You can’t (currently) perform a filter on a BLOB field. New versions of relational databases, such as Informix, support searching on BLOBs. Once this functionality isadded to the core dbExpress technology, TClientDataSet might very well be updated tosupport it also.

Chapter 4168

It is beyond the scope of this book to present a detailed discussion of Delphi’s streamingsupport. For more information on streaming, please refer to the Component Writer’sGuide or to the ultimate reference, the VCL/CLX source itself. If you are fortunateenough to own a copy of Danny Thorpe’s Delphi Component Design, it also has aninformative chapter on Delphi’s streaming mechanism.

NOTE

Page 182: Delphi Kylix Database Development

• You can’t perform a locate or other search technique on a BLOB field.

• The reconciliation features of DataSnap, which are discussed in Chapter 8, “DataSnap,”don’t work with BLOB fields. However, there is a workaround, as you’ll see in that samechapter.

The following example demonstrates some of the BLOB techniques discussed in this section.Listing 4.4 shows the source code for the BLOBs application.

LISTING 4.4 BLOBs—MainForm.pas

unit MainForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls,DB, QDBCtrls, QExtCtrls, QComCtrls, DBClient, JPEG;

typeTfrmMain = 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;

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

169

Page 183: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.CreateDataSet;

end;

procedure TfrmMain.DataSource1DataChange(Sender: TObject; Field: TField);varBlobStream: TStream;JPEGImage: TJPegImage;Ext: string;

beginif (Field = nil) or (Field = ClientDataSet1Image) then beginif ClientDataSet1ImageType.AsString <> ‘’ then beginBlobStream := ClientDataSet1.CreateBlobStream(ClientDataSet1Image,bmRead);

tryExt := UpperCase(ClientDataSet1ImageType.AsString);if Ext = ‘.BMP’ thenImage1.Picture.Bitmap.LoadFromStream(BlobStream)

else if Ext = ‘.JPG’ then beginJPEGImage := TJPEGImage.Create;tryJPEGImage.LoadFromStream(BlobStream);

Chapter 4170

LISTING 4.4 Continued

Page 184: Delphi Kylix Database Development

Image1.Picture.Assign(JPEGImage);finallyJPEGImage.Free;

end;end;

finallyBlobStream.Free;

end;end elseImage1.Picture := nil;

end;end;

procedure TfrmMain.btnLoadImageClick(Sender: TObject);beginif OpenPictureDialog1.Execute then beginClientDataSet1.Edit;ClientDataSet1ImageType.AsString :=ExtractFileExt(OpenPictureDialog1.FileName);

ClientDataSet1Image.LoadFromFile(OpenPictureDialog1.FileName);end;

end;

procedure TfrmMain.btnClearImageClick(Sender: TObject);beginImage1.Picture := nil;

end;

procedure TfrmMain.btnLoadAttachmentClick(Sender: TObject);beginif OpenDialog1.Execute then beginClientDataSet1.Edit;ClientDataSet1AttachedFile.AsString := OpenDialog1.FileName;ClientDataSet1Attachment.LoadFromFile(OpenDialog1.FileName);

end;end;

procedure TfrmMain.btnSaveAttachmentClick(Sender: TObject);beginif SaveDialog1.Execute thenClientDataSet1Attachment.SaveToFile(SaveDialog1.FileName);

end;

end.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

171

LISTING 4.4 Continued

Page 185: Delphi Kylix Database Development

The BLOBs application enables you to store a note, an image, and a file attachment in a singlerecord. 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 ofimage stored in the Image field. If ImageType is blank, the image is assumed to be NULL.Notice the way that the image is displayed—The program handles the data source’sDataChange event. If the Field parameter sent to the event is nil (or if it references the Imagefield), the Image component loads the picture from the Image field using one method for BMPfiles and another method for JPG files. Of course, a real application would recognize moreimage types than just the two.

Also note the parallel between the code used to load an image and the code used to load anattachment. Any file can be loaded into a BLOB field through the field’s LoadFromFilemethod, and can be saved back to disk through the field’s SaveToFile method.

The BLOBs application doesn’t save the data to disk (although it could do so by adding aSaveToFile method in the code), and it is rather useless. However, it serves to illustrate thecorrect way (or one of the correct ways, in the case of images) to use BLOB fields in your pro-grams.

Figure 4.3 shows this application at runtime.

Chapter 4172

FIGURE 4.3Notes and images are a part of many modern applications.

Nested DatasetsNested datasets are TClientDataSet’s answer to master/detail relationships. Nested datasetsphysically nest the detail dataset inside the master dataset as a field. Figure 4.4 illustrates thisconcept.

Page 186: Delphi Kylix Database Development

FIGURE 4.4The 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/childrelationship between three datasets. You can also create a parent with multiple children, or amixture of both (where one master contains three details and each of those contains twodetails).

When you save a nested dataset to a file or stream, the entire hierarchy is saved in a single fileor 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 allthe 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 afield 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. Createthe fields that make up the detail dataset.

The only remaining piece of business is to link them together. To do this, click the detaildataset and set the DataSetField property to the name of the DataSet field that you created onthe master. That’s all there is to it.

At this point, you can connect data sources and data-aware components to either dataset. Asyou scroll through the master dataset, the detail dataset is automatically updated to reflect onlythe 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

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

173

Customers

Name

Address

Phone

E-mail

Orders

Orders

Quantity

Description

Unit Price

Page 187: Delphi Kylix Database Development

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

LISTING 4.5 Nested—MainForm.pas

unit MainForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms,QDialogs, QExtCtrls, QDBCtrls, QGrids, QDBGrids, DB, DBClient, QStdCtrls;

typeTfrmMain = 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

Chapter 4174

Page 188: Delphi Kylix Database Development

{ Public declarations }end;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.cdsOrderCalcFields(DataSet: TDataSet);beginDataSet.FieldByName(‘TotalPrice’).AsFloat :=DataSet.FieldByName(‘Quantity’).AsInteger *DataSet.FieldByName(‘UnitPrice’).AsFloat;

end;

procedure TfrmMain.FormCreate(Sender: TObject);begincdsCustomer.CreateDataSet;

end;

procedure TfrmMain.btnSaveClick(Sender: TObject);beginif SaveDialog1.Execute thencdsCustomer.SaveToFile(SaveDialog1.FileName);

end;

procedure TfrmMain.btnLoadClick(Sender: TObject);beginif OpenDialog1.Execute thencdsCustomer.LoadFromFile(OpenDialog1.FileName);

end;

end.

Looking at the code, you see that there are only four methods. FormCreate creates the masterdataset. It is important to understand that this creates the detail dataset(s) also. You seldomneed 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.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

175

LISTING 4.5 Continued

Page 189: Delphi Kylix Database Development

When you run this application, you must either enter some data from scratch, or load thedatasets from a file. Accompanying the source code for this book is a previously created datafile named NESTED.CDS. You might want to load this file instead of entering customers andorders manually.

Figure 4.5 shows the Nested application at runtime.

Chapter 4176

FIGURE 4.5Nested 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 customerto customer in the top grid, the orders for that customer are displayed in the bottom grid. Thisis done automatically, with no coding effort.

Second, if you add or modify an order, you’ll notice that the customer record enters edit mode(as evidenced by the glyph displayed in the indicator column of the current customer record).If you programmatically manipulate nested datasets, you want to keep the following in mind:The master record needs to be posted after adding or modifying detail records.

Undo SupportTCustomClientDataSet and its descendents support built-in undo functionality, so you canprovide for what-if scenarios in your application. For instance, you can enable the user tochange the values of certain fields in the dataset (perhaps graphing, or otherwise displaying, an

Page 190: Delphi Kylix Database Development

analysis of the data). If the user doesn’t like the results, he can revert to the previous data byundoing his changes, either one at a time or in large chunks.

CancelYou are probably already familiar with the Cancel method, but I’ll mention it here anyway forcompleteness.

The lowest level of undo support is simply discarding changes to the current record before theyhave 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 LogTo support more advanced undo operations, client datasets incorporate a change log. Thechange log is used to remember each change that’s made to the dataset until the changes areeither merged into the data, undone, or canceled. The following sections examine the differentmethods used to commit and roll back changes.

The change log is saved with the data when you call SaveToFile or SaveToStream. When thedataset is read back in from the file or stream, the change log is in the same state that it wasprior to the save. This means that you can even perform undo operations between invocationsof your application.

LogChangesIn order for the change log to be active, the dataset’s LogChanges property must be set to True(which is the default). If you don’t intend to provide undo support in your application, you canset LogChanges to False, slightly reducing memory requirements and increasing performance.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

177

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 theclient dataset and applying those changes to the underlying database.

CAUTION

Page 191: Delphi Kylix Database Development

UndoLastChangeYou 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 thedataset should position itself to the record that was affected by the undo operation. IfFollowChange is True, the client dataset positions its cursor to the record that was undone orrestored. If FollowChange is False, the most recently modified record is still restored, but thecurrent 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 thefirst record in the dataset, and then modifies the second record again. Finally, it moves to theend of the dataset. The call to UndoLastChange undoes only the second change to the secondrecord, and repositions the dataset at the second record (because True was passed toUndoLastChange).

If you were to issue a second call to UndoLastChange, the modification to the first record in thedataset would be undone. A third call to UndoLastChange would undo the first modification tothe second record.

RevertRecordRevertRecord undoes all changes to the current record in the dataset. Modifying the precedingcode snippet slightly, we get the following:

Chapter 4178

Page 192: Delphi Kylix Database Development

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 movesoff the second record and back onto it. (This is just for the purpose of demonstration—Youdon’t need to do it.) Finally, the call to RevertRecord undoes both changes that were made tothe second record, but it leaves the change to the first record intact.

SavePointSavePoint provides a means of establishing a baseline for database operations, and thenreturning to that baseline at a later point in time.

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

varBaseline: Integer;

beginBaseline := ClientDataSet.SavePoint;

Later, after making modifications to the database, you can return to the baseline by setting theSavePoint property:

ClientDataSet.SavePoint := Baseline;

Setting SavePoint discards all changes made to the dataset after the baseline was established.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

179

Page 193: Delphi Kylix Database Development

You can also retrieve multiple savepoints, like this:

varBaseline1: Integer;Baseline2: Integer;

beginBaseline1 := ClientDataSet.SavePoint;// Perform more dataset work hereBaseline2 := ClientDataSet.SavePoint;

Be careful when using SavePoint along with RevertRecord or UndoLastChange. If youretrieve a SavePoint, and then undo your most recent modifications past the point of the save,an exception is raised. The following code, shown in Listing 4.6, is just asking for trouble:

LISTING 4.6 Incorrect Use of SavePoints

ClientDataSet1.First;ClientDataSet1.Next;ClientDataSet1.Edit;ClientDataSet1.FieldByName(‘Last’).AsString := ‘Williams’; // Change 1ClientDataSet1.Post;

ClientDataSet1.First;ClientDataSet1.Edit;ClientDataSet1.FieldByName(‘Last’).AsString := ‘Carter’; // Change 2ClientDataSet1.Post;

Baseline := ClientDataSet1.SavePoint; // 2 changes on the “stack”

ClientDataSet1.Next;ClientDataSet1.Edit;ClientDataSet1.FieldByName(‘First’).AsString := ‘Sam’; // Change 3ClientDataSet1.Post;

ClientDataSet1.UndoLastChange(True); // 2 changes on the “stack”ClientDataSet1.UndoLastChange(True); // 1 change on the “stack”ClientDataSet1.SavePoint := Baseline; // Exception is raised here

In this code snippet, two changes are made, and then a baseline is established. Next, a thirdchange is made, and then the third and second changes are undone. Finally, the code attemptsto revert to the save point. Because the change log has been reversed past the point of the savepoint, Delphi raises an exception.

Chapter 4180

Page 194: Delphi Kylix Database Development

CancelUpdatesThe final level of undo support is undoing all changes in the change log. To do this, simplycall CancelUpdates, like this:

ClientDataSet1.CancelUpdates;

CancelUpdates discards all changes made to all records in the dataset by clearing the changelog.

ChangeCountYou can determine how many changes were made to the dataset by looking at the ChangeCountproperty:

if ClientDataSet1.ChangeCount > 0 thenShowMessage(‘It is okay to call UndoLastChange’);

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

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

181

When a client dataset is connected to a provider, you seldom call MergeChangeLogdirectly. Instead, you call ApplyUpdates, which makes a call to MergeChangeLog afterthe changes have been applied to the underlying dataset.

NOTE

StatusFilterThe StatusFilter property provides for a type of filter on the dataset, but I didn’t discuss itin the “Ranges and Filters” section because it relates directly to the change log.

As records are added, modified, or deleted in a client dataset, they are tagged with a status.That status can be one (or more) of the values shown in Table 4.4.

TABLE 4.4 TUpdateStatus Values

Value Description

usUnmodified The record has not been modified in any way.

usInserted The record has been newly inserted into the dataset.

usModified The record was modified.

usDeleted The record has been deleted.

Page 195: Delphi Kylix Database Development

If you would like to view only those records that have been added to the dataset, you can setStatusFilter to usInserted. To view only added or modified records, set StatusFilter tousInserted, usModified.

Viewing the Change LogBecause, in reality, the change log is just another dataset, you can view it in a grid just like youcan with any other dataset. To do this, you need to assign the change log from the data of onedataset to the data of another dataset, like this:

cdsChangeLog.Data := ClientDataSet1.Delta;

If ClientDataSet1’s change log is empty, this statement causes a Delta is empty exceptionto be raised. So, you should always check the ChangeCount property before attempting to dothis.

The following sample application demonstrates the techniques discussed in this section. Listing4.7 contains the source code for the main form of the application.

LISTING 4.7 ChangeLog—MainForm.pas

unit MainForm;

interface

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

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

Chapter 4182

Page 196: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

uses ChangeLogForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);ClientDataSet1.MergeChangeLog;

end;

procedure TfrmMain.btnFilterClick(Sender: TObject);beginClientDataSet1.StatusFilter := [usInserted];

end;

procedure TfrmMain.btnRemoveFilterClick(Sender: TObject);beginClientDataSet1.StatusFilter := [];

end;

procedure TfrmMain.btnUndoClick(Sender: TObject);beginClientDataSet1.UndoLastChange(True);

end;

procedure TfrmMain.btnRevertRecordClick(Sender: TObject);beginClientDataSet1.RevertRecord;

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

183

LISTING 4.7 Continued

Page 197: Delphi Kylix Database Development

end;

procedure TfrmMain.btnCancelUpdatesClick(Sender: TObject);beginClientDataSet1.CancelUpdates;

end;

procedure TfrmMain.btnSetSavepointClick(Sender: TObject);beginFSavePoint := ClientDataSet1.SavePoint;

end;

procedure TfrmMain.btnGotoSavepointClick(Sender: TObject);beginClientDataSet1.SavePoint := FSavePoint;

end;

procedure TfrmMain.btnViewChangeLogClick(Sender: TObject);varfrmChangeLog: TfrmChangeLog;

beginif ClientDataSet1.ChangeCount > 0 then beginfrmChangeLog := TfrmChangeLog.Create(nil, ClientDataSet1);tryfrmChangeLog.ShowModal;

finallyfrmChangeLog.Free;

end;end elseShowMessage(‘There are no changes to view.’);

end;

end.

Listing 4.8 shows the source code for the form that displays the change log.

LISTING 4.8 ChangeLog—ChangeLogForm.pas

unit ChangeLogForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms,QDialogs, QExtCtrls, QStdCtrls, QGrids, QDBGrids, DB, DBClient;

Chapter 4184

LISTING 4.7 Continued

Page 198: Delphi Kylix Database Development

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

begininherited Create(AOwner);

ClientDataSet1.Data := ADataSet.Delta;end;

procedure TfrmChangeLog.ClientDataSet1AfterScroll(DataSet: TDataSet);begincase ClientDataSet1.UpdateStatus ofusUnmodified: 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 theEMPLOYEE.CDS dataset.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

185

LISTING 4.8 Continued

Page 199: Delphi Kylix Database Development

Chapter 4186

FIGURE 4.6The 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 addedemployee. The second line shows the data for a deleted employee. The third and fourth linesshow modifications to an existing employee. The third line contains the employee data beforeany modifications were made, but the fourth line contains data for only those fields that weremodified.

Cloning Data from Another Client DatasetClient datasets have the unique capability to clone data from another client dataset. When youclone a dataset, there is only one physical copy of the data, but there are two (or more) differentdatasets accessing the same copy of that data. Changes to one dataset immediately affect theother dataset’s view of the data.

Why would you want to do this? I have run into several situations in my projects wherecloning provides an elegant solution to an otherwise sticky situation. The following list outlinesa few of the benefits:

• You can traverse a clone of a dataset without disturbing the original dataset’s currentrecord pointer.

• When viewing a dataset in a grid, you can insert a new record in a dialog using data-awarecontrols that are connected to the clone (without temporarily opening a new, empty linein the grid).

• You can apply ranges or filters on the clone without affecting the display of the originaldataset.

Page 200: Delphi Kylix Database Development

To clone a dataset, create a second client dataset, and then call the second dataset’sCloneCursor method, like this:

varcdsClone: TClientDataSet;

begincdsClone := TClientDataSet.Create(nil);trycdsClone.CloneCursor(ClientDataSet1, False, False);

// Perform some operations on the clone here.finallycdsClone.Free;

end;end;

This code snippet creates a clone of ClientDataSet1, performs some operation(s) on theclone, and then frees the clone. Any Insert, Edit, or Delete operations performed on theclone are automatically reflected in ClientDataSet1.

CloneCursor is defined like this:

procedure CloneCursor(Source: TCustomClientDataSet; Reset: Boolean;KeepSettings: Boolean = False); virtual;

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

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

• Filter, Filtered, FilterOptions, OnFilterRecord

• IndexName

• MasterSource, MasterFields

• ReadOnly

• RemoteServer, ProviderName

If Reset and KeepSettings are set to False, all the previous properties are copied from theoriginal 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 ofKeepSettings), the previous properties are cleared on the clone. Table 4.5 depicts this relationship.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

187

Page 201: Delphi Kylix Database Development

TABLE 4.5 Relationship between Reset and KeepSettings

Reset KeepSettings Behavior

False False Properties are copied from the original dataset to the clone.

False True Properties are not changed for the clone.

True N/A Properties are cleared on the clone.

If you want the clone to copy some of the listed properties from the original dataset, but not tocopy others, you have to write some code. One way to handle this situation is to set both Resetand KeepSettings to False copying all the properties listed previously from the originaldataset to the clone. Then, reset the clone’s properties that were overwritten by the originaldataset. Alternately, you could set Reset and KeepSettings to True, and then set the appropriateproperties on the clone.

Chapter 4188

After cloning a client dataset, the clone does not contain any persistent fields types.This means that you’ll typically use FieldByName to access fields in the clone. Also, theclone does not inherit any standard calculated fields (internal calculated fields areinherited) from the original dataset, so be careful not to try accessing any calculatedfields in the clone.

NOTE

The following example program shows how to effectively use a cloned dataset in the situationslisted at the beginning of this section. Listing 4.9 contains the complete source code for theClone application.

LISTING 4.9 Clone—MainForm.pas

unit MainForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms,QDialogs, QGrids, QDBGrids, DB, DBClient, QExtCtrls, QStdCtrls;

typeTfrmMain = class(TForm)pnlClient: TPanel;pnlBottom: TPanel;ClientDataSet1: TClientDataSet;

Page 202: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

ClientDataSet1.AddIndex(‘byID’, ‘ID’, [ixPrimary, ixUnique]);ClientDataSet1.IndexName := ‘byID’;

FCloneDS := TClientDataSet.Create(nil);FCloneDS.CloneCursor(ClientDataSet1, False, False);

end;

procedure TfrmMain.FormDestroy(Sender: TObject);beginFCloneDS.Free;

end;

procedure TfrmMain.btnUpdateClick(Sender: TObject);begin

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

189

LISTING 4.9 Continued

Page 203: Delphi Kylix Database Development

Screen.Cursor := crHourglass;tryFCloneDS.First;while not FCloneDS.EOF do beginFCloneDS.Edit;FCloneDS.FieldByName(‘Salary’).AsFloat :=FCloneDS.FieldByName(‘Salary’).AsFloat * 1.10;

FCloneDS.Post;

FCloneDS.Next;end;

finallyScreen.Cursor := crDefault;

end;end;

procedure TfrmMain.btnInsertClick(Sender: TObject);beginif FCloneDS.State <> dsBrowse thenExit;

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);beginif FCloneDS.State <> dsInsert thenExit;

tryFCloneDS.Post;ClientDataSet1.GotoCurrent(FCloneDS);

exceptFCloneDS.Cancel;raise;

end;end;

procedure TfrmMain.btnRangeClick(Sender: TObject);begin

Chapter 4190

LISTING 4.9 Continued

Page 204: Delphi Kylix Database Development

Screen.Cursor := crHourglass;tryFCloneDS.SetRange([100], [199]);tryFCloneDS.First;while not FCloneDS.EOF do beginFCloneDS.Edit;FCloneDS.FieldByName(‘Salary’).AsFloat := 50000.0;FCloneDS.Post;

FCloneDS.Next;end;

finallyFCloneDS.CancelRange;

end;finallyScreen.Cursor := crDefault;

end;end;

end.

Figure 4.7 shows the Clone application at runtime.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

191

LISTING 4.9 Continued

FIGURE 4.7Cloned datasets can be extremely useful for inserting and updating records.

Looking at this code, you might notice several things. One of them is that I’ve created thecloned dataset in the FormCreate method and destroyed it in the FormDestroy method. I didthis for simplification. In a real application, you usually create a clone in the method(s) inwhich it is needed, and immediately destroy it afterward. By creating and destroying it once inthis sample program, I saved a few lines of code.

Page 205: Delphi Kylix Database Development

The Update button runs through the entire dataset and gives all employees a 10% raise. Noticethat even though the code doesn’t call DisableControls (discussed near the beginning of thischapter), the grid still doesn’t scroll as the dataset is traversed. That’s because the grid is connected to ClientDataSet1, and the update is performed on the clone dataset.

The Insert (Part 1) and Insert (Part 2) buttons perform two halves of an insert operation on theclone. 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 originaldataset (synchronizing the original dataset with the clone). GotoCurrent makes the originaldataset 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 seewhat happens during an insert. Click Insert (Part 1). Now scroll down to the bottom of the grid.You will not see a line in the grid for employee 99999—the newly inserted, but not-yet-postedrecord. When you click Insert (Part 2), the new record appears in the grid. In contrast, if youchange the code in the Insert button event handlers to use ClientDataSet1, instead ofFCloneDS, 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 arange to the dataset, and then sets the salary for all employees in that range to $50,000. Thegrid is not updated to reflect the range because the range is applied only to the clone datasetand not to ClientDataSet1.

Maintained AggregatesRecords in a dataset often aren’t completely isolated from one another. Many times, you wantto obtain the sum or the average of either an entire column, or some subset of that column. Forexample, you might want to calculate the average salary of all employees in the Sales department,or you might need to retrieve the number of employees whose last name is Jones.

If you’re using an SQL-based database, you can issue SQL statements to calculate these values.For example,

SELECT AVG(SALARY) FROM EMPLOYEE WHERE DEPARTMENT = ‘SALES’;

SELECT COUNT(*) FROM EMPLOYEE WHERE LASTNAME = ‘Jones’;

However, you can’t execute SQL statements directly against a client dataset.

Chapter 4192

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 inthe client dataset. This technique is discussed in Chapter 7.

NOTE

Page 206: Delphi Kylix Database Development

Instead, client datasets support a powerful feature called maintained aggregates. Maintainedaggregates automatically calculate the sum, average, count, minimum value, or maximumvalue for the entire dataset (or for a group or records).

Creating a Maintained Aggregate at Design TimeCreating 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 AggregatesYou create a persistent aggregate in much the same way that you create a data, calculated, orlookup 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 beAggregate for maintained aggregates.

4. Select the Aggregate radio button in the Field type group box.

5. Click OK to create the aggregate.

Figure 4.8 shows the field editor after adding an aggregated field named AvgSalary.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

193

FIGURE 4.8The field editor shows aggregates in a separate section.

After the aggregate is created, you need to set some additional properties (such as the expressionto 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.” Fornow, you can use Avg(Salary).

Page 207: Delphi Kylix Database Development

3. Set the Active property to True. By default, aggregates are not active, which means thatyou aren’t able to access them. Set AggregatesActive to True to activate aggregatefields.

Creating a persistent aggregate automatically creates a component of type TAggregateField,which you can use to reference the aggregate value. Unlike most fields, however, you don’t usethe AsFloat property to obtain an aggregate’s value. Instead, you use the Value property, likethis:

ShowMessage(‘The average salary is ‘ + ClientDataSet1AvgSalary.Value);

Because Value is a variant, you can reference it as though it were either a string or a floating-point number. Therefore, the following code is also correct:

varAvgRaisedSalary: Double

beginAvgRaisedSalary := ClientDataSet1AvgSalary.Value * 1.05;

end;

Nonpersistent AggregatesTo 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 aggregateeditor.

The aggregate editor looks and acts a lot like the field editor. Click the Add New button on thetoolbar (or press Ins) to create a new aggregate.

Again, you need to set some additional properties (such as the expression to aggregate on, andwhether 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 inthis manner. Instead, you access the aggregate through the dataset’s Aggregates property, likethis:

ShowMessage(‘The average salary is ‘ + ClientDataSet1.Aggregates.Find(‘AvgSalary’).Value);

Chapter 4194

Page 208: Delphi Kylix Database Development

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 RuntimeCreating a maintained aggregate at runtime is similar to creating a nonpersistent aggregatebecause you make use of the Aggregates property. The following code snippet shows how tocreate an aggregate at runtime:

varAggregate: TAggregate;

beginAggregate := ClientDataSet1.Aggregates.Add;Aggregate.AggregateName := ‘AvgSalary’;Aggregate.Expression := ‘Avg(Salary)’;Aggregate.Active := True;

end;

Aggregate ExpressionsIn the previous code snippets, I used an expression of Avg(Salary). As you might guess, thisexpression calculates the average value of the Salary field.

Delphi supports the aggregate types listed in Table 4.6.

TABLE 4.6 Aggregate Types

Aggregate Type Description

Sum Calculates the sum of a field.

Avg Calculates the average value of a field.

Count Calculates the number of values for a field that are not blank.

Min Calculates the minimum value of a field.

Max Calculates the maximum value of a field.

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

Aggregate expressions do not have to be simple expressions, such as Avg(Salary). They caninclude multiple functions, such as Sum(SalesPrice) - Sum(NetCost). However, you can’tnest functions. Avg(Sum(SalesPrice)) is not a valid aggregate expression.

The Delphi help topic, “Specifying Aggregates,” provides additional examples of both validand invalid aggregate expressions.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

195

Page 209: Delphi Kylix Database Development

Aggregates Across a Group of RecordsThe 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 indexdefined on the dataset. For purposes of the EMPLOYEE.CDS dataset, we’ll create an indexnamed byBirthday on the Birthday field.

1. Drop a TClientDataSet on the main form of your application.

2. Right-click the dataset, and select Load from MyBase Table… on the pop-up menu.

3. Select the file C:\EMPLOYEE.CDS and click Open.

4. Create an index named byBirthday on the Birthday field. (If you’ve forgotten how tocreate 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 AddAll Fields from the field editor pop-up menu.

6. Create a persistent aggregate field, named NumSameBirthday, using the expressionCount(Birthday). Delphi will create a component namedClientDataSet1NumSameBirthday.

7. Drop a TDataSource, TDBGrid, and TLabel on the main form and connect the data sourceand grid to the client dataset.

8. In the data source’s OnDataChange event, write the following code:

Label1.Caption := ClientDataSet1NumSameBirthday.Value;

Now we have our starting application. If you run it, you should see the value 10000 appear inthe label (indicating that there are 10,000 records with a non-NULL birthday in the dataset).

Now that we’ve established our baseline, let’s change the aggregate so that it calculates thenumber of employees who have the same birthday.

To do this, go back into the field editor and click the NumSameBirthday aggregate field. In theObject Inspector, set the IndexName to byBirthday. This tells the aggregate field to calculateits 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 whencalculating the aggregate value.)

For example, say that you have an index on the fields LastName;FirstName. If GroupingLevelis set to 1, only the first field in the index is used to calculate the aggregate. For an expressionof Avg(Salary), the aggregate calculates the average salary for all employees having the samelast name as the current record.

Chapter 4196

Page 210: Delphi Kylix Database Development

If you set GroupingLevel to 2, the aggregate calculates the average salary for all employeeshaving the same last name and the same first name as the current employee.

Enabling and Disabling AggregatesUsually, you leave aggregates enabled (Active = True). However, you can disable an individualaggregate by setting its Active property to False, or you can disable all aggregates by settingthe dataset’s AggregatesActive property to False.

Disabling aggregates results in a slightly speedier application because Delphi doesn’t continuallyhave to recalculate aggregate values whenever a record is inserted, edited, or deleted. If youplan to add a large number of records at one time, you might want to disable aggregates, addthe records, and then re-enable aggregates so that Delphi only has to calculate them once (atthe time that you re-enable them).

GetGroupStateYou can determine the relative position of a record within an aggregate by calling the dataset’sGetGroupState 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 thegroup, or any other record in the group).

Rather than presenting a sample aggregate application here, I’ll refer you to the Aggregatedemo in Delphi’s DEMOS\MIDAS\AGGREGATE directory.

Miscellaneous PropertiesThis section lists several additional properties of the client dataset that don’t logically fall intoone of the previously discussed categories.

ConstraintsConstraints provide a way of validating a record’s data before posting. Constraints are mostuseful when the validation relies on a relationship between two or more fields in the record.For example, a record’s data might (nonsensically) be determined invalid if the Salary field isless than 1000 times the employee’s age. In other words, a 30-year-old employee must earn atleast $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 ObjectInspector. 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

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

197

Page 211: Delphi Kylix Database Development

Back in the Object Inspector, enter the constraint into the CustomConstraint property. In theErrorMessage property, type the message that you’d like to be displayed when the constraint isnot met—for example, “The salary must be at least 1000 times the employee’s age.”

Figure 4.9 shows a screen capture of the Object Inspector and the constraint editor after addinga constraint.

Chapter 4198

FIGURE 4.9Setting up a constraint.

You are not limited to a single constraint for a dataset. You can add as many constraints as arenecessary.

When the record is posted to the dataset, Delphi checks all the constraints imposed on thedataset. If one of the constraints fails, the message specified in the ErrorMessage property isdisplayed, and the post is aborted.

DisableStringTrimNormally, when records are posted to a dataset, any trailing spaces in a string are automaticallyremoved. For example, if a user types John (note the two trailing spaces) in a data-awareedit control, only four characters are actually written to the underlying field in the datasetbecause the dataset automatically removes the additional two spaces.

Client datasets globally trim trailing spaces from string fields when the DisableStringTrimproperty is set to False (which is the default). However, if you want to retain trailing spaces,you can set DisableStringTrim to True.

Page 212: Delphi Kylix Database Development

DisableStringTrim is a global property, in that it affects all the string fields in the dataset. Itdoesn’t allow you to trim trailing spaces from the FirstName field, and still retain trailingspaces for the LastName field. If you want to retain trailing spaces for some fields and removethem for others, you need to set DisableStringTrim to True, and then remove trailing spacesmanually from the appropriate fields (perhaps in the dataset’s BeforePost event handler).

ReadOnlyBy default, client datasets are read/write datasets. You can make a client dataset read-only (if the underlying data is stored on a CD-ROM drive, for instance) by setting the dataset’sReadOnly property to True.

SummaryIn addition to the basic functionality presented in the preceding chapter, TClientDataSetsupports 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 certainoperations occur. In addition, you can raise an exception during the BeforeXxx events toprevent the operations from occurring.

• You can increase performance dramatically by disabling data-aware controls duringlengthy processes.

• Delphi datasets provide support for BLOBs, which can be used to store notes, images,and other unformatted data.

• Nested datasets provide simplified master/detail support in client datasets.

• TClientDataSet’s undo support enables you to perform what-if scenarios in your applications.

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

• Maintained aggregates support the automatic calculation of sums, minimums, maximums,counts, and averages for groups of records or for the entire dataset.

The following chapter begins a two-chapter introduction to data-aware components.

Advanced Client Dataset Operations

4

AD

VA

NC

EDC

LIENT

DA

TASET

OPER

ATIO

NS

199

Page 213: Delphi Kylix Database Development
Page 214: Delphi Kylix Database Development

CHAPTER

5Data-Aware Components

IN THIS CHAPTER• What Are Data-Aware Components? 202

• TDataSource 204

• Common Data-Aware ComponentCharacteristics 205

• Simple Data-Aware Components 211

• VCL-Only Data-Aware Controls 222

• Lookup Data-Aware Controls 222

• TDBNavigator 223

• Creating Your Own Data-Aware Components 225

• Sample Application 232

Page 215: Delphi Kylix Database Development

Chapter 5202

The preceding two chapters concentrated on client datasets, Delphi’s flexible in-memorydatasets. This chapter introduces the concept of data-aware components, which are ready-madecomponents 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, andthird-party datasets. However, this chapter shows how to use them with client datasets becausethat 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 informationfrom 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 thedata, and what you do with the new string after the user enters it in the edit control, is entirelyup to you. You might store it in a dataset. You might store it in an INI file or in the Windowsregistry. You might even simply use it to perform some sort of calculation, and never store itanywhere 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. Table5.1 lists the data-aware components, along with their standard counterparts.

TABLE 5.1 Delphi Data-Aware Components and Their Non–Data-Aware Equivalents

Data-Aware Component Non–Data-Aware Equivalent

TDBText TLabel

TDBEdit TEdit

TDBMemo TMemo

TDBCheckBox TCheckBox

TDBRadioGroup TRadioGroup

TDBComboBox TComboBox

TDBListBox TListBox

TDBLookupComboBox TComboBox

TDBLookupListBox TListBox

TDBImage TImage

TDBGrid TStringGrid

TDBRichEdit TRichEdit (VCL-only component)

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

Page 216: Delphi Kylix Database Development

TDBNavigator None. Provides a visual means of navigating and manipu-lating datasets without code.

TDataSource Not a data-aware component per se. Provides a conduitbetween a dataset and one or more data-aware components.

As you can see in Table 5.1, the similarities between the standard components and the data-aware components are self-explanatory (with the possible exception of the TDBLookupComboBoxand TDBLookupListBox components).

Except for TDataSource, you’ll find all the components listed in Table 5.1 on the DataControls 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 linkbetween the component and the dataset. The DataField property determines from which fieldin the dataset the data-aware component retrieves its data.

Later sections in this chapter discuss each of these components, with the exception of TDBGrid.Because the grid is such an involved component, I’ll spend the following chapter investigatingit. Rather than providing numerous small sample applications throughout this chapter, I’ll deferan example until the end.

For the most part, the data-aware components mirror their non–data-aware counterparts, so Ihave not spent a lot of time and space here discussing each of their properties, events, andmethods. Instead, I’ve concentrated on issues that are specific to the data-aware version of thecomponent. If you need basic information about the component’s properties or methods, pleaserefer to either the online help or to one of the excellent general-purpose Delphi booksavailable.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

203

TABLE 5.1 Continued

Data-Aware Component Non–Data-Aware Equivalent

Some Delphi programmers shy away from data-aware components—mostly becausethey are aware of the implementation problems with data-aware components inVisual Basic. They might also shy away because data-aware components received abad reputation in Delphi’s early days. Rest assured that data-aware componentsexhibit good performance characteristics under Delphi, especially when used withTClientDataSets (which they are in this chapter).

NOTE

Page 217: Delphi Kylix Database Development

TDataSourceAs Table 5.1 indicates, TDataSource provides a conduit between a dataset and one or moredata-aware controls that are connected to it. You cannot connect a data-aware componentdirectly to a dataset. Instead, you connect a TDataSource to the dataset, and then connect oneor more data-aware components to the data source (as Figure 5.1 illustrates).

Chapter 5204

Dataset Data Source

Data aware component

Data aware component

Data aware component

Data aware component

FIGURE 5.1Relationship between datasets, data sources, and data-aware components.

TDataSource is a rather simple component, publishing just three events and three properties, inaddition to the Name and Tag properties common to all components. Table 5.2 lists the pub-lished properties and Table 5.3 lists the data source’s events. OnDataChange andOnStateChange (the most commonly used of the events) are applied in Listing 5.4, later in thischapter.

TABLE 5.2 TDataSource Properties

Property Description

AutoEdit When True, the underlying dataset is automatically placed into editmode as soon as the user starts to type into a data-aware componentthat is connected to this data source. When False, you must specifi-cally call the dataset’s Edit method before the user can type intoany of the connected data-aware controls.

DataSet Provides a link to the dataset from which the data-aware compo-nents retrieve data.

Enabled When Enabled is True, the data-aware components connected tothis data source display the data contained in the dataset. When it’sFalse, data-aware controls are blank.

Page 218: Delphi Kylix Database Development

TABLE 5.3 TDataSource Events

Event Description

OnDataChange Fires when the dataset’s current record data is changed; eitherbecause the dataset’s cursor is moved to a new record, or becauseone of the fields is modified.

OnStateChange Fires when the underlying dataset’s State property changes. Forexample, when the dataset transitions from browse mode to editmode, or from insert mode to browse mode.

OnUpdateData Fires immediately before the underlying dataset posts changes to thedatabase.

It is easy to forget about TDataSource when writing database applications. After dropping thedata source on a form and connecting the data-aware components to it, the data source oftenseems to serve no useful purpose. However, the three events listed in Table 5.3 are extremelyuseful in a variety of situations. An example of their usefulness is shown in the sample applica-tion at the end of this chapter.

Common Data-Aware Component CharacteristicsBefore discussing the specifics of each individual data-aware component, there are some com-mon characteristics that you should understand. To effectively use data-aware components inyour applications, you should keep in mind the following considerations:

Modifying Component Data from CodeIf you want to change the value that’s displayed in a data-aware component from within yourcode, you should edit the underlying field rather than attempting to manipulate the data-awarecomponent.

For example, say you have a TDBEdit named ecFirstName connected to a field namedFirstName. If you want to programmatically set the edit control so that it displays John, youmight 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 toJohn, like this:

ClientDataSet1.Edit;ClientDataSet1FirstName.AsString := ‘John’;

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

205

Page 219: Delphi Kylix Database Development

If you are not using persistent field objects, you would do this instead:

ClientDataSet1.Edit;ClientDataSet1.FieldByName(‘FirstName’).AsString := ‘John’;

The important thing to remember is to call the dataset’s Edit method before attempting to setthe field value. If the dataset is already in edit or insert mode, the redundant call to Edit does-n’t have any adverse effects. If for some reason the dataset can’t be edited (for example, if thedataset’s ReadOnly property is set to True), the call to Edit raises an exception, which youshould be prepared to handle gracefully.

Controlling When the User Is Allowed to Edit DataBy default, as soon as the user starts typing into a data-aware component, VCL/CLX puts theassociated dataset into edit mode. If you want to control the user’s ability to make edits fromwithin your code, you have four options:

• Set the dataset’s ReadOnly property to True, preventing the user from changing any val-ues in the dataset. This also prevents any changes being made to the data through code.

• Set the underlying field’s ReadOnly property to True. This prevents the user from modi-fying the field, and also prevents the field from being modified through code.

• Set the data-aware component’s ReadOnly property to True, preventing the user fromchanging a single value in the dataset. Note that this has no effect on any changes thatyou make in your code using the method described in the preceding section. Setting thecomponent’s ReadOnly property does not in any way make the field itself read-only. Itmerely prevents the user from making direct modifications to the data through the com-ponent.

• Set the data source’s AutoEdit property to False, preventing the dataset from automati-cally entering an edit state when the user starts typing into a data-aware component. Ifyou go this route, you will typically provide a menu item or a button on the form, whichthe user clicks to put the dataset into edit mode. Alternately, you can use aTDBNavigator, which is discussed later in this chapter.

Formatting and Editing Field ValuesData-aware components don’t have a built-in mechanism for controlling the formatting of fieldvalues during input and output, so at first glance, you might assume that there is no way to dis-play nicely formatted numeric and string data. However, it turns out that data formatting istaken care of at the field level rather than at the component level. For this reason, you can set aspecific output format for a field, and the same format will be used anywhere that a data-awarecomponent is used to display that field.

Chapter 5206

Page 220: Delphi Kylix Database Development

Numeric FieldsWhen you connect a data-aware component to a numeric field, the data that is displayed in acomponent is formatted according to the underlying field’s DisplayFormat property.DisplayFormat is a string property that consists of up to three parts, separated by semicolons,in the following format:

<Positive>;<Negative>;<Zero>

The different sections of the string determine how the value is displayed when it is positive,negative, or zero (respectively). Null values are always displayed as a blank.

Table 5.4 lists the characters that can be used within the DisplayFormat string.

TABLE 5.4 DisplayFormat Specifiers

Character Description

# Digit placeholder. If the formatted value does not require a digit at thatposition, the position is not filled. For example, the value 1.2 formattedwith a DisplayFormat of ###.## yields 1.2, with no leading or trailingspaces.

0 Digit placeholder. If the formatted value does not require a digit at thatposition, the position is filled with a 0. For example, the value 1.2 format-ted with a DisplayFormat of 000.00 yields 001.20.

. Decimal point. Determines where the radix point occurs in the outputstring. The decimal point is replaced by the character stored in theDecimalSeparator global variable.

, Thousands separator. The occurrence of a comma in the DisplayFormatindicates that the value should be formatted using thousands separators.The comma does not occupy a position in the output string—it onlyserves as an indication that thousands separators are needed. At runtime,the comma is replaced by the character stored in the ThousandSeparatorglobal variable.

E+/– Scientific notation. If E+, E–, e+, or e– is present in the DisplayFormat,the value is formatted using scientific notation. E+ indicates that all expo-nents should be preceded by a sign. E– indicates that only negative expo-nents should be preceded by a sign. The E+ or E– is followed by one tofour zeros, specifying the minimum number of digits to include in theexponent.

; Separator character. Used between positive, negative, and zero portions ofthe string.

‘ or “ Literal. Characters enclosed in single or double quotes are copied literallyto the output string, and are not interpreted as formatting characters.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

207

Page 221: Delphi Kylix Database Development

Table 5.5 lists some examples of DisplayFormat settings.

TABLE 5.5 DisplayFormat Settings

Value DisplayFormat Output

1.2 ##0.00 1.20

1.2 000.## 001.2

1 #0.000 1.000

1 00.## 1 (The radix point is not displayed because it isnot needed.)

12.34 $##0.00 $12.34

1234.56 $,0.00 $1,234.56

12.345 X=#.## X=12.35 (Notice that the part to the left of theradix point is automatically expanded to show twodigits, but the part to the right of the radix point isrounded to the specified number of digits.)

100000 #0.000E+00 10.000E+04

–15 ##0;(##0);zero (15)

0 ##0;(##0);zero zero

10 Room “#”#0 Room #10 (The # enclosed in quotes is copied verbatim to the output result.)

If you only specify a single substring in DisplayFormat, it is used to format all numbers. Touse a different output format for negative or zero values, separate the specifiers with semi-colons, like this:

$,0.00;($,0.00);<zero>

This DisplayFormat string formats positive numbers as dollars and cents, negative numberswithin parentheses, and zero values as the string <zero>.

You can omit a portion of the string by simply leaving its specifier empty. In this case, the pos-itive format is used instead. For example:

$,0.00;;<zero>

In this case, positive and negative values are both formatted using $,0.00. Zero values are dis-played as <zero>.

If the DisplayFormat property is left completely blank, the value is displayed using generalfloating-point output with 15 significant digits.

Chapter 5208

Page 222: Delphi Kylix Database Development

By default, the same format is used when editing a field’s value. You can set a different formatto use when editing by setting the field’s EditFormat property in addition to, or instead of theDisplayFormat property. EditFormat works the same as DisplayFormat: It contains a semi-colon-delimited set of formats to use when displaying positive, negative, and zero values.

For example, suppose that you have a floating-point field that you want displayed as 15.25%,but when editing, you don’t want the percent sign displayed. You would set DisplayFormat to#0.00%, and EditFormat to #0.00.

String FieldsString fields do not have separate DisplayFormat and EditFormat properties. Instead, theyhave an EditMask property, which is used for both displaying and editing a field’s value.EditMask holds a Paradox-style edit mask that determines how the string is both displayed andedited.

Like the DisplayFormat and EditFormat properties, EditMask consists of three parts, sepa-rated by semicolons. The first part is the mask to use when formatting the string value. Thesecond 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 aspart of the string value. The third part represents the character that’s displayed to representblanks, or characters that have not yet been entered.

For example, the following EditMask accepts a U.S. Social Security number, storing thehyphens in the underlying field and displaying underscores where numbers are to be entered.

000-000-0000;1;_

Table 5.6 shows the valid EditMask specifiers for string fields.

TABLE 5.6 EditMask Specifiers

Character Description

L Requires an alphabetic character.

l Allows an alphabetic character, but does not require it.

A Requires an alphanumeric character.

a Allows an alphanumeric character, but does not require it.

C Requires a character.

c Allows a character, but does not require it.

0 Requires a numeric character.

9 Allows a numeric character, but does not require it.

# Allows a numeric character, or a plus or minus sign, but does not require it.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

209

Page 223: Delphi Kylix Database Development

: Time separator. This character is replaced with the time separator that isdefined in the control panel under regional settings when it’s other than :.

/ Date separator. This character is replaced with the date separator that isdefined in the control panel under regional settings when it’s other than /.

_ Underscore. This character inserts a space in the text. When editing afield, the cursor automatically skips over the _ character.

; Separator character. Used between mask, literal, and blank portions of thestring.

! If the ! character appears anywhere in the mask, extra and optional char-acters are represented as leading blanks. Otherwise, extra and optionalcharacters are represented as trailing blanks.

> All characters following the > character are forced to uppercase until a <character is encountered.

< All characters following the < character are forced to lowercase until a >character is encountered.

<> All characters are accepted in whatever case the user enters them.

\ Literal. The character following the \ character is inserted in the string, lit-erally, and is not interpreted as a mask character.

Chapter 5210

TABLE 5.6 Continued

Character Description

Each character in the mask represents one byte in the string—not one character. Forthat reason, when working with multibyte character sets, each character in the stringis represented by two characters in the EditMask. For example, AA and LL each repre-sent a single multibyte character. When inserting a literal into a mask, only single-byte literal characters can be entered.

NOTE

Table 5.7 lists some examples of EditMask settings.

TABLE 5.7 EditMask Settings

Stored Value EditMask Displayed Value Remarks

5615551212 (000)_000-0000 (561) 555-1212 Phone number. Formattingcharacters are not stored.

123-456-7890 000-000-0000;1;_ 123-456-7890 Social Security number.Formatting characters(hyphens) are stored.

Page 224: Delphi Kylix Database Development

33467-0708 00000-0000;1;_ 33467-0708 ZIP code. Hyphen isstored.

5/28/01 !99/99/00;1;_ 5/28/01 Date. Slashes are stored.Extra spaces are stored atthe beginning of the stringrather than at the end.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

211

TABLE 5.7 Continued

Stored Value EditMask Displayed Value Remarks

The built-in Delphi edit masks leave something to be desired if you’re wanting to useanything more than a simple edit mask. The standard data-aware components don’tvalidate complex masks well (such as phone numbers, Social Security numbers, andthe like). For this reason, you might want to consider a third-party library to assistyou with data entry and validation. I use Orpheus, from TurboPower SoftwareCompany. You can find TurboPower’s Web site at http://www.turbopower.com.

NOTE

Simple Data-Aware ComponentsMost data-aware components fall into a category that I’ve defined as simple data-aware com-ponents. For purposes of this discussion, a simple data-aware component is one that links to asingle field in a single record of a single dataset. For instance, a component that enables you todisplay and edit values for the FirstName field of a dataset is a simple data-aware component.

This contrasts with more complex data-aware components; which either display multiple val-ues from the same dataset (such as TDBGrid), or which look up information from one datasetfor inclusion in another dataset (such as TDBLookupComboBox and TDBLookupListBox).

In this section, I’ll discuss the simple data-aware components, and following sections willcover some that are more complex. Don’t be too concerned at this point with the mechanics ofcreating data-aware components. Near the end of this chapter, I’ll show you how to take anon–data-aware component and create a data-aware descendent from it.

TDBTextTDBText is the simplest of all the data-aware components. It is a display-only component, simi-lar to the standard TLabel. To use it, drop a TDBText component on a form, and set theDataSource and DataField properties. The data is displayed according to the output formatdiscussed in the preceding section.

Page 225: Delphi Kylix Database Development

TDBEditTDBEdit corresponds to the standard TEdit component. It’s used to display and edit numeric,string, or data/time data contained in a dataset.

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

TDBMemoTDBMemo is similar in concept to TDBEdit, except that it can display and edit multiline textfields, such as unformatted notes. A TDBMemo is usually connected to a CLOB (Character LargeObject) field, although you can also use it to edit string fields.

TDBCheckBoxTDBCheckBox is used like a standard TCheckBox—to display and enter yes/no or true/false val-ues. TDBCheckBox can be connected to a Logical or a Yes/No field in desktop databases, suchas Paradox or Access. Most SQL databases, however, don’t directly support these field types.In these cases, you connect the TDBCheckBox to a string field, which is frequently a single char-acter.

To define the relationship between checked/unchecked and the underlying field data, you setthe component’s ValueChecked and ValueUnchecked properties. ValueChecked refers to thevalue of the field when the checkbox is marked. ValueUnchecked determines the value of thefield when the checkbox is not marked. When the form containing the TDBCheckBox is first dis-played, and the underlying field contains a value that does not equal either of these two proper-ties, the checkbox is initially grayed out.

By default, the values of these two properties are true and false, respectively. In my own appli-cations, I use a single character field (VARCHAR(1)) for Boolean field types. I use T for true andF for false. Because of this, whenever I drop a TDBCheckBox on a form, I set ValueChecked to T

and ValueUnchecked to F.

Chapter 5212

If you decide to always use a single character field for Boolean field types, you mightwant to consider creating a simple component derived from TDBCheckBox that setsValueChecked to T and ValueUnchecked to F (by default). That way, you don’t have tomanually set these properties every time you use the component.

In addition, you should probably consider creating a domain in the database to spec-ify character fields. This is the domain that I create for my own InterBase databases:

CREATE DOMAIN DOM_BOOLEAN AS VARCHAR(1) DEFAULT ‘F’ NOT NULL CHECK (VALUE IN (‘F’, ‘T’));

NOTE

Page 226: Delphi Kylix Database Development

TDBRadioGroupTDBRadioGroup is used in cases when you want the user to select one option from a short listof options. By default, the value of the selected item is stored in the underlying dataset field,which means you typically connect the TDBRadioGroup component to a string field.

In my applications, I’ve found that I most often want to store the index of the selected item inan Integer field. This is straightforward to accomplish if you make use of the component’sValues property. Values is a string list that corresponds to the Items property in the followingmanner:

• If Values is empty, the strings contained in the Items property are stored in the underly-ing dataset.

• If Values is not empty, it should contain the same number of string values as the Itemsproperty. When an item is selected in the radio group, the corresponding value in theValues property is stored in the dataset.

Using the second rule, you can store a sequential list of numbers in the Values property andconnect the component to an Integer field. Delphi is then smart enough to store the numericrepresentation of the selected item in the dataset. Figure 5.2 shows this concept. When the userselects Tuesday from the list, the number three is stored in the associated field.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

213

Items

Sunday

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Tuesday

Value

1

2

3

4

5

6

7

3

FIGURE 5.2Relationship between TDBRadioGroup’s Items and Values properties.

TDBComboBoxTDBComboBox works similarly to TDBRadioGroup because it enables the user to select an itemfrom a list and store it in a dataset. Unfortunately, it doesn’t support the Values property, soyou can’t use it to store the index of the selected item in a dataset. In my applications, this isoften a severe limitation, so I’ve created a descendent component named TETHDBComboBox thatsupports assigning a value to each string contained in the Items property.

Page 227: Delphi Kylix Database Development

Listing 5.1 contains the source code for the TETHDBComboBox component.

LISTING 5.1 ETHDBComboBox.pas

unit ETHDBComboBox;

interface

usesWindows, Messages, SysUtils, Classes, Controls, StdCtrls, DBCtrls;

typeTETHDBComboBox = 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;

Chapter 5214

The Values property only comes into play when the component’s Style property isset to csDropDownList, csOwnerDrawFixed, or csOwnerDrawVariable. If the style is setto 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.

NOTE

Page 228: Delphi Kylix Database Development

procedure Register;

implementation

procedure Register;beginRegisterComponents(‘ETH’, [TETHDBComboBox]);

end;

{ TETHDBComboBox }

constructor TETHDBComboBox.Create(AOwner: TComponent);beginFValues := TStringList.Create;

inherited Create(AOwner);end;

destructor TETHDBComboBox.Destroy;beginFValues.Free;

inherited;end;

procedure TETHDBComboBox.CreateWnd;begininherited;

FDataLink := TFieldDataLink(SendMessage(Handle, CM_GETDATALINK, 0, 0));FDataLink.OnDataChange := DataChange;FDataLink.OnUpdateData := UpdateData;

end;

procedure TETHDBComboBox.SetValues(const Value: TStrings);beginFValues.Assign(Value);DataChange(Self);

end;

function TETHDBComboBox.GetComboValue(Index: Integer): string;beginif (Index < FValues.Count) and (FValues[Index] <> ‘’) thenResult := FValues[Index]

else if Index < Items.Count thenResult := Items[Index]

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

215

LISTING 5.1 Continued

Page 229: Delphi Kylix Database Development

elseResult := ‘’;

end;

function TETHDBComboBox.GetComboText: string;beginif Style in [csDropDown, csSimple] thenResult := Text

else if ItemIndex >= 0 thenResult := GetComboValue(ItemIndex)

elseResult := ‘’;

end;

procedure TETHDBComboBox.SetComboText(const Value: string);varI: Integer;Index: Integer;Redraw: Boolean;

beginif Value <> GetComboText then beginif Style <> csDropDown then beginRedraw := (Style <> csSimple) and HandleAllocated;if Redraw thenSendMessage(Handle, WM_SETREDRAW, 0, 0);

tryif Value = ‘’ thenI := -1

else beginI := -1;for Index := 0 to Items.Count - 1 doif Value = GetComboValue(Index) then beginI := Index;Break;

end;end;

ItemIndex := I;finallyif Redraw then beginSendMessage(Handle, WM_SETREDRAW, 1, 0);Invalidate;

end;end;

Chapter 5216

LISTING 5.1 Continued

Page 230: Delphi Kylix Database Development

if I >= 0 thenExit;

end;

if Style in [csDropDown, csSimple] thenText := Value;

end;end;

procedure TETHDBComboBox.DataChange(Sender: TObject);beginif not (Style = csSimple) and DroppedDown thenExit;

if FDataLink.Field <> nil thenSetComboText(FDataLink.Field.Text)

else if csDesigning in ComponentState thenSetComboText(Name)

elseSetComboText(‘’);

end;

procedure TETHDBComboBox.UpdateData(Sender: TObject);beginFDataLink.Field.Text := GetComboText;

end;

end.

Listing 5.1 contains some code that you might not be familiar with, so I’ll examine some ofthe individual routines in more detail.

Create and Destroy simply create and free the new FValues property, and then pass controlonto TDBComboBox’s constructor and destructor.

CreateWnd sends a CM_GETDATALINK message to the component to obtain a reference to thecomponent’s internal FDataLink field. Because TETHDBComboBox derives from TDBComboBox,we’re actually retrieving TDBComboBox’s FDataLink. TDBComboBox.FDataLink is private andTDBComboBox doesn’t provide a property to access the value, so there’s no way to directly get ahold of the data link. Fortunately, TDBComboBox supports the CM_GETDATALINK message, whichaccomplishes the same thing.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

217

LISTING 5.1 Continued

Page 231: Delphi Kylix Database Development

When it has a reference to the data link, CreateWnd sets up new event handlers forOnDataChange and OnUpdateData. OnDataChange is fired automatically when the underlyingfield data changes because either the current record changed, or because a new value wasassigned to the field. OnUpdateData is fired when the user selects a value in the combo box,and the underlying field should be updated.

TDBComboBox provides a handler for both of these methods, but the handlers don’t take intoaccount our new FValues property. So, it’s necessary to override them.

SetValues is called when you assign a new string list to the Values property, like this:

ETHDBComboBox1.Values := MyStringList;

It first assigns the string list, and then calls DataChange directly, which ensures that the combobox is updated to display the correct data.

GetComboValue is a helper function that retrieves the correct value for a given index. It firstchecks the Values property to see if a value was assigned to the item in question. If so, itreturns the value from that list. If not, it returns the value directly from the Items list.

GetComboText returns the text for the currently displayed item in the combo box. If the combobox allows text entry—in other words, if the style is csDropDown or csSimple—the functionsimply returns the text displayed in the combo box. Otherwise, it calls GetComboValue toobtain the value of the current item.

SetComboText works in reverse. It determines the index of a given string and makes that thecurrent ItemIndex of the component.

DataChange, as mentioned earlier, fires when the underlying field data changes. This methodsimply calls SetComboText to update the text displayed in the combo box.

Conversely, UpdateData updates the underlying field so that it contains the correct value forthe currently selected combo box item.

TDBComboBox is used in a manner similar to the TDBRadioGroup component. If you leave theValues 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 storedin the underlying field, which can be either a string field or a numeric field (depending onwhether the Values list contains text or numbers).

TDBListBoxTDBListBox is conceptually identical to TDBComboBox because it enables the user to select anitem from a list of items. It also has the same limitation of TDBComboBox because it does notsupport a Values property. For that reason, I’ve created my own version of TDBListBox.

Chapter 5218

Page 232: Delphi Kylix Database Development

Listing 5.2 contains the source code for TETHDBListBox (a descendent of TDBListBox that sup-ports a Values property).

LISTING 5.2 ETHDBListBox.pas

unit ETHDBListBox;

interface

usesWindows, Messages, SysUtils, Classes, Controls, StdCtrls, DBCtrls;

typeTETHDBListBox = class(TDBListBox)private{ Private declarations }FDataLink: TFieldDataLink;FValues: TStrings;procedure DataChange(Sender: TObject);procedure UpdateData(Sender: TObject);function GetListValue(Index: Integer): string;function IndexOfItem(const Value: string): Integer;procedure SetValues(const Value: TStrings);

protected{ Protected declarations }procedure CreateWnd; override;

public{ Public declarations }constructor Create(AOwner: TComponent); override;destructor Destroy; override;

published{ Published declarations }property Values: TStrings read FValues write SetValues;

end;

procedure Register;

implementation

procedure Register;beginRegisterComponents(‘ETH’, [TETHDBListBox]);

end;

{ TETHDBListBox }

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

219

Page 233: Delphi Kylix Database Development

constructor TETHDBListBox.Create(AOwner: TComponent);beginFValues := TStringList.Create;

inherited Create(AOwner);end;

destructor TETHDBListBox.Destroy;beginFValues.Free;

inherited;end;

procedure TETHDBListBox.CreateWnd;begininherited;

FDataLink := TFieldDataLink(SendMessage(Handle, CM_GETDATALINK, 0, 0));FDataLink.OnDataChange := DataChange;FDataLink.OnUpdateData := UpdateData;

end;

procedure TETHDBListBox.SetValues(const Value: TStrings);beginFValues.Assign(Value);DataChange(Self);

end;

function TETHDBListBox.GetListValue(Index: Integer): string;beginif (Index < FValues.Count) and (FValues[Index] <> ‘’) thenResult := FValues[Index]

else if Index < Items.Count thenResult := Items[Index]

elseResult := ‘’;

end;

function TETHDBListBox.IndexOfItem(const Value: string): Integer;varI: Integer;Index: Integer;

Chapter 5220

LISTING 5.2 Continued

Page 234: Delphi Kylix Database Development

beginI := -1;for Index := 0 to Items.Count - 1 doif Value = GetListValue(Index) then beginI := Index;Break;

end;

Result := I;end;

procedure TETHDBListBox.DataChange(Sender: TObject);beginif FDataLink.Field <> nil thenItemIndex := IndexOfItem(FDataLink.Field.Text)

elseItemIndex := -1;

end;

procedure TETHDBListBox.UpdateData(Sender: TObject);beginif ItemIndex >= 0 thenFDataLink.Field.Text := GetListValue(ItemIndex)

elseFDataLink.Field.Text := ‘’;

end;

end.

The source code for TETHDBListBox is similar to that of TETHDBComboBox, so I won’t go into itin detail here.

TDBImageTDBImage is used to display bitmaps contained in a dataset’s BLOB field. Unfortunately,TDBImage cannot be used to display nonbitmap images (such as JPEG, PNG, and the like).Chapter 4, “Advanced Client Dataset Operations,” explains how you can store and retrievenonbitmap images from database BLOB fields.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

221

LISTING 5.2 Continued

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

NOTE

Page 235: Delphi Kylix Database Development

VCL-Only Data-Aware ControlsVCL supports a few additional data-aware controls that are not supported under CLX. Theseinclude

• TDBRichEdit

• TDBChart

• TDBCtrlGrid

These components are not included with CLX because they rely on one of the following:underlying Win32 implementations (TDBRichEdit), not-yet-available third-party components(TDBChart), or unsupported/obsolete functionality (TDBCtrlGrid).

Nevertheless, these components have use in VCL applications, so I’ll mention TDBRichEdit inthis chapter and TDBCtrlGrid in the next. Because date entry is something that many applica-tions require, I’ll present a data-aware implementation of the Win32 TDateTimePicker compo-nent later in this chapter.

TDBRichEdit is similar to TDBMemo because it is used to display and edit multiline text.However, TDBMemo displays and edits unformatted text, while TDBRichEdit works with rich text(text formatted using RTF, or rich text format).

Rich text enables the user to format paragraphs, words, or individual characters using differentfont styles and formatting techniques—such as bullets, numbering, tabs, and indentation.Although TDBRichEdit and its non–data-aware counterpart, TRichEdit, support this function-ality through a wide array of properties and methods, it is up to you to provide the user with amenu, 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 ControlsThe preceding section discussed simple data-aware components that connect to a single field ina single dataset. In this section, I’ll introduce lookup components. Lookup components storedata to a single field in a dataset, but display a list of available data from another dataset.

For example, let’s assume that we’re dealing with a standard order-entry system containing anORDERDETAIL table and a PARTS table. The PARTS table consists of a PartNumber field and aDescription 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 theirdescriptions, select a part, and have the corresponding part number automatically stored in theORDERDETAIL table.

Chapter 5222

Page 236: Delphi Kylix Database Development

This is what the lookup data-aware controls are designed for—displaying a list from onedataset and enabling the user to select an item to be stored in another dataset.

To complete the link to the lookup dataset, lookup data-aware components provide four addi-tional properties: ListSource, ListField, KeyField, and ListFieldIndex.

• ListSource references the data source of the dataset from which to retrieve the list ofvalues.

• ListField is a semicolon-delimited list of field names that are to be displayed in thecomponent.

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

• ListFieldIndex is a zero-based number that determines the field to be used for incre-mental searching in the component. For example, say that you set the ListField prop-erty to FirstName;LastName. This instructs the component to display the first name andthe last name in the list. If you set ListFieldIndex to 1, as the user types into the con-trol, it performs automatic incremental searching on the LastName field.

Lookup data-aware components consist of TDBLookupComboBox and TDBLookupListBox. Thesecomponents look and act like TDBComboBox and TDBListBox (respectively), except that ratherthan populating the items manually, TDBLookupComboBox and TDBLookupListBox retrieve theiritems from the dataset referenced through the component’s ListSource property.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

223

You can duplicate the functionality of the TETHDBComboBox and TETHDBListBox com-ponents by using TDBLookupComboBox and TDBLookupListBox. To do this, create aTClientDataSet that contains the Items and Values associations that are set in theTETHDBComboBox or in the TETHDBListBox. If you only have a single occurrence of thisin your application, you might elect to go this route. However, if you have numerousoccurrences, your form becomes littered with lookup datasets and you might find iteasier to use TETHDBComboBox and TETHDBListBox instead.

NOTE

TDBNavigatorThe 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 prede-fined buttons (actually, TSpeedButtons), which are listed in Table 5.8.

Page 237: Delphi Kylix Database Development

TABLE 5.8 TDBNavigator Buttons

Button Dataset Method

First

Prior

Next

Last

Insert

Delete

Edit

Post

Cancel

Refresh (discussed in Chapter 7, “Dataset Providers”)

When the user clicks one of the buttons in the TDBNavigator, VCL/CLX calls the correspond-ing dataset method automatically. You can control which buttons are displayed through theVisibleButtons property, which is implemented as a Pascal set: Simply remove the buttonsthat you don’t want shown from the VisibleButtons property.

It is possible to change the image that appears on one or more of the buttons, although themethod for doing this isn’t well documented. TDBNavigator encapsulates a number ofTSpeedButtons to display the individual images, so you can access an individual speed buttonthrough 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 theabsolute position of the button within the navigator’s button array. However, a more fail-safemethod of accessing the button involves using the TNavigateBtn enumerated type, which isdefined in DBCtrls.pas like this:

TNavigateBtn = (nbFirst, nbPrior, nbNext, nbLast,nbInsert, nbDelete, nbEdit, nbPost, nbCancel, nbRefresh);

Chapter 5224

Page 238: Delphi Kylix Database Development

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 ComponentsCreating your own data-aware components isn’t all that difficult when you understand the stepsthat you must take to provide a data-aware version of an existing standard control. In this sec-tion, I provide working code for a data-aware version of the Win32 TDateTimePicker compo-nent. Along the way, I’ll provide an overview of the steps required to create a data-awarecomponent. While reading the following sections, please refer to the source code shown inListing 5.3.

TFieldDataLinkTFieldDataLink is a helper class that establishes a link between the data-aware componentand the underlying dataset field. TFieldDataLink provides only a small number of methods,properties, and events that you need to concern yourself with when writing a data-aware com-ponent. Tables 5.9, 5.10, and 5.11 list the most often-used methods, properties, and events(respectively).

TABLE 5.9 TFieldDataLink Methods

Method Description

Edit Try to put the dataset into edit mode. Edit returns False if the datasetdoes not allow editing, and returns True otherwise.

Modified Call the Modified method when the data-aware component is changed;either because the user types into it, or because the contents of thecomponent were changed in some other way (such as through a clickor other interaction with the component).

Reset Call the Reset method when an action occurs that causes the contentsof the underlying field to be reset to its original value. For example, adata-aware component might support a key (such as Ctrl+R) that resetsthe original value of the field.

TABLE 5.10 TFieldDataLink Properties

Property Description

CanModify Read-only property that returns True if the corresponding field can bemodified, and returns False if it cannot. CanModify returns False ifthe dataset, the field, or the data-aware component is read-only.

Control References the link data-aware control.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

225

Page 239: Delphi Kylix Database Development

Field References the field object to which the data-aware control is bound.The field object might be a persistent field, or it might be an automati-cally generated field object for a nonpersistent field.

FieldName The name of the field to which the data-aware component is bound.

TABLE 5.11 TFieldDataLink Events

Event Description

OnDataChange Fires when there is a change to the underlying field.

OnEditingChange Fires when the associated data source changes from an editing mode toa browse mode, or vice versa.

OnUpdateData Fires when the data contained in the data-aware component should bewritten out to the dataset.

OnActiveChange Fires when the underlying dataset changes from active to inactive, orvice versa.

The following sections explain how to incorporate a TFieldDataLink class into a componentto create a data-aware version of that component. They also show the proper way to make useof the methods, properties, and events listed in the preceding tables.

Setting Up the TFieldDataLinkThe first step in creating a data-aware component is to add a private field of typeTFieldDataLink to the component.

Next, override the Create and Destroy constructor and destructor.

Create is responsible for creating the TFieldDataLink object and establishing the connectionto this component through the Control property. Notice in Listing 5.3 that the Create methodadds the csReplicatable setting to the ControlStyle property. This informs the componentthat it can be used in a TDBCtrlGrid, as discussed in the following chapter.

Create also sets up event handlers for the TFieldDataLink’s OnDataChange and OnUpdateData

events. You can also create handlers for the OnEditingChange and OnActiveChange events ifyou want or need to, but I haven’t done that here.

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

Chapter 5226

TABLE 5.10 Continued

Property Description

Page 240: Delphi Kylix Database Development

Finally, you should handle the CM_GETDATALINK message and return a reference to the internalTFieldDataLink field. CMGetDataLink provides this service in Listing 5.3.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

227

If you remember from the section titled “TDBComboBox,” we took advantage of theCM_GETDATALINK message when writing the TETHDBComboBox and TETHDBListBox

components. If the authors of TDBComboBox and TDBListBox had not provided theCM_GETDATALINK message handler, we would have no way of obtaining a reference tothe component’s internal TFieldDataLink.

NOTE

Setting Up a Connection to the Data SourceThe next step that you will take is to create properties for DataSource and DataField. Theseproperties simply make calls to GetDataSource/SetDataSource andGetDataField/SetDataField. For most data-aware components, you can copy the code pre-sented in Listing 5.3 for these methods verbatim.

In addition, you should provide an overridden Notification method, which is called when alinked component is freed. In the case of most data-aware components, we want to be notifiedif the user removes the associated TDataSource component from the form or from the datamodule at design time. If this occurs, the reference to the data source is no longer valid, so weset the DataSource property to nil.

Responding to Changes in the DatasetAt this point, you should create a DataChange event handler. DataChange does the job ofupdating the data-aware component so that it reflects the current state of the linked data field.In the example presented here, DataChange sets the component’s Date property to the value ofthe associated field. If there is no associated field, the component displays today’s date.

Next, you should provide an overridden implementation of the Loaded event. Loaded simplycalls the DataChange event when the component is in design mode. At runtime, DataChangeautomatically gets called.

Updating the DatasetNow that the component updates itself correctly when the underlying data changes, we need towrite the code that updates the data when the component changes. To do that, we need to writethe UpdateData event handler.

Page 241: Delphi Kylix Database Development

In many cases, UpdateData contains a single line of code, which gets the current value fromthe 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 fireswhen the value of the component is changed. In many cases, this includes a Change event han-dler. In some cases, it requires a Click handler instead of (or in addition to) the Change eventhandler. You should be familiar with the component that you are working with so that youknow what events might be fired as a result of a change to the component’s value.

In this case, I’ve overridden TDateTimePicker’s Click and Change dynamic methods to addcalls to the data link’s Edit and Modified methods. The logic is this: First, call Edit to attemptputting the underlying dataset into edit mode. Next, call the component’s inherited method.Finally, call Modified to let the data link know that the field was changed.

Message HandlersTypically, a data-aware component updates the dataset when focus leaves the component. Toaccomplish this, we must provide a message handler for the CM_EXIT message in the form ofthe 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 tothe component and the exception is raised again. You can generally copy this message han-dler’s code into your own data-aware components without modification.

Action HandlersThe final two methods that you should provide in your data-aware component are overrides forExecuteAction and UpdateAction. These overridden methods ensure that the componentworks correctly with the standard DataSet actions provided with Delphi. Again, you can copythe code verbatim from this component into your own data-aware components.

Data-Aware TDateTimePickerListing 5.3 contains the complete source code for TETHDBDateTimePicker (a data-awaredescendent of TDateTimePicker).

LISTING 5.3 ETHDBDateTimePicker.pas

unit ETHDBDateTimePicker;

interface

usesWindows, Messages, SysUtils, Classes, Controls, ComCtrls, DB, DBCtrls;

Chapter 5228

Page 242: Delphi Kylix Database Development

typeTETHDBDateTimePicker = class(TDateTimePicker)private{ Private declarations }FDataLink: TFieldDataLink;function GetDataField: string;function GetDataSource: TDataSource;procedure SetDataField(const Value: string);procedure SetDataSource(const Value: TDataSource);function GetField: TField;procedure DataChange(Sender: TObject);procedure UpdateData(Sender: TObject);procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK;

protected{ Protected declarations }procedure Loaded; override;procedure Notification(AComponent: TComponent;Operation: TOperation); override;

procedure Change; override;procedure Click; override;procedure CMExit(var Message: TCMExit); message CM_EXIT;

public{ Public declarations }constructor Create(AOwner: TComponent); override;destructor Destroy; override;function ExecuteAction(Action: TBasicAction): Boolean; override;function UpdateAction(Action: TBasicAction): Boolean; override;property Field: TField read GetField;

published{ Published declarations }property DataField: string read GetDataField write SetDataField;property DataSource: TDataSource read GetDataSource write SetDataSource;

end;

procedure Register;

implementation

procedure Register;beginRegisterComponents(‘ETH’, [TETHDBDateTimePicker]);

end;

{ TETHDBDateTimePicker }

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

229

LISTING 5.3 Continued

Page 243: Delphi Kylix Database Development

constructor TETHDBDateTimePicker.Create(AOwner: TComponent);begininherited Create(AOwner);

ControlStyle := ControlStyle + [csReplicatable];FDataLink := TFieldDataLink.Create;FDataLink.Control := Self;FDataLink.OnDataChange := DataChange;FDataLink.OnUpdateData := UpdateData;

end;

destructor TETHDBDateTimePicker.Destroy;beginFDataLink.Free;FDataLink := nil;inherited Destroy;

end;

procedure TETHDBDateTimePicker.Loaded;begininherited Loaded;if (csDesigning in ComponentState) thenDataChange(Self);

end;

procedure TETHDBDateTimePicker.Notification(AComponent: TComponent;Operation: TOperation);

begininherited Notification(AComponent, Operation);if (Operation = opRemove) and

(FDataLink <> nil) and(AComponent = DataSource) thenDataSource := nil;

end;

procedure TETHDBDateTimePicker.CMGetDataLink(var Message: TMessage);beginMessage.Result := Integer(FDataLink);

end;

procedure TETHDBDateTimePicker.Change;beginFDataLink.Edit;inherited Change;FDataLink.Modified;

end;

Chapter 5230

LISTING 5.3 Continued

Page 244: Delphi Kylix Database Development

procedure TETHDBDateTimePicker.Click;beginFDataLink.Edit;inherited Click;FDataLink.Modified;

end;

function TETHDBDateTimePicker.GetDataSource: TDataSource;beginResult := FDataLink.DataSource;

end;

procedure TETHDBDateTimePicker.SetDataSource(const Value: TDataSource);beginif not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) thenFDataLink.DataSource := Value;

if Value <> nil thenValue.FreeNotification(Self);

end;

function TETHDBDateTimePicker.GetDataField: string;beginResult := FDataLink.FieldName;

end;

procedure TETHDBDateTimePicker.SetDataField(const Value: string);beginFDataLink.FieldName := Value;

end;

function TETHDBDateTimePicker.GetField: TField;beginResult := FDataLink.Field;

end;

procedure TETHDBDateTimePicker.DataChange(Sender: TObject);beginif FDataLink.Field <> nil thenDate := FDataLink.Field.AsDateTime

elseDate := Now;

end;

procedure TETHDBDateTimePicker.UpdateData(Sender: TObject);

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

231

LISTING 5.3 Continued

Page 245: Delphi Kylix Database Development

beginFDataLink.Field.AsDateTime := Date;

end;

procedure TETHDBDateTimePicker.CMExit(var Message: TCMExit);begintryFDataLink.UpdateRecord;

exceptSetFocus;raise;

end;end;

function TETHDBDateTimePicker.ExecuteAction(Action: TBasicAction): Boolean;beginResult := inherited ExecuteAction(Action) or (FDataLink <> nil) andFDataLink.ExecuteAction(Action);

end;

function TETHDBDateTimePicker.UpdateAction(Action: TBasicAction): Boolean;beginResult := inherited UpdateAction(Action) or (FDataLink <> nil) andFDataLink.UpdateAction(Action);

end;

end.

Sample ApplicationListing 5.4 is a sample application that makes use of many (but not all) of the data-aware com-ponents discussed in this chapter. As you can see from Listing 5.4, there is very little code inthis application. Thanks to VCL/CLX, the data-aware components encapsulate almost every-thing needed to display and update datasets in your applications.

LISTING 5.4 DataAware—MainForm.pas

unit MainForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DB, DBClient,QStdCtrls, QExtCtrls, QButtons, Mask, QComCtrls, QDBCtrls, QMask;

Chapter 5232

LISTING 5.3 Continued

Page 246: Delphi Kylix Database Development

typeTfrmMain = 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;

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

233

LISTING 5.4 Continued

Page 247: Delphi Kylix Database Development

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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);

procedure AddLookupItem(ID: Integer; const Description: string);begincdsLookup.Append;cdsLookupID.AsInteger := ID;cdsLookupDescription.AsString := Description;cdsLookup.Post;

end;

begin// Create the lookup dataset and populate with some datacdsLookup.CreateDataSet;AddLookupItem(1, ‘Widgit’);AddLookupItem(2, ‘Gadget’);AddLookupItem(3, ‘Thingamabob’);

ClientDataSet1.CreateDataSet;end;

Chapter 5234

LISTING 5.4 Continued

Page 248: Delphi Kylix Database Development

procedure TfrmMain.ClientDataSet1NewRecord(DataSet: TDataSet);beginDataSet.FieldByName(‘Enabled’).AsString := ‘T’;DataSet.FieldByName(‘Active’).AsString := ‘T’;

end;

procedure TfrmMain.DataSource1DataChange(Sender: TObject; Field: TField);beginif Field = nil thenlbDSEvents.Items.Add(‘Data Change: Field = nil’)

elselbDSEvents.Items.Add(‘Data Change: Field = ‘ + Field.FieldName);

lblCurrent.Caption := Format(‘(Record %d of %d)’,[ClientDataSet1.RecNo, ClientDataSet1.RecordCount]);

end;

procedure TfrmMain.DataSource1StateChange(Sender: TObject);beginlbDSEvents.Items.Add(‘State Change’);

case DataSource1.State ofdsInactive: lblState.Caption := ‘Inactive’;dsBrowse: lblState.Caption := ‘Browse’;dsEdit: lblState.Caption := ‘Edit’;dsInsert: lblState.Caption := ‘Insert’;

end;end;

procedure TfrmMain.DataSource1UpdateData(Sender: TObject);beginlbDSEvents.Items.Add(‘Update Data’)

end;

procedure TfrmMain.btnLoadClick(Sender: TObject);beginif OpenDialog1.Execute then beginClientDataSet1.Edit;ClientDataSet1Image.LoadFromFile(OpenDialog1.FileName);

end;end;

procedure TfrmMain.btnClearClick(Sender: TObject);

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

235

LISTING 5.4 Continued

Page 249: Delphi Kylix Database Development

beginif not ClientDataSet1Image.IsNull then beginClientDataSet1.Edit;ClientDataSet1Image.Clear;

end;end;

end.

Figure 5.3 shows the data-aware application at runtime.

Chapter 5236

LISTING 5.4 Continued

FIGURE 5.3DataAware demonstrates the use of many of the provided data-aware components.

SummaryThis chapter introduced you to data-aware components. The components that we’ve coveredare

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

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

• TDBMemo provides a means of displaying and editing unformatted multiline text.

• TDBRichEdit (a VCL-only data-aware component) can be used to display and edit for-matted multiline text.

• TDBCheckBox and TDBRadioGroup support the selection of one or more options from anavailable list of options.

• TDBComboBox and TDBListBox enable the user to select a field value from a list of prede-fined values. I also provided you with code for descendents of these two components thatallows finer control over the value stored in the associated data field.

Page 250: Delphi Kylix Database Development

• TDBLookupComboBox and TDBLookupListBox elaborate on TDBComboBox and TDBListBox

by obtaining the list of items from another dataset, and then saving the primary key ofthe lookup dataset back to the dataset that’s being edited.

• TDBImage is used to display bitmaps that are stored in a dataset’s BLOB field.

• TDBNavigator can be used to provide a code-free form of dataset navigation and manip-ulation.

• With a little effort, you can create data-aware versions of standard VCL/CLX compo-nents.

The following chapter continues this discussion of data-aware components with a look at data-aware grids.

Data-Aware Components

5

DA

TA-AW

AR

EC

OM

PON

ENTS

237

Page 251: Delphi Kylix Database Development
Page 252: Delphi Kylix Database Development

CHAPTER

6Data-Aware Grids

IN THIS CHAPTER• TDBGrid 240

• TClientDataSetGrid 263

• TDBCtrlGrid 266

• Third-Party Data-Aware Grids 271

Page 253: Delphi Kylix Database Development

Chapter 6240

The preceding chapter introduced you to data-aware components—in particular, to data-awarecomponents that display and edit one field at a time. This chapter discusses data-aware grids,which display information from a number of records at one time.

In this chapter, I’ll examine three different data-aware grids: TDBGrid, TClientDataSetGrid,and TDBCtrlGrid. TDBGrid is the only one of the three that comes standard with both Delphiand Kylix. TClientDataSetGrid is a derivative work, written by John Kaster, that providesbuilt-in support for user-configurable columns as well as code that can automatically sort aclient dataset when the user clicks a column heading (more on that later in this chapter).

TDBCtrlGrid is supplied with Delphi, but not with Kylix. It allows for a nonlinear grid layout—for example, a grid where each record occupies several lines instead of a single line.

TDBGridTDBGrid provides the cornerstone for Delphi’s grid-based, data-aware components. UsingTDBGrid, you can create screens that look like the one shown in Figure 6.1 without a lot of programming effort.

FIGURE 6.1A sample screen created using a TDBGrid.

Later in this chapter, we’ll investigate the code required to produce this screen. In this section,I’ll explore the TDBGrid component. The following sections introduce two other grids that areeither included with Delphi or are available as a free download.

TDBGrid Basic OperationLike the components discussed in the preceding chapter, TDBGrid publishes a DataSourceproperty, 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 DataFieldproperty. Instead, TDBGrid provides a Columns property that enables you to specify whichfields 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.”

Page 254: Delphi Kylix Database Development

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

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S241

FIGURE 6.2The default grid is functional, but not eye-catching.

Figure 6.2 points out examples of title cells, data cells, indicator cells, and grid background.

Using a combination of the grid’s properties and events, you can create a grid that looks muchmore pleasing to the eye. In the following sections, I’ll examine those properties and events in detail.

Customizing ColumnsGenerally, the most basic level of customization that you want to perform is adjusting eitherthe 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 displayedin the grid.

You can think of the Columns property as being similar to a dataset’s Fields property.If there are no columns specifically defined, the grid simply displays all columns in theorder that they appear in the dataset. If persistent fields are defined for the dataset,the grid displays columns only for those fields.

To create persistent column objects for the grid (similar to a dataset’s persistent fieldobjects), you use the columns editor.

NOTE

Page 255: Delphi Kylix Database Development

Double-click the grid component at design time (or right-click it and select Columns Editor…from the pop-up menu) to display the columns editor. The columns editor works like most collection editors in Delphi—press Ins to create a new TColumn object, or right-click and selectAdd from the pop-up menu.

Each column supports a number of properties that can be used to customize the column’s lookand feel. (For the ultimate in display flexibility, see the “Custom Drawing” section later in thischapter.) These properties are listed in Table 6.1.

TABLE 6.1 Basic TColumn Properties

Property Description

Alignment Sets the alignment of the data displayed in the column to left-justified,centered, or right-justified.

Color Sets the background color of the individual column.

FieldName Specifies the name of the field in the underlying dataset that is to bedisplayed in this column. Any field can be displayed (including datafields, calculated fields, lookup fields, and aggregate fields).

Font Customizes the font used to display the column data.

ReadOnly When True, the column data cannot be edited, even if the underlyingfield and dataset allow editing.

Title Enables customization of the column’s title cell. This property is discussed later in the “Column Titles” section.

Visible When False, the column is not displayed.

Width Sets the width of the column in screen pixels.

When you set the FieldName property, Delphi sets the Alignment and Width propertiesautomatically (based on the size and type of the field). Unfortunately, even though Delphi setsAlignment to taRightJustify for a numeric field, it doesn’t automatically set the title’salignment to taRightJustify. So, you need to set the title’s alignment manually.

Chapter 6242

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, youneed to set the column’s Alignment and Width properties manually, and you must usethe grid’s custom draw functionality to paint the cell contents for that column. Forexample, you might want to create a column with no associated field to display anicon in certain rows.

NOTE

Page 256: Delphi Kylix Database Development

Column TypesMost columns are displayed and edited as a simple string. For cases in which you want theuser to select from a list of values, or want to display a dialog that enables the user to selectthe cell value, TDBGrid supports two types of embellishments that can be made to a column’sactive cell:

• A column can display a lookup combo box to enable the user to select from a predefinedlist of values. If a column is linked to a lookup field in a dataset, the column automaticallydisplays a combo box of acceptable values when the user is editing that column.

• A column can display an ellipsis button, which can be programmed to display a dialog,or programmed to perform some other function when the user clicks it.

The properties listed in Table 6.2 are used to set options for the various column styles.

TABLE 6.2 Additional TColumn Properties

Property Description

ButtonStyle When set to bsAuto (the default value), the column automatically displays a combo box for lookup fields. You can manually set thisproperty to bsEllipsis (displaying an ellipsis button) or to bsNone(suppressing the combo box for lookup fields).

DropDownRows Specifies the maximum number of items to display in the column’scombo box when it is dropped down.

PickList For columns that are not connected to a lookup field, you can specify alist of acceptable field values in the PickList property. If this propertyis used, the column automatically displays a combo box when it isedited (unless the column’s ButtonStyle property is set to bsNone).

For columns with a ButtonStyle of bsEllipsis, the grid’s OnEditButtonClick event is firedwhen the user clicks the ellipsis button. The sample program presented at the end of this sectionshows how you might respond to that event.

Column TitlesIn addition to customizing the look of the column data, you can customize the look of thecolumns’ titles. To change the font used for all column titles at the same time, you can set thegrid’s TitleFont property accordingly.

However, for control over each column title, you should resort to the individual column’sTitle property. The Title property expands to enable the following properties to be set for thecolumn title.

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S243

Page 257: Delphi Kylix Database Development

TABLE 6.3 TColumn Title Properties

Property Description

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

Caption Specifies the text to be displayed in the column title.

Color Sets the background color of the column title.

Font Sets the font for the text displayed in the column title.

As mentioned earlier, Delphi does not automatically set the title’s Alignment property totaRightJustify for numeric fields. So, you should make sure that you check the title’salignment when creating persistent columns.

Grid OptionsAfter 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 availableoptions.

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 thedgRowSelect option automatically forces dgEditing off.

dgAlwaysShowEditor The grid is automatically placed into edit mode as soon as the usertabs into a cell. The user does not need to press F2 to begin editing.Like dgEditing, dgAlwaysShowEditor is forced off if dgRowSelectis set.

dgTitles When this option is set, column titles are displayed.

dgIndicator This option forces the display of a narrow column at the extremeleft of the grid that shows the state of the current record (insert, edit,or browse mode).

dgColumnResize Setting this option enables individual columns to be moved orresized at runtime.

dgColLines When set, vertical lines are drawn between columns.

dgRowLines When set, horizontal lines are drawn between rows.

dgTabs When set, the user can press the Tab and Shift+Tab keys to movefrom cell to cell in the grid. When clear, pressing Tab or Shift+Tabcauses focus to move to the next or the preceding control on theform, respectively.

Chapter 6244

Page 258: Delphi Kylix Database Development

dgRowSelect When set, clicking a row highlights the entire row rather thanselecting an individual cell. Row highlighting can also be accomplished manually by custom drawing the grid, as explainedlater in this chapter.

dgAlwaysShow Set this option to highlight the current cell even when the grid doesSelection not have focus.

dgConfirmDelete If the grid’s ReadOnly property is not set, this option causes theVCL to display a delete confirmation message when the userpresses Ctrl+Delete while in the grid. If this option is not set, thecurrent record is deleted when the user presses Ctrl+Delete. Note thatCtrl+Delete deletes the current record even if dgEditing is not set.

dgCancelOnExit This option affects how newly inserted rows are treated when theuser tabs out of the grid. When set, newly inserted rows for whichno data has been entered are canceled. If not set, inserted rows thatare left empty are posted to the dataset.

dgMultiSelect When set, the user can select multiple rows in the grid by pressingCtrl and clicking individual rows.

By default, Options is set to [dgEditing, dgTitles, dgIndicator, dgColumnResize,dgColLines, dgRowLines, dgTabs, dgConfirmDelete, dgCancelOnExit]. I find that when I usea TDBGrid, I turn off the dgEditing and dgIndicator options, and set the grid’s ReadOnly

property to True. Instead of allowing my users to edit directly in the grid, I display a dialogwhen they press Enter and enable them to edit field values for the current record there. Ofcourse, your mileage might vary, and you’ll determine your own favorite set of options as youuse the grid in your applications.

EventsIn addition to the properties listed previously, TDBGrid provides a number of events that youcan respond to for finer control over the grid’s display and functionality. These events arelisted 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 clicksa title cell, the indicator, or the grid background.

OnColEnter Fires immediately after focus enters the current column.

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S245

TABLE 6.4 Continued

Option Description

Page 259: Delphi Kylix Database Development

OnColExit Fires immediately before focus leaves the current column. CallingAbort in this handler prevents the grid from switching to a new column.

OnColumnMoved Fires after the user moves (but not after the user resizes) a column at runtime.

OnDrawColumnCell Fires when a cell is about to be drawn. It’s used to implement custom drawing, which is explained later in this chapter.

OnDrawDataCell Obsolete and included for backward compatibility only.

OnEditButtonClick Fires when the user clicks the ellipsis button in a cell.

OnTitleClick Occurs when the user clicks a title cell (assuming that the optiondgTitles is set). TClientDataSetGrid makes internal use of thisevent to automatically sort the underlying dataset when the userclicks a column title.

The following example program, shown in Listing 6.1, demonstrates when the different gridevents are fired. The next section, “Custom Drawing” explores the OnDrawColumnCell event inmore detail.

LISTING 6.1 Options—MainForm.pas

unit MainForm;

interface

usesSysUtils, Classes, QGraphics, QControls, QForms, QDialogs, DB, QGrids,QDBGrids, DBClient, QExtCtrls, QStdCtrls, QDBCtrls;

typeTfrmMain = class(TForm)ClientDataSet1: TClientDataSet;DataSource1: TDataSource;pnlOptions: TPanel;pnlClient: TPanel;grid: TDBGrid;cbEditing: TCheckBox;cbAlwaysShowEditor: TCheckBox;cbTitles: TCheckBox;cbIndicator: TCheckBox;

Chapter 6246

TABLE 6.5 Continued

Event Description

Page 260: Delphi Kylix Database Development

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

privateprocedure RetrieveOptions;procedure UpdateOption(Option: TDBGridOption; Active: Boolean);{ Private declarations }

public{ Public declarations }

end;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S247

LISTING 6.1 Continued

Page 261: Delphi Kylix Database Development

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);

RetrieveOptions;end;

// Options set/get methods

procedure TfrmMain.RetrieveOptions;begincbEditing.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);beginif Active thengrid.Options := grid.Options + [Option]

elsegrid.Options := grid.Options - [Option];

RetrieveOptions;end;

procedure TfrmMain.cbEditingClick(Sender: TObject);

Chapter 6248

LISTING 6.1 Continued

Page 262: Delphi Kylix Database Development

beginUpdateOption(dgEditing, cbEditing.Checked);

end;

procedure TfrmMain.cbAlwaysShowEditorClick(Sender: TObject);beginUpdateOption(dgAlwaysShowEditor, cbAlwaysShowEditor.Checked);

end;

procedure TfrmMain.cbTitlesClick(Sender: TObject);beginUpdateOption(dgTitles, cbTitles.Checked);

end;

procedure TfrmMain.cbIndicatorClick(Sender: TObject);beginUpdateOption(dgIndicator, cbIndicator.Checked);

end;

procedure TfrmMain.cbColumnResizeClick(Sender: TObject);beginUpdateOption(dgColumnResize, cbColumnResize.Checked);

end;

procedure TfrmMain.cbColLinesClick(Sender: TObject);beginUpdateOption(dgColLines, cbColLines.Checked);

end;

procedure TfrmMain.cbRowLinesClick(Sender: TObject);beginUpdateOption(dgRowLines, cbRowLines.Checked);

end;

procedure TfrmMain.cbTabsClick(Sender: TObject);beginUpdateOption(dgTabs, cbTabs.Checked);

end;

procedure TfrmMain.cbRowSelectClick(Sender: TObject);beginUpdateOption(dgRowSelect, cbRowSelect.Checked);

end;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S249

LISTING 6.1 Continued

Page 263: Delphi Kylix Database Development

procedure TfrmMain.cbAlwaysShowSelectionClick(Sender: TObject);beginUpdateOption(dgAlwaysShowSelection, cbAlwaysShowSelection.Checked);

end;

procedure TfrmMain.cbConfirmDeleteClick(Sender: TObject);beginUpdateOption(dgConfirmDelete, cbConfirmDelete.Checked);

end;

procedure TfrmMain.cbCancelOnExitClick(Sender: TObject);beginUpdateOption(dgCancelOnExit, cbCancelOnExit.Checked);

end;

procedure TfrmMain.cbMultiSelectClick(Sender: TObject);beginUpdateOption(dgMultiSelect, cbMultiSelect.Checked);

end;

// Grid event handlers

procedure TfrmMain.gridColExit(Sender: TObject);beginlbEvents.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);beginlbEvents.Items.Add(‘OnColEnter - Col ‘ + IntToStr(grid.SelectedIndex) +‘, Field ‘ + grid.SelectedField.FieldName + ‘)’);

end;

procedure TfrmMain.gridEditButtonClick(Sender: TObject);beginlbEvents.Items.Add(‘OnEditButtonClick - Col ‘ +IntToStr(grid.SelectedIndex) + ‘, Field ‘ +grid.SelectedField.FieldName + ‘)’);

end;

Chapter 6250

LISTING 6.1 Continued

Page 264: Delphi Kylix Database Development

procedure TfrmMain.gridCellClick(Column: TColumn);beginlbEvents.Items.Add(‘OnCellClick - Col ‘ + IntToStr(grid.SelectedIndex) +‘, Field ‘ + grid.SelectedField.FieldName + ‘)’);

end;

procedure TfrmMain.gridColumnMoved(Sender: TObject; FromIndex,ToIndex: Integer);

beginlbEvents.Items.Add(‘Column moved from ‘ + IntToStr(FromIndex) +‘ to ‘ + IntToStr(ToIndex));

end;

// Command buttons

procedure TfrmMain.btnClearEventLogClick(Sender: TObject);beginlbEvents.Items.Clear;

end;

procedure TfrmMain.btnShowSelectionsClick(Sender: TObject);varIndex: Integer;s: string;

beginif not (dgMultiSelect in grid.Options) thenraise Exception.Create(‘dgMultiSelect not set’);

if grid.SelectedRows.Count = 0 thenraise Exception.Create(‘No rows selected’);

for Index := 0 to grid.SelectedRows.Count - 1 do beginClientDataSet1.Bookmark := grid.SelectedRows[Index];if s <> ‘’ thens := s + #13;

s := s + Format(‘%d: %s’, [ClientDataSet1.FieldByName(‘ID’).AsInteger,ClientDataSet1.FieldByName(‘Name’).AsString]);

end;

ShowMessage(s);end;

end.

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S251

LISTING 6.1 Continued

Page 265: Delphi Kylix Database Development

Figure 6.3 shows the Options application at runtime.

Chapter 6252

FIGURE 6.3The Options application lets you experiment with the TDBGrid component’s options.

Custom DrawingAs you can see from Figure 6.2, the grid’s default appearance is pleasing to look at, but notespecially eye-catching. Using custom drawing, we can spruce up the look of the grid considerably.

To implement custom drawing in your grid, you need to handle the grid’s OnDrawColumnCellevent. You might notice that the grid contains a similarly named event, OnDrawDataCell.OnDrawDataCell is an obsolete event that is included for backward compatibility with earlyversions 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 thebounding rectangle of the cell that is about to be drawn. DataCol is a zero-based index into theabsolute position of the column that is about to be drawn. State is a set containing one ormore of the values listed in Table 6.6.

Page 266: Delphi Kylix Database Development

TABLE 6.6 TGridDrawState Values

Value Description

gdSelected The cell is selected.

gdFocused The cell has the focus.

gdFixed The cell is fixed (that is, it’s the indicator cell).

The difference between gdSelected and gdFocused can get confusing (especially becausethese values change meaning slightly as the Options property changes), so I’ll clarify it here.

When dgRowSelect is not set (the default), only the current cell has the gdSelected value set.If the grid currently has focus, the current cell has gdFocused set in addition to having thegdSelected 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 gdFocusedset. You probably want to ignore the gdFocused value when using dgRowSelect, as it has nouseful meaning.

The DefaultDrawing PropertyThe grid’s DefaultDrawing property determines how drawing is performed in the grid. Whenthis property is True (the default), VCL/CLX draws each cell in the grid as usual, and thenpasses control to the OnDrawColumnCell handler that you set up. OnDrawColumnCell is calledfor every cell in the grid, so you want to make sure that whatever code you write in that eventhandler executes quickly.

When DefaultDrawing is False, Delphi paints the cell with the appropriate background color,and sets the grid’s Brush and Font properties in readiness to draw the cell. Then, it callsOnDrawColumnCell so that you can draw the contents of the cell yourself.

In practice, you will often find that when you implement custom drawing, Delphi’s defaultdrawing code does about 90% of what you need. You might simply want to change the color ofselected 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 isTrue, the cell is drawn using its default settings, and then you turn around and draw over thetop of it. On the other hand, if it is False, you need to draw every single cell manually—eventhose that you don’t need any special drawing for.

Fortunately, this isn’t the case. The solution is to set DefaultDrawing to False, and then insidethe OnCustomDrawColumn event handler, call the grid’s DefaultDrawColumnCell, like this:

procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;DataCol: Integer; Column: TColumn; State: TGridDrawState);

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S253

Page 267: Delphi Kylix Database Development

beginif Column.FieldName = ‘Salary’ then beginif Column.Field.AsFloat > 50000.0 then beginDBGrid1.Canvas.Brush.Color := clYellow;

if gdFocused in State thenDBGrid1.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 greaterthan $50,000, the background of the cell is drawn in yellow. If the cell is focused, the salary isdrawn 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 appropriatechanges (if any) are made to the brush and font colors.

The way that Delphi’s internal VCL/CLX painting code works, all you need to do in this handler is to set the canvas’ Brush and Font properties so that they reflect the color and fontthat you want to use when painting the cell. The call to DefaultDrawColumnCell then uses thesettings that you specified when drawing the cell contents.

Chapter 6254

Notice, in the preceding code snippet, that I checked the FieldName property of thecolumn to see if the code is drawing the Salary column. You might be tempted touse the DataCol parameter to check for this. However, DataCol is the zero-basedabsolute index of the cell being drawn. If the user reorders the columns at runtime,this value changes.

NOTE

The following sample program demonstrates several ways to custom draw grid cells. Listing6.2 contains the complete source code for the CustomDraw application.

LISTING 6.2 CustomDraw—MainForm.pas

unit MainForm;

interface

Page 268: Delphi Kylix Database Development

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

typeTfrmMain = 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;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.CDS’);

end;

procedure TfrmMain.DBGrid1DrawColumnCell(Sender: TObject;const Rect: TRect; DataCol: Integer; Column: TColumn;State: TGridDrawState);

varRetirementBirthdate: TDateTime;X: Integer;

beginif Odd(ClientDataSet1.RecNo) thenDBGrid1.Canvas.Brush.Color := clAqua

elseDBGrid1.Canvas.Brush.Color := clWhite;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S255

LISTING 6.2 Continued

Page 269: Delphi Kylix Database Development

if gdSelected in State then beginDBGrid1.Canvas.Font.Color := clGreen;DBGrid1.Canvas.Font.Style := [fsBold];

end;

if Column.ID = 0 then beginDBGrid1.Canvas.FillRect(Rect);

RetirementBirthdate := IncYear(Date, -50);if ClientDataSet1.FieldByName(‘Birthday’).AsDateTime <=RetirementBirthdate then begin// Eligible for retirementX := (Column.Width - Image1.Picture.Width) div 2 + Rect.Left;DBGrid1.Canvas.Draw(X, Rect.Top, Image1.Picture.Graphic);

end;end elseDBGrid1.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 rowsare drawn using a background color of clAqua, and even rows are drawn using a backgroundcolor of clWhite. This gives a checkbook-style look to the grid.

Second, the current row (determined by the fact that the State parameter includes thegdSelected option) is drawn in a bold, green font. The State parameter includes thegdSelected option because dgRowSelect is specified in the grid’s options.

Finally, the code checks the birthday of the employee. If the employee is 50 years old (orolder), a watch icon is drawn in the first column. The first column has an ID of 0. Notice thatthe code doesn’t check the DataCol parameter to see if it’s 0 because the user could rearrangethe columns at runtime. Instead, it checks for the column ID, which is a zero-based integer thatwas established at design time and doesn’t change.

Chapter 6256

LISTING 6.2 Continued

If the dgIndicator option were turned on, the column ID would be 1 instead of 0because the indicator column would have an ID of 0.

NOTE

Page 270: Delphi Kylix Database Development

Solutions to Common Grid QuestionsIn this section, I’ll attempt to answer a number of commonly asked questions about theTDBGrid component. The solutions to these problems are not overly difficult, but in most casesthey involve more than simply calling a method or setting a property.

Determining the Current Row or ColumnSometimes you might need to determine what row, column, or cell is currently focused.Depending on the information that you need, there are several ways to go about this.

If all you need to know is what row has the focus, the easiest thing to do is to check the underlying dataset. The dataset’s current record is the one that has focus. So, if you want to get the value of the current Salary column, you can do the following:

varCurrentSalary: Double;

beginCurrentSalary := DBGrid1.DataSource.DataSet.FieldByName(‘Salary’).AsFloat;...

end;

To get the index of the focused column, you can access the grid’s SelectedIndex property.SelectedIndex is a zero-based number indicating the absolute position of the selected column.SelectedIndex adjusts for the indicator, so if the indicator is present, the first data column isindex 1. If the indicator is not displayed, the first data column is index 0.

To retrieve the field object for the current column, you can reference the grid’s SelectedFieldproperty. SelectedField returns the underlying dataset’s TField object, so you can directlyaccess it to retrieve the value of the current cell.

ShowMessage(‘The current cell value is ‘ + DBGrid1.SelectedField.AsString);

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S257

If the current column is not connected to a dataset field, SelectedIndex returns –1and SelectedField returns nil. If you display any columns in your grid that are nottied to a field, you should always check for –1 or nil before attempting to do some-thing with the SelectedIndex or SelectedField properties.

NOTE

Getting the Cell at a Given Mouse CoordinateWith a little effort, you can determine the row and column of the cell at any mouse position.Normally, you don’t need to do this because you can use the dataset’s current record and thegrid’s SelectedIndex or SelectedField properties to determine the current cell. However,you might want to know about a cell that isn’t current.

Page 271: Delphi Kylix Database Development

For example, say that you want to write an event handler that tracks the current position of themouse and provides information about the field at that location. A more ambitious projectmight be to write code that displays a tooltip when the mouse hovers over a cell whose contentsare too long to be fully displayed in the cell.

In either case, you can call the grid’s MouseCoord method passing in the X and Y coordinatesof 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 atthat mouse position.

TGridCoord’s X and Y values deserve a little explanation. These are absolute indexes, meaningthat they take the indicator column and title rows into account. If the indicator is displayed, theX position of the first data column is 1. If the indicator is not displayed, it is 0. This is also truefor the rows: If the grid titles are not displayed (dgTitles is not set in the grid’s Options

property), the Y position of the first row of data is 0. If titles are displayed, it is 1.

If the mouse position is not over a cell (for instance, the mouse is over the background area ofthe grid), the returned TGridCoord’s X and Y values are both –1.

The following code snippet shows how you might update a label to show the X and Y positions, as well as the field name, of the cell at the current mouse position.

procedure TForm1.DBGrid1MouseMove(Sender: TObject; Shift: TShiftState; X,Y: Integer);

varGC: TGridCoord;IndicatorOffset: Integer;TitleOffset: Integer;

beginGC := DBGrid1.MouseCoord(X, Y);if GC.X = -1 thenLabel1.Caption := IntToStr(GC.X) + ‘, ‘ + IntToStr(GC.Y)

else beginif dgIndicator in DBGrid1.Options thenIndicatorOffset := 1

elseIndicatorOffset := 0;

if dgTitles in DBGrid1.Options thenTitleOffset := 1

elseTitleOffset := 0;

if GC.X < IndicatorOffset thenLabel1.Caption := IntToStr(GC.X) + ‘, ‘ + IntToStr(GC.Y) +

Chapter 6258

Page 272: Delphi Kylix Database Development

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

// To move the dataset to the corresponding record, clone the dataset,// and set the clone’s RecNo property to GC.Y - TitleOffset.

end;end;

At the end of that code snippet is a comment explaining how to retrieve the data for the appropriate cell. Here is the code required to do that (assuming that the grid is connected to a client dataset):

varCloneDS: TClientDataSet;FieldValue: string;

begin...

CloneDS := TClientDataSet.Create(nil);tryCloneDS.CloneCursor(DBGrid1.DataSource.DataSet);CloneDS.RecNo := GC.Y - TitleOffset;

// Now access the fields of the cloned datasetFieldValue := CloneDS.FieldByName(DBGrid1.Columns[GC.X – IndicatorOffset].FieldName).AsString;

finallyCloneDS.Free;

end;end;

Setting Edit Mode ManuallyIf dgAlwaysShowEditor is True, the grid automatically enters edit mode when the user enters acell. If dgAlwaysShowEditor is False, the grid enters edit mode when the user presses F2.(The user can simply start typing a value to overwrite a cell’s contents.) However, what if (forcompatibility with another software package) you want to enter edit mode when the userpresses a different key—perhaps F9 instead of F2?

To achieve this, you need to take control over when the grid enters edit mode. The best placeto do this is in the grid’s OnKeyDown event. Within that handler, set the grid’s EditorMode

property to True to enter edit mode.

The following code snippet shows how to enter edit mode when the user presses F9 (not whenhe or she presses F2).

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S259

Page 273: Delphi Kylix Database Development

procedure TForm1.DBGrid1KeyDown(Sender: TObject; var Key: Word;Shift: TShiftState);

beginif Shift = [] then begincase Key ofVK_F2: Key := 0;VK_F9: DBGrid1.EditorMode := True;

end;end;

end;

Chapter 6260

For this code to work, you must make sure the dgEditing option is set. If it isn’t, thegrid doesn’t enter edit mode even when the code sets EditorMode to True.

NOTE

Detecting When a Column Is ResizedTDBGrid provides an OnColumnMoved event, but no OnColumnSized event. This means that youcan easily tell when a column is moved, but it’s more difficult to determine when a column isresized. Fortunately, we can achieve this functionality by writing a grid descendent with a singleoverridden method.

Listing 6.3 contains the source code for the TETHDBGrid component.

LISTING 6.3 ETHDBGrid.pas

unit ETHDBGrid;

interface

usesWindows, Messages, SysUtils, Classes, Controls, Grids, DBGrids;

typeTETHDBGrid = class(TDBGrid)private{ Private declarations }FOnColumnSized: TNotifyEvent;

protected{ Protected declarations }procedure ColWidthsChanged; override;

public{ Public declarations }

published{ Published declarations }

Page 274: Delphi Kylix Database Development

property OnColumnSized: TNotifyEvent read FOnColumnSized write FOnColumnSized;

end;

procedure Register;

implementation

procedure Register;beginRegisterComponents(‘ETH’, [TETHDBGrid]);

end;

{ TETHDBGrid }

procedure TETHDBGrid.ColWidthsChanged;begininherited ColWidthsChanged;

if Assigned(FOnColumnSized) thenFOnColumnSized(Self);

end;

end.

The overridden method, ColWidthsChanged, calls the inherited version, and then fires theOnColumnSized event (if you provide an event handler for it in your application).

This code does nothing more than notify you that a column was resized. It doesn’t tell youwhich column was resized, what the old width was, or what the new width is. Providing thatadditional information would require a lot more work and duplicate a good deal of code that isin TDBGrid. I prefer to keep my descendent components simple and direct.

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S261

LISTING 6.3 Continued

TClientDataSetGrid (discussed later in this chapter) does not provide this event han-dler, either. So, if you intend to use TClientDataSetGrid instead of TDBGrid in yourapplications, you might want to change the code shown here so that it derives fromTClientDataSetGrid.

NOTE

Page 275: Delphi Kylix Database Development

Persisting Grid SettingsUsers expect customizations to be persistent across program invocations, and grid settings areno exception. When your users resize or reorder the columns in a grid, they usually expect thecolumn order and size to be the same the next time they run the application.

It isn’t difficult to persist grid column settings. There are two approaches that you can take:

• Save the settings to a stream or to a separate file.

• Save the settings to the Windows registry or to an ini file.

The first option is simpler, so I’ll cover it first. TDBGrid’s Columns property providesSaveToFile and SaveToStream methods that you can use to save column configurations, toeither a file or a stream, with a minimum of fuss. The following code snippet shows how youcan 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, youneed separate configuration files to persist each one. If that is the case, you might want to lookat SaveToStream instead of SaveToFile. With a little effort, you can use SaveToStream to savea grid’s column configuration to an ini file or to the Windows registry. The following proceduresaves column information to an ini file:

procedure SaveColumnConfiguration(const FileName: string; Grid: TDBGrid;const SectionName: string; const Name: string);

varini: TIniFile;MemStream: TMemoryStream;

beginMemStream := TMemoryStream.Create;tryGrid.Columns.SaveToStream(MemStream);MemStream.Seek(0, soFromBeginning);

ini := TIniFile.Create(FileName);tryini.WriteBinaryStream(SectionName, Name, MemStream);

finallyini.Free;

end;finallyMemStream.Free;

end;end;

Chapter 6262

Page 276: Delphi Kylix Database Development

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 thiscode to use the Windows registry instead of an ini file.

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

varini: TIniFile;MemStream: TMemoryStream;

beginMemStream := TMemoryStream.Create;tryini := TIniFile.Create(FileName);tryini.ReadBinaryStream(SectionName, Name, MemStream);if MemStream.Size > 0 thenGrid.Columns.LoadFromStream(MemStream);

finallyini.Free;

end;finallyMemStream.Free;

end;end;

LimitationsFor all its power, TDBGrid does have some limitations. The most notable one is that it doesn’tdisplay memos or images. You can draw images or memos manually using the custom drawingfeatures of the grid, but because each grid row is the same height, this can lead to difficultieswhen 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 theend of this chapter, there is a quick overview of some of the third-party grids that you mightwant to look into.

TClientDataSetGridTClientDataSetGrid is a TDBGrid descendent written by John Kaster. It takes advantage ofsome of the functionality of client datasets to provide automatic sorting of the grid when theuser clicks a column title.

In addition, TClientDataSetGrid can automatically persist column information to and from aseparate configuration file. It doesn’t support saving and loading column information to and

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S263

Page 277: Delphi Kylix Database Development

from an ini file, or to and from the Windows registry, so you might still want to make use ofthe SaveColumnConfiguration and LoadColumnConfiguration procedures provided in thepreceding section. (TClientDataSetGrid is available as ID 15099 on Code Central athttp://codecentral.borland.com.)

Automatic SortingAs indicated previously, TClientDataSetGrid enables the user to sort the grid in ascending ordescending order, on a single column or on multiple columns. It can even sort one column inascending order and another column in descending order.

To enable this functionality, you must set the component’s TitleSort property to True. (It isFalse by default, allowing the component to be used with nonclient datasets. If you setTitleSort to True, TClientDataSetGrid does not work with datasets that do not derive fromTCustomClientDataSet.)

To indicate the current sort order, TClientDataSetGrid draws three-dimensional arrows in thetitle of the sorted column(s). The colors used to draw these arrows are set through theArrowColor, ArrowHighlight, and ArrowShade properties.

Figure 6.4 shows how the grid looks when sorted by Name, and then by Birthday.

Chapter 6264

FIGURE 6.4TClientDataSetGrid 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 sorton. 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 currentsort order. Repeat this for every column you want to sort on.

Page 278: Delphi Kylix Database Development

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

Column CustomizationIn addition to automatic sorting capabilities, TClientDataSetGrid provides a separate dialogthat can be used to set the visible columns for the grid (as shown in Figure 6.5). To display thisdialog, call the grid’s ConfigureColumns method, like this:

ClientDataSetGrid1.ConfigureColumns;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S265

FIGURE 6.5TClientDataSetGrid enables the user to hide columns that he doesn’t want to see.

Using this dialog, the user can hide or show individual columns.

If you want the grid to automatically save and restore its column configuration (including column order and the visibility state of individual columns), set the ConfigFile property tothe name of the file that you want to use for persisting the column information. Make sure touse a different filename for each grid, as the current version of this component doesn’t supportsaving multiple configurations in a single file.

Page 279: Delphi Kylix Database Development

TDBCtrlGridI’m not going to spend a lot of time on TDBCtrlGrid because it isn’t CLX-compatible, andbecause there isn’t any new development going on in terms of TDBCtrlGrid.

TDBCtrlGrid is a grid-like component, although it relies on other data-aware components toperform the actual data input and output. To use a TDBCtrlGrid in your application, drop it ona form and connect the DataSource property to your data source. Then, populate the grid withother data-aware components, such as TDBEdit, TDBCheckBox, and so on.

TDBCtrlGrid replicates these components at runtime, displaying each component for everyrecord displayed in the grid. Every cell in the grid corresponds to a single record in the dataset.

Most data-aware controls are replicable (that is, they can be used in a TDBCtrlGrid). Some arenot (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 itsControlStyle property (typically set in the component’s constructor). The following is a snippetfrom TDBEdit’s constructor:

constructor TDBEdit.Create(AOwner: TComponent);begininherited Create(AOwner);inherited ReadOnly := True;ControlStyle := ControlStyle + [csReplicatable];...

end;

The following sections discuss the properties and events that TDBCtrlGrid introduces.

Chapter 6266

Because the code for TClientDataSetGrid is freely available, I hope to see someenterprising Delphi programmers providing enhancements to it in the future. My per-sonal wish list includes

• Saving and loading column configuration to and from an ini file and theWindows registry.

• Enhancing the Configure Columns dialog to support column reordering in addi-tion to hiding or showing columns.

• Enhancing the Configure Columns dialog to support locking individual columnsso that they cannot be hidden or moved.

• Adding support for an OnColumnSized event.

NOTE

Page 280: Delphi Kylix Database Development

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

TABLE 6.7 TDBCtrlGrid Properties

Property Description

AllowInsert When True, the user can scroll past the last record in the grid toinsert a new record.

AllowDelete When True, the user can delete the current record by pressingCtrl+Delete.

ColCount Determines the number of columns displayed in the grid.

Orientation Determines the direction in which the grid scrolls to display more data.

PanelBorder Possible values are gbNone and gbRaised. gbRaised causes the gridto have a raised look. You can achieve other looks, such as loweredor bump, by setting PanelBorder to gbNone and dropping theTDBCtrlGrid on a panel with the desired bevel.

PanelHeight Refers to the height, in pixels, of a single panel (cell).

PanelWidth Refers to the width, in pixels, of a single panel (cell).

RowCount Determines the number of rows displayed at a time in the grid.

SelectedColor Determines the background color of the current cell.

ShowFocus When True, a focus rectangle is drawn around the current cell.

The ColCount, PanelWidth, and Width properties are directly related. ColCount×PanelWidth isapproximately equal to Width (allowing for the grid border and vertical scrollbar). SettingColCount 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, settingColCount automatically adjusts PanelWidth.)

Similarly, the RowCount, PanelHeight, and Height properties are related. Setting one propertyaffects the others.

EventsTDBCtrlGrid only contains one new event, named OnPaintPanel. OnPaintPanel fires justbefore each panel is about to be drawn. OnPaintPanel looks like this:

procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;Index: Integer);

begin

end;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S267

Page 281: Delphi Kylix Database Development

Index refers to the zero-based index of the panel about to be drawn, and is a number betweenzero and RowCount—one, inclusive.

You might notice that there is no Rect parameter passed to this function, so at first glance itisn’t obvious how to determine the bounding rectangle of the current cell. Upon entry to thismethod, the grid canvas’ origin is set to the upper-left corner of the current panel. In otherwords, 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 codesnippet taken from Listing 6.4 shows).

procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;Index: Integer);

beginif Index <> DBCtrlGrid.PanelIndex thenDBCtrlGrid1.Canvas.Draw(0, 0, Image1.Picture.Graphic);

end;

This code checks the index passed into the method to see if we’re drawing the current panel.(The public property PanelIndex contains the number of the current panel.) All noncurrentpanels are drawn with a background graphic.

Listing 6.4 contains the complete source code for the CtrlGrid demo application, whichenables you to play with some of TDBCtrlGrid’s properties.

LISTING 6.4 CtrlGrid—MainForm.pas

unit MainForm;

interface

usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, dbcgrids, ExtCtrls, DB, DBClient, StdCtrls, Mask, DBCtrls,ComCtrls;

typeTForm1 = class(TForm)ClientDataSet1: TClientDataSet;DataSource1: TDataSource;pnlClient: TPanel;DBCtrlGrid1: TDBCtrlGrid;ecID: TDBEdit;ecName: TDBEdit;ecSalary: TDBEdit;Label1: TLabel;Label2: TLabel;

Chapter 6268

Page 282: Delphi Kylix Database Development

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;

varForm1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);beginClientDataSet1.LoadFromFile(‘C:\Employee.cds’);

end;

procedure TForm1.cbAllowInsertClick(Sender: TObject);beginDBCtrlGrid1.AllowInsert := cbAllowInsert.Checked;

end;

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S269

LISTING 6.4 Continued

Page 283: Delphi Kylix Database Development

procedure TForm1.cbAllowDeleteClick(Sender: TObject);beginDBCtrlGrid1.AllowDelete := cbAllowDelete.Checked;

end;

procedure TForm1.cbShowFocusClick(Sender: TObject);beginDBCtrlGrid1.ShowFocus := cbShowFocus.Checked;

end;

procedure TForm1.ecRowCountChange(Sender: TObject);begintryDBCtrlGrid1.RowCount := StrToInt(ecRowCount.Text);

exceptDBCtrlGrid1.RowCount := 1;

end;end;

procedure TForm1.ecColCountChange(Sender: TObject);begintryDBCtrlGrid1.ColCount := StrToInt(ecColCount.Text);

exceptDBCtrlGrid1.ColCount := 1;

end;end;

procedure TForm1.cbOrientationClick(Sender: TObject);beginDBCtrlGrid1.Orientation := TDBCtrlGridOrientation(cbOrientation.ItemIndex);

end;

procedure TForm1.DBCtrlGrid1PaintPanel(DBCtrlGrid: TDBCtrlGrid;Index: Integer);

beginif Index <> DBCtrlGrid.PanelIndex thenDBCtrlGrid1.Canvas.Draw(0, 0, Image1.Picture.Graphic);

end;

end.

Figure 6.6 shows CtrlGrid at runtime.

Chapter 6270

LISTING 6.4 Continued

Page 284: Delphi Kylix Database Development

Data-Aware Grids

6

DA

TA-AW

AR

EG

RID

S271

FIGURE 6.6CtrlGrid demonstrates TDBCtrlGrid’s behavior.

Third-Party Data-Aware GridsThough TDBGrid and TClientDataSetGrid are useful grid components, they don’t come closeto the flexibility and power that’s provided by third-party data-aware grids. This section listsseveral of the more popular third-party data-aware grids that you might want to investigate ifyou’re looking for more functionality than the built-in Delphi grids offer.

Table 6.8 lists some of the better known third-party TDBGrid replacements.

TABLE 6.8 TDBGrid Replacement Components

Product Description

Orpheus TurboPower Software Company’s flagship Delphi-VCL add-onlibrary contains two data-aware grid components that can be usedfor formatted data entry, multilevel data display, and automaticsubtotaling and totaling of columns. Visit www.turbopower.comfor more information about Orpheus or to download a free trialversion.

InfoPower 2000 This popular general-purpose Delphi library includes theTwwDBGrid component, which works like TDBGrid and adds newfunctionality (such as memo display and automatic footers). Goto www.woll2woll.com for more information about theInfoPower product.

ExpressQuantumGrid An extremely powerful TDBGrid replacement that offers numerousadvanced grid features—too many to list here. Some of thenotable features include multiline data display, runtime sorting

Page 285: Delphi Kylix Database Development

and grouping of data, and extensive customization (both at design time and at runtime). For more information, visit www.devexpress.com.

TopGrid Provides both data-aware and non—data-aware versions of itspowerful grid component, which allow display and editing ofmultiline notes, cells that contain controls (such as combo boxesand check boxes), and numerous customization features. You canfind TopGrid at www.objectinsight.com/TopGridOverview.htm.

Others Numerous other freeware, shareware, and commercial grids areavailable to Delphi and Kylix programmers. The ones listed inthis table are just some of the more popular and more widelyknown grids that are commercially available today.

SummaryThis chapter continued the discussion of data-aware components with an overview of data-awaregrids, 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 andusing the default values. To customize the columns in the resulting grid, use the Columnsproperty.

• Various grid options exist for altering the look and feel of the grid. (See Table 6.4 for asummary.)

• You can use various grid events to gain control over what happens when the user clicks acell or performs some other action. (See Table 6.5 for a summary.)

• For the ultimate control over how the grid looks, use the OnDrawColumnCell event.

• TClientDataSetGrid is a free component that you can use for automatic column sortingand additional column customization.

• TDBCtrlGrid is a VCL-specific component that offers the capability to arrange data for asingle 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 6272

TABLE 6.8 Continued

Product Description

Page 286: Delphi Kylix Database Development

CHAPTER

7Dataset Providers

IN THIS CHAPTER• What Is a Dataset Provider? 274

• Connecting to a Dataset 275

• Resolving Changes to Data 276

• Provider Options 293

• Provider Events 295

• Changing Field Values on the Server 297

• Intercepting Data 298

• Optional Parameters 300

• Master/Detail Relationships 301

• Providing and Resolving Data from StoredProcedures and Joins 302

• Connecting to a Local Database 308

Page 287: Delphi Kylix Database Development

Chapter 7274

Earlier in this book, you learned about dbExpress—a high-performance, low-overhead data-base technology. You learned that it is read-only and unidirectional, making it cumbersome touse directly for most interactive database applications.

Later, I introduced you to client datasets. Client datasets are especially well suited to databaseapplication front ends because they are fast, flexible, and powerful. However, they are inher-ently single user because they are RAM-based. Also, they read from and write to a proprietaryfile format.

So far, these may seem like disparate technologies used to solve different types of program-ming problems. This chapter and the next tie them together, showing you how you can useclient datasets to read and write data using dbExpress as the underlying database technology.

This chapter shows you how to use providers to create multitier database applications.

What Is a Dataset Provider?A dataset provider forms a conduit between a client dataset and an external data store—typi-cally another dataset, such as a TSQLDataSet. It provides data to the client dataset on requestand sends data back to the underlying data store when the client changes it—a technique calledresolving.

The component used to accomplish this is TDataSetProvider, found on the Data Access tab ofthe component palette.

Nothing about dataset providers ties them to dbExpress specifically. The informationpresented in this chapter is applicable to other database technologies as well, such asBDE and dbGo (formerly called ADOExpress).

For a dataset to be compatible with TDataSetProvider, it must support theIProviderSupport interface, defined in DB.pas like this:

IProviderSupport = interfaceprocedure 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;

NOTE

Page 288: Delphi Kylix Database Development

Connecting to a DatasetIn this section I’ll show you how to connect a client dataset to another dataset. To do so, followthese 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 1and 2.

2. Drop a TDataSetProvider on the main form. Set the DataSet property to theTSQLDataSet 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 datasetprovider you created in the previous step.

4. Connect a TDataSource component to the client dataset and hook up a TDBGrid compo-nent to the data source.

5. Create an event handler for the form’s FormCreate event and add the following line ofcode to it:

ClientDataSet1.Open;

6. Run the application.

If you set everything up correctly, you should see the data from the dbExpress database dis-played in the grid. Notice that you can scroll forward and backward through the data, and you

Dataset Providers

7

DA

TASET

PR

OV

IDER

S275

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;

TDataSet implements stub functions for these methods, which generally do nothingor raise an exception. The datasets included with Delphi (BDE, dbExpress, and dbGo)override these methods to provide a specific implementation.

Page 289: Delphi Kylix Database Development

can make changes to the data. However, if you leave the application and run it again, none ofyour changes have been saved to the database. We’ll remedy that situation in the next section.For now, I just want you to see how easy it is to establish the relationship between a clientdataset and another dataset. This relationship is important because dbExpress datasets don’tsupport editing or bidirectional scrolling on their own—they must be connected to a clientdataset to provide these capabilities.

Figure 7.1 shows the results of the preceding steps after connecting to the CONMAN database.

Chapter 7276

FIGURE 7.1Sample application at design time and runtime.

You should leave this sample application loaded in Delphi or save it to disk somewhere; we’llembellish on it in the following sections.

Resolving Changes to DataIf you’ll recall from Chapter 4, client datasets store changes to data in a change log rather thanapplying changes to the underlying data immediately. Because of that, when you changed thegrid’s data in the preceding section, those changes didn’t get reflected in the underlying data-base.

Applying UpdatesTo save changes to the database permanently, you need to call the client dataset’sApplyUpdates method. ApplyUpdates detects that the dataset is connected to a provider andtakes care of sending changes back through the provider to the database.

Page 290: Delphi Kylix Database Development

Add a button to your sample application and create an OnClick handler for it. In the OnClickhandler, add the following code:

if ClientDataSet1.ApplyUpdates(0) > 0 thenShowMessage(‘Failed to update database’);

Now run the application again, modify some data, and click the button. If you quit the applica-tion and rerun it, you’ll see that the changes were indeed saved to the database.

The call to ApplyUpdates takes a single parameter, which indicates the “tolerance level” forerrors. In this case, I’ve specified a zero-error tolerance level. What this means is that if anyerrors occur during the update process, the changes are rolled back and none of the updates arecommitted to the underlying database.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S277

When resolving data to a database, VCL/CLX automatically wraps the updates in atransaction, so either all the changes are made or none of them are. You don’t needto write any code to deal with transactions in this case.

NOTE

At times, you might be willing to tolerate one or more errors when resolving data. Forinstance, if the user changes three rows in the grid, but only two of the changes can be savedsuccessfully, you might still want those two changes saved. If this is the case, pass the maxi-mum number of errors that you will allow to ApplyUpdates. If you don’t care how many errorsoccur, call ApplyUpdates with a parameter of –1.

After the call to ApplyUpdates, any successful updates are removed from the client dataset’schange 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 underlyingdatabase. The most common reason is that another user changed the same row while you wereviewing it or deleted the row before you had a chance to save your changes. Other reasonsmay include a broken connection to the database server.

TDataSetProvider and TClientDataSet give you control over how to detect andrespond to data clashes. Later in this chapter, I’ll cover some of the various techniquesyou can use.

NOTE

Page 291: Delphi Kylix Database Development

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

In some cases, you must set ResolveToDataSet to True. The most common reasons are listednext:

• The provider’s DataSet is not connected to a database—for example, it is aTClientDataSet.

• The provider’s DataSet does not provide the necessary implementation of theIProviderSupport interface.

In these cases, updates will be applied to the dataset referenced by the provider’s DataSetproperty. You can then handle the provider’s AfterApplyUpdates method to make thosechanges persistent (in the case of a TClientDataSet, you could call the dataset’s SaveToFile

method, for example). AfterApplyUpdates, as well as other provider events, are discussed inthe section titled “Provider Events,” later in this chapter.

Reconciliation ErrorsBy default, if one or more errors occur during reconciliation, ApplyUpdates returns a numbergreater than zero, indicating the number of errors that occurred. This is fine if all you want toknow is whether there were errors. However, it doesn’t give you any control over how to han-dle reconciliation errors.

For greatest control over reconciliation errors, you should provide an event handler for theclient dataset’s OnReconcileError event. An empty handler looks like the following:

procedure TfrmMain.ClientDataSet1ReconcileError(DataSet: TCustomClientDataSet; E: EReconcileError;UpdateKind: TUpdateKind; var Action: TReconcileAction);

Begin

end;

DataSet refers to the client dataset for which the reconciliation error occurred. E is an excep-tion that gives more information about the error. UpdateKind is one of the values listed inTable 7.1. You should set Action to one of the values listed in Table 7.2, which instructs theVCL as to what action to take for the offending record.

The OnReconcileError event is fired for each offending record. If eight updates are resolvedback 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 numberreturned from ApplyUpdates may be 0, 1, or 2. This is explained in more detail in Table 7.2.

Chapter 7278

Page 292: Delphi Kylix Database Development

The value returned from ApplyUpdates is also dependent on the parameter passed toApplyUpdates. The return value will never be more than one greater than the value specifiedby the parameter. For instance, if you pass 0 to ApplyUpdates (which is typically what’s done),the return value will be either 0 or 1.

TABLE 7.1 TUpdateKind Values

Value Description

ukInsert The record is a newly inserted record.

ukModify Modifications were made to an existing record.

ukDelete The record refers to a deleted record.

TABLE 7.2 TReconcileAction Values

Value Description

raSkip Don’t apply updates to this record. Leave the unapplied changes in theclient dataset’s change log. This record will be counted in the returnvalue from ApplyUpdates.

raAbort Abort the whole operation. No updates are made to the underlyingdatabase at all, and all changes are left in the client dataset’s changelog. All records are counted in the return value from ApplyUpdates.

raMerge Merge the record with the record in the underlying database. Thisworks only if the fields that are changed in the record don’t conflictwith fields that were changed by someone else. This record will not becounted in the return value from ApplyUpdates. You must set thepfInKey flag (discussed later in the section titled “Update Modes”) forall fields in the primary key for this option to be available.

raCorrect Indicates that changes were made to the current record inside theOnReconcileError event handler. VCL/CLX should try to update againwith the new field values.

raCancel Cancel changes to this record, reverting to the original record data.This record will not be counted in the return value from ApplyUpdates.

raRefresh Cancel changes to this record and reread the record data from the data-base. This record will not be counted in the return value fromApplyUpdates. You must set the pfInKey flag for all fields in the pri-mary key for this option to be available.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S279

Page 293: Delphi Kylix Database Development

Fortunately, in most situations you don’t have to worry about writing a complicated event han-dler for OnReconcileError. Delphi comes with a prewritten class that you can use to handlereconcile errors. To use this class, perform the following steps:

1. From the Delphi main menu, Select File, New, Other.

2. On the Dialogs tab of the New Items dialog, select the Reconcile Error Dialog icon.Make sure Copy is selected in the option buttons below the list of icons (see Figure 7.2).

3. Click OK.

4. Save the resulting unit as something like ReconcileErrorForm.pas. (the name doesn’tmatter).

5. Add the new unit to the uses clause of the form that contains the client dataset (in thisexample, it’s the main form).

6. Add the following code to the OnReconcileError event for the client dataset:

Action := HandleReconcileError(DataSet, UpdateKind, E);

I won’t go into detail about the inner working of HandleReconcileError here. You should takea look at the unit’s source code to gain an understanding of how it works.

Chapter 7280

FIGURE 7.2Inserting 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.

Page 294: Delphi Kylix Database Development

LISTING 7.1 Updates—MainForm.pas

unit MainForm;

interface

usesSysUtils, Variants, Classes, QGraphics, QControls, QForms, QStdCtrls,QDialogs, QExtCtrls, DBXpress, FMTBcd, QGrids, QDBGrids, DB, Provider,DBClient, SqlExpr, QDBCtrls, QTypes;

typeTfrmMain = 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;

Dataset Providers

7

DA

TASET

PR

OV

IDER

S281

Page 295: Delphi Kylix Database Development

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

Chapter 7282

LISTING 7.1 Continued

Page 296: Delphi Kylix Database Development

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);privatefunction GetNextID: Integer;procedure Log(const s: string);{ Private declarations }

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

uses RecErrorForm;

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);

Dataset Providers

7

DA

TASET

PR

OV

IDER

S283

LISTING 7.1 Continued

Page 297: Delphi Kylix Database Development

beginClientDataSet1.Open;

end;

procedure TfrmMain.Timer1Timer(Sender: TObject);beginlblUpdates.Caption := IntToStr(ClientDataSet1.ChangeCount) + ‘ Update(s)’;

end;

procedure TfrmMain.btnApplyUpdatesClick(Sender: TObject);beginShowMessage(‘ApplyUpdates returned a value of ‘ +IntToStr(ClientDataSet1.ApplyUpdates(0)));

end;

procedure TfrmMain.btnCancelUpdatesClick(Sender: TObject);beginClientDataSet1.CancelUpdates;

end;

procedure TfrmMain.ClientDataSet1ReconcileError(DataSet: TCustomClientDataSet; E: EReconcileError;UpdateKind: TUpdateKind; var Action: TReconcileAction);

beginAction := HandleReconcileError(DataSet, UpdateKind, E);

end;

function TfrmMain.GetNextID: Integer;beginsqlID.ExecSQL;Result := sqlID.ParamByName(‘AValue’).AsInteger;

end;

procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;UpdateKind: TUpdateKind; var Applied: Boolean);

beginLog(‘TDataSetProvider.BeforeUpdateRecord’);

if UpdateKind = ukInsert thenif DeltaDS.FieldByName(‘ID’).OldValue <= 0 thenDeltaDS.FieldByName(‘ID’).NewValue := GetNextID;

end;

Chapter 7284

LISTING 7.1 Continued

Page 298: Delphi Kylix Database Development

procedure TfrmMain.Log(const s: string);beginlbEvents.Items.Add(s);

end;

procedure TfrmMain.DataSetProvider1AfterApplyUpdates(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.AfterApplyUpdates’);

end;

procedure TfrmMain.DataSetProvider1AfterExecute(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.AfterExecute’);

end;

procedure TfrmMain.DataSetProvider1AfterGetParams(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.AfterGetParams’);

end;

procedure TfrmMain.DataSetProvider1AfterGetRecords(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.AfterGetRecords’);

end;

procedure TfrmMain.DataSetProvider1AfterRowRequest(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.AfterRowRequest’);

end;

procedure TfrmMain.DataSetProvider1AfterUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;UpdateKind: TUpdateKind);

beginLog(‘TDataSetProvider.AfterUpdateRecord’);

end;

procedure TfrmMain.DataSetProvider1BeforeApplyUpdates(Sender: TObject;var OwnerData: OleVariant);

Dataset Providers

7

DA

TASET

PR

OV

IDER

S285

LISTING 7.1 Continued

Page 299: Delphi Kylix Database Development

beginLog(‘TDataSetProvider.BeforeApplyUpdates’);

end;

procedure TfrmMain.DataSetProvider1BeforeExecute(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.BeforeExecute’);

end;

procedure TfrmMain.DataSetProvider1BeforeGetParams(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.BeforeGetParams’);

end;

procedure TfrmMain.DataSetProvider1BeforeGetRecords(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.BeforeGetRecords’);

end;

procedure TfrmMain.DataSetProvider1BeforeRowRequest(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TDataSetProvider.BeforeRowRequest’);

end;

function TfrmMain.DataSetProvider1DataRequest(Sender: TObject;Input: OleVariant): OleVariant;

beginLog(‘TDataSetProvider.OnDataRequest’);

end;

procedure TfrmMain.DataSetProvider1GetData(Sender: TObject;DataSet: TCustomClientDataSet);

beginLog(‘TDataSetProvider.OnGetData’);

end;

procedure TfrmMain.DataSetProvider1GetDataSetProperties(Sender: TObject;DataSet: TDataSet; out Properties: OleVariant);

beginLog(‘TDataSetProvider.OnDataSetProperties’);

end;

Chapter 7286

LISTING 7.1 Continued

Page 300: Delphi Kylix Database Development

procedure TfrmMain.DataSetProvider1GetTableName(Sender: TObject;DataSet: TDataSet; var TableName: String);

beginLog(‘TDataSetProvider.OnGetTableName’);

end;

procedure TfrmMain.DataSetProvider1UpdateData(Sender: TObject;DataSet: TCustomClientDataSet);

beginLog(‘TDataSetProvider.OnUpdateData’);

end;

procedure TfrmMain.DataSetProvider1UpdateError(Sender: TObject;DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind;var Response: TResolverResponse);

beginLog(‘TDataSetProvider.OnUpdateError’);

end;

procedure TfrmMain.ClientDataSet1AfterApplyUpdates(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.AfterApplyUpdates’);

end;

procedure TfrmMain.ClientDataSet1AfterExecute(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.AfterExecute’);

end;

procedure TfrmMain.ClientDataSet1AfterGetParams(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.AfterGetParams’);

end;

procedure TfrmMain.ClientDataSet1AfterGetRecords(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.AfterGetRecords’);

end;

procedure TfrmMain.ClientDataSet1AfterRefresh(DataSet: TDataSet);

Dataset Providers

7

DA

TASET

PR

OV

IDER

S287

LISTING 7.1 Continued

Page 301: Delphi Kylix Database Development

beginLog(‘TClientDataSet.AfterRefresh’);

end;

procedure TfrmMain.ClientDataSet1AfterRowRequest(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.AfterRowRequest’);

end;

procedure TfrmMain.ClientDataSet1BeforeApplyUpdates(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.BeforeApplyUpdates’);

end;

procedure TfrmMain.ClientDataSet1BeforeExecute(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.BeforeExecute’);

end;

procedure TfrmMain.ClientDataSet1BeforeGetParams(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.BeforeGetParams’);

end;

procedure TfrmMain.ClientDataSet1BeforeGetRecords(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.BeforeGetRecords’);

end;

procedure TfrmMain.ClientDataSet1BeforeRefresh(DataSet: TDataSet);beginLog(‘TClientDataSet.BeforeRefresh’);

end;

procedure TfrmMain.ClientDataSet1BeforeRowRequest(Sender: TObject;var OwnerData: OleVariant);

beginLog(‘TClientDataSet.BeforeRowRequest’);

end;

Chapter 7288

LISTING 7.1 Continued

Page 302: Delphi Kylix Database Development

procedure TfrmMain.btnClearEventLogClick(Sender: TObject);beginlbEvents.Items.Clear;

end;

end.

Run two occurrences of this application on your computer. In the first occurrence, changeJohn’s first name to Eddie and apply updates. In the second occurrence, notice that the recordstill shows a first name of John. Change John’s last name to Smith and apply updates. TheOnReconcileError event fires, indicating that the record has been changed by someone else inthe meantime (see Figure 7.3).

Dataset Providers

7

DA

TASET

PR

OV

IDER

S289

LISTING 7.1 Continued

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

By default, this dialog shows conflicting values only—in other words, fields that were changedby another user. Uncheck the Show Conflicting Fields Only check box to display all field val-ues. If you do, you’ll see that this application changed the LAST field from Lombardo to Smith.Another application changed the FIRST field from John to Eddie.

The upper-right corner of the dialog contains a list of option buttons that the user can use toinstruct Delphi how to deal with the error. The option buttons correspond to the raSkip,raCancel, raCorrect, raRefresh, and raMerge values for TReconcileAction. No raAbortoption is listed, but clicking the Cancel button will result in a value of raAbort being returnedfrom the event handler.

Page 303: Delphi Kylix Database Development

Like several of the other sample applications presented in this book, the Updates applicationcontains an event log list box that shows you when and in what order interesting events fire forboth 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 FieldsTDataSetProvider doesn’t automatically check for conflicts on BLOB fields, includingmemos. If two users change the contents of a BLOB field or memo, the second user’s changeswill overwrite the first user’s changes without warning.

Probably the best way to deal with this situation is to add a corresponding integer field to yourdatabase for each BLOB field. The integer field contains an “update number” for the BLOBfield. For instance, if the database table contains a column named IMAGE, add an integer col-umn named IMAGEUNIQUE, or something similar. Create a trigger for the IMAGE column suchthat 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, whichindicates that the IMAGE column was changed also.

Refreshing Data from the ServerEarlier chapters discussed the TDBNavigator component, with the exception of one button—the Refresh button.

TDBNavigator’s Refresh button (which calls the TDataSet.Refresh method) is used to refreshthe dataset from the underlying database. For TClientDataSet, Refresh refreshes the data byrefetching all rows through the dataset provider.

Chapter 7290

Refresh will raise an exception if the dataset’s change log is not empty. Before youcall TClientDataSet.Refresh directly, you should check the ChangeCount property tosee if it is zero. If it is nonzero, either apply or cancel the updates first (usingApplyUpdates or CancelUpdates) or refrain from calling Refresh.

CAUTION

Refreshing data from the provider is useful in those cases in which a reconciliation erroroccurs and you want to retrieve the latest data from the underlying database. You can also callit at other times to ensure that the local copy of the data is up to date.

Page 304: Delphi Kylix Database Development

Rather than calling Refresh to refresh the entire dataset, you can also callTClientDataSet.RefreshRecord to refresh only the current record. RefreshRecord does notraise an exception if the dataset’s change log is not empty. Rather, it leaves the entire changelog intact, including any changes that may have been made to the refreshed record.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S291

RefreshRecord requires that pfInKey be set for all fields in the primary key (as dis-cussed in the following section). In addition, you should usually call RefreshRecordonly when the UpdateStatus of the current record is usUnmodified.

if ClientDataSet1.UpdateStatus = usUnmodified thenClientDataSet1.RefreshRecord;

NOTE

Update ModesWhen resolving data to the database, TDataSetProvider automatically builds the necessarySQL statement to send to the server to perform the update. An example of such an SQL state-ment is

UPDATE CONTACTS SET LAST = ‘Smith’ WHERE ID = 5

TDataSetProvider allows you some control over how the SQL statement is built. The firstlevel of control is afforded by the TDataSetProvider.UpdateMode property. UpdateMode maybe set to one of the values shown in Table 7.3.

TABLE 7.3 TUpdateMode Values

Value Description

upWhereAll All designated fields are included in the WHERE clause.

upWhereChanged Only key fields as well as modified fields are included in the WHEREclause.

upWhereKeyOnly Only key fields are included in the WHERE clause.

Assume a table named EMPLOYEES has four columns: ID, NAME, BIRTHDAY, and SALARY. ID is theprimary key for the table. One particular record in the table contains the values 1, “JohnSmith”, 5/1/1958, $40,000.

The following SQL statements show what SQL statement would be generated for the varioussettings of UpdateMode, assuming John Smith’s SALARY field was changed from $40,000 to$45,000.

Page 305: Delphi Kylix Database Development

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 orkey field, anyway? Each persistent field created on the server data module has aProviderFlags property, which is used to instruct the provider how to treat the field.ProviderFlags is a set property and can contain any or all the values listed in Table 7.4.

ProviderFlags is a property of TField, which means you don’t need to have persistent fieldsto use ProviderFlags. You can set ProviderFlags for a nonpersistent field, like this:

sqlClients.FieldByName(‘ID’).ProviderFlags := [pfInWhere, pfInKey];

TABLE 7.4 TUpdateMode Values

Value Description

pfInUpdate The field can be modified.

pfInWhere The field is included in the WHERE clause when UpdateMode is set toupWhereAll or upWhereChanged.

pfInKey The field is part of the primary key and controls such features asrefreshing records through a call to RefreshRecord, as well as recon-ciliation options such as merging.

pfHidden The field is included in data packets sent to and from the client only toserve as a way to make each record unique. The client dataset can’t seeor modify the field.

Chapter 7292

Nonpersistent fields also have a ProviderFlags property, which is automatically set to[pfInUpdate, pfInWhere].

NOTE

Continuing with the previous EMPLOYEES table example, you would set up persistent fields forthe table as shown in Table 7.5.

Page 306: Delphi Kylix Database Development

TABLE 7.5 ProviderFlags Settings for the Fictitious EMPLOYEES Table Fields

Field ProviderFlags

ID [pfInUpdate, pfInWhere, pfInKey]

NAME [pfInUpdate, pfInWhere]

BIRTHDAY [pfInUpdate, pfInWhere]

SALARY [pfInUpdate, pfInWhere]

ID is the primary key for the table.

NAME, BIRTHDAY, and SALARY can all be updated, and they should all be included in the WHEREclause of an update SQL statement when the provider’s UpdateMode property is set toupWhereAll.

Provider OptionsTDataSetProvider supports various options that determine the way in which data is sent to theclient dataset, what changes are allowed to the data, and the way in which updates to the dataare handled. TDataSetProvider.Options is a set property that enables you to customize theseoptions 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 aspart of the data packet. The client application must callTClientDataSet.FetchBlobs to retrieve BLOB data.When False, BLOBs are returned as part of the datapacket.

poFetchDetailsOnDemand Used when the provider is part of a master/detail relation-ship. When True, detail records are not returned from theserver as part of the data packet. The client applicationmust call TClientDataSet.FetchDetails to retrieve detailrecords. When False, detail records are returned as part ofthe data packet.

poIncFieldProps When True, field properties including Alignment,Currency, DisplayFormat, DisplayLabel, DisplayValues,DisplayWidth, EditFormat, EditMask, MaxValue,MinValue, and Visible are sent to the client along with thedata.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S293

Page 307: Delphi Kylix Database Development

poCascadeDeletes Used when the provider is part of a master/detail relation-ship. When True, the server deletes detail records automati-cally when the master record is deleted.

poCascadeUpdates Used when the provider is part of a master/detail relation-ship. When True, the server automatically updates detailrecords when the key value(s) of the master recordchanges.

poReadOnly When True, you can’t edit the data in the client dataset.

poAllowMultiRecordUpdates When True, allows updates that affect multiple records.When False, updates that affect more than one record raisean exception.

poDisableInserts When True, client datasets will not be able to Insert orAppend new records.

poDisableEdits When True, client datasets will not be able to Edit existingrecords.

poDisableDeletes When True, client datasets will not be able to Delete exist-ing records.

poNoReset When True, calls to AS_GetRecords ignore the reset flag.

poAutoRefresh When True, the provider automatically refreshes updatedrecords with the latest data from the database.

It is important to note that this option is not yet imple-mented in Delphi as of version 6.

poPropogateChanges When True, any changes made to data during theBeforeUpdateRecord or AfterUpdateRecord event han-dlers are sent back to the client.

poAllowCommandText When True, the client dataset can override theCommandText property of the provider’s dataset. WhenFalse, attempting to set the client dataset’s CommandText

property raises an exception.

poRetainServerOrder When True, alerts the client dataset that it should notattempt to sort the data returned from the server.

At the time of this writing, the poAutoRefresh option is not implemented. poAutoRefresh willbe useful for refreshing an updated record from the database when the database fills in fieldvalues automatically.

Chapter 7294

TABLE 7.6 Continued

Option Description

Page 308: Delphi Kylix Database Development

For example, a database might include a trigger for autogenerating a table’s primary key, usinga generator. At the time a new record is posted to the database, the value of that field may beNULL. During the post operation, the database will fill in the value of the primary key, andpoAutoRefresh will then reread the record so the Delphi application knows the value of theprimary key.

Without the poAutoRefresh option, you can still autoassign primary key values to a record, butyou will need to perform a little work in the server-side data module. This technique isexplained later, in the section titled “Changing Field Values on the Server.”

Provider EventsTDataSetProvider publishes two types of events—OnXxx and BeforeXxx/AfterXxx. TheBeforeXxx/AfterXxx events fire before and after “interesting” things happen in the provider,such as when updates are applied to the underlying database.

Table 7.7 lists the Before and After events supported by TDataSetProvider.

TABLE 7.7 TDataSetProvider BeforeXxx/AfterXxx Events

Event Description

AfterApplyUpdates Fired after the updates to the database are complete.

AfterExecute Fired after the server executes the query or stored procedurethat will ultimately return data to the client.

AfterGetParams Fired after the server returns output parameters from thedataset to the client dataset.

AfterGetRecords Fired after the provider creates the data packet to send to theclient.

AfterRowRequest Fired after the provider refreshes the current record because ofa call to TClientDataSet.RefreshRecord or any othermethod that fetches data.

AfterUpdateRecord Fired after a record is successfully updated.

BeforeApplyUpdates Fired before updates are applied to the database.

BeforeExecute Fired before the server executes a query or stored procedure.

BeforeGetParams Fired before the server returns output parameters from thedataset to the client dataset.

BeforeGetRecords Fired before the provider creates the data packet to send to theclient.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S295

Page 309: Delphi Kylix Database Development

BeforeRowRequest Fired before the provider refreshes the current record becauseof a call to TClientDataSet.RefreshRecord or any othermethod that fetches data.

BeforeUpdateRecord Fired before each record’s updates are applied to the database.

Most of the BeforeXxx and AfterXxx events pass a parameter named OwnerData. OwnerData isa variant that contains user-defined data. The data is passed from the client dataset to theprovider and back to the client dataset during certain method calls, such as ApplyUpdates. Theflow is as follows:

In the client dataset BeforeXXX event (such as BeforeGetRecords), OwnerData can be set toanything that you want to pass to the provider. Because it is a variant, it can contain a simplevalue such as an integer—or something more complex, such as an array of values.

As flow passes to the provider, the OwnerData parameter is passed to the provider’s BeforeXxx

event. The value of OwnerData may be changed in the BeforeXxx event, if needed.

Next, flow continues to the provider’s AfterXxx event, where OwnerData may again bechanged if necessary.

Finally, flow passes to the client dataset’s AfterXxx event, when you may inspect the (possiblymodified) value of OwnerData. The client dataset’s AfterXxx event is the end of the line, sothere is no reason to modify the value of OwnerData in the client dataset’s AfterXxx event.

The following chapter shows an example of how you can use the OwnerData parameter toimplement 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 butbefore the data is returned to the client. You can handle thisevent to modify the data in some way before passing it onto the client. For example, you might encrypt fields, com-press the data, or weed out certain data that the clientshould never see. This event is discussed in the“Intercepting Data” section later in this chapter.

Chapter 7296

TABLE 7.7 Continued

Event Description

Page 310: Delphi Kylix Database Development

OnGetDataSetProperties Fired after data is fetched from the underlying database butbefore the data is returned to the client. Using the optionalparameters (discussed later in this chapter in the sectiontitled “Optional Parameters”), you can send additionalinformation to the client.

OnGetTableName Used when the provider returns data from a join or storedprocedure. You can handle this event to instruct theprovider which table to apply updates to. This option isdiscussed in the section titled “Providing and ResolvingData from a Join,” later in this chapter.

OnUpdateData OnUpdateData is the counterpart of OnGetData. It is firedjust before updates are sent to the database server. You canhandle this event to decrypt data before it is saved to thedatabase, for example. This event is discussed in the“Intercepting Data” section later in this chapter.

OnUpdateError Fired when an error occurs while reconciling data. If youdon’t handle this event, the error is sent back to the clientapplication. You can handle this event to ignore certainerrors or attempt to correct them on the server before send-ing them back to the client.

Some of the more interesting events will be discussed in more detail in the sections thatfollow.

Changing Field Values on the ServerSometimes, you will want the server to make modifications to data that is passed to it by theclient. The most common example is when a table contains an ID field that is the primary keyfor the table. The database server is often responsible for assigning unique IDs to each individ-ual record.

To accomplish this, you must follow this procedure:

1. Create a stored procedure in the database that will return the next unique ID.

2. Include the poPropogateChanges setting in the TDataSetProvider’s Options property.

3. Include the pfInKey setting in the ID field’s ProviderFlags.

4. Provide an event handler for the provider’s BeforeUpdateRecord event.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S297

TABLE 7.8 Continued

Event Description

Page 311: Delphi Kylix Database Development

In the BeforeUpdateRecord event handler for a newly inserted record, obtain the next value ofthe ID field and assign it to the record. The following code snippet shows a typical implemen-tation for the BeforeUpdateRecord event handler.

procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;UpdateKind: TUpdateKind; var Applied: Boolean);beginif UpdateKind = ukInsert thenif DeltaDS.FieldByName(‘ID’).OldValue <= 0 thenDeltaDS.FieldByName(‘ID’).NewValue := GetNextID;

end;

Where GetNextID looks like this:

function TfrmMain.GetNextID: Integer;beginsqlID.ExecSQL;Result := sqlID.ParamByName(‘AValue’).AsInteger;end;

sqlID is a TSQLDataSet component that executes a stored procedure on the database server,which in turn returns the next unique ID number in an output parameter named AValue.

I won’t provide a program example using this technique at this time, but you’ll see this tech-nique implemented in later examples in this chapter.

Intercepting DataTable 7.4 lists two events, OnGetData and OnUpdateData, that can be used to intercept data onits way from the provider to the client and also on its way from the client to the provider.

In this chapter, we’re working with a single application; in other words, the client and serverportions of the data are both contained in a single application. In the next chapter, we’ll createclient and server applications that may exist on separate machines that can be located down thehall from each other or on different continents.

When data travels from machine to machine, unfortunately there is always the chance thatsome hacker may be attempting to listen in on the exchange of data. If the data includes any-thing sensitive, such as account numbers or the like, you might want to consider encryptingthat data before sending it over the wire or across cyberspace.

OnGetData and OnUpdateData provide the two hooks on the server side for implementing thisfunctionality. The following code snippet shows implementations of both OnGetData andOnUpdateData that encrypt data on its way to the client and decrypt data on its way back. Thefunctions EncryptData and DecryptData referenced by the code are fictitious routines that youwould need to supply if you were to implement this functionality in your own applications.

Chapter 7298

Page 312: Delphi Kylix Database Development

procedure TForm1.ProviderGetData(Sender: TObject;DataSet: TCustomClientDataSet);

beginwhile not DataSet.EOF do beginDataSet.Edit;DataSet.FieldByName(‘AccountNumber’).AsString :=EncryptData(DataSet.FieldByName(‘AccountNumber’).AsString);

DataSet.Post;DataSet.Next;

end;end;

procedure TfrmMain.DataSetProvider1UpdateData(Sender: TObject;DataSet: TCustomClientDataSet);

beginwhile not DataSet.EOF do beginif DataSet.UpdateStatus <> usDeleted then beginDataSet.Edit;DataSet.FieldByName(‘AccountNumber’).AsString :=DecryptData(DataSet.FieldByName(‘AccountNumber’).AsString);

DataSet.Post;DataSet.Next;

end;end;

end;

On the client side, the AccountNumber field will be encrypted.ClientDataSet1.FieldByName(‘AccountNumber’).AsString will return an encrypted accountcode, which you should then decrypt before displaying. Note that the code shown here is arather simplistic implementation of encrypting/decrypting data. For one thing, only the accountnumber is encrypted and decrypted. In a real application, you might want to encrypt anddecrypt all string fields by looping through all fields in the dataset.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S299

For a good third-party encryption library that supports encryption standards such asDES, Blowfish, and Rijndael (AES), take a look at TurboPower Software’s LockBoxproduct at www.tpx.turbopower.com/products/LockBox.

NOTE

Page 313: Delphi Kylix Database Development

Optional ParametersOptional parameters are custom data that pertain to the dataset passed to the client. Optionalparameters relate to the dataset as a whole, rather than to individual records. You can use anoptional parameter to pass data back to the client, such as the date and time the data was pro-vided, the length of time required to run the query on the server, or any other data.

To pass optional parameters to the client, provide an event handler for TDataSetProvider’sOnGetDataSetProperties event. Within this event handler, use the Properties parameter toset the values to send back to the client. The following code snippet shows how to send boththe time required to execute the query and the time the query was executed.

procedure TForm1.DataSetProvider1GetDataSetProperties(Sender: TObject;DataSet: TDataSet; out Properties: OleVariant);

beginProperties := VarArrayCreate([0, 1], varVariant);Properties[0] := VarArrayOf([‘TimeQueried’, Now, True]);Properties[1] := VarArrayOf([‘QueryPerformance’, FTimeToQuery, True]);

end;

Properties is a variant array of variant arrays. This code snippet creates an array of two vari-ants. Each variant in the array is an array of three values: the name of the optional parameter,the value of the parameter, and a Boolean value that indicates whether the optional parametershould be sent back to the server as part of the delta.

In this example, FTimeToQuery is a private variable that is calculated using the following code:

procedure TForm1.SQLDataSet1BeforeOpen(DataSet: TDataSet);beginFTimeToQuery := GetTickCount;

end;

procedure TForm1.SQLDataSet1AfterOpen(DataSet: TDataSet);beginFTimeToQuery := 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);varQP: DWord;TimeQueried: TDateTime;

beginQP := ClientDataSet1.GetOptionalParam(‘QueryPerformance’);TimeQueried := ClientDataSet1.GetOptionalParam(‘TimeQueried’);

Chapter 7300

Page 314: Delphi Kylix Database Development

ShowMessage(‘The query took ‘ + IntToStr(QP) + ‘ms and was executed on ‘ +DateToStr(TimeQueried) + ‘ at ‘ + TimeToStr(TimeQueried));

end;

Master/Detail RelationshipsBack in Chapter 2, “dbExpress Datasets,” you learned how to create a master/detail relation-ship between two or more dbExpress datasets.

Later, in Chapter 4, you learned how to create nested TClientDataSets to create amaster/detail relationship at the TClientDataSet level.

Using providers, you will establish the master/detail relationship on the server-side data mod-ule, using TSQLDataSet components. You will then connect a TDataSetProvider component tothe master dataset only. Drop a single TClientDataSet component on the client-side data mod-ule and connect it to the master dataset’s provider. This will automatically create a nesteddataset on the client side.

It is good practice to create one data module for the dbExpress components and datasetproviders and another data module for the client datasets. I refer to these data modules asserver-side data modules and client-side data modules. This is explained more fully in the sec-tion titled, “Connecting to a Local Database,” later in this chapter.

The MasterDetail sample application, included in the downloads for this book, illustrates thistechnique. I haven’t included a listing here, because almost no code is required—everything isdone 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.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S301

FIGURE 7.4Components needed for a master/detail relationship.

Page 315: Delphi Kylix Database Development

Providing and Resolving Data from StoredProcedures and JoinsSo far, the examples in this chapter focused on providing data from a single table using a sim-ple select statement, such as SELECT * FROM CONTACTS. Frequently, data is provided from astored procedure on the server or is generated as a join between multiple tables, as the follow-ing two snippets show:

// Stored procedureSELECT * FROM CONTACTSBYSTATE(‘FL’);

or

// JoinSELECT CONTACTS.FIRST, CONTACTS.LAST, TODOS.SCHEDULED, TODOS.DESCRIPTIONFROM CONTACTS, TODOSWHERE CONTACTS.ID = TODOS.CONTACTID;

Providing and Resolving Data from a Stored ProcedureIn the preceding example, the stored procedure returns all contacts for a given state—in thiscase, Florida. TDataSetProvider attempts to intelligently determine what database table toupdate when resolving data, but in this case, CONTACTSBYSTATE is a stored procedure and not atable.

TDataSetProvider needs a little help to know what table should be updated with any datachanges. To do this, you need to provide an event handler for the provider’s OnGetTableNameevent. In the event handler, specify the name of the table to be updated, like this:

procedure TForm1.DataSetProvider1GetTableName(Sender: TObject;DataSet: TDataSet; var TableName: String);

beginTableName := ‘CONTACTS’;

end;

In addition, you need to set the ProviderFlags to [] for all fields in the stored proceduredataset that are not updated.

Providing and Resolving Data from a JoinBy definition, a join returns data from more than one table. In many cases, the user will be ableto update data from only one of the tables. For example, in the select statement shown previ-ously, data is retrieved from the CONTACTS table and the TODOS table. On the client side, theuser 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 screenthat he’s viewing todos on).

Chapter 7302

Page 316: Delphi Kylix Database Development

If this is the case, you can specify the TODOS table name in the OnGetTableName event handler,as explained earlier.

Sometimes, though, the user will be able to update multiple tables at once. To handle this situ-ation, you will need to write some code. Again, we turn to TDataSetProvider’sBeforeUpdateRecord event handler. The trick is to apply the necessary updates to the individ-ual tables ourselves inside the event handler.

The following code snippet shows the general outline that you will follow to apply updates tomultiple tables at once. It is not compilable code.

procedure TForm1.SQLClientDataSet1BeforeUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;var Applied Boolean);

varSQL: string;Connection: TSQLConnection;

begin// Obtain a pointer to the connection from the source datasetConnection := (SourceDS as TCustomSQLDataSet).SQLConnection;

case UpdateKind ofukInsert: begin// Insert into the first tableSQL := // SQL INSERT STATEMENT FOR TABLE 1Connection.Execute(SQL, nil, nil);

// Insert into the second tableSQL := // SQL INSERT STATEMENT FOR TABLE 2Connection.Execute(SQL, nil, nil);

end;

ukModify: begin// Update the first tableSQL := // SQL UPDATE STATEMENT FOR TABLE 1Connection.Execute(SQL, nil, nil);

// Update the second tableSQL := // SQL UPDATE STATEMENT FOR TABLE 2Connection.Execute(SQL, nil, nil);

end;

ukDelete: begin// Delete from the first tableSQL := // SQL DELETE STATEMENT FOR TABLE 1Connection.Execute(SQL, nil, nil);

Dataset Providers

7

DA

TASET

PR

OV

IDER

S303

Page 317: Delphi Kylix Database Development

// Delete from the second tableSQL := // SQL DELETE STATEMENT FOR TABLE 2Connection.Execute(SQL, nil, nil);

end;end;

Applied := True;end;

As you can see from the preceding listing, the general idea is to determine what kind of opera-tion is taking place (Insert, Modify, or Delete) and then call the TSQLConnection component toexecute the appropriate SQL statements directly. At the end of the method, set Applied to True

so the provider knows that you’ve already handled the update manually.

This process works equally well for three-, four-, or n-table joins.

The following example shows how you can resolve updates in a simple two-way join.

Listing 7.2 contains the source code for the main form of the Joins application.

LISTING 7.2 Joins—MainForm.pas

unit MainForm;

interface

usesSysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,QStdCtrls, DBXpress, FMTBcd, DB, SqlExpr, QGrids, QDBGrids, Provider,DBClient, Variants;

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

Chapter 7304

Page 318: Delphi Kylix Database Development

private{ Private declarations }FNextID: Integer;function GetNextID: Integer;

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginClientDataSet1.Open;

end;

procedure TfrmMain.ClientDataSet1NewRecord(DataSet: TDataSet);beginDec(FNextID);DataSet.FieldByName(‘CONTACTID’).AsInteger := FNextID;

end;

function TfrmMain.GetNextID: Integer;beginsqlID.ExecSQL;Result := sqlID.ParamByName(‘AValue’).AsInteger;

end;

procedure TfrmMain.DataSetProvider1BeforeUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;UpdateKind: TUpdateKind; var Applied: Boolean);

varSQL: string;Connection: TSQLConnection;ID: Integer;

begin// Obtain a pointer to the connection from the source datasetConnection := (SourceDS as TCustomSQLDataSet).SQLConnection;

case UpdateKind ofukInsert: beginID := GetNextID;

Dataset Providers

7

DA

TASET

PR

OV

IDER

S305

LISTING 7.2 Continued

Page 319: Delphi Kylix Database Development

// Insert into the first tableSQL := 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 tableSQL := 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 tableSQL := ‘’;

if not VarIsEmpty(DeltaDS.FieldByName(‘FIRST’).NewValue) thenSQL := SQL + Format(‘FIRST = %s’,[QuotedStr(DeltaDS.FieldByName(‘FIRST’).NewValue)]);

if not VarIsEmpty(DeltaDS.FieldByName(‘LAST’).NewValue) then beginif SQL <> ‘’ thenSQL := SQL + ‘, ‘;

SQL := SQL + Format(‘LAST = %s’,[QuotedStr(DeltaDS.FieldByName(‘LAST’).NewValue)]);

end;

if SQL <> ‘’ then beginID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;SQL := Format(‘UPDATE CONTACTS SET %s ‘ +‘WHERE CONTACTID = %d’, [SQL, ID]);

Connection.Execute(SQL, nil, nil);end;

// Update the second tableSQL := ‘’;

if not VarIsEmpty(DeltaDS.FieldByName(‘SPOUSE’).NewValue) then beginID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;

if VarIsNull(DeltaDS.FieldByName(‘SPOUSE’).OldValue) thenSQL := Format(‘INSERT INTO CONTACTS2 (CONTACTID, SPOUSE) ‘ +‘VALUES (%d, %s)’,

Chapter 7306

LISTING 7.2 Continued

Page 320: Delphi Kylix Database Development

[ID, QuotedStr(DeltaDS.FieldByName(‘SPOUSE’).NewValue)])elseSQL := Format(‘UPDATE CONTACTS2 SET SPOUSE = %s ‘ +‘WHERE CONTACTID = %d’,[QuotedStr(DeltaDS.FieldByName(‘SPOUSE’).NewValue), ID]);

Connection.Execute(SQL, nil, nil);end;

end;

ukDelete: beginID := DeltaDS.FieldByName(‘CONTACTID’).OldValue;

// Delete from the second tableSQL := Format(‘DELETE FROM CONTACTS2 WHERE CONTACTID = %d’, [ID]);Connection.Execute(SQL, nil, nil);

// Delete from the first tableSQL := Format(‘DELETE FROM CONTACTS WHERE CONTACTID = %d’, [ID]);Connection.Execute(SQL, nil, nil);

end;end;

Applied := True;end;

procedure TfrmMain.btnApplyUpdatesClick(Sender: TObject);beginClientDataSet1.ApplyUpdates(0);

end;

end.

The CONMAN database contains a CONTACTS2 table, which has a one-to-one correspondence tothe CONTACTS table. CONTACTS2 is only in the database for purposes of this example.

The DataSetProvider1BeforeUpdateRecord method handles all insert, modify, and deleterequests by dynamically building the appropriate SQL statements and sending them directly tothe database.

To keep the SQL statements simple, this sample application works with just two fields fromthe CONTACTS table and a single field from the CONTACTS2 table.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S307

LISTING 7.2 Continued

Page 321: Delphi Kylix Database Development

Connecting to a Local DatabaseThis chapter explains how to take advantage of providers in a multitier application in which theclient and server are both physically located in the same executable. Even under this arrange-ment, it is beneficial to structure the application such that it is a relatively simple matter tomove the server into its own application if you ever decide to move to separate server andclient applications.

To facilitate this, you should put server-side components on a separate data module fromclient-side components. Under this arrangement, your application would have the followingstructure:

• Server data module This contains the connection to the database, along with the neces-sary datasets and providers.

• Client data module This contains the client datasets and (optionally) data sources.

• Forms and units The forms and supports units in the application will reference the datafrom the client data module.

Other than from inside the server data module, you should not reference any components onthe server side of the equation, including the database connection or datasets. All data accessshould be performed through the client data module. If you impose this restriction on yourcode, it will go a long way toward simplifying moving the server data module out into its ownapplication.

Using Providers Located on a Different FormAfter you move the data access components and providers onto a separate data module, theclient dataset won’t be able to directly connect to the dataset provider. In other words, theTClientDataSet’s ProviderName property won’t list the provider in its drop-down list, becausethe 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, youeither need to call TClientDataSet.SetProvider or use a TLocalConnection component, dis-cussed in the next chapter.

For now, at runtime, you can call TClientDataSet.SetProvider to establish a connection to aprovider on a different form or data module. The following line of code shows how this isdone:

ClientDataSet1.SetProvider(ServerDM.pvContacts);

Chapter 7308

Page 322: Delphi Kylix Database Development

One-Stop Shopping: TSQLClientDataSetIf you write a lot of applications using dbExpress, and you don’t want to plan ahead for easymigration to a separate application server, you may find that the TSQLClientDataSet compo-nent simplifies your application somewhat.

TSQLClientDataSet encapsulates a TSQLDataSet, TDataSetProvider, and TClientDataSetcomponent into a single component. You still need to set up a TSQLConnection component asexplained in Chapter 2, but then you can drop a single TSQLClientDataSet component on yourdata module rather than setting up three components for each dataset.

The downside to using TSQLClientDataSet is that if you later decide to move to a separateapplication server, you will need to create new TSQLDataSet and TDataSetProvider compo-nents on the server side and then replace the TSQLClientDataSet component with aTClientDataSet component on the client side. In addition, TSQLClientDataSet limits theamount of control you have over individual provider and dataset events.

It doesn’t require much more effort to set up the separate components to begin with, and itmakes migration to a multitier application much easier in the future. For that reason, I don’trecommend using TSQLClientDataSet at all in most applications.

Limiting the Amount of Data Returned by the ServerBy default, when you open a client dataset, all data returned by the server-side datasets arereturned to the client application. In many cases, this is acceptable. In other cases, you maywant to limit the number of records returned at a single time. For example, if a query returns aresult set of 10,000 records, it may not be a good idea to return all records to the client at onetime when the client application is across a slow LAN or even slower WAN.

To limit the amount of data returned at once from the server, set the TClientDataSet’sPacketRecords property. By default, this property is set to –1, meaning that all records shouldbe returned at once from the server.

If you set PacketRecords to a number greater than zero, it determines the maximum numberof records to return at a given time from the server. When PacketRecords is set to zero, onlymetadata information is returned from the server—no actual row data is returned.

Generally, each data packet is fetched from the server automatically as you scroll through the client dataset, whether in code using Next commands or through use of a data-aware component such as TDBGrid. If you set TClientDataSet.FetchOnDemand to False,additional data packets are not automatically retrieved. In this case, you must callTClientDataSet.GetNextPacket to return the next data packet from the server.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S309

Page 323: Delphi Kylix Database Development

Fetching BLOBs ManuallyOne way to limit the amount of data sent from the server to the client is to fetch BLOB datamanually, rather than automatically. By default, BLOB data is returned by the provider alongwith the rest of the data packet. However, if you don’t always need BLOB information, youcan turn on the TDataSetProvider’s poFetchBlobsOnDemand option. You also need to setTClientDataSet.FetchOnDemand to False.

Chapter 7310

If your client application has no need for BLOB data at all, you should change theTSQLDataset’s CommandText property so that the SELECT statement doesn’t retrievethe BLOB field from the database. poFetchBlobsOnDemand is used when the clientapplication sometimes needs the BLOB data, but not always.

NOTE

When you set the poFetchBlobsOnDemand option, BLOB data is not returned to the clientdataset. If you try to access a BLOB field in the client dataset, an exception will be raised. Ifthe client needs BLOB information for the current record, it should call the client dataset’sFetchBlobs method, like this:

ClientDataSet1.FetchBlobs;

FetchBlobs retrieves all BLOB fields for the current record only. If the dataset contains three BLOB fields, there is no way to retrieve only a single BLOB value from the server—FetchBlobs will return all three BLOBs.

Note that if the client dataset’s FetchOnDemand property is set to True, the client dataset willcall FetchBlobs automatically. To fetch BLOBs manually, you must set both thepoFetchBlobsOnDemand option on the provider and the client dataset’s FetchOnDemand

property.

Fetching Detail Records ManuallyAnother way to limit the amount of data returned by the server is to fetch detail records from amaster/detail relationship manually. By default, all detail records (in the form of a nesteddataset) are sent to the client along with the master record. If you don’t always need detailinformation, you can turn on the TDataSetProvider’s poFetchDetailsOnDemand option.

When you set the poFetchDetailsOnDemand option, detail data is not returned to the clientdataset. Any nested datasets will return a RecordCount of –2147483648 (-MaxInt – 1). If theclient needs detail information for the current record, it should call the client dataset’sFetchDetails method, like this:

ClientDataSet1.FetchDetails;

Page 324: Delphi Kylix Database Development

As with fetching BLOBs, you must set the client dataset’s FetchOnDemand property to False,or the client dataset will automatically call FetchDetails, even if thepoFetchDetailsOnDemand option is set on the provider.

FetchDetails retrieves all detail fields for the current record only. If the dataset contains threenested 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 shows the source code for the main form of the DataFetch application. To simplifythe sample somewhat, all components are on the main form of the application. In a real appli-cation, you should create separate server-side and client-side data modules for the data accesscomponents.

LISTING 7.3 DataFetch—MainForm.pas

unit MainForm;

interface

usesSysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,QStdCtrls, DBXpress, FMTBcd, Provider, SqlExpr, DB, DBClient, QGrids,QDBGrids, QDBCtrls;

typeTfrmMain = 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;

Dataset Providers

7

DA

TASET

PR

OV

IDER

S311

Page 325: Delphi Kylix Database Development

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

privateprocedure ShowTodoCount;{ Private declarations }

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

{$R *.xfm}

procedure TfrmMain.FormCreate(Sender: TObject);begincdsContacts.Open;

end;

procedure TfrmMain.btnFetchDetailsClick(Sender: TObject);begincdsContacts.FetchDetails;ShowTodoCount;

end;

Chapter 7312

LISTING 7.3 Continued

Page 326: Delphi Kylix Database Development

procedure TfrmMain.btnFetchBlobsClick(Sender: TObject);begincdsContacts.FetchBlobs;ShowTodoCount;

end;

procedure TfrmMain.cdsContactsAfterScroll(DataSet: TDataSet);beginShowTodoCount;

end;

procedure TfrmMain.ShowTodoCount;varTestStream: TStream;

begintryTestStream := cdsContacts.CreateBlobStream(cdsContactsNOTES, bmRead);TestStream.Free;lblBLOBs.Caption := ‘BLOBs have been fetched for this record’;

exceptlblBLOBs.Caption := ‘BLOBs have not been fetched for this record’;

end;

case cdsTodos.RecordCount of-MaxInt - 1:lblDetails.Caption := ‘Todos have not been fetched for this record’;

1:lblDetails.Caption := ‘1 todo has been fetched for this record’;

elselblDetails.Caption := IntToStr(cdsTodos.RecordCount) +‘ todos have been fetched for this record’;

end;end;

end.

A few interesting things are going on in this application. Let’s take a look at the methods oneat a time.

First, however, let’s examine the structure of the application.

Figure 7.5 shows the main form of the DataFetch application at design time.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S313

LISTING 7.3 Continued

Page 327: Delphi Kylix Database Development

FIGURE 7.5DataFetch main form showing relationships between components.

Two dbExpress datasets—sqlContacts and sqlTodos—are linked in a master/detail relation-ship. A TDataSetProvider is connected to the sqlContacts (master) dataset. Remember that aprovider is not needed for the detail dataset. This constitutes the server side of the equation.

Next, two client datasets—cdsContacts and cdsTodos—represent the tables on the client side.cdsTodos is a nested dataset inside the cdsContacts dataset.

The FormCreate method opens the cdsContacts client dataset. This causes a chaining effectthat opens the sqlContacts dbExpress dataset and sends the results back to the client throughthe pvContacts provider.

Two buttons on the form, labeled Fetch Details and Fetch Blobs, call thebtnFetchDetailsClick and btnFetchBlobsClick events, respectively. These two events, inturn, call the client dataset’s FetchDetails and FetchBlobs methods. They then call theShowTodoCount method, which we’ll examine shortly.

cdsContactsAfterScroll is fired whenever the current record changes on the cdsContactsdataset. It also makes a call to ShowTodoCount.

cdsContactsApplyUpdates is a method that is connected to cdsContacts’ AfterDelete andAfterPost events. What it does is ensure that whenever a record is posted to the cdsContactsdataset, it is automatically resolved back to the underlying database. This eliminates the needto place an Apply Updates button in the sample application.

ShowTodoCount is a method that checks the current contact record to see if BLOBs and/ordetails have been fetched for it. To check the status of BLOBs, it attempts to create a BLOBstream on the NOTES field. The call to CreateBlobStream will raise an exception if BLOBshave not been fetched, and the program updates a label on the main form accordingly.

Chapter 7314

Page 328: Delphi Kylix Database Development

To check whether details have been fetched for the current contact, the code looks atcdsTodos.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.

Dataset Providers

7

DA

TASET

PR

OV

IDER

S315

FIGURE 7.6DataFetch shows how to fetch details and BLOBs on demand.

SummaryThis chapter introduced you to providers and multitier database development, including thefollowing key concepts:

• A TDatasetProvider is a conduit between a client dataset and an external data store. Itprovides data to the client dataset on request and resolves data back to the database whenthe client applies updates.

• You need to call the client dataset’s ApplyUpdates method to save changes to a databasepermanently.

• For greatest control over reconciliation errors, you should provide an event handler forthe client dataset’s OnReconcileError event. Delphi comes with a prewritten function,named HandleReconcileError, that you can use to facilitate this process.

• By calling TClientDataSet.Refresh, you can ensure that the client always has the latestcopy of the data from the server.

• You can set the provider’s update mode to upWhereAll, upWhereChanged, orupWhereKeyOnly for finer control over how each record is updated.

• You can take advantage of numerous provider options and events for finer control overthe entire provide/resolve process.

• When creating a master/detail relationship on the server-side components, the clientdataset will represent that relationship as a nested dataset.

Page 329: Delphi Kylix Database Development

• By handling the provider’s OnGetTableName event, you can update data returned from astored procedure.

• By handling the provider’s BeforeUpdateRecord event, you can update data returnedfrom a join operation.

• You can call TClientDataSet.SetProvider at runtime to establish a connection betweenclient datasets and providers on different forms or data modules.

• To replace separate TSQLDataSet, TDataSetProvider, and TClientDataSet components,use TSQLClientDataSet.

• To limit the amount of data returned by the server at a single time, set the provider’spoFetchBlobsOnDemand and/or poFetchDetailsOnDemand options, and then callTClientDataSet.FetchBlobs or TClientDataSet.FetchDetails manually.

The next chapter expands on this discussion to show you how to create a multitier applicationwith separate server and client executables.

Chapter 7316

Page 330: Delphi Kylix Database Development

CHAPTER

8DataSnap

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 ClientDataSets 343

• Brokering Connections Between MultipleServers 348

Page 331: Delphi Kylix Database Development

Chapter 8318

The previous chapter gave an overview of providers and explained how to use them in a multi-tier application where the client and application server are physically located in a single exe-cutable.

This chapter expands the discussion to show you how to create multitier database applicationswhere the client and application server are in different executables, which may then run on thesame machine or, more likely, on two separate computers.

Delphi supports a number of underlying protocols to connect to an application server, includ-ing DCOM, CORBA, HTTP, and SOAP. This book does not attempt to explain any of thesetechnologies in detail. Rather, it describes how to set up the required connection component foreach of the technologies. It is assumed that you already have the necessary software installedand functioning properly for the protocol that you will use to connect to the application server.

What Is DataSnap?DataSnap is the technology that allows client applications to connect to providers in an appli-cation server. DataSnap is implemented by a number of components that can be used to con-nect different machines through such underlying technologies as sockets, DCOM, CORBA,HTTP, and SOAP.

In earlier versions of Delphi, DataSnap was named MIDAS. Because of internationaltrademark considerations, the name MIDAS has been changed to DataSnap.

NOTE

Creating the Application ServerThis section describes the steps necessary to create an application server. The applicationserver contains all the code necessary to connect to the underlying database and provide theresulting data to the client application. As you’ll learn later in this section, the applicationserver can also provide other, possibly non-database related, services to the client.

Remote Data ModulesWhen creating the application server, the first order of business is to set up one or moreremote data modules. A remote data module is simply a data module that can be accessedremotely from a client application. The remote data module contains the components that weplaced on the server-side data module in the previous chapter.

Page 332: Delphi Kylix Database Development

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. Somereasons for doing so include the following:

• The application server needs to connect to multiple databases, and you want to create adistinct remote data module for each database connection.

• Two types of users will run this application—perhaps a “normal” user and an administra-tor. You may want to create two separate remote data modules, in which the remote datamodule 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 datamodule 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 RemoteData Module.”

Creating a Remote Data ModuleCreating a remote data module for non-SOAP applications is a straightforward operation.

• Create a new application, which will be the application server.

• From Delphi’s main menu, select File, New, Other. Delphi’s New Items dialog is dis-played. Select the Multitier tab. The New Items dialog should look like the image shownin Figure 8.1.

• Select one of the remote data modules from the list and click OK. The different datamodules are discussed in the following sections.

DataSnap

8

DA

TASN

AP

319

FIGURE 8.1Delphi supports three remote data modules—Remote Data Modules, Transactional Data Modules, and CORBA DataModules.

Page 333: Delphi Kylix Database Development

Creating a Standard Remote Data ModuleThe examples presented in this book all use a standard remote data module. This type of datamodule is used for clients connecting through sockets, DCOM, or HTTP connections.

To create a TRemoteDataModule, select Remote Data Module from the Multitier page of theNew Items dialog and click OK. The dialog shown in Figure 8.2 appears.

Chapter 8320

FIGURE 8.2Creating a standard remote data module.

Enter a CoClass name for the data module, such as ContactDataServer or some other mean-ingful name. The application name and CoClass together form the name by which the remotedata module is referenced. For example, if the application name is ContactServer and theCoClass name is ContactDataServer, the remote data module is referenced from the clientapplication as ContactServer.ContactDataServer.

Select the type of instancing to use for the remote data module. Table 8.1 lists the possible val-ues for this selection.

TABLE 8.1 Instancing Values

Value Description

Internal The remote data module cannot be created from an externalclient. Internal remote data modules are always created fromwithin the server application.

Single Instance Each client that attempts to connect to the server will cause aseparate instance of the server executable to run.

Multiple Instance Only one copy of the server executable will run, but it willinstantiate a separate remote data module for each client thatconnects to it.

In most situations, you should leave the Instancing combo box set to Multiple Instance.

Lastly, select the threading model to use for the remote data module. Table 8.2 lists the possi-ble values for the Threading Model combo.

Page 334: Delphi Kylix Database Development

TABLE 8.2 Threading Model Values

Value Description

Single The data module will receive requests from only a single client at atime. Because of this, you don’t need to deal with threading issues.

Apartment Each instance of the data module will service only a single request at atime. However, the server may handle multiple requests on multipledata modules at the same time. Therefore, you need to handle multi-threading issues on global data.

Free The data module can receive multiple requests from multiple clients atthe same time. In addition to dealing with thread conflicts on globaldata, you must also protect instance data.

Both The same as Free with the exception that any callbacks made to clientinterfaces are serialized.

Neutral Multiple clients can make requests to the same data module at thesame time, but COM ensures that no two requests conflict with eachother. This is supported only under COM+. When not running underCOM+, this is treated the same as the Apartment model.

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

Creating a MTS Remote Data ModuleA TMTSDataModule should be used for application servers that will be installed under MTS orCOM+. These data modules can also be used for clients connecting through sockets, DCOM,or HTTP connections.

TMTSDataModules can be created only in an ActiveX library—not in an application.

To create a TMTSDataModule, select Transactional Data Module from the Multitier page ofthe New Items dialog. The dialog shown in Figure 8.3 appears.

DataSnap

8

DA

TASN

AP

321

FIGURE 8.3Creating a transactional remote data module.

Page 335: Delphi Kylix Database Development

Enter a CoClass name for the data module, such as ContactDataServer, or some other mean-ingful name.

Select the threading model to use for the remote data module. Transactional data modules sup-port the Single, Apartment, and Both threading models from Table 8.2.

Finally, select the transaction model to use for the data module. Because we won’t be creatingTMTSDataModules in this book, I’ll refer you to the Delphi help for an explanation of the differ-ent transactional models.

Click OK to create the TMTSDataModule.

Creating a CORBA Remote Data ModuleYou should create a TCORBADataModule if you want to use the application server to providedata to clients across a CORBA connection.

To create a TCORBADataModule, select CORBA Data Module from the Multitier page of the NewItems dialog and click OK. The dialog shown in Figure 8.4 appears.

Chapter 8322

FIGURE 8.4Creating a CORBA remote data module.

Enter a CoClass name for the data module, such as ContactDataServer, or some other mean-ingful name.

Select the instancing type and threading models to use for the remote data module. Becausewe won’t be creating TCORBADataModules in this book, I’ll refer you to the Delphi help for anexplanation of the valid instancing types and threading models for a CORBA data module.

Click OK to create the TCORBADataModule.

Creating a SOAP Remote Data ModuleTSOAPDataModule is used to provide data to clients that are set up to access data from a Webservice.

Page 336: Delphi Kylix Database Development

To create a remote data module for a SOAP application server, perform the following steps.

• From Delphi’s main menu, select File, New, Other. Delphi’s New Items dialog is dis-played. Select the WebServices tab.

• Select SOAP Server Application from the New Items dialog and click OK. The dialogshown in Figure 8.5 appears.

DataSnap

8

DA

TASN

AP

323

FIGURE 8.5Delphi supports five types of SOAP server applications.

• Select the type of SOAP server application to create. If you’re creating a Web AppDebugger executable, enter a CoClass name to use for the COM object that the Web AppDebugger uses to call your Web module. Click OK to create the SOAP ServerApplication.

• Again, select File, New, Other from Delphi’s main menu. Select the WebServices tab inthe New Items dialog.

• Select SOAP Server Data Module on the WebServices tab and click OK. The dialogshown in Figure 8.6 appears.

FIGURE 8.6Creating a SOAP Server Data Module.

• Enter a class name for the data module and click OK. The class name may be anythingyou want, such as MyDataServer, CustomerDataServer, or any other meaningful name.

Page 337: Delphi Kylix Database Development

Placing Components on the Remote Data ModuleRegardless of which remote data module you create, it is now time to populate it with compo-nents.

Basically, the components that go on the remote data module are the same components thatyou put on the server-side data module in Chapter 7—namely, the database connection compo-nent, datasets, and providers.

Because we’re using dbExpress for these examples, this means that you’ll populate the datamodule with a TSQLConnection, one or more TSQLDataSets, and one or moreTDataSetProvider components. If you were using BDE, ADO, or a third-party database serverinstead of dbExpress, you would use the corresponding third-party connection and datasetcomponents.

Adding Methods to the Remote Data ModuleIn addition to serving data in the form of datasets, remote data modules can also provide otherservices to a client application. These services might include anything from returning theserver machine’s date and time to calculating pi to 1,000 decimal places.

To add a method to the remote data module, right-click anywhere in the source code editor forthe remote data module. From the context menu, select the Add to Interface menu item. TheAdd 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.

Chapter 8324

FIGURE 8.7Adding a method to a remote data module.

When you click OK, Delphi writes an empty method for you, using the declaration you justentered. At this point, you need to flesh out the method call. The following code snippet showsthe simple GetServerTime method created in Figure 8.7.

function TMethodsDM.GetServerTime: TDateTime;beginResult := Now;end;

Later in this chapter, you’ll see how to call this method from a client application.

Page 338: Delphi Kylix Database Development

CallbacksIn addition to receiving calls from a client, the server application can also make calls to aclient application, using a callback interface.

DataSnap

8

DA

TASN

AP

325

This section assumes that you have some knowledge of interfaces, what they are, andhow to use them. If you do not have this knowledge, I suggest picking up a copy ofmy book, Delphi COM Programming.

NOTE

To create a callback interface, select View, Type Library from the Delphi main menu. The typelibrary editor appears, shown in Figure 8.8.

FIGURE 8.8Creating a callback interface.

Click the New Interface button (the first button on the toolbar) and name the new interfacesomething original like ITestCallback. Now click the New Method toolbar button, and namethe new method Test. To keep things simple, Test won’t take any parameters or return aresult.

Close the type library editor.

Now that the server application knows about ITestCallback, you’ll need a way for the clientto pass an ITestCallback interface to the server. This is accomplished by creating a methodon the server that takes a parameter of type ITestCallback and saves the value in a variablelocal to the remote data module, like this:

Page 339: Delphi Kylix Database Development

typeTMethodsDM = class(TRemoteDataModule, IMethodsDM)...private{ Private declarations }FCallback: ITestCallback;...end;...procedure TMethodsDM.SetCallback(const Callback: ITestCallback);beginFCallback := Callback;end;

The client application will call this method at some point to set up a callback interface with theserver. The server can then make calls to the interface later.

This technique is illustrated in the Methods sample application, presented later in this chapterin the section titled “A Complete Example.”

Chapter 8326

Callbacks have a couple of limitations with certain types of connections (discussedlater in this chapter, in the section titled “Creating the Client Application”). First,when using a socket connection, you must make sure to setTSocketConnection.SupportsCallbacks to True. In addition, the callback interfacemust derive from IDispatch. Second, TWebConnection and TSOAPConnection don’tsupport callbacks at all, so keep that in mind if you intend to use HTTP as the commu-nication protocol between the client and server applications.

NOTE

Creating the Application Server’s User InterfaceUsually, the application server will run on a dedicated server machine that is not being used asa workstation. Often nobody is sitting at the server to see error messages, enter data, respondto prompts, and so on. For that reason, the application server generally won’t have much of auser interface.

However, it is often useful to provide a minimal user interface for the application server to dis-play information such as how many users are logged in, database statistics, or other pertinentinformation. Even if you don’t want to distribute your application with this user interface, itcan still be invaluable while debugging a multitier application to see a list of current connec-tions to the server.

Figure 8.9 shows an example of such a minimal user interface.

Page 340: Delphi Kylix Database Development

FIGURE 8.9Application server with minimal user interface.

To create this interface, all you need to do is notify the main form each time a remote datamodule is created or destroyed. A reasonable place to do this is in the data module’s OnCreateand 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 applicationserver.

LISTING 8.1 MethodsServer—MainForm.pas

unit MainForm;

interface

usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, StdCtrls;

constUM_CONNECT = WM_USER + 1;

typeTfrmMain = class(TForm)lblConnections: TLabel;

private{ Private declarations }FConnections: Integer;procedure UpdateConnections;procedure UMConnect(var Msg: TMessage); message UM_CONNECT;

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

DataSnap

8

DA

TASN

AP

327

Page 341: Delphi Kylix Database Development

{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);beginFConnections := FConnections + Msg.WParam;UpdateConnections;

end;

procedure TfrmMain.UpdateConnections;beginif FConnections = 1 thenlblConnections.Caption := ‘1 connection’

elselblConnections.Caption := IntToStr(FConnections) + ‘ connections’;

end;

end.

The remote data module then calls the main form’s Connect and Disconnect methods on cre-ation and destruction, as the following code snippet shows.

procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, 1, 0); end;

procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, -1, 0);

end;

Preparing the Application Server for TestingWhen you have finished writing the application server, you should install it onto the servermachine and register it for testing. I highly recommend that you test it on your developmentmachine 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 numerousmistakes. You’ll frequently need to modify, recompile, and redeploy the applicationserver. It’s a lot easier to do this on your local development machine.

• Your application server may be prone to hang up or crash before it is debugged. It’s amuch better practice to crash your development machine than to crash a productionserver.

Chapter 8328

LISTING 8.1 Continued

Page 342: Delphi Kylix Database Development

After the application server is debugged locally, you should then move it to a server machinefor further testing and debugging.

For application servers implementing TRemoteDataModule, registering the server is a simplematter of running it once. Running the server automatically registers it with the computer.When you modify the application server or move it to a new directory or computer for testing,you should run it again.

Other remote data modules, such as TMTSDataModule or TCORBADataModule, will require regis-tering the server with MTS or CORBA.

Creating the Client ApplicationNow that the application server has been created and registered, it’s time to create the clientapplication.

The client application is functionally identical to the applications created in the previous chap-ter, except that it doesn’t contain the server-side data module. In addition, the data module inthe client application will contain a remote connection component, discussed in the followingsections.

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

Now create a data module, which will house the client datasets that retrieve data from theapplication server.

Connecting to a Local Database ConnectionIn the previous chapter, you learned how to connect to a provider that is on the same form ordata module as a client dataset. You also learned how to call TClientDataSet.SetProvider atruntime 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 toproviders 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 andproviders. TLocalConnection can be found on the DataSnap page of the componentpalette, along with the remote connection components.

2. Add the server-side data module to the uses clause of the client-side data module.

DataSnap

8

DA

TASN

AP

329

Page 343: Delphi Kylix Database Development

3. In the client-side data module, set the client dataset’s RemoteServer property to theTLocalConnection component.

4. In the client dataset’s ProviderName property, select the provider from the drop-downlist.

After you complete these steps, all you need to do is open the client dataset to pull the datafrom the database into the client dataset.

The LocalConn application example, included with the source code for this book, shows thecorrect way to set up an application that uses a local connection to access data. Because almostno source code is in the application that isn’t generated by Delphi for you, I haven’t listed thesource code here.

Connecting to a Remote Database ConnectionRemote connection components allow you to access providers that are located in a separateexecutable from the client application. The server application may be located on the samemachine or on a machine half a world away.

Whereas the TLocalConnection component is placed on the server-side data module alongwith the dbExpress datasets and providers, remote connection components are placed on theclient’s data module, along with the client dataset components. Delphi supports five types ofremote connections.

The following sections discuss the remote connection components supported by Delphi.

Socket ConnectionsTSocketConnection uses sockets as a transport between the application server and client appli-cation. It is the simplest of the connection protocols to set up and use and is the one that isused in this book’s examples. No additional software or licensing fees are required when usingsockets (other than the standard DataSnap licensing issues, which are discussed in Appendix Aof this book). Also, sockets are supported by any machine that has a TCP/IP address.

The disadvantage of a socket connection is that sockets don’t provide very good support forsecurity. However, you can work around this limitation by writing a data packet interceptor,discussed later in this chapter in the section titled “Intercepting Data Packets.”

In addition, sockets can’t always be used to connect to a server located behind a firewall(unless the firewall is configured to allow access to the port that the socket server listens on,which is discussed in the following section).

Table 8.3 lists the most important properties for TSocketConnection.

Chapter 8330

Page 344: Delphi Kylix Database Development

TABLE 8.3 TSocketConnection Properties

Property Description

Address Specifies the IP address of the remote machine to connect to. Youmay specify the name of the remote machine instead of the IPaddress by setting the Host property rather than the Addressproperty.

Host Specifies the name of the remote machine to connect to. You mayspecify the IP address of the remote machine instead of the nameby setting the Address property rather than the Host property.

InterceptGUID Specifies the GUID of a COM object that intercepts data packetssent between the client and the application server. You may spec-ify the ProgID of the COM object rather than the GUID by set-ting the InterceptName property instead of InterceptGUID. Datainterceptors are discussed in the section titled “Intercepting DataPackets,” later in this chapter.

InterceptName Specifies the ProgID of a COM object that intercepts data pack-ets sent between the client and the application server. You mayspecify the GUID of the COM object rather than the ProgID bysetting the InterceptGUID property instead of InterceptName.Data interceptors are discussed in the section titled “InterceptingData Packets,” later in this chapter.

Port Specifies the port that the socket server uses to listen to socketrequests. By default, this is set to 211.

ServerGUID Specifies the GUID of the application server that the client willconnect to. You can set the server name using the ServerNameproperty instead of ServerGUID.

ServerName Specifies the name of the application server that the client willconnect to. You can set the server GUID using the ServerGUIDproperty instead of ServerName.

SupportCallbacks Set to True if your server supports callbacks to the client applica-tion. If this property is True, WinSock 2 must be present on theserver machine. When False, the server machine does notrequire WinSock 2 to operate properly, but it also doesn’t supportcallbacks.

The Socket ServerTo connect to a remote application server using sockets, you must run a socket server on theserver machine. Although you can write your own socket server if you want, Delphi ships witha prewritten server named ScktSrvr. Source code for the server is included with the VCLsource, so you can modify it or use it as a basis for writing your own server.

DataSnap

8

DA

TASN

AP

331

Page 345: Delphi Kylix Database Development

You must run the socket server on the remote machine before attempting to connect to anapplication server using sockets. If you don’t run the socket server first, the attempted connec-tion to the server will appear to hang and will eventually time out.

Chapter 8332

You can install the socket server as a service on a Windows NT machine by running scktsrvr –install at the command prompt. To remove the service, run scktsrvr –uninstall. After installing as a service, you must either reboot your com-puter or start the service manually before it will run the first time.

NOTE

The Borland socket server usually sits inconspicuously in the tray. However, you can double-click it to open the socket server, as Figure 8.10 shows.

FIGURE 8.10Socket 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 fordoing so, you should leave it set to the default value. If you change the port, you will also needto 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 packetstransferred between the client and server. The following section discusses this in more detail.

Page 346: Delphi Kylix Database Development

Intercepting Data PacketsSome applications may deal with sensitive data, such as account numbers, and the like. Bydefault, data transmitted between the server and client applications is not encrypted or com-pressed in any way, meaning that anyone bright enough to figure out how to listen in on thesocket server port could obtain this data for himself.

When using sockets, you can write a COM object that intercepts data being sent over thesocket connection. This object can compress, encrypt, or otherwise modify the data to keep itfrom 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 Delphiships with a demo that shows how to write this object (located in C:\Program Files\Borland\Delphi6\Demos\Midas\Intrcpt), I won’t duplicate that example in this book.

2. Register the COM object on both the server machine and all client machines.

3. Set the TSocketConnection’s ServerName property to the name of the application server.

4. Inside the socket server, set the Intercept Name to the same name.

5. Run the application as usual.

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

DCOM ConnectionsA TDCOMConnection uses Distributed COM (DCOM) as its underlying protocol, tying it to theWindows operating system. DCOM is a little tricky to set up properly, especially if the appli-cation server isn’t running on a Windows domain server.

I assume at this point that if you’re going to use TDCOMConnection, you know how to set upand use DCOM. If you need additional information on configuring DCOM, you can look inmy COM book or see Dan Miser’s Web page at www.distribucon.com.

Table 8.4 lists the most important properties for TDCOMConnection.

TABLE 8.4 TDCOMConnection Properties

Property Description

ComputerName Specifies the name of the remote computer to connect to. IfComputerName is blank, TDCOMConnection assumes that the applicationserver is located on the local machine.

ServerGUID Specifies the GUID of the application server that the client will con-nect to. You can set the server name using the ServerName propertyinstead of ServerGUID.

DataSnap

8

DA

TASN

AP

333

Page 347: Delphi Kylix Database Development

ServerName Specifies the name of the application server that the client will connectto. You can set the server GUID using the ServerGUID property insteadof ServerName.

HTTP ConnectionsTWebConnection uses HTTP as its transport protocol, which makes it useful for writing appli-cations that run over the Internet. HTTP offers some additional features over a socket connec-tion so that you can take advantage of SSL security. Also, you can connect to a servercomputer 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 brokersthe HTTP call from the client to the COM object on the server application. In addition, theclient machine must contain a copy of wininet.dll. Wininet.dll is included with InternetExplorer 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 maybe left blank if the host does not require authentication.

Proxy Semicolon-delimited list of proxy servers that can be used to resolvethe IP address of the host machine.

ServerGUID Specifies the GUID of the application server that the client will con-nect to. You can set the server name using the ServerName propertyinstead of ServerGUID.

ServerName Specifies the name of the application server that the client will connectto. You can set the server GUID using the ServerGUID property insteadof ServerName.

URL The URL used to locate httpsrvr on the host machine.

UserName Valid username used for authentication on the host machine. This maybe left blank if the host does not require authentication.

Chapter 8334

TABLE 8.4 Continued

Property Description

Page 348: Delphi Kylix Database Development

SOAP ConnectionsTSOAPConnection uses SOAP to communicate between the client and server applications.TSOAPConnection is similar to TWebConnection in that it uses HTTP as the underlying protocol.

As with TWebConnection, the client machine must contain a copy of wininet.dll.

Table 8.6 lists the most important properties for TSOAPConnection.

TABLE 8.6 TSOAPConnection Properties

Property Description

Password Valid password used for authentication on the host machine. This maybe left blank if the host does not require authentication.

Proxy Semicolon-delimited list of proxy servers that can be used to resolvethe IP address of the host machine.

URL The URL used to locate the application server on the host machine.

UserName Valid username used for authentication on the host machine. This maybe left blank if the host does not require authentication.

CORBA ConnectionsTCORBAConnection uses CORBA, or IIOP, as its underlying protocol. To establish a CORBAconnection to an application server, you must be running a CORBA Smart Agent somewhereon 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 thisproperty is blank, the connection component will connect to the firstavailable machine that supports the interface specified by theRepositoryID property.

ObjectName Set this property if the interface specified by RepositoryID must beimplemented by a specific instance of the object. Leave this propertyblank if only one object supports the specified interface.

RepositoryID Specifies the CORBA data module to connect to. May take one of twoforms:IDL:ProjectName/CorbaDataModuleName:1.0

orProjectName/CorbaDataModuleName

DataSnap

8

DA

TASN

AP

335

Page 349: Delphi Kylix Database Development

A Complete ExampleThe following example uses a TSocketConnection to connect to the application serverremotely. Also, it demonstrates how to call additional methods on the server and how theserver can use a callback interface to call methods on the client or fire events back to the clientapplication.

Listing 8.2 contains the source code for the server’s remote data module.

LISTING 8.2 MethodsServer—ServerDataModule.pas

unit ServerDataModule;

{$WARN SYMBOL_PLATFORM OFF}

interface

usesWindows, Messages, SysUtils, Classes, ComServ, ComObj, VCLCom, DataBkr,DBClient, MethodsServer_TLB, StdVcl, DBXpress, FMTBcd, DB, SqlExpr,Provider, Variants;

typeTMethodsDM = class(TRemoteDataModule, IMethodsDM)conn: TSQLConnection;sqlContacts: TSQLDataSet;pvContacts: TDataSetProvider;procedure RemoteDataModuleCreate(Sender: TObject);procedure RemoteDataModuleDestroy(Sender: TObject);

private{ Private declarations }FCallback: OleVariant;

protectedclass 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

Chapter 8336

Page 350: Delphi Kylix Database Development

usesMainForm;

{$R *.DFM}

class procedure TMethodsDM.UpdateRegistry(Register: Boolean;const ClassID, ProgID: string);

beginif Register thenbegininherited UpdateRegistry(Register, ClassID, ProgID);EnableSocketTransport(ClassID);EnableWebTransport(ClassID);

end elsebeginDisableSocketTransport(ClassID);DisableWebTransport(ClassID);inherited UpdateRegistry(Register, ClassID, ProgID);

end;end;

function TMethodsDM.GetServerTime: TDateTime;beginResult := Now;

end;

procedure TMethodsDM.TestCallbacks;varIndex: Integer;

beginif not VarIsEmpty(FCallback) thenfor Index := 1 to 3 doFCallback.Test;

end;

procedure TMethodsDM.SetCallback(Callback: OleVariant);beginFCallback := Callback;

end;

procedure TMethodsDM.RemoteDataModuleCreate(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, 1, 0);

end;

DataSnap

8

DA

TASN

AP

337

LISTING 8.2 Continued

Page 351: Delphi Kylix Database Development

procedure TMethodsDM.RemoteDataModuleDestroy(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, -1, 0);

end;

initializationTComponentFactory.Create(ComServer, TMethodsDM,Class_MethodsDM, ciMultiInstance, tmApartment);

end.

Listing 8.3 contains the source code for the main form of the client application.

LISTING 8.3 MethodsClient—MainForm.pas

unit MainForm;

interface

usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, DB, Grids, DBGrids, StdCtrls, MethodsServer_TLB, ComObj, ActiveX;

typeTTest = class(TAutoIntfObject, ITestCallback)protectedprocedure 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;

Chapter 8338

LISTING 8.2 Continued

Page 352: Delphi Kylix Database Development

varfrmMain: TfrmMain;

implementation

uses DataModule;

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);beginDM.cdsContacts.Open;

end;

procedure TfrmMain.btnGetServerTimeClick(Sender: TObject);varD: TDateTime;

beginD := DM.SocketConnection1.AppServer.GetServerTime;ShowMessage(TimeToStr(D));

end;

procedure TfrmMain.btnTestCallbacksClick(Sender: TObject);vartypelib: ITypeLib;

beginOleCheck(LoadRegTypeLib(LIBID_MethodsServer, 1, 0, 0, typelib));FTest := TTest.Create(typelib, ITestCallback);

DM.SocketConnection1.AppServer.SetCallback(FTest as IDispatch);

DM.SocketConnection1.AppServer.TestCallbacks;end;

procedure TTest.Test;beginShowMessage(‘In callback’);

end;

end.

DataSnap

8

DA

TASN

AP

339

LISTING 8.3 Continued

Page 353: Delphi Kylix Database Development

The Briefcase ModelMultitier applications lend themselves to something called the briefcase model. When in theoffice, the user is presumably connected to the server and is running an application using livedata. Before leaving the office, the user downloads any data that may be needed onto the user’slocal machine, for use while away from the office. The general overview of the briefcasemodel is as follows:

When in the office, the user works with live data directly from the application server. While onthe road, the user works with a local copy of the data, making additions or modifications to thedata as needed. Upon return to the office, the user reconnects to the server and reconciles allupdates back to the server.

This magic is performed using the techniques you have already learned in this book. Whenconnected to the server, dataset providers provide a live copy of the data to the client applica-tion’s client datasets. Before the user leaves the office, the data is saved locally by callingTClientDataSet.SaveToFile.

While on the road, the client is unable to connect to the application server; instead, it loads thelocal copy of the data by calling TClientDataSet.LoadFromFile. Changes are accumulated inthe change log, which is persisted by calling TClientDataSet.SaveToFile before the userexits the application.

When the user returns to the office, the client application again callsTClientDataSet.LoadFromFile to load the local copy of the data, but then callsTClientDataSet.ApplyUpdates to resolve any updates back to the application server.

Note that the server application does not change in any way—only the client application con-tains code to support the briefcase model.

Because Delphi ships with a useful briefcase demo application, I won’t duplicate its function-ality here. Instead, I encourage you to take a look at the Delphi demo. For a typical Delphiinstallation, look at C:\Program Files\Borland\Delphi6\Demos\Midas\Brfcase.

Chapter 8340

Rather than calling LoadFromFile and SaveToFile in your application, you can simplyassign the client dataset’s FileName property to the name of the local file to save toand load from. Delphi will then take care of loading and saving the data at theappropriate times.

NOTE

Page 354: Delphi Kylix Database Development

Stateless ServersIn the last chapter, I showed you how you can limit the amount of data returned at a given timefrom the application server. The catch with limiting the amount of data returned from theserver is that you must let the server know from where to start retrieving data each time itfetches a new data packet. Delphi database applications are stateless, meaning that the server-side database components don’t keep track of their current location in the dataset. When theprovider 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 onlyfor a short period of time, a single server-side dataset can service multiple client applications,reducing memory load and increasing performance. This isn’t much of an issue with a localconnection, 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 ofthe last record previously retrieved.

• The server retrieves the next batch of records that occurs after the last record previouslyretrieved.

To implement a stateless server, you need to supply event handlers for three provider events onthe server, as well as for two client dataset events on the client. Let’s start with the server:

In the application server, you need to set up the TSQLDataSet as a parameterized query, likethis:

SELECT * FROM CONTACTS WHERE ID > :MinID ORDER BY ID

This will select only contacts with an ID greater than the last ID that has already been fetched.

Now, provide an event handler for the provider’s OnGetData event. In this event, you’ll savethe last ID retrieved from the contact table.

procedure TStatelessServerData.pvContactsGetData(Sender: TObject;DataSet: TCustomClientDataSet);beginDataSet.Last;pvContacts.Tag := DataSet.FieldByName(‘ID’).AsInteger;end;

DataSnap

8

DA

TASN

AP

341

Page 355: Delphi Kylix Database Development

The call to DataSet.Last does not operate directly on the TSQLDataSet component. Instead, itmoves to the last record of the batch of records that is about to be sent back to the client appli-cation. The ID of that last fetched record is then saved temporarily in the Tag property of theprovider.

Next, write an event handler for the provider’s AfterGetRecords event handler. In this handler,you’ll pass the last retrieved ID back to the client application via the OwnerData parameter.

procedure TStatelessServerData.pvContactsAfterGetRecords(Sender: TObject;var OwnerData: OleVariant);beginOwnerData := pvContacts.Tag;end;

Finally, on the server, you need to handle the provider’s BeforeGetRecords event handler. Inthis event, you’ll set the value of the MinID parameter so that the query knows what record tofetch next. The following code snippet shows how to accomplish this:

procedure TStatelessServerData.pvContactsBeforeGetRecords(Sender: TObject;var OwnerData: OleVariant);beginif not VarIsEmpty(OwnerData) then beginsqlContacts.Params.ParamValues[‘MinID’] := OwnerData;sqlContacts.Refresh;end;end;

That’s all you need to do on the server. In the client application, you need to first provide anevent handler for the client dataset’s AfterGetRecords event. In this event handler, you’ll savethe last ID retrieved from the server, as the following code snippet illustrates:

procedure TForm1.cdsContactsAfterGetRecords(Sender: TObject;var OwnerData: OleVariant);begincdsContacts.Tag := OwnerData;end;

In this example, the last retrieved ID (provided in the OwnerData parameter) is again stored inthe client dataset’s Tag property. You could also set up a form variable named FLastContactIDto save the ID.

Finally, provide an event handler for the client dataset’s BeforeGetRecords event. In this eventhandler, you’ll pass the last retrieved ID back to the server so that it knows where to startfetching data from.

Chapter 8342

Page 356: Delphi Kylix Database Development

procedure TForm1.cdsContactsBeforeGetRecords(Sender: TObject;var OwnerData: OleVariant);beginif cdsContacts.Active thenOwnerData := cdsContacts.Tag;end;

The Stateless application example incorporates these concepts into a stateless server and client.Because most of the source code was listed previously in the form of code snippets, I won’tshow it again here.

Sharing a Connection Between Multiple ClientDataSetsSome real-world applications allow the user to choose between connection protocols; forexample, the user may opt for a socket connection, a DCOM connection, or an HTTP connec-tion. In this way, the client application lets the user make the decision as to which connectiontype is most appropriate for a particular situation.

To provide support for multiple connection types, you need to place multiple connection com-ponents on the client’s data module. For example, you may drop a TSocketConnection, aTDCOMConnection, and a TWebConnection component on the data module. Switching from oneconnection to another means that you need to set the RemoteServer property for eachTClientDataSet component to the correct connection at runtime.

In small sample applications, this isn’t a big problem, because most examples use only one ortwo client datasets. However, most real-world applications contain many more than that. Forexample, I am currently working on an application that utilizes more than 50 TClientDataSetcomponents in the client application.

To facilitate switching from one connection component to another, Delphi provides aTConnectionBroker component. To use a TConnectionBroker, drop one on the client’s datamodules and set the ConnectionBroker property for each TClientDataSet component to theTConnectionBroker. Then, set TConnectionBroker’s RemoteServer property to the correctconnection component. Switching connections is then a trivial matter of changing the connec-tion broker’s RemoteServer property to the desired connection component.

In addition to easily switching from connection to connection, you gain another advantage byusing a connection broker. If you make calls to the application server in your code, such as thefollowing:

SocketConnection1.AppServer.ExecuteSomeMethod;

DataSnap

8

DA

TASN

AP

343

Page 357: Delphi Kylix Database Development

You don’t need to change that code if the connection type changes. Instead of calling themethod on SocketConnection1 (or any other connection component), you can call it on theconnection broker, like this:

ConnectionBroker1.Connection.AppServer.ExecuteSomeMethod;

In this way, the code works regardless of the active connection.

Brokering Connections Between Multiple ServersIn large-scale development, a single instance of the application server may not be enough tohandle the large number of clients connected to it. You also may want to run a copy of theapplication server on multiple server machines so that if one machine goes down, the client canstill 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 choosingbetween them at runtime. If it selects a server that is down at the moment, it should try againwith a different server.

To incorporate connection brokering into your application, drop a TSimpleObjectBroker com-ponent onto your client’s data module. In the Servers property, enter a list of servers that theclient application can attempt to connect to. Set the connection object’s ObjectBroker propertyto the name of the TSimpleObjectBroker.

When the TSimpleObjectBroker’s LoadBalanced property is set to False, the client willattempt to connect to the first server in the Servers property. If that connection fails, it will trythe second server, and so on. When LoadBalanced is True, the broker attempts to select a dif-ferent server for each connection in the current client application only. If the client applicationcontains 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 anumber of clients that connect to the server. In this case, your best bet is to randomize the listof 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. Ifthere are six instances of the client application, and you randomize the order of the servers foreach client, potentially each client’s server order could be the following:

Client 1: Server1, Server2, Server3

Client 2: Server1, Server3, Server2

Client 3: Server2, Server1, Server3

Client 4: Server2, Server3, Server1

Chapter 8344

Page 358: Delphi Kylix Database Development

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 threeservers, with two clients connected to each server.

SummaryThis chapter discussed DataSnap, Delphi’s remote access technology for multitier databasedevelopment.

• The first step in creating a separate application server is to set up one or more remotedata modules. Delphi supports the creation of standard, MTS, CORBA, and SOAPremote 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 themain form whenever a remote data module is created or destroyed. A reasonable place todo this is in the data module’s OnCreate and OnDestroy event handlers.

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

• The briefcase model allows your application to download data to the local machine foruse when it is impossible to connect to the server.

• To facilitate the use of multiple application servers in a large-scale application, Delphiprovides the TConnectionBroker component.

The next chapter introduces a complete application that uses many of the techniques discussedin this chapter and throughout the rest of the book.

DataSnap

8

DA

TASN

AP

345

Page 359: Delphi Kylix Database Development
Page 360: Delphi Kylix Database Development

CHAPTER

9The ConMan Application

IN THIS CHAPTER• What Is ConMan? 348

• Database Structure 349

• Overview of the Code 352

• The Server Application 352

• The Client Application 358

• Room for Improvement 373

Page 361: Delphi Kylix Database Development

Chapter 9348

In this book, I’ve discussed quite a number of techniques for writing database applications inDelphi and Kylix. Along the way, I’ve presented a number of small sample applications tosolidify many of the important points discussed in each chapter.

In this chapter, I’ll present a complete multiuser database application that incorporates many ofthe ideas discussed in this book. The application is called ConMan (short for ContactManager).

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 eachcontact and store reminders.

Multiple users can run this application simultaneously, and provisions in the application allowa user to store the database onto a local machine to use while away from the office (briefcasemodel).

Although ConMan demonstrates many important multiuser database concepts, it isn’t going toreplace your production-quality contact manager (such as ACT! or GoldMine) anytime soon.Many important features of a commercial contact manager are missing from ConMan, includ-ing the capability to dial the phone, automatically write letters, faxes, and memos using yourfavorite word processor, and so on. The intent of ConMan is not to write a salable product.After all, if that were the case, I wouldn’t be including the source code for free in a databasebook. Rather, the intent is to show a real-life application that makes use of important Delphidatabase technologies to give you a concrete understanding of how you might take advantageof the same technologies in your own applications.

Figure 9.1 shows ConMan at runtime.

Because ConMan takes advantage of technologies not yet supported by Kylix—such asDataSnap—it is presented as a VCL application only.

Page 362: Delphi Kylix Database Development

FIGURE 9.1ConMan displays information about a contact, as well as his or her picture, notes, and scheduled todos.

Database StructureYou have already accessed some of the tables in the ConMan database through the sample pro-grams provided with earlier chapters. Listing 9.1 contains the partial SQL script used to createa new, empty version of the CONMAN database.

The Contacts table is where company names, addresses, phone numbers, and the like arestored. One record is created for each contact, so if multiple contacts exist for the same com-pany, the company data is stored with each contact.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N349

In reality, this database should also contain a Companies table, with each contactholding a reference back to the company to which he or she is associated. To keepthe size of this application manageable, I elected to store both company and contactdata in the Contacts table.

NOTE

Page 363: Delphi Kylix Database Development

The Todos table stores reminders for each contact. For example, you might want to store areminder to phone a certain contact on a given date to place an order. The Todos table andContacts table are linked through the Todos table’s ContactID field.

In addition to the tables, the database contains a stored procedure named ContactsByState.This procedure is provided for use in the sample applications shown in Chapters 1 and 2; itwon’t be used in the ConMan application.

Finally, the script creates a generator used to populate the primary keys of the Contacts andTodos 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(

Chapter 9350

Page 364: Delphi Kylix Database Development

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_GENRETURNS (AVALUE INTEGER)ASBEGINAValue = GEN_ID(ID_GENERATOR, 1);

END ^

CREATE TRIGGER CONTACTS_INSERT FOR CONTACTSACTIVE BEFORE INSERT POSITION 0ASBEGINIF (New.CONTACTID IS NULL) THENNew.CONTACTID = GEN_ID(ID_GENERATOR, 1);

END ^

CREATE TRIGGER TODOS_INSERT FOR TODOSACTIVE BEFORE INSERT POSITION 0ASBEGINIF (New.TODOID IS NULL) THENNew.TODOID = GEN_ID(ID_GENERATOR, 1);

END ^

SET TERM ; ^

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N351

LISTING 9.1 Continued

Page 365: Delphi Kylix Database Development

Overview of the CodeConMan is separated into an application server and a client, providing true multitier function-ality.

The entire source code for ConMan is contained in seven units—two of which Delphi automat-ically writes for us. The server-side source units include the following:

• MainForm.pas Contains the code for the main form of the server (which does nothingbut show us the number of clients currently connected).

• RemoteDataModule.pas Houses the remote data module where the database connectioncomponents reside.

• ConManServer_TLB.pas Type library import file for the application server. Delphi auto-matically creates and maintains this unit.

The client application is composed of the following units:

• DataModule.pas Client-side data module, including DataSnap components andTClientDataSets.

• RecErrorForm.pas Form to handle reconciliation errors. This form is automaticallygenerated by Delphi (see Chapter 7 for more information on handling reconciliationerrors).

• MainForm.pas The application’s main form.

• TodoForm.pas A form that facilitates adding or editing a todo.

The following sections describe the server- and client-side applications in more detail.

The Server ApplicationAs noted in the preceding section, the application server consists of three source files, only twoof which you write code for.

Listing 9.2 contains the source code for the application server’s remote data module. As youcan see from Figure 9.2, the remote data module contains six components. These are

• conn A TSQLConnection component for connecting to the database.

• sqlContacts A TSQLDataSet component for retrieving data from the Contacts table.

• sqlTodos A TSQLDataSet component for retrieving data from the Todos table.

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

Chapter 9352

Page 366: Delphi Kylix Database Development

• dsContacts A TDataSource component used to establish a server-side master/detailrelationship between sqlContacts and sqlTodos.

• pvContacts A TDataSetProvider component used to provide contacts and todorecords to the client application. Because the contacts and todos are connected in a mas-ter/detail relationship, only one provider is needed at the master level (refer to Chapter 7if you need to revisit server-side master/detail relationships).

Figure 9.2 shows the remote data module at design time.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N353

FIGURE 9.2The remote data module and its data access components.

LISTING 9.2 ConManServer—RemoteDataModule.pas

unit RemoteDataModule;

{$WARN SYMBOL_PLATFORM OFF}

interface

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

typeTConManDataServer = class(TRemoteDataModule, IConManDataServer)conn: TSQLConnection;sqlContacts: TSQLDataSet;sqlTodos: TSQLDataSet;dsContacts: TDataSource;pvContacts: TDataSetProvider;sqlID: TSQLDataSet;sqlContactsCONTACTID: TIntegerField;sqlContactsFIRST: TStringField;sqlContactsLAST: TStringField;sqlContactsDEAR: TStringField;

Page 367: Delphi Kylix Database Development

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;

protectedclass procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;

public{ Public declarations }

end;

implementation

uses MainForm;

resourcestringSDatabaseIsOpen = ‘Cannot perform this operation on an open database’;

{$R *.DFM}

Chapter 9354

LISTING 9.2 Continued

Page 368: Delphi Kylix Database Development

class procedure TConManDataServer.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);

beginif Register thenbegininherited UpdateRegistry(Register, ClassID, ProgID);EnableSocketTransport(ClassID);EnableWebTransport(ClassID);

end elsebeginDisableSocketTransport(ClassID);DisableWebTransport(ClassID);inherited UpdateRegistry(Register, ClassID, ProgID);

end;end;

procedure TConManDataServer.RemoteDataModuleCreate(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, 1, 0);

end;

procedure TConManDataServer.RemoteDataModuleDestroy(Sender: TObject);beginPostMessage(frmMain.Handle, UM_CONNECT, -1, 0);

end;

procedure TConManDataServer.pvContactsBeforeUpdateRecord(Sender: TObject;SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;UpdateKind: TUpdateKind; var Applied: Boolean);

beginif UpdateKind = ukInsert thenif SourceDS = sqlContacts then beginif DeltaDS.FieldByName(‘CONTACTID’).OldValue <= 0 thenDeltaDS.FieldByName(‘CONTACTID’).NewValue := GetNextID;

end else beginif DeltaDS.FieldByName(‘TODOID’).OldValue <= 0 thenDeltaDS.FieldByName(‘TODOID’).NewValue := GetNextID;

end;end;

function TConManDataServer.GetNextID: Integer;beginsqlID.ExecSQL;Result := sqlID.ParamByName(‘AValue’).AsInteger;

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N355

LISTING 9.2 Continued

Page 369: Delphi Kylix Database Development

end;

initializationTComponentFactory.Create(ComServer, TConManDataServer,Class_ConManDataServer, ciMultiInstance, tmApartment);

end.

Notice that the RemoteDataModuleCreate and RemoteDataModuleDestroy methods post aUM_CONNECT message to the main form. You’ll see in Listing 9.3 how the main form respondsto that message to update its user interface accordingly.

pvContactsBeforeUpdateRecord is called automatically just before updates are made to thedatabase. 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 seeif the primary key for that record is less than or equal to zero (you’ll see on the client side thatprimary keys are generated with a negative value, which I’ll explain when I discuss the client-side code). If so, the code calls the GetNextID method, which executes the GEN_ID stored pro-cedure to retrieve the next available ID for the record.

Listing 9.3 contains the source code for the application server’s main form. As discussed inChapter 8, application servers often don’t display a main form, but I’ve found that it is oftenuseful to display a small main form that shows the number of active connections.

LISTING 9.3 ConManServer—MainForm.pas

unit MainForm;

interface

usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, ExtCtrls, ComCtrls;

constUM_CONNECT = WM_USER + 101;

typeTfrmMain = class(TForm)StatusBar1: TStatusBar;pnlConnections: TPanel;Timer1: TTimer;procedure Timer1Timer(Sender: TObject);

private

Chapter 9356

LISTING 9.2 Continued

Page 370: Delphi Kylix Database Development

{ Private declarations }FConnections: Integer;procedure UMConnect(var Msg: TMessage); message UM_CONNECT;

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

resourcestringSOneConnection = ‘1 Connection’;SConnections = ‘%d Connections’;

SHeapAllocated = ‘%s bytes allocated’;

{$R *.dfm}

{ TfrmMain }

procedure TfrmMain.UMConnect(var Msg: TMessage);beginInc(FConnections, Msg.WParam);if FConnections = 1 thenpnlConnections.Caption := SOneConnection

elsepnlConnections.Caption := Format(SConnections, [FConnections]);

end;

procedure TfrmMain.Timer1Timer(Sender: TObject);varHS: THeapStatus;

beginHS := GetHeapStatus;

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

end;

end.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N357

LISTING 9.3 Continued

Page 371: Delphi Kylix Database Development

Listing 9.3 defines a user-defined message, UM_CONNECT. The remote data module posts thismessage to the main form on creation and destruction. The UMConnect method fires in responseto the UM_CONNECT message and updates a label showing the number of current connections tothe application server.

The only other code in Listing 9.3 is the Timer1Timer method, which fires every second to dis-play the amount of RAM currently being used by the application server.

Figure 9.3 shows the application server at runtime.

Chapter 9358

FIGURE 9.3The application server with a minimal user interface.

The Client ApplicationWith the application server out of the way, we can now turn our attention to the client applica-tion. The first unit that we’ll look at is the data module. Listing 9.4 contains the source codefor the client-side data module.

LISTING 9.4 ConMan—DataModule.pas

unit DataModule;

interface

usesSysUtils, Classes, SConnect, DB, DBClient, MConnect, Dialogs;

typeTDM = class(TDataModule)SocketConnection1: TSocketConnection;cdsContacts: TClientDataSet;cdsTodos: TClientDataSet;cdsContactsCONTACTID: TIntegerField;cdsContactsFIRST: TStringField;cdsContactsLAST: TStringField;cdsContactsDEAR: TStringField;cdsContactsTITLE: TStringField;cdsContactsCOMPANYNAME: TStringField;cdsContactsADDRESS1: TStringField;

Page 372: Delphi Kylix Database Development

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;

varDM: TDM;

implementation

uses RecErrorForm;

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N359

LISTING 9.4 Continued

Page 373: Delphi Kylix Database Development

resourcestringSConnectCaption = ‘Database Server’;SConnectPrompt = ‘Server:’;

{$R *.dfm}

procedure TDM.DataModuleCreate(Sender: TObject);begincdsContacts.Open;

end;

procedure TDM.DataModuleDestroy(Sender: TObject);begincdsContacts.Close;SocketConnection1.Close;

end;

// Dataset events

procedure TDM.cdsContactsNewRecord(DataSet: TDataSet);beginDataSet.FieldByName(‘CONTACTID’).AsInteger :=GetNextID(DataSet as TCustomClientDataSet, ‘CONTACTID’);

end;

procedure TDM.cdsContactsCalcFields(DataSet: TDataSet);beginDataSet.FieldByName(‘FullName’).AsString :=DataSet.FieldByName(‘FIRST’).AsString + ‘ ‘ +DataSet.FieldByName(‘LAST’).AsString;

end;

procedure TDM.cdsContactsReconcileError(DataSet: TCustomClientDataSet;E: EReconcileError; UpdateKind: TUpdateKind;var Action: TReconcileAction);

beginAction := HandleReconcileError(DataSet, UpdateKind, E);

end;

procedure TDM.cdsTodosNewRecord(DataSet: TDataSet);beginDataSet.FieldByName(‘TODOID’).AsInteger :=GetNextID(DataSet as TCustomClientDataSet, ‘TODOID’);

end;

Chapter 9360

LISTING 9.4 Continued

Page 374: Delphi Kylix Database Development

function TDM.GetNextID(DataSet: TCustomClientDataSet;const PrimaryKey: string): Integer;

varCloneDS: TClientDataSet;

beginCloneDS := TClientDataSet.Create(nil);tryCloneDS.CloneCursor(DataSet, False);CloneDS.IndexFieldNames := PrimaryKey;CloneDS.First;if CloneDS.FieldByName(PrimaryKey).AsInteger > 0 thenResult := -1

elseResult := CloneDS.FieldByName(PrimaryKey).AsInteger - 1;

finallyCloneDS.Free;

end;end;

// Connection events

procedure TDM.SocketConnection1BeforeConnect(Sender: TObject);varServer: string;

beginif InputQuery(SConnectCaption, SConnectPrompt, Server) thenSocketConnection1.Address := Server;

end;

end.

The first two methods of interest are the DataModuleCreate and DataModuleDestroy methods.DataModuleCreate opens the cdsContacts table, which attempts to load local data from thefile CONMAN.CDS because the component’s FileName property is set to CONMAN.CDS. Ifthe file does not exist, the program will display a blank screen—it doesn’t attempt to connectto the application server automatically.

DataModuleDestroy closes the cdsContacts table and also the connection to the applicationserver, if a connection was established. Because the cdsContacts.FileName property is set, theprogram automatically writes the data to the file CONMAN.CDS. In addition, the todo datasetis stored in the same CDS file because it is a nested dataset of cdsContacts.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N361

LISTING 9.4 Continued

Page 375: Delphi Kylix Database Development

The cdsContactsNewRecord and cdsTodosNewRecord events make a call to the local methodGetNextID. GetNextID clones the dataset in question and moves to the first record in thedataset. The first record is the record with the lowest numbered ID. It then assigns the nextlower number to the ID of that record, ensuring that the record has a negative primary key.

The reason this is done is to ensure that all records that have not yet been added to the data-base have a negative ID. The server-side data module checks for a negative ID and then callsthe database’s GEN_ID stored procedure to assign a unique ID. This ensures that all addedrecords receive a unique ID, regardless of what computer they were added from. (Rememberthat multiple users may be running this application at the same time.)

Chapter 9362

An alternative to using the cloned dataset to retrieve the next negative ID is to set upa private variable and retrieve the next ID at program startup. That way you can sim-ply decrement the variable to obtain the next ID. The reason I don’t do this is becausein real-world applications I sometimes have 40 or 50 datasets in my data module. Idon’t like to set up 40 or 50 variables—one for each dataset. Instead, I use the com-mon function to retrieve the next ID on the fly.

Note that cloning a dataset does temporarily require a small amount of memory, andalso takes a small amount of time to do. If you need to generate temporary IDs asquickly as possible, you should consider using a private variable instead.

NOTE

Listing 9.5 contains the source code for the client’s main form, which is where the bulk of thecode for this application lies.

LISTING 9.5 ConMan—MainForm.pas

unit MainForm;

interface

usesWindows, 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;

typeTfrmMain = class(TForm)StatusBar1: TStatusBar;ImageList1: TImageList;

Page 376: Delphi Kylix Database Development

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;

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N363

LISTING 9.5 Continued

Page 377: Delphi Kylix Database Development

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;

Chapter 9364

LISTING 9.5 Continued

Page 378: Delphi Kylix Database Development

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

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N365

LISTING 9.5 Continued

Page 379: Delphi Kylix Database Development

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

privateprocedure DoConnectDisconnect(Sender: TObject);{ Private declarations }

public{ Public declarations }

end;

varfrmMain: TfrmMain;

implementation

uses DataModule, TodoForm;

resourcestringSConnect = ‘&Connect to Database Server’;SDisconnect = ‘&Disconnect from Database Server’;

SOneUpdate = ‘1 update pending’;SUpdates = ‘%d updates pending’;

SOnline = ‘Online’;SOffline = ‘Offline’;

SChangesPending = ‘The current contact has been changed. ‘ +‘Do you want to save changes to this record?’;

Chapter 9366

LISTING 9.5 Continued

Page 380: Delphi Kylix Database Development

{$R *.dfm}

// Form event handlers

procedure TfrmMain.FormCreate(Sender: TObject);beginDM.SocketConnection1.AfterConnect := DoConnectDisconnect;DM.SocketConnection1.AfterDisconnect := DoConnectDisconnect;

end;

procedure TfrmMain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);beginif DM.cdsContacts.State <> dsBrowse then begincase MessageDlg(SChangesPending, mtWarning, [mbYes, mbNo, mbCancel], 0) ofmrYes: 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 statusStatusBar1.Invalidate;

end;

// Status bar event handlers

procedure TfrmMain.StatusBar1DrawPanel(StatusBar: TStatusBar;Panel: TStatusPanel; const Rect: TRect);

beginif Panel.Index = 0 then beginif DM.SocketConnection1.Connected then beginStatusBar.Canvas.Font.Color := clGreen;StatusBar.Canvas.TextRect(Rect, Rect.Left + 2, Rect.Top + 1, SOnline);

end else beginStatusBar.Canvas.Font.Color := clRed;StatusBar.Canvas.TextRect(Rect, Rect.Left + 2, Rect.Top + 1, SOffline);

end;end;

end;

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N367

LISTING 9.5 Continued

Page 381: Delphi Kylix Database Development

// Database component event handlers

procedure TfrmMain.dsContactsDataChange(Sender: TObject; Field: TField);varBlobStream: TStream;

beginif (Field = nil) or (Field = DM.cdsContactsIMAGE) then beginBlobStream := DM.cdsContacts.CreateBlobStream(DM.cdsContactsIMAGE, bmRead);tryif BlobStream.Size = 0 thenimgPhoto.Picture := nil

elseimgPhoto.Picture.Bitmap.LoadFromStream(BlobStream);

finallyBlobStream.Free;

end;end;

end;

// Image controls

procedure TfrmMain.ImageLoad1Execute(Sender: TObject);beginif OpenPictureDialog1.Execute then beginDM.cdsContacts.Edit;DM.cdsContactsIMAGE.LoadFromFile(OpenPictureDialog1.FileName);

end;end;

procedure TfrmMain.ImageClear1Execute(Sender: TObject);beginDM.cdsContacts.Edit;DM.cdsContactsIMAGE.Clear;

end;

// File menu

procedure TfrmMain.FileConnect1Execute(Sender: TObject);beginDM.SocketConnection1.Connected := not DM.SocketConnection1.Connected;

end;

procedure TfrmMain.FileConnect1Update(Sender: TObject);

Chapter 9368

LISTING 9.5 Continued

Page 382: Delphi Kylix Database Development

beginwith Sender as TAction do beginif DM.SocketConnection1.Connected thenCaption := SDisconnect

elseCaption := SConnect

end;end;

// Dataset menu

procedure TfrmMain.DataSetApplyUpdatesExecute(Sender: TObject);beginif DM.cdsContacts.ApplyUpdates(0) = 0 thenDM.cdsContacts.Refresh;

end;

procedure TfrmMain.DataSetCancelUpdatesExecute(Sender: TObject);beginDM.cdsContacts.CancelUpdates;

end;

procedure TfrmMain.OnHaveUpdates(Sender: TObject);begin(Sender as TAction).Enabled := (DM.cdsContacts.ChangeCount > 0);

end;

procedure TfrmMain.DataSetNumUpdatesUpdate(Sender: TObject);beginif DM.cdsContacts.ChangeCount = 1 thenStatusBar1.Panels[1].Text := SOneUpdate

elseStatusBar1.Panels[1].Text := Format(SUpdates,[DM.cdsContacts.ChangeCount]);

end;

// Todo popup menu

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

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N369

LISTING 9.5 Continued

Page 383: Delphi Kylix Database Development

beginfrmTodo := TfrmTodo.Create(nil);tryif frmTodo.ShowModal = mrOk then beginDM.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;finallyfrmTodo.Free;

end;end;

procedure TfrmMain.TodoEditExecute(Sender: TObject);varfrmTodo: TfrmTodo;DT: TDateTime;

beginfrmTodo := TfrmTodo.Create(nil);tryfrmTodo.ecDescription.Text := DM.cdsTodosDescription.AsString;frmTodo.dtDate.Date := DM.cdsTodosScheduled.AsDateTime;frmTodo.dtTime.Time := DM.cdsTodosScheduled.AsDateTime;if frmTodo.ShowModal = mrOk then beginDM.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;finallyfrmTodo.Free;

end;end;

procedure TfrmMain.TodoEditUpdate(Sender: TObject);begin(Sender as TAction).Enabled := not DM.cdsTodos.IsEmpty;

end;

Chapter 9370

LISTING 9.5 Continued

Page 384: Delphi Kylix Database Development

procedure TfrmMain.TodoDeleteExecute(Sender: TObject);beginDM.cdsTodos.Delete;

end;

procedure TfrmMain.TodoMarkDone1Execute(Sender: TObject);beginDM.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 ofevent handlers in the source code.

The form’s OnCreate event handler sets the data module’s TSocketConnection.AfterConnect

and AfterDisconnect event handlers to point to the main form’s DoConnectDisconnect

method. This way, when a connection is established to or broken from the application server,the main form will learn of the connect or disconnect. DoConnectDisconnect simply invali-dates the status bar, which will then be updated to show the new connection status.

These event handlers are assigned at runtime rather than design time because the event handleris not located in the same unit as the data module. Because the data module doesn’t use themain form, the event handlers can’t be assigned at design time.

dsContactsDataChange is fired whenever the contact data source detects a change in the data.The event handler checks to see if the modified field is nil (meaning the current recordchanged) or if it references the IMAGE field (meaning the image for the current recordchanged). If so, the code loads the current image from the dataset and displays it on the mainform.

ImageLoad1Execute and ImageClear1Execute fire when the user loads a new image for thecurrent contact or clears the contact’s image, respectively. Both event handlers put the contactdataset into edit mode and then either clear the image or load a new image, respectively. As aresult of either of these events, dsContactsDataChange fires automatically, causing the dis-played image to update.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N371

LISTING 9.5 Continued

Page 385: Delphi Kylix Database Development

The only interesting action on the File menu is the Connect/Disconnect menu item.FileConnect1Execute simply toggles the socket connection’s Connected property to connectto or disconnect from the application server. FileConnect1Update takes care of setting theaction’s caption appropriately, depending on the current connection status.

Finally, the todo pop-up menu contains several actions for manipulating the todo items for thecurrent contact. TodoAddExecute and TodoEditExecute display the frmTodo form, whichallows the user to enter or modify a todo. TodoMarkDone1Execute sets the completed date andtime for the current todo to the current date and time.

The rest of the code in the main form should be fairly self-explanatory, so I won’t go over it indetail.

The final unit in this application is TodoForm.pas. Listing 9.6 contains the source code for thisfile, which enables the user to enter a new todo for a contact or edit an existing todo.

LISTING 9.6 ConMan—TodoForm.pas

unit TodoForm;

interface

usesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, ExtCtrls, ComCtrls, StdCtrls;

typeTfrmTodo = 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;

Chapter 9372

Page 386: Delphi Kylix Database Development

implementation

{$R *.dfm}

procedure TfrmTodo.FormCreate(Sender: TObject);begindtDate.Date := Date;dtTime.Time := StrToTime(‘12:00 pm’);

end;

end.

The todo form’s FormCreate handler simply defaults the date and time of the todo to noon ofthe current date. The user will modify the date and time accordingly.

Room for ImprovementBecause ConMan is a sample application, there is considerable room for improvement. Inaddition to the omissions mentioned in the section titled “What Is ConMan?” at the beginningof 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 schedulingcapabilities.

• The capability to query the database. As it grows to a large size, it may take a consider-able amount of time to load all the data onto a client machine.

I’ll leave it to you to implement these suggestions if you see fit. That will give you a spring-board to learning multitier development techniques on your own.

SummaryThis chapter pulled together many of the concepts discussed in prior chapters to create a com-plete multitier database application. Specifically, this application implements the following:

• The application server makes use of dbExpress components such as TSQLConnection andTSQLDataSet for connecting to an Interbase database. These components are discussed inChapters 1 and 2.

The ConMan Application

9

TH

EC

ONM

AN

APPLIC

ATIO

N373

LISTING 9.6 Continued

Page 387: Delphi Kylix Database Development

• The application server contains a minimal user interface that displays the number of cur-rent connections along with the amount of RAM in use by the server, as shown inChapter 8.

• The server’s pvContactsBeforeUpdateRecord event handler shows how to ensure thateach record has a unique primary key just before posting to the database, as discussed inChapter 7.

• The client application makes use of a calculated field on the cdsContact dataset.Calculated fields were discussed in Chapter 3.

• The client application’s dsContactsDataChange event handler takes care of displaying animage from the dataset in a non data-aware TImage component. This concept was dis-cussed in Chapter 4.

• The client application’s cdsContactsReconcileError event handler takes advantage ofDelphi’s built-in reconciliation form to handle reconciliation errors, as discussed inChapter 7.

Chapter 9374

Page 388: Delphi Kylix Database Development

APPENDIX

ARedistributing dbExpressApplications

IN THIS APPENDIX• Redistributable Files 376

• Licensing Issues 378

• CD-ROM-Based Applications 378

Page 389: Delphi Kylix Database Development

Appendix A376

This appendix tells you what you need to know to redistribute your database applications afteryou’ve written them. Because this book assumes that you’re using dbExpress as the data accesstechnology behind your applications, I won’t explain how to redistribute the BDE or ADO.Instead, I’ll concentrate specifically on dbExpress and DataSnap.

Redistributable FilesRedistributing an application built with dbExpress and DataSnap is incredibly simple—espe-cially so if you’re used to messing around with a huge BDE- or ADO-based installation set. Tosupport dbExpress and DataSnap, you need to redistribute only two files along with your appli-cation.

Well, that’s two files in addition to whatever else your application might need. Forexample, if you build your application with runtime packages enabled, you will alsoneed to redistribute the required runtime packages for the application.

NOTE

Redistributing a Windows ApplicationFor a VCL or CLX application running under Microsoft Windows, you need to redistribute thedriver for the database back end that your application uses. The database engines and theirassociated Windows drivers are listed in Table A.1.

TABLE A.1 dbExpress Windows Drivers

Database Windows Driver

InterBase Dbexpint.dll

Oracle dbexpora.dll

DB2 dbexpdb2.dll

MySQL Dbexpmys.dll

In addition to the dbExpress driver, you need to redistribute MIDAS.DLL, which contains thecode necessary for the client dataset technology.

You can place both the dbExpress driver and MIDAS.DLL anywhere where your applicationcan find them, which could be either the application directory or the SYSTEM32 directory.Neither file needs to be registered with Windows.

Page 390: Delphi Kylix Database Development

If you prefer, you can statically link DataSnap into your client application by includingMidasLib to your project’s uses clause and rebuilding. If you do this, you don’t need to redis-tribute MIDAS.DLL with the client application (it must be redistributed with the server appli-cation, however).

You can also statically link the dbExpress drivers with your application by linking in theappropriate file(s), as listed in Table A.2.

TABLE A.2 dbExpress Statically-Linked Driver Units

Database Unit

InterBase Dbexpint

Oracle Dbexpora

DB2 dbexpdb2

MySQL Dbexpmy

Redistributing dbExpress Applications

A

RED

ISTRIB

UTIN

GD

BEX

PRESS

APPLIC

ATIO

NS

377

Even though Delphi 6 supports statically linking dbExpress and/or DataSnap to yourapplication, you should be aware that the static linking is a new technology that isn’tcompletely compatible with all DataSnap code. For example, attempting to callTClientDataSet.SaveToFile to save a client dataset to an XML file doesn’t save tothe same format when statically linking as when dynamically linking. The bottom lineis, if you decide to statically link your application, please test thoroughly beforereleasing to your clients.

NOTE

Redistributing a Linux ApplicationFor a CLX application running under Linux, you need to redistribute the appropriate LinuxdbExpress driver. Table A.3 lists the database engines currently supported by dbExpress andthe associated Linux driver.

TABLE A.3 dbExpress Linux Drivers

Database Linux Driver

InterBase libsqlib.so

Oracle libsqlor.so

DB2 libsqldb2.so

MySQL libsqlmy.so

Page 391: Delphi Kylix Database Development

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

Licensing IssuesIf you’re writing database applications that work with local data (that is, data does not travelfrom one machine to another), you don’t need to worry about any licensing fees for your appli-cations. However, if data packets travel from one machine to another, you will need to pay asmall redistribution fee to Borland for use of the DataSnap technology.

For the particulars about when you need to pay the redistribution fees and how much they are,visit Borland’s Web site at http://www.borland.com/midas.

CD-ROM-Based ApplicationsA 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 soon, are often redistributed with a large database on CD. The end user doesn’t need to updatethe data—the user needs only to access the data from the CD.

To redistribute a read-only InterBase database on CD, you need to first run the gfix commandagainst 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 setReadOnly to True for any TClientDataSets. This will ensure that none of the database-relatedcomponents attempt to update any data in the database.

Appendix A378

Page 392: Delphi Kylix Database Development

APPENDIX

BdbExpress Plus

IN THIS APPENDIX• What Is dbExpress Plus? 380

• For More Information 384

Page 393: Delphi Kylix Database Development

Appendix B380

This appendix takes a quick look at dbExpress Plus, Thomas Miller’s open source add-onlibrary for dbExpress. dbExpress Plus can be downloaded from CodeCentral atcodecentral.borland.com. It is ID #15945.

What Is dbExpress Plus?dbExpress Plus is an open source effort to extend dbExpress by introducing new componentsin three key areas:

• Scripting

• Enhanced metadata

• Data pumping

The following sections provide a quick overview of these components.

ScriptingTSQLScript provides a means of executing multiple SQL statements at once through what isknown as a script. You’re probably familiar with SQL script files. The following snippet showsa simple script file. This is actually part of the SQL script used to create the CONMAN database.

CREATE TABLE CONTACTS(CONTACTID INTEGER NOT NULL,FIRST VARCHAR(20),LAST VARCHAR(30),DEAR VARCHAR(40),TITLE VARCHAR(30),COMPANYNAME VARCHAR(50),ADDRESS1 VARCHAR(50),ADDRESS2 VARCHAR(50),CITY VARCHAR(30),STATE VARCHAR(20),POSTALCODE VARCHAR(10),COUNTRY VARCHAR(30),PHONE VARCHAR(20),FAX VARCHAR(20),CELLULAR VARCHAR(20),PAGER VARCHAR(20),EMAIL VARCHAR(40),IMAGE BLOB SUB_TYPE 0 SEGMENT SIZE 4096,NOTES BLOB SUB_TYPE TEXT SEGMENT SIZE 4096,PRIMARY KEY (CONTACTID)

);

Page 394: Delphi Kylix Database Development

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

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

);

Ordinarily, you’d need to execute these three statements (CREATE TABLE, CREATE INDEX, andCREATE TABLE) separately by calling TSQLConnection.ExecuteDirect three times, once foreach statement.

Using TSQLScript, you can use the following two lines of code:

SQLScript1.SQL.LoadFromFile(ScriptFile);SQLScript1.ExecuteDirect;

This code snippet assumes that ScriptFile is a string variable that contains the filename ofthe SQL script file.

Enhanced MetadataTSQLMetaData provides additional, easy-to-use methods to retrieve metadata information froma database. Because it derives from TSQLConnection, you can use TSQLMetaData in place of aTSQLConnection in your projects in which you need enhanced metadata.

Retrieving Table, View, and Field NamesTSQLMetaData allows easy retrieval of table names, system table names, view names, and synonym names through the GetTableNames, GetSysTableNames, GetViewNames, andGetSynonymNames methods, respectively. Each of these methods takes a single parameter,which designates a string list into which the results are returned.

SQLMetaData1.GetTableNames(ListBox1.Items);

TSQLMetaData.GetFieldNames retrieves the fields that make up a table. Pass in the table nameto retrieve the field names for a string list for the results, and a flag that indicates whetherfields should be returned in the order in which they are declared or in alphabetical order.TSQLMetaData.GetFieldNames is defined like this:

procedure GetFieldNames(const ATableName: string; AList: TStrings;ASortOrder: TMetaDataSortOrder = soPosition); overload;

dbExpress Plus

B

DBE

XPR

ESSP

LUS

381

Page 395: Delphi Kylix Database Development

By default, fields are returned in the order in which they were declared. By passing soName asthe final parameter to GetFieldNames, the field names are returned in alphabetical order.

Retrieving Additional Field MetadataIf 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 TFieldMetaDatarecord containing information about the field in question. TFieldMetaData is defined like this:

TFieldMetaData = recordColumnName: 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 datacontained in the column, and other pertinent information about the field.

Retrieving Index DataOne useful method that TSQLMetaData provides is a way to get information about an index ona table. Two forms are provided. The first form retrieves information about any index, given itsname. Two methods are provided for this use.

procedure GetIndexFieldNames(const ATableName, AIndexName: string;AList: TStrings);

GetIndexFieldNames returns a string list composed of the fields that make up the indexAIndexName on table ATableName.

function GetIndexFields(const ATableName, AIndexName: string): string;

GetIndexFields returns the same list as a semicolon-delimited list.

The second form of index data retrieval is to retrieve data about the primary key, rather than anarbitrary index. To retrieve a list of fields that make up the primary key, you could pass in theprimary key name either to GetIndexFieldNames or GetIndexFields. However, you might not

Appendix B382

Page 396: Delphi Kylix Database Development

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

procedure GetPrimaryKeyFieldNames(const ATableName: string; AList: TStrings);

function GetPrimaryKeyFields(const ATableName: string): string;

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

Additional MethodsTSQLMetaData also provides a collection of methods to return the INSERT, UPDATE, and SELECTSQL statements for a given table. The three methods are listed next:

function GetInsertStatement(const ATableName: string;ASQL: TStrings): Integer;

function GetUpdateStatement(const ATableName: string;ASQL: TStrings): Integer;

function GetSelectStatement(const ATableName: string;ASQL: TStrings): Integer;

Given a table name, these three methods return the corresponding INSERT, UPDATE, or SELECTSQL statement in the ASQL parameter.

Data PumpingTSQLDataPump provides an answer to the BDE’s TBatchMove component, allowing you to eas-ily move data from one table to another.

To do this, TSQLDataPump publishes SQLMetaDataSource and SQLMetaDataDestination prop-erties, which reference the TSQLMetaData components that point to the source and destinationdatabase connections, respectively.

After you specify the source and destination databases, set the SQLSource property to a validSELECT statement for the source database, such as

SELECT EMP_NO, FIRST_NAME, LAST_NAME, PHONE_EXT, HIRE_DATE, DEPT_NO,JOB_CODE, JOB_GRADE, JOB_COUNTRY, SALARY FROM EMPLOYEE

Next, set the DestinationTable property to the name of the table in the destination database,and then double-click the DestinationFields property, which allows you to set up a mappingbetween 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 modesthat may be used.

dbExpress Plus

B

DBE

XPR

ESSP

LUS

383

Page 397: Delphi Kylix Database Development

TABLE B.1 Valid DataMoveMode Values

Value Description

dmAlwaysInsert Inserts all selected records into the destination table.

DmAppend Appends selected records to the destination table.

dmAppendUpdate Updates the record if it already exists in the destination table.Otherwise, appends the record to the destination table.

DmDelete Deletes matching records from the destination table.

DmUpdate Updates matching records in the destination table.

At this point, all the required properties are set, so you need to call onlyTSQLDataPump.Execute to perform the batch move operation.

For More InformationThis has been only a brief overview of the capabilities provided by dbExpress Plus. Because itis an open source project, it stands to reason that the code may grow to provide more functionalitythan that discussed here. The dbExpress Plus download also comes with a demo applicationthat shows how to use many of the methods discussed in this appendix.

Appendix B384

Page 398: Delphi Kylix Database Development

INDEXSYMBOLS

+ (Addition) operator, 128* (asterisk) operator, 129\ (backslash), 210/ (data separator), 210. (decimal point), 207# (digit placeholder), 2070 (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), 2070 specifier, 2099 specifier, 209– (Subtraction) operator, 128, (thousands separator), 207: (time separator), 210_ (underscore), 210

Page 399: Delphi Kylix Database Development

A specifier386

AA specifier, 209accessing

fields (client datasets),103

nonpersistent fields,104-105

persistent fields, 103Fields objects, 64providers on different

form, 308ACID (Atomic,

Consistent, Isolated,Durable)properties, 37

action handlers, 228activating indexes

(client datasets), 121Active property, 60Add Connection

button, 10Add method, 102Add to Interface

command (remotedata module contextmenu), 324

Add To Interface dialogbox, 324

AddFieldDef method,102

AddIndex method, 121adding

fields (client datasets),102

methods (remote datamodules), 324

Addition (+) operator,128

Address property, 331Advanced application

(MainForm.pas), codelisting, 76-79

advantagesclient datasets, 94-95ConMan, 373

AfterApplyUpdatesevent, 295

AfterCancel event, 150AfterClose event, 150AfterConnect event, 14AfterDelete event, 150AfterDisconnect

event, 14AfterEdit event, 150AfterExecute event,

295AfterGetParams event,

295AfterGetRecords event,

295, 342AfterInsert event, 150AfterOpen event, 150AfterPost event, 150AfterRowRequest

event, 295AfterScroll event, 150AfterUpdateRecord

event, 295aggregate expressions,

195Aggregate fields, 96aggregate types, 195aggregates

across datasets, 196-197determining position of

record, 197enabling/disabling, 197

grouped, 196maintained, 192-193

creating at designtime, 193-195

creating at runtime,195

Aggregates property,194-195

Alignment property,242-244

AllowDelete property,267

AllowInsert property,267

And operator, 128Apartment value, 321Append method, 105application servers

ConMan, 352-353creating, 318testing, 328-329

applications. See alsoindividualapplications

clientConMan, 358,

361-362, 371-372CORBA connections,

335creating, 329-330DCOM connections,

333HTTP connections,

334intercepting data

packets, 333multiple connection

types, 343-344

Page 400: Delphi Kylix Database Development

ButtonStyle property387

SOAP connections,335

socket connections,330

socket servers,331-332

RangeFilter, 135redistributing, 376

CD-ROM-based, 378fees, 378Linux, 377-378Windows, 376-377

server, creating userinterface, 326-328

ApplyUpdates method,276-277

arithmetic operators(filters)), 128

AsString method, 162AsString property, 103asterisk (*) operator,

129AutoCalcFields prop-

erty, 98AutoEdit property, 204automatic sorting

(grids), 264-265AutoSave property, 49Avg aggregate type,

195

Bbackslash (\), 210Basic application, code

listingsDatasetTypeForm.pas,

68-69MainForm.pas, 65-68

BDE (Borland DatabaseEngine), 59

BeforeApplyUpdatesevent, 295

BeforeCancel event,149

BeforeClose event, 149BeforeConnect

event, 14BeforeDelete event,

149BeforeDisconnect

event, 14BeforeEdit event, 149BeforeExecute event,

295BeforeGetParams

event, 295BeforeGetRecords

event, 295, 342BeforeInsert event, 149BeforeOpen event, 149BeforePost event, 149BeforeRowRequest

event, 296BeforeScroll event, 149BeforeUpdateRecord

event, 296Binary Large Objects.

See BLOBsBLANK operator, 128BLOB column,

removing, 70BLOBs (Binary Large

Objects), 162fetching manually, 310fields

limitations, 168-169,172

resolving changes,290

storingfiles, 168images, 162-164notes, 162streamed data,

165-167streaming components,

167support, 69-70

BLOBs application(MainForm.pas), codelisting, 169-171

BlobSize property, 70bookmarks, 114-115Borland Database

Engine (BDE), 59Borland Web site, 378Both value, 321briefcase model, 340btnRetrieveClick

method, 87buffering messages, 50buttons

Add Connection, 10Cancel, 224Delete, 224Edit, 224First, 224Insert, 224Last, 224New Interface, 325Next, 224Post, 224Prior, 224Refresh, 224, 290TDBNavigator, 224

ButtonStyle property,243

Page 401: Delphi Kylix Database Development

c specifier388

Cc specifier, 209calculated fields, 98

internal, 98-99providing values,

99-100standard, 98

callback handlers,removing, 49

callback events, trace(SQL operations feedback), 47-49

callbacks, 325-326Cancel button, 224Cancel method, 177CancelUpdates

property, 181CanModify property,

225Caption property, 244CaseInsFields property,

120cbProcedureClick

method, 25cbUseCallbackClick

event handler, 54CD-ROM-based

applications,redistributing, 378

CDS (ClientDataset)application(MainForm.pas), codelisting, 108-111

CDSIndex application(MainForm.pas), codelisting, 124-126

cell (grids), 257-259

change log, 177properties

CancelUpdates, 181ChangeCount, 181LogChanges, 177MergeChangeLog,

181RevertRecord,

178-179SavePoint, 179-180StatusFilter, 181-182UndoLastChange,

178viewing, 182, 186

ChangeCount property,181

CHANGEINDEX index,123

ChangeLog applica-tion, code listings

ChangeLogForm.pas,184-185

MainForm.pas, 182-185ChangeLogForm.pas

(ChangeLogapplication), code listing, 184-185

changingcursor (SQL operations

feedback), 47field data (servers),

297-298check boxes

Log Trace, 54Use Callback, 54

classesTCustomClientDataSet,

94TFieldDataLink,

225-226

client applicationsConMan, 358, 361-362,

371-372CORBA connections,

335creating, 329

local connections,329-330

remote connections,330

DCOM connections,333

HTTP connections, 334intercepting data

packets, 333multiple connection

types, 343-344SOAP connections, 335socket connections, 330socket servers, 331-332

client datasets, 94advantages/

disadvantages, 94-95cloning, 186-188,

191-192creating, 95

design-time, 96example, 108runtime, 101-103

field definitions,creating, 96

fieldsaccessing, 103-105adding, 102calculated, 98-100data, 96-97lookup, 100-101

file formats, 107

Page 402: Delphi Kylix Database Development

code listings389

indexes, 118-119activating, 121creating, 119-121retrieving

information,122-123

switching between,121-122

manipulatingdeleting records, 106example, 108modifying records,

105-106navigating, 113

code listing, 116-118random-access navi-

gation, 114-116sequential naviga-

tion, 113populating, 105

example, 108from a file, 106-107from a stream,

106-107from another data-

base, 106manually, 105

ClientData field, 48Clone application

(MainForm.pas), codelisting, 188-191

CloneCursor method,187

cloning client datasets,186-188, 191-192

closingdatabase connections,

64datasets, 64

CMExit method, 228Code Central Web site,

264, 380code listings

Advanced application(MainForm.pas),76-79

Basic applicationDatasetTypeForm.pas,

68-69MainForm.pas,

65-68BLOBs application

(MainForm.pas),169-171

CDS (ClientDataset)application(MainForm.pas),108-111

CDSIndex application(MainForm.pas),124-126

ChangeLog applicationChangeLogForm.pas,

184-185MainForm.pas,

182-185Clone application

(MainForm.pas),188-191

ConManDataModule.pas,

358-361MainForm.pas,

362-371TodoForm.pas,

372-373ConMan.SQL, 350-351

ConManServerMainForm.pas,

356-357RemoteDataModule.

pas, 353-357,359-361, 363-371,373

creating empty data-bases 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 applicationMainForm.pas,

151-155OptionsForm.pas,

156-157Events application

(MainForm.pas), 14-17

Page 403: Delphi Kylix Database Development

code listings390

feedback(MainForm.pas),51-54

Joins application(MainForm.pas),304-307, 312-313

MetaData(MainForm.pas),20-25

MethodsClientapplication(MainForm.pas),338-339

MethodsServer application

MainForm.pas,327-328

ServerDataModule.pas, 336-339

navigating clientdatasets, 116-118

Nested application(MainForm.pas),174-175

options(MainForm.pas),246-251

RangeFilterFilterForm.pas, 133MainForm.pas,

130-132RangeForm.pas,

134-135SavePoint property, 180Schema application

(MainForm.pas),81-87

Search applicationMainForm.pas,

141-143SearchForm.pas, 144

Trans application(MainForm.pas),42-46

Updates application(MainForm.pas),159-161, 281-289

ColCount property, 267colon (:), 210Color property, 242-244column (grid)

current, determining,257

mouse coordinate,determining, 257-259

column titles, 243-244column types, 243columns

BLOB, removing, 70customizing, 241-242,

265column titles,

243-244column types, 243

resized, detecting,260-261

schemastColumns value, 89stIndexes value,

90-91stProcedureParams

value, 89-90stProcedures value,

88stSystemTables

value, 88stTables value, 88

columns editor, 242Columns property,

240-241combo boxes

Driver Name, 10Instancing, 320Threading Model, 321

comma (,), 207commands

DDL, 27databases, creating,

28-29executing, 27tables, creating,

27-28DML, 29

parameterized SQLstatements, 30-32

SELECT statement,32-33

simple SQL statements, 30

File menu (New), 319gfix, 378New menu (Other), 280,

319remote data module

context menu (Add toInterface), 324

CommandText property, 60, 310

CommandType property, 60

committingtransactions, 40

comparison operators(filters), 127-128

Page 404: Delphi Kylix Database Development

components391

componentsconn, 352data-aware, 202-203

controlling user editing, 206

creating, 225disabling, 158-159,

162formatting/editing

field values, 206numeric fields, 207-209string fields, 209-210

modifying data fromcode, 205-206

non-data-awareequivalents,202-203

simple. See simpledata-aware components

data-aware. Seeindividual components

dsContacts, 353populating remote data

modules, 324pvContacts, 353sqlContacts, 352sqlID, 352sqlTodos, 352streaming (BLOBs), 167TClientDataSet, 94. See

also client datasetsTClientDataSetGrid,

261-264automatic sorting,

264-265columns,

customizing, 265

TConnectionBroker,343

TCORBAConnection,335

TDataSetProvider, 274,293-295

TDBCheckBox, 212TDBComboBox, 213,

217-218TDBCtrlGrid, 266

events, 267-268properties, 267

TDBEdit, 212TDBGrid, 240-241

custom drawing,252-254

customizing columns,241-244

edit mode, setting,259-260

events, 245-246grid options,

244-245grid settings, persist-

ing, 262-263limitations, 263resized columns,

detecting, 260-261row/column/cell,

determining,257-259

TDBImage, 221TDBListBox, 218-221TDBLookupComboBox

, 223TDBLookupListBox,

223TDBMemo, 212

TDBNavigator,223-225, 290

TDBRadioGroup, 213TDBText, 211TDCOMConnection,

333-334TFieldDataLink,

226-227TLocalConnection,

329-330TSimpleObjectBroker,

344TSOAP, 335TSocketConnection,

330-331TSQLClientDataSet,

309TSQLDataPump, 383TSQLDataSet, 60

general-purpose dataaccess, 62-63

properties, 60-61query-level access,

61-62stored procedure

access, 62table-level access, 61

TSQLMetaData, 381field metadata, 382index data, 382-383methods, 383table/view/field

names, 381-382TSQLMonitor, 49, 54TSQLQuery, 60TSQLScript, 380-381TSQLStoredProc, 60TSQLTable, 60TWebConnection, 334

Page 405: Delphi Kylix Database Development

ComputerName property392

ComputerNameproperty, 333

ConfigFile property,265

ConfigureColumnsmethod, 265

ConMan, 348advantages/

disadvantages, 373application server,

352-353client application, 358,

361-362, 371-372database structure,

349-350code 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, 352connect events, 14, 17Connected property, 8connecting to data-

bases, 8-9controlling login, 12-13dbExpress, 8local, 308named connections,

9-11setting database

parameters, 11-12unnamed

connections, 11

connecting to datasets,275-276

connection brokering,344-345

Connection Editor,10-11

Connection Name listbox, 10

ConnectionNameproperty, 8

connectionsdatabase, closing, 64DCOM (Distributed

COM), 333HTTP, 334creating

local, 329-330remote, 330

named, 9-11SOAP, 335sockets, 330unnamed, 11

constraints, 197-198Constraints property,

197Contacts table, 349ContactsByState

procedure, 350Control property, 225controlling login, 12-13controls (data-aware)

lookup, 222-223VCL-only, 222

CORBA remote datamodules, creating,322

Count aggregate type,195

CreateBlobStreammethod, 166

creatingapplication servers, 318callbacks, 325-326client application, 329

local connections,329-330

remote connections,330

client datasets, 95design-time, 96example, 108runtime, 101-103

data-aware components,225

databases (DDL commands), 28-29

DataChange event handler, 227

empty databases from aresource, code listing,29

field definitions (clientdatasets), 96

grouped aggregates, 196indexes (client datasets),

119at design-time,

119-120at runtime, 121

maintained aggregatesdesign time, 193-195runtime, 195

master/detailrelationships, 74-76

remote data modules,319

CORBA remote datamodules, 322

MTS remote datamodules, 321-322

Page 406: Delphi Kylix Database Development

data-aware grids393

SOAP remote datamodules, 322-323

standard remote datamodules, 320-321

tables (DDL com-mands), 27-28

TColumn object, 242user interfaces (server

applications), 326-328CtrlGrid application

(MainForm.pas), codelisting, 268-270

cursor, changing (SQLoperationsfeedback), 47

custom drawing,252-254

CustomContraintproperty, 198

CustomDraw(MainForm.pas), codelisting, 254-256

CustomIsolationfield, 39

customizing columns,241-242, 265

column titles, 243-244column types, 243

DDan Miser Web site,

333data

fields (servers), chang-ing, 297-298

intercepting, 298-299ordering, 73

queries, 74tables, 73

returned, limiting,309-311, 314-315

server, refreshing,290-291

streamed, storing(BLOBs), 165-168

data clashes, 277Data Definition

Language (DDL)statements, 27

data fields, 96-97Data Manipulation

Language (DML)statements, 27

data modules, remote,318-319

adding methods, 324creating, 319-322populating with

components, 324data packets, inter-

cepting, 333data pumping

(dbExpress Plus),383-384

data sources, settingup connection to, 227

data-aware compo-nents, 202-203

controlling user editing,206

creating, 225disabling, 158-159, 162formatting/editing field

values, 206numeric fields,

207-209string fields, 209-210

modifying data fromcode, 205-206

non-data-aware equivalents, 202-203

simple, 211TDBCheckBox, 212TDBComboBox, 213,

217-218TDBEdit, 212TDBImage, 221TDBListBox,

218-221TDBLookupComboB

ox, 223TDBLookupListBox,

223TDBMemo, 212TDBRadioGroup,

213TDBText, 211

TDataSource, 204-205TDBNavigator, 223-225

data-aware controlslookup, 222-223VCL-only, 222

data-aware gridsTClientDataSetGrid,

261, 263-264automatic sorting,

264-265columns, customiz-

ing, 265TDBCtrlGrid, 266

events, 267-268properties, 267

TDBGrid, 240-241custom drawing,

252-254customizing columns,

241-244

Page 407: Delphi Kylix Database Development

data-aware grids394

edit mode, setting,259-260

events, 245-246grid options, 244-245grid settings,

persisting, 262-263limitations, 263resized columns,

detecting, 260-261row/column/cell,

determining,257-259

third party, 271-272ExpressQuantumGrid,

271InfoPower 2000, 271Orpheus, 271TopGrid, 272

DataAware application(MainForm.pas), codelisting, 232-236

database connections,closing, 64

database events, monitoring, 49

buffering messages, 50logging messages,

49-50Database Login dialog

box, 12database metadata,

retrieving, 18database parameters,

11-12Database property, 12database structure

(ConMan), 349-350

databasesconnecting to, 8-9

controlling login,12-13

dbExpress, 8named connections,

9-11setting database

parameters, 11-12unnamed

connections, 11creating (DDL

commands), 28-29creating empty

databases from aresource, code listing, 29

disconnecting from, 13automatically, 13-14manually, 13

local, connecting to,308

saving changes,276-277

DataChange eventhandler, creating, 227

DataFetch application(MainForm.pas), codelisting, 311-313

DataModule.pas, 352,358-361

DataModuleCreatemethod, 361

DataModuleDestroymethod, 361

DataMoveModevalues, 384

dataset events, 148AfterXxx events,

149-150BeforeXxx events, 149event handlers, 150

DataSet property, 204dataset providers,

274-275datasets. See also

queries; stored procedures; tables

aggregates, 196-197client, 94

advantages/disad-vantages, 94-95

calculated fields,98-100

cloning, 186-188,191-192

creating, 95-96,101-103, 108

data fields, 96-97field definitions, cre-

ating, 96fields, accessing,

103-105fields, adding, 102file formats, 107indexes. See client

datasets, indexeslookup fields,

100-101manipulating,

105-108navigating, 113-118populating, 105-108

closing, 64connecting to, 275-276

Page 408: Delphi Kylix Database Development

dgRowSelect option395

constraints, 197-198dbExpress, 58-59field contents,

retrieving, 64-65navigating, 65, 68nested, 172-176opening, 63-64resolving to, 278responding to changes,

227updating, 227-228

DatasetTypeForm.pas(Basic application),code listing, 68-69

DataSize property, 107DataSnap, 318DataSource property,

60, 240Date function, 129date separator (/), 210date/time functions

(filters), 129Day function, 129dbExpress

connecting to databases, 8

Linux drivers, 377statically-linked driver

unites, 377dbExpress Connection

Editor, 10-11dbExpress datasets,

58-59dbExpress Plus, 380

data pumping, 383-384metadata, 381-383scripting, 380-381

dbExpress Windowsdrivers, 376

DCOM (DistributedCOM) connections,333

DDL (Data DefinitionLanguage)statements, 27

DDL commands, 27creating tables, 28databases, creating,

28-29executing, 27tables, creating, 27

DDLSQL(MainForm.pas), codelisting, 33-36

decimal point (.), 207Declaration edit box,

324DefaultDrawing

property, 253-254DEFAULT_ORDER

index, 123Delete button, 224Delete method, 106DeleteIndex method,

121deleting

indexes (client datasets),121

records (client databases), 106

Delphi Randomizer,111

DescFields property,120

design-timeclient datasets,

creating, 96

indexes, creating,119-120

maintained aggregates,creating, 193-195

detail records, fetchingmanually, 310-311,314-315

detectingresized columns,

260-261transaction support, 38

determiningposition of record in

aggregate, 197row/cell

current, 257mouse coordinate,

257-259Developer Express Web

site, 272dgAlwaysShowEditor

option, 244, 259dgAlwaysShowSelectio

n option, 245dgCancelOnExit option,

245dgColLines option, 244dgColumnResize

option, 244dgConfirm option, 245dgEditing option, 244,

260dgIndicator option, 244dgMultiSelect option,

245dgRowLines option,

244dgRowSelect option,

245, 253

Page 409: Delphi Kylix Database Development

dgTabs option396

dgTabs option, 244dgTitles option, 244dialog boxes

Add to Interface, 324Database Login, 12New Field, 97New Items, 280, 319

Dialogs tab, 280digit placeholders, 207DisableControls

method, 159DisableStringTrim

property, 198-199disabling

aggregates, 197data-aware components,

158-159, 162disadvantages

client datasets, 95ConMan, 373

disconnect events, 14, 17

disconnecting fromdatabases, 13

automatically, 13-14manually, 13

DisplayFormatproperty, 207-209

settings, 208specifiers, 207

Distributed COM connections, 333

Division ([db]) operator, 128

dmAlwaysInsert value,384

DmAppend value, 384dmAppendUpdate

value, 384

DmDelete value, 384DML (Data

ManipulationLanguage)statements, 27

DML commands, 29SELECT statement,

32-33SQL statements

parameterized, 30-32simple, 30

DmUpdate value, 384double quotation mark

(“), 207drawing, 252-254Driver Name combo

box, 10DriverName property, 8drivers

Linux, 377statistically-linked, 377Windows, 376

DropDownRows property, 243

dsContacts component,353

EE+/–, 207edit boxes,

Declaration, 324Edit button, 224Edit method, 105, 225edit mode, setting,

259-260editing

data-aware componentdata, 206

field values, 206numeric fields,

207-209string fields, 209-210

EditKey method, 140EditMask property, 209

settings, 210-211specifiers, 209-210

EditorMode property,259

editors (columns), 242empty databases, cre-

ating from a resource(code listing), 29

EmptyDataSet method,106

EnableControlsmethod, 159

Enabled property, 204enabling aggregates,

197encryption libraries,

299equality test (=)

operator, 127ETHDBComboBox.pas,

code listing, 214-217,220-221

ETHDBDateTimePicker.pas, code listing,228-236

ETHDBGrid.pas, codelisting, 260-261

ETHDBListBox.pas,code listing, 219-221

eTraceCat field, 48event handlers

cbUseCallbackClick, 54DataChange, 227

Page 410: Delphi Kylix Database Development

feedback397

stateless servers,341-343

writing, 50EventLog application,

code listingsMainForm.pas, 151-155OptionsForm.pas,

156-157events

AfterApplyUpdates, 295AfterCancel, 150AfterClose, 150AfterConnect, 14AfterDelete, 150AfterDisconnect, 14AfterEdit, 150AfterExecute, 295AfterGetParams, 295AfterGetRecords, 295,

342AfterInsert, 150AfterOpen, 150AfterPost, 150AfterRowRequest, 295AfterScroll, 150AfterUpdateRecord, 295BeforeApplyUpdates,

295BeforeCancel, 149BeforeClose, 149BeforeConnect, 14BeforeDelete, 149BeforeDisconnect, 14BeforeEdit, 149BeforeExecute, 295BeforeGetParams, 295BeforeGetRecords, 295,

342

BeforeInsert, 149BeforeOpen, 149BeforePost, 149BeforeRowRequest, 296BeforeScroll, 149BeforeUpdateRecord,

296connect/disconnect,

14, 17database, monitoring,

49-50dataset, 148-150OnActiveChange, 226OnCalcFields, 150OnCellClick, 245OnColEnter, 245OnColExit, 246OnColumnMoved, 246OnDataChange, 205,

226OnDeleteError, 150OnDrawColumnCell,

246, 252OnDrawDataCell, 246,

252OnEditButtonClick, 246OnEditError, 150OnEditingChange, 226OnFilterRecord, 130,

150OnGetData, 296-298,

341OnGetDataSetProperties,

297OnGetTableName, 297OnLogin, 12, 14OnLogTrace, 50OnNewRecord, 150

OnPaintPanel, 267-268OnPostError, 150OnReconcileError, 278OnStateChange, 205OnTitleClick, 246OnTrace, 50OnUpdateData, 205,

226, 297-298OnUpdateError, 297TDataSetProvider

component, 295-297TDataSource, 205TDBCtrlGrid

component, 267-268TDBGrid, 245-246TFieldDataLink, 226

Events application(MainForm.pas), codelisting, 14-17

exclamation point (!),210

ExecuteAction method,228

ExecuteDirect method,27-28

executing DDL commands, 27

Expression property,120

expressions (aggregates), 195

ExpressQuantumGrid,271

Ffeedback

MainForm.pas, codelisting, 51-54

Page 411: Delphi Kylix Database Development

feedback398

SQL operations, 46-47changing cursor, 47multiple feedback,

50-51, 54trace callback events,

47-49fees, redistributing

applications, 378FetchBlobs method,

310FetchDetails method,

310fetching

BLOBs, 310detail records, 310-311,

314-315FetchOnDemand

property, 310-311field data (servers),

changing, 297-298field definitions (client

datasets), creating, 96field names, retrieving,

18-19, 381-382field objects,

retrieving, 257Field property, 226field values,

formatting/editing,206

numeric fields, 207-209string fields, 209-210

FieldByName method,104

FieldCount property, 65FieldDefs property, 102FieldName property,

226, 242

fieldsAggregate, 96BLOBs

limitations, 168-169,172

resolving changes,290

calculated, 98internal, 98-99providing values,

99-100standard, 98

client datasetaccessing, 103-105adding, 102

ClientData, 48CustomIsolation, 39data, 96-97eTraceCat, 48FSupportsMultiTrans,

41GlobalID, 39IsolationLevel, 39-40lookup, 100-101metadata, retrieving,

382nonpersistent, 104-105persistent, 103pszTrace, 48retrieving contents from

datasets, 64-65separate, storing images

(BLOBs), 163TransactionID, 39uTotalMsgLen, 48

Fields object, access-ing, 64

Fields property, 104,120

file formats (clientdatasets), 107

File menu commands(New), 319

FileName property, 49files

BLOBs, storing, 168MyBase, 107

Filter property, 130FilterForm.pas

(RangeFilterapplication), code listing, 133

filters, 126-127, 130arithmetic operators,

128comparison operators,

127-128date/time functions, 129functions, 129logical operators, 128operators, 129string functions,

128-129FindField method, 104FindKey method,

138-139FindNearest method,

139First button, 224First method, 113Font property, 242-244formats, files (client

datasets), 107formatting field values,

206numeric fields, 207-209string fields, 209-210

Page 412: Delphi Kylix Database Development

indexes399

FormCreate method,111, 175

Free value, 321freeing bookmarks, 114FSupportsMultiTrans

field, 41functions

Date, 129date/time (filters), 129Day, 129filters, 129GetDate, 129Hour, 129Lower, 128Minute, 129Month, 129Second, 129string (filters), 128-129SubString, 128Time, 129Trim, 128TrimLeft, 129TrimRight, 129Upper, 128Year, 129

GgdFixed value, 253gdFocused value, 253gdSelected value, 253general-purpose data

access (TSQLDataSetcomponent), 62-63

GetDate function, 129GetDriverFunc

property, 8

GetFieldNamesproperty, 18-19

GetGroupStatemethod, 197

GetIndexNamesmethod, 122

GetIndexNamesproperty, 19

GetProcedureNamesproperty, 19

GetProcedureParamsproperty, 19-20, 25-27

GetTableNames property, 18

gfix command, 378GlobalID field, 39GotoKey method,

139-140GotoNearest method,

140greater than (>)

operator, 127greater than or equal

to (>=) operator, 127greater than sign (>),

210greater than/less than

sign (< >), 210grid options (TDBGrid),

244-245grid settings, persist-

ing, 262-263grids

data-aware. See individ-ual data-aware grids

sorting, 126, 264-265grouped aggregates,

creating, 196GroupingLevel prop-

erty, 120

H-Ihandlers (callback),

removing, 49Host property, 331HostName property,

335Hour function, 129HTTP connections, 334

icons, Reconcile ErrorDialog, 280

ImageLib CorporateSuite, 165

images (BLOBs), storing, 162-164

IN operator, 129index names,

retrieving, 19IndexDefs property,

119indexed search

methods, 138FindKey, 138-139FindNearest, 139GotoKey, 139-140GotoNearest, 140

indexesCHANGEINDEX, 123client datasets, 118-119

activating, 121creating, 119-121deleting, 121retrieving

information,122-123

switching between,121-122

data, retrieving, 382-383

Page 413: Delphi Kylix Database Development

indexes400

DEFAULT_ORDER,123

retrieving, 257IndexFieldNames

property, 73, 122IndexName property,

73inequality test (< >)

operator, 127InfoPower 2000, 271Insert button, 224Insert method, 105Instancing combo box,

320InterceptGUID

property, 331intercepting

data, 298-299data packets, 333

InterceptName property, 331

interfaces (callbacks)creating, 325-326limitations, 326

internal calculatedfields, 98-99

Internal value, 320InTransaction

property, 41IS NOT NULL operator,

128IS NULL operator, 128IsolationLevel field,

39-40IxCaseInsensitive

option, 120IxDescending option,

120

IxExpression option,120

IxNonMaintainedoption, 120

IxPrimary option, 120IxUnique option, 120

J-Kjoins, providing/

resolving data,302-304, 307

Joins application(MainForm.pas), codelisting, 304-307,312-313

KeepConnectionproperty, 8

KeepSettings property,187-188

KeyFields parameter,136

KeyValues parameter,136

LL specifier, 209Last button, 224Last method, 113less than (<) operator,

127less than or equal to

(<=) operator, 127less than sign (<), 210LibraryName

property, 8

licensing, 378LIKE operator, 129limitations

callbacks, 326fields (BLOBs),

168-169, 172TDBGrid component,

263limiting returned data,

309BLOBs, fetching manu-

ally, 310detail records, fetching

manually, 310-311,314-315

Linuxapplications,

redistributing, 377-378drivers, 377

list boxes, ConnectionName, 10

Load1E XEcutemethod, 112

LoadBalanced property,344

LoadFromFile method,168

LoadParamsOnConnectproperty, 9

local connections(client applications),creating, 329-330

local databases, connecting to, 308

loCaseInsensitveoption, 136

Locate method,136-137

Page 414: Delphi Kylix Database Development

methods401

Log Trace check box, 54LogChanges property,

177logging messages,

49-50logical operators

(filters), 128login, controlling,

12-13LoginPrompt

property, 9lookup data-aware

controls, 222-223lookup fields, 100-101Lookup method,

137-138loPartialKey option,

136Lower function, 128

MMainForm.pas code

listings, 352Advanced application,

76-79Basic application, 65-68BLOBs application,

169-171CDS (ClientDataset)

application, 108-111CDSIndex, 124-126ChangeLog application,

182-185Clone application,

188-191ConMan application,

362-371

ConManServer application, 356-357

CtrlGrid application,268-270

CustomDraw applica-tion, 254-256

DataAware application,232-236

DataFetch application,311-313

DDLSQL application,33-36

EventLog application,151-155

Events application,14-17

feedback, 51-54Joins application,

304-307, 312-313MetaData, 20-25MethodsClient

application, 338-339MethodsServer

application, 327-328navigating client

datasets, 116-118Nested application,

174-175options, 246-251RangeFilter application,

130-132Schema application,

81-87Search application,

141-143Trans, 42-46Updates 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, 195manipulating client

datasetsdeleting records, 106example, 108modifying records,

105-106manually populating

client datasets, 105master/detail

relationships, 74-76,301

Max aggregate type,195

MaxBlobSizeproperty, 61

MergeChangeLogproperty, 181

message handlers, 228messages

buffering, 50logging, 49-50

metadata (dbExpressPlus), 381-383

MetaData application(MainForm.pas), codelisting, 20-25

methodsAdd, 102AddFieldDef, 102AddIndex, 121

Page 415: Delphi Kylix Database Development

methods402

adding to remote datamodules, 324

Append, 105ApplyUpdates, 276-277AsString, 162btnRetrieveClick, 87Cancel, 177cbProcedureClick, 25CloneCursor, 187CMExit, 228ConfigureColumns, 265CreateBlobStream, 166DataModuleCreate, 361DataModuleDestroy,

361Delete, 106DeleteIndex, 121DisableControls, 159Edit, 105, 225EditKey, 140EmptyDataSet, 106EnableControls, 159Execute Direct, 27-28ExecuteAction, 228FetchBlobs, 310FetchDetails, 310FieldByName, 104FindField, 104FindKey, 138-139FindNearest, 139First, 113FormCreate, 111, 175GetGroupState, 197GetIndexNames, 122GotoKey, 139-140GotoNearest, 140Insert, 105Last, 113

Load1E XEcute, 112LoadFromFile, 168Locate, 136-137Lookup, 137-138Modified, 225MonitorTrace, 54Next, 113OnCalcFields, 99Populate1E XEcute,

111Prior, 113Refresh, 290RefreshRecord, 291Reset, 225Save1E XEcute, 112SaveToFile, 50, 106SaveToStream, 106StartTransaction, 39Statistics1E XEcute,

112TFieldDataLink, 225TIndexDefs, 123TSQLMetaData

component, 383UpdateAction, 228

MethodsClientapplication(MainForm.pas), codelisting, 338-339

MethodsServerapplication, code listings

MainForm.pas, 327-328ServerDataModule.pas,

336-339Microsoft ADO,

incompatibility withMyBase, 107

MIDAS, 318

Min aggregate type,195

Minute function, 129modes, update,

291-293Modified method, 225modifying

data-aware componentdata from code,205-206

records (client databases), 105-106

monitoring databaseevents, 49

buffering messages, 50logging messages,

49-50MonitorTrace

method, 54Month function, 129MTS remote data

modules, creating,321-322

multiple connectiontypes (client applications), 343-344

multiple feedback (SQLoperations), 50-51, 54

Multiple Instancevalue, 320

multiple transactions,40-42

Multiplication ([ts])operator, 128

Multitier tab, 319MyBase,

incompatibility withMicrosoft ADO, 107

MyBase file, 107

Page 416: Delphi Kylix Database Development

OnTrace event403

NName property, 120named connections,

9-11navigating

client datasets, 113random-access

navigation, 114-116sequential

navigation, 113datasets, 65, 68

Nested application(MainForm.pas), codelisting, 174-175

nested datasets,172-176

nested transactions, 40Neutral value, 321New command (File

menu), 319New Field dialog box,

97New Interface button,

325New Items dialog box,

280, 319New menu commands

(Other), 280, 319New Method toolbar

button, 325Next button, 224Next method, 113nonindexed search

methods, 136Locate method, 136-137Lookup method,

137-138

nonpersistentaggregates, creatingat design time,194-195

nonpersistent fields,104-105

Not operator, 128notes (BLOBs), storing,

162numeric fields,

formatting/editing,207-209

OObject-Insight Web

site, 272ObjectBroker property,

344ObjectName property,

335objects

field, retrieving, 257Fields, accessing, 64TColumn, creating, 242TFieldDef, 102

ObjectView property, 61

OnActiveChangeevent, 226

OnCalcFields event,150

OnCalcFieldsmethod, 99

OnCellClick event, 245OnColEnter event, 245OnColExit event, 246

OnColumnMovedevent, 246

OnDataChange event,205, 226

OnDeleteError event,150

OnDrawColumnCellevent, 246, 252

OnDrawDataCell event,246, 252

one-to-manyrelationships, 74

OnEditButtonClickevent, 246

OnEditError event, 150OnEditingChange

event, 226OnFilterRecord event,

130, 150OnGetData event,

296-298, 341OnGetDataSetPropertie

s event, 297OnGetTableName

event, 297OnLogin event, 12, 14OnLogTrace event, 50OnNewRecord event,

150OnPaintPanel event,

267-268OnPostError event, 150OnReconcileError

event, 278OnStateChange event,

205OnTitleClick event, 246OnTrace event, 50

Page 417: Delphi Kylix Database Development

OnUpdateData event404

OnUpdateData event,205, 226, 297-298

OnUpdateError event,297

opening datasets,63-64

operatorsAddition (+), 128And, 128arithmetic (filters), 128asterisk (*), 129BLANK, 128comparison (filters),

127-128Division ([db]), 128equality test (=), 127filters, 129greater than (>), 127greater than or equal to

(>=), 127IN, 129inequality test (< >),

127IS NOT NULL, 128IS NULL, 128less than (<), 127less than or equal to

(<=), 127LIKE, 129logical (filters), 128Multiplication ([ts]),

128Not, 128Or, 128Subtraction (–), 128

optional parameters,300-301

optionsdgAlwaysShowEditor,

244, 259dgAlwaysShowSelectio

n, 245dgCancelOnExit, 245dgColLines, 244dgColumnResize, 244dgConfirm, 245dgEditing, 244, 260dgIndicator, 244dgMultiSelect, 245dgRowLines, 244dgRowSelect, 245, 253dgTabs, 244dgTitles, 244grid (TDBGrid),

244-245IxCaseInsensitive, 120IxDescending, 120IxExpression, 120IxNonMaintained, 120IxPrimary, 120IxUnique, 120loCaseInsensitive, 136loPartialKey, 136MainForm.pas, code

listing, 246-251poAllowCommandText,

294poAllowMultiRecordUp

dates, 294poAutorRefresh, 294poCascadeUpdates, 294poDisableDeletes, 294poDisableEdits, 294poDisableInserts, 294poFetchBlobsOnDemand,

293, 310

poFetchDetailsOnDemand, 293, 310

poIncFieldProps, 293poNoReset, 294poPropogateChanges,

294poReadOnly, 294poRetainServerOrder,

294TDataSetProvider

component, 293-295Options parameter, 136Options property, 120,

293-294OptionsForm.pas

(EventLogapplication), code listing, 156-157

Or operator, 128ordering data, 73

queries, 74tables, 73

Orientation property,267

Orpheus, 271Other command (New

menu), 280, 319overlapped

transactions, 40

PPacketRecords

property, 309PanelBorder property,

267PanelHeight property,

267

Page 418: Delphi Kylix Database Development

properties405

PanelWidth property,267

ParamCheck property,61, 72-73

parameterized queries,71-73, 75

parameterized SQLstatements, 30-32

parametersdatabase, 11-12KeyFields, 136KeyValues, 136optional, 300-301Options, 136Properties, 300retrieving, 19-20, 25-27

Params property, 9-12, 61

Password property, 12,334-335

passwords, setting(namedconnections), 11

period (.), 207persistent aggregates,

creating at designtime, 193-194

persistent fields, 103persisting grid settings,

262-263pfHidden value, 292pfInKey value, 292pfInUpdate value, 292pfInWhere value, 292PickList property, 243poAllowCommandText

option, 294poAllowMultiRecordUp

dates option, 294

poAutoRefresh option,294

poCascadeUpdatesoption, 294

poDisableDeletesoption, 294

poDisableEdits option,294

poDisableInsertsoption, 294

poFetchBlobsOnDemand option, 293, 310

poFetchDetailsOnDemand option, 293, 310

poIncFieldProps option,293

poNoReset option, 294poPropogateChanges

option, 294Populate1E XEcute

method, 111populating

client datasets, 105-108remote data modules,

324poReadOnly option,

294poRetainServerOrder

option, 294Port property, 331Post button, 224pound sign (#),

207-209Prior button, 224Prior method, 113procedures

ContactsByState, 350stored,

providing/resolvingdata, 302. See alsodatasets

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

253-254DescFields, 120DisableStringTrim,

198-199DisplayFormat, 207-209

Page 419: Delphi Kylix Database Development

properties406

settings, 208specifiers, 207

DropDownRows, 243EditMask, 209

settings, 210-211specifiers, 209-210

EditMode, 259Enabled, 204Expression, 120FetchOnDemand,

310-311Field, 226FieldCount, 65FieldDefs, 102FieldName, 226, 242Fields, 104, 120FileName, 49Filter, 130Font, 242, 244GetFieldNames, 18-19GetIndexNames, 19GetProcedureNames, 19GetProcedureParams,

19-20, 25-27GetTableNames, 18GroupingLevel, 120Host, 331HostName, 335IndexDefs, 119IndexFieldNames, 73,

122IndexName, 73InterceptGUID, 331InterceptName, 331InTransaction, 41KeepSettings, 187-188LoadBalanced, 344LogChanges, 177

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

SupportCallbacks, 331TableScope, 18TColumn, 242-243TCORBAConnection

component, 335TDataSource, 204TDBCtrlGrid

component, 267TDCOMConnection,

333-334TFieldDataLink,

225-226Title, 242-244TitleSort, 264TraceList, 50TransactionSupported,

38TSOAPConnection, 335TSocketConnection

component, 331TSQLConnection, 8-9

Connected, 8ConnectionName, 8DriverName, 8GetDriverFunc, 8KeepConnection, 8LibraryName, 8LoadParamsOn-

Connect, 9LoginPrompt, 9Params, 9, 11-12TableScope, 9VendorLib, 9

TSQLDataSetcomponent, 60-61

TWebConnection component, 334

UndoLastChange, 178

Page 420: Delphi Kylix Database Development

remote data modules407

UpdateMode, 291URL, 334-335UserName, 12, 334-335Values, 214Visible, 242Width, 242

Properties parameter,300

ProviderFlags property,292-293

providers, accessing ondifferent form, 308

providingdata

joins, 302-304, 307stored procedures,

302values (calculated

fields), 99-100Proxy property,

334-335pszTrace field, 48pvContacts component,

353

Q-Rqueries. See also

datasetsordering data, 74parameterized, 71-75

query-level access(TSQLDataSetcomponent), 61-62

raAbort value, 279raCancel value, 279raCorrect value, 279

Raize Software Website, 47

raMerge value, 279random-access

navigation (clientdatasets), 114-116

bookmarks, 114-115record numbers, 115

Randomizer, 111RangeFilter

application, code listings

FilterForm.pas, 133MainForm.pas, 130-132RangeForm.pas,

134-135RangeForm.pas

(RangeFilterapplication), code listing, 134-135

ranges, 126-127raRefresh value, 279raSkip value, 279ReadOnly property,

199, 242RecErrorForm.pas, 352RecNo property,

115-116Reconcile Error Dialog

icon, 280reconciliation errors,

278-280, 289-290record numbers, 115records

deleting (client data-bases), 106

determining position inaggregate, 197

modifying (client data-bases), 105-106

TransactionDesc, 39redistributing

applications, 376CD-ROM-based, 378fees, 378Linux, 377-378Windows, 376-377

Refresh button, 224,290

Refresh method, 290refreshing data

(servers), 290-291RefreshRecord method,

291relationships

master/detail, 74-76,301

one-to-many, 74remote connections

(client applications),creating, 330

remote data modulecontext menu commands (Add toInterface), 324

remote data modules,318-319

creating, 319CORBA remote data

modules, 322MTS remote data

modules, 321-322SOAP remote data

modules, 322-323standard remote data

modules, 320-321

Page 421: Delphi Kylix Database Development

remote data modules408

methods, adding, 324populating with

components, 324RemoteDataModule.pa

s (ConManServerapplication), code listing, 352-357,359-361, 363-371, 373

removingBLOB column, 70callback handlers, 49

RepositoryIDproperties, 335

Reset method, 225Reset property, 187-188resized columns,

detecting, 260-261ResolveToDataSet

property, 278resolving, 274

changes to BLOB fields,290

datajoins, 302-304, 307stored procedures,

302datasets, 278

responding to datasetchanges, 227

result sets, returning,33

retrievingdatabase metadata, 18field contents (datasets),

64-65field metadata, 382field names, 18-19field objects, 257

index data, 382-383index information

(client datasets), 122GetIndexNames

method, 122TIndexDefs method,

123index names, 19indexes, 257parameters, 19-20,

25-27schema information,

79-81, 87-88stored procedures, 19table/view/field names,

381tables, 18

returned data, limiting,309

BLOBs, fetching manually, 310

detail records, fetchingmanually, 310-311,314-315

returningbookmarks, 114result sets, 33

RevertRecord property,178-179

rolling back transac-tions, 40

row (grids), determin-ing

current, 257mouse coordinate,

257-259RowCount property,

267

runtimeclient datasets, creating,

101-103creating indexes, 121creating maintained

aggregates, 195deleting indexes, 121

SSave1E XEcute method,

112SavePoint property,

179-180SaveToFile method, 50,

106SaveToStream method,

106saving changes to

databases, 276-277Schema application

(MainForm.pas), codelisting, 81-87

schema columnsstColumns value, 89stIndexes value, 90-91stProcedureParams

value, 89-90stProcedures value, 88stSystemTables value,

88stTables value, 88

schema information,retrieving, 79-81,87-88

scientific notation(E+/–), 207

ScktSrvr, 331-332

Page 422: Delphi Kylix Database Development

Source property409

scripting (dbExpressPlus), 380-381

Search application,code listings

MainForm.pas, 141-143SearchForm.pas, 144

search methods, 136indexed, 138

FindKey, 138-139FindNearest, 139GotoKey, 139-140GotoNearest, 140

nonindexed, 136Locate method,

136-137Lookup method,

137-138SearchForm.pas (Search

application), code listing, 144

Second function, 129SELECT statement,

32-33SelectedColor property,

267SelectedField property,

257SelectedIndex property,

257separate fields, storing

images (BLOBs), 163separator character (;),

207, 210sequential navigation

(client datasets), 113server applications

callbackscreating, 325-326limitations, 326

user interface, creating,326-328

ServerDataModule.pas(MethodsServer appli-cation), code listing,336-339

ServerGUID property,331-334

ServerName property,331, 334

serversapplication

creating, 318testing, 328-329

data, refreshing,290-291

field data, changing,297-298

socket, 331-332stateless, 341-343

settingbookmarks, 114database parameters,

11-12edit mode, 259-260passwords (named

connections), 11setting up

connection to datasource, 227

TFieldDataLinkcomponent, 226-227

settingsDisplayFormat property,

208EditMask property,

210-211TableScope property, 18

ShowFocus property,267

simple data-awarecomponents, 211

TDBCheckBox, 212TDBComboBox, 213,

217-218TDBEdit, 212TDBImage, 221TDBListBox, 218-221TDBLookupComboBox,

223TDBLookupListBox,

223TDBMemo, 212TDBRadioGroup, 213TDBText, 211

simple SQL statements,30

Single Instance value,320

single quotation mark(‘), 207

Single value, 321Skyline Tools Imaging

(ImageLib CorporateSuite), 165

Skyline Tools ImagingWeb site, 165

SOAP connections, 335SOAP remote data

modules, creating,322-323

socket connections, 330socket servers, 331-332SortFieldNames

property, 61sorting grids, 126,

264-265Source property, 120

Page 423: Delphi Kylix Database Development

specifiers410

specifiers0, 2099, 209A, 209c, 209DisplayFormat property,

207EditMask property,

209-210l, 209

SQL operations (feedback), 46-47

changing cursor, 47multiple feedback,

50-51, 54trace callback events,

47-49SQL statements

parameterized, 30-32result sets, returning, 33simple, 30

SQLConnectionproperty, 61

sqlContactscomponent, 352

SQLHourGlassproperty, 47

sqlID component, 352sqlTodos component,

352standard calculated

fields, 98standard remote data

modules, creating,320-321

starting transactions,39-40

StartTransactionmethod, 39

stateless servers,341-343

statementsDDL (Data Definition

Language), 27DML, 27SELECT, 32-33SQL

parameterized, 30-32result sets, returning,

33simple, 30

static linking, 377statically-linked driver

units, 377Statistics1E XEcute

method, 112StatusFilter property,

181-182stColumns value, 80,

89stIndexes value, 80,

90-91stNoSchema value, 80stored procedure

access (TSQLDataSetcomponent), 62

stored proceduresproviding/resolving

data, 302retrieving, 19

stored procedures, 60.See also datasets

storingfiles, 168images, 162-164notes, 162streamed data, 165-167

stProcedureParamsvalue, 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, 80stSystemTables value

(schema columns), 88stTables value, 80, 88SubString function, 128Subtraction (–)

operator, 128Sum aggregate type,

195support

BLOB (Binary LargeObject), 69-70

transactions, 37-38undo, 176-177

SupportCallbacks prop-erty, 331

switching betweenindexes (clientdatasets), 121-122

Ttable names,

retrieving, 381-382table-level access

(TSQLDataSetcomponent), 61

Page 424: Delphi Kylix Database Development

third-party data-aware grids411

tables, 59. See alsodatasets

Contacts, 349creating (DDL

commands), 27-28ordering data, 73retrieving, 18Todos, 350

TableScope property,9, 18

tabsDialogs, 280Multitier, 319WebServices, 323

TClientDataSetcomponent, 94. See also clientdatasets

TClientDataSetGridcomponent, 261-264

automatic sorting,264-265

columns, customizing,265

TColumn objects, creating, 242

TColumn properties,242-243

TConnectionBrokercomponent, 343

TCORBAConnectioncomponent, 335

TCustomClientDataSetclass, 94

TDataSetProvider component, 274,293-297

TDataSource component, 204-205

TDataSource events,205

TDataSource properties, 204

TDBCheckBoxcomponent, 212

TDBComboBoxcomponent, 213,217-218

TDBCtrlGridcomponent, 266

events, 267-268properties, 267

TDBEdit component,212

TDBGrid component,240-241

columns, customizing,241-244

custom drawing,252-254

edit mode, setting,259-260

events, 245-246grid options, 244-245grid settings, persisting,

262-263limitations, 263resized columns,

detecting, 260-261row/column/cell,

determining, 257-259TDBGrid options,

244-245TDBImage component,

221TDBListBox

component, 218-219,221

TDBLookupComboBoxcomponent, 223

TDBLookupListBoxcomponent, 223

TDBMemo component,212

TDBNavigator buttons,224

TDBNavigatorcomponent, 223-225,290

TDBRadioGroup component, 213

TDBText component,211

TDCOMConnectioncomponent, 333-334

testing applicationservers, 328-329

TFieldDataLink class,225-226

TFieldDataLinkcomponent, 226-227

TFieldDataLink events,226

TFieldDataLinkmethods, 225

TFieldDataLinkproperties, 225-226

TFieldDef object, 102TGridDrawState values,

253third-party data-aware

grids, 271-272ExpressQuantumGrid,

271InfoPower 2000, 271Orpheus, 271TopGrid, 272

Page 425: Delphi Kylix Database Development

third-party imaging libraries412

third-party imaginglibraries, storingimages (BLOBs), 164

thousands separator(,), 207

Threading Modelcombo box, 321

Time function, 129time separator (:), 210TIndexDefs method,

123Title property, 242-244TitleSort property, 264TLocalConnection

component, 329-330TodoForm.pas, code

listings, 352, 372-373Todos table, 350toolbar buttons, New

Method, 325TopGrid, 272trace callback events

(SQL operations feedback), 47-49

traceBLOB value, 48traceDATAIN value, 48traceDATAOUT

value, 48traceERROR value, 48TraceList property, 50traceMISC value, 48traceQEXECUTE

value, 48traceQPREPARE

value, 48traceSTMT value, 48traceTRANSACT

value, 48

traceVENDOR value, 48Trans application

(MainForm.pas), codelisting, 42-46

TransactionDesc record, 39

TransactionID field, 39transactions, 37-38

ACID (Atomic,Consistent, Isolated,Durable) properties,37

committing, 40multiple, 40-42nested, 40overlapped, 40rolling back, 40starting, 39-40support, 37-38

TransactionSupportedproperty, 38

TReconciliationActionvalue, 279

Trim function, 128TrimLeft function, 129TrimRight function, 129TSchemaType

values, 80TSimpleObjectBroker

component, 344TSOAPConnection

component, 335TSocketConnection

component, 330-331TSQLClientDataSet

component, 309

TSQLConnectioncomponent

events, 14, 17properties, 8-9

Connected, 8ConnectionName, 8DriverName, 8GetDriverFunc, 8KeepConnection, 8LibraryName, 8LoadParamsOnConn

ect, 9LoginPrompt, 9Params, 9, 11-12TableScope, 9VendorLib, 9

TSQLDataPumpcomponent, 383

TSQLDataSetcomponent, 60

general-purpose dataaccess, 62-63

properties, 60-61query-level access,

61-62stored procedure

access, 62table-level access, 61

TSQLMetaDatacomponent, 381

methods, 383retrieving

field metadata, 382index data, 382-383table/view/field

names, 381-382TSQLMonitor

component, 49, 54

Page 426: Delphi Kylix Database Development

values413

TSQLQuerycomponent, 60

TSQLScript component,380-381

TSQLStoredProc component, 60

TSQLTable component, 60

TUpdateKind values,279

TUpdateMode values,291-292

TUpdateStatus value,181

TurboPower SoftwareCompany Web site,211

TurboPower SoftwareWeb site, 299

TurboPower Web site,271

TWebConnection component, 334

UukDelete value, 279ukInsert value, 279ukModify value, 279underscore (_), 210undo support, 176-177

Cancel method, 177change log, 177

CancelUpdatesproperty, 181

ChangeCount property, 181

LogChangesproperty, 177

MergeChangeLogproperty, 181

RevertRecord property, 178-179

SavePoint property,179-180

StatusFilter property,181-182

UndoLastChangeproperty, 178

viewing, 182, 186UndoLastChange

property, 178unnamed

connections, 11update modes, 291-293UpdateAction method,

228UpdateMode property,

291Updates application

(MainForm.pas), codelisting, 159-161,281-289

updating datasets,227-228

Upper function, 128upWhereAll value, 291upWhereChanged

value, 291upWhereKeyOnly

value, 291URL property, 334-335usDeleted value, 181Use Callback check

box, 54user interfaces (server

applications),creating, 326-328

UserName property, 12,334-335

usInserted value, 181usModified values, 181usUnmodified value,

181uTotalMsgLen field, 48

Vvalues

Apartment, 321Both, 321DataMoveMode, 384dmAlwaysInsert, 384DmAppend, 384dmAppendUpdate, 384DmDelete, 384DmUpdate, 384Free, 321gdFixed, 253gdFocused, 253gdSelected, 253Internal, 320Multiple Instance, 320Neutral, 321pfHidden, 292pfInKey, 292pfInUpdate, 292pfInWhere, 292providing (calculated

fields), 99-100raAbort, 279raCancel, 279raCorrect, 279raMerge, 279raRefresh, 279raSkip, 279

Page 427: Delphi Kylix Database Development

values414

Single, 321Single Instance, 320stColumns, 80, 89stIndexes, 80, 90-91stNoSchema, 80stProcedureParams, 80,

89-90stProcedures, 80, 88stSysTables, 80stSystemTables, 88stTables, 80, 88TGridDrawState, 253traceBLOB, 48traceDATAIN, 48traceDATAOUT, 48traceERROR, 48traceMISC, 48traceQEXECUTE, 48traceQPREPARE, 48traceSTMT, 48traceTRANSACT, 48traceVENDOR, 48TReconciliationAction,

279TSchemaType, 80TUpdateKind, 279TUpdateMode, 291-292TUpdateStatus, 181ukDelete, 279ukInsert, 279ukModify, 279upWhereAll, 291upWhereChanged, 291upWhereKeyOnly, 291usDeleted, 181usInserted, 181usModified, 181usUnmodified, 181xilCUSTOM, 40

xilDIRTYREAD, 39xilREADCOMMITTED,

39xilREPEATABLEREAD,

40Values property, 214VCL-only data-aware

controls, 222VendorLib property, 9view names,

retrieving, 381-382viewing change log,

182, 186Visible property, 242

WWeb sites

Borland, 378Code Central, 264, 380Dan Miser, 333Developer Express, 272Object-Insight, 272Raize Software, 47Skyline Tools Imaging,

165TurboPower Software,

211, 271, 299Woll2Woll, 271

WebServices tab, 323Width property, 242Windows applications,

redistributing,376-377

Windows drivers, 376Woll2Woll Web site,

271writing event

handlers, 50

X-ZxilCUSTOM value, 40xilDIRTYREAD value, 39xilREADCOMMITTED

value, 39xilREPEATABLEREAD

value, 40

Year function, 129

zero (0), 207