Using TTable and TDataSet  - Part 02

 

 

  tblCustomer->SetRange(OPENARRAY(TVarRec, (AStart)), OPENARRAY(TVarRec, (AFinish)));

 

}

 

This TTable SetRange method takes two OpenArray templates, the first to cover the starting range, the second to cover the ending range. The idea here is that the table might be indexed on multiple fields, and you can use an OpenArray to specify the values associated with more than one field, if needed.

Filtering with the OnFilterRecord Event

The OnFilterRecord event enables you to set up filters on fields even if they are not keyed. You can use this event in two different ways.

The first technique involves setting the TTable Filtered property to True. When you do this, you will see only the records that are designated by the formula defined in the OnFilterRecord event. For instance, if you had a State field in your dataset and the OnFilterRecord event said to accept only records from New Hampshire , you would see only the records from New Hampshire when Filtered was set to True.

The second technique enables you to search for records even when Filtered is set to False. For instance, if you set up an OnFilterRecord event that accepted only records from New Hampshire , you could call Table->FindFirst to find the first of these records, and Table->FindNext to find the next one, and so on. There are also FindPrior and FindLast properties that you can use with the OnFilterRecord event. The key point to remember is that as long as the OnFilterRecord event is implemented correctly, you can use FindFirst, and so on, even when Filtered is not set to True.

An example of the OnFilterRecord event is shown in the Filter program found on this book's CD-ROM. The rest of this section describes how to create that program from scratch.

To see the OnFilterRecord event in a live program, start by dragging the Country table off the Database Explorer onto a blank form or data module from a new project. (It sometimes helps to close the Database Explorer after the drop operation, rather than trying to switch between the two tools by changing their focus.) Drop down a panel and set up the Align property for the panel and for the TDBGrid as explained in the previous examples from this chapter.

Place a TCheckBox object on the panel and set its caption to Filtered. Associate the following method with the OnClick event for the checkbox:

void __fastcall TForm1::FFilterClick(TObject *Sender)

 

{

 

  FCountry->Filtered = FFilter->Checked;

 

}

 

This code ensures that the table will be filtered whenever the checkbox is checked.

Use the Fields Editor for the Table1 object to create field objects for all the fields in the database. Drag the Continent field off the Fields Editor onto the form, as shown in Figure 9.8.

 

FIGURE 9.8. The main form for the Filter program includes a grid, a panel, a checkbox, a TDBEdit control, and a button.

Turn to the Events page for the TTable object, and associate the following code with the OnFilterRecord event:

void __fastcall TForm1::FCountryFilterRecord(TDataSet *DataSet, Boolean &Accept)

 

{

 

  Accept = tblCountry->FieldByName("Continent")->AsString == " South America ";

 

}

 

This code states that the OnFilterRecord event will accept all records where the Continent field of the Country table contains the word " South America ". The Continent field will have either the value North America or South America in it. If you click the checkbox to turn the filter on, you will see only the records from South America . In short, the filter will automatically accept only those records whose Continent field matches the value " South America ".

If you wanted, you could change the code so that it always filtered on the value currently in the Continent field:

void __fastcall TForm1::FCountryFilterRecord(TDataSet *DataSet, Boolean &Accept)

 

{

 

  Accept = tblCountry->FieldByName("Continent")->AsString == DBEdit1->Text;

 

}

 

For this code to work, you must drop down a DBEdit control, and hook it up to the Continent field of the Country table.

It's important to note that the Accept field of the OnFilterRecord event is a Boolean value. This means that you can set up any kind of a Boolean statement in order to set the value of this field. For instance, in addition to the = operator, you could also use the following operators: <>, >, or <`.

The FindNext, FindFirst, and FindPrior functions are extremely easy to use. For instance, if you wanted to find the next record in the database that satisfied the requirements specified in the OnFilterRecord event, you could write the following code to be fired in response to clicking a button:

void __fastcall TForm1::FFindNextClick(TObject *Sender)

 

{

 

  tblCountry->FindNext();

 

}

 

The other functions work exactly the same way. This is a Boolean function that will return False if the search fails. Remember that these methods work even if the Filtered property of the TTable object is set to False.

Using the Refresh Function

As you already know, any table that you open is always subject to change. In short, you should regard a table as a fluid, rather than as a static, entity. Even if you are the only person using a particular table and even if you are not working in a networked environment, there is always the possibility that the program you are running may have two different ways of changing a piece of data. As a result, you should always be aware of the need to update, or refresh, your current view of a table. Furthermore, BCB will not always update data after you perform an action in the background. As a result, you may need to update data by performing a refresh, particularly after certain kinds of delete operations.

The Refresh function is related to the Open function because it retrieves the data, or some portion of the data, associated with a given table. For instance, when you open a table, BCB retrieves data directly from a database file. Similarly, when you refresh a table, BCB goes out and retrieves data directly from a table. You can therefore use this function to update a table if you think it might have changed. It is faster, and much more efficient, to call Refresh than to call Close and then Open.

 

 


NOTE: In a networked environment, refreshing a table can sometimes lead to unexpected results. For instance, if a user is viewing a record that has been deleted, it will seem to disappear out from under the user the moment the program calls Refresh. Similarly, if another user has edited data, a call to Refresh can result in data dynamically changing while a user is viewing it. Of course, it is unlikely that one user will change or delete a record while another is viewing it, but it is possible. As a result, you should use calls to Refresh with caution.


Bookmarks

It is often useful to mark a particular location in a table so that you can quickly return to it when desired. BCB provides this functionality through three methods of TDataSet that use the metaphor of a bookmark. When you use these functions, it is as if you have left a bookmark in the dataset, and you can therefore turn back to it quickly whenever you want:

void __fastcall FreeBookmark(System::Pointer Bookmark);

 

System::Pointer __fastcall GetBookmark(void);

 

void __fastcall GotoBookmark(System::Pointer Bookmark);

 

As you can see, the GetBookmark call returns a variable of type pointer, which is in fact just a pointer to a Bookmark. A Bookmark pointer contains enough information to enable BCB to find the location to which it refers. Therefore, you can simply pass this bookmark to the GotoBookmark function, and you will immediately be returned to the location with which the bookmark is associated.

It's important to note that a call to GetBookmark allocates memory for the bookmark, and so you must remember to call FreeBookmark before you exit your program and before every attempt to reuse a bookmark. For instance, here is a typical set of calls for freeing a bookmark, setting a bookmark, and moving to a bookmark:

void __fastcall TForm1::bMarkClick(TObject *Sender)

 

{

 

  if (Bookmark == NULL)

 

    Bookmark = DataMod->tblCountry->GetBookmark();

 

}

 

 

 

void __fastcall TForm1::bReturnClick(TObject *Sender)

 

{

 

  if (Bookmark != NULL)

 

  {

 

    DataMod->tblCountry->GotoBookmark(Bookmark);

 

    DataMod->tblCountry->FreeBookmark(Bookmark);

 

    Bookmark = NULL;

 

  }

 

}

 

 

 

void __fastcall TForm1::FormDestroy(TObject *Sender)

 

{

 

  DataMod->tblCountry->FreeBookmark(Bookmark);

 

}

 

The code shown here is excerpted from a program called Bookmark, which comes with the CD-ROM that accompanies this book. In the declaration for TForm1, a variable called Bookmark is declared in the private section. Every time the MarkClick function is called, the first step is to be sure the Bookmark is freed. It is never a mistake to call FreeBookmark, because the function checks to make sure Bookmark is not set to NULL. After de-allocating any existing copies of the Bookmark, a new one is allocated. You can then call GotoBookmark and repeat the cycle.

Bookmarks are powerful features that can be of great benefit under certain circumstances. The developers of BCB, for instance, used bookmarks frequently in order to develop the database components. They often have several different bookmarks open at the same time.

 

 


NOTE: Most of the features surfaced in TDataSet are built into the BDE. For instance, filters, searching for keys, and bookmarks are available to anyone who uses the BDE. What the developers of BCB have done is surface these features so that they can be easily accessed using object-oriented programming techniques. The calls from the BDE are available to people who purchase the Borland Database Engine from Borland or to patient BCB spelunkers who spend some time with BDE.hpp.


More on TDataSet and TTable

TTable adds several frequently used properties to TDataSet:

__property System::Boolean ReadOnly;

 

// From TTable

 

__property System::Boolean Exclusive;

 

__property System::AnsiString MasterFields;

 

__property DB::TDataSource *MasterSource;

 

__property System::AnsiString TableName;

 

Of the properties shown here, the most common ones are probably TableName and ReadOnly. You can use the TableName property to specify the table you want to open, and you can set the ReadOnly property to True or False depending on whether you want to allow the user to change the data in a dataset. Neither of these properties can be used when a table is active.

The Exclusive property lets you open up a table in a mode that guarantees that no other user will be able to access it at the same time. You will not be able to set Exclusive to True if another user is currently accessing the table.

The MasterSource property is used to specify a TDataSource from which the current table needs to obtain information. For instance, if you linked two tables in a master/detail relationship, the detail table can track the events occurring in the first table by specifying the first table's data source in this property. This technique is demonstrated in the following section on linked cursors.

Creating Linked Cursors

Linked cursors enable programmers to easily define a one-to-many relationship. For instance, it is sometimes useful to link the CUSTOMER and ORDERS tables so that each time the user views a particular customer's name, he or she can also see a list of the orders related to that customer. In short, the user can view one customer's record, and then see only the orders related to that customer.

To understand linked cursors, you first need to see that the CUSTOMER table and the ORDERS table are related to one another through the CustNo field. This relationship exists specifically because there needs to be a way to find out which orders are associated with which customer.

The Links program on the book's CD-ROM demonstrates how to create a program that uses linked cursors. To create the program on your own, place two tables, two data sources, and two grids on a form. Wire the first set of controls to the CUSTOMER table and the second set to the ORDERS table. If you run the program at this stage, you should be able to scroll through all the records in either table, as shown in Figure 9.9.

 

FIGURE 9.9. The Links program shows how to define a relationship between two tables.

The next step is to link the ORDERS table to the CUSTOMER table so that you view only those orders associated with the current customer record. To do this, you must take three steps, each of which requires some explanation:

1. Set the MasterSource property of Table2 to DataSource1.

2.
Set the
MasterField property in Table2 to CustNo.

3. Set the
IndexName property of Table2 to ByCustNo.

If you now run the program, you will see that both tables are linked together, and that every time you move to a new record in the CUSTOMER table, you can see only those records in the ORDERS table that belong to that particular customer.

The MasterSource property in Table2 specifies the DataSource from which Table2 can draw information. Specifically, it allows the ORDERS table to know which record currently has the focus in the CUSTOMER table.

The question then becomes this: what other information does Table2 need in order to properly filter the contents of the ORDERS table? The answer to this question is twofold:

1. It needs the name of the field that links the two tables.

2.
It needs the index of the field in the
ORDERS table that is going to be linked to the CUSTOMER table.

In order to correctly supply the information described here, you must first ensure that both the CUSTOMER table and the ORDERS table have the correct indexes. Specifically, you must ensure that there are indexes on both the CustNo field and the CustNo field in the ORDERS table. If the index in question is a primary index, there is no need to specifically name that index, and therefore you can leave the IndexName field blank in both tables. However, if either of the tables is linked to the other through a secondary index, you must explicitly designate that index in the IndexName field of the table that has a secondary index.

In the example shown here, the CUSTOMER table has a primary index on the CustNo field, so there is no need to specify the index name. However, the ORDERS table does not have a primary index on the CustNo field, and so you must explicitly declare it in the IndexName property by typing in or selecting the word CustNo.

 

 


NOTE: To simplify the process described previously, the developers put in a dialog that appears when you click the MasterFields property. This dialog simplifies the process and helps to automate the task of setting up a link between two tables.

In particular, to use the MasterFields dialog, start a new project and drag the
Customer and Orders tables off the Explorer onto the main form. Arrange the grids with the Customer grid on top and the Orders grid beneath it. Set the DataSource property of the Orders TTable object to the TDataSource object associated with the Customer table.

Pop up the MasterFields dialog and make sure the Available Indexes is set to Primary Index. Click the
CustNo field in both Detail Fields and MasterFields list boxes. Click Add. The two fields will appear in the Joined Fields list box. At this stage, you are all done, so you can click the OK button.


Some indexes can contain multiple fields, so you must explicitly state the name of the field you want to use to link the two tables. In this case, you should enter the name CustNo in the MasterFields property of Table2. If you wanted to link two tables on more than one field, you should list all the fields, placing a pipe symbol between each one:

Table->MasterFields := "CustNo | SaleData | ShipDate";

 

In this particular case, however, I'm guilty of a rather shady bit of expediency. In particular, the statement shown here makes no sense, because the SaleData and ShipDate fields are neither indexed nor duplicated in the CUSTOMER table. Therefore, you should only enter the field called CustNo in the MasterFields property. You can specify this syntax directly in a property editor, or else write code that performs the same chore.

It's important to note that this section covered only one of several ways you can create linked cursors using BCB. Chapter 10, "SQL and the TQuery Object," describes a second method that will appeal to people who are familiar with SQL. The Database Expert provides a third means of achieving this end. As you have seen, the Database Expert is an easy-to-use visual tool. The Query Builder is yet a fourth way of creating a one-to-many relationship between two tables. Like the Database Expert, the Query Builder is a visual tool that can save you much time. However, it's best not to rely entirely on visual tools, because there are times when you might feel limited by their functionality. In such cases, you will be glad if you understand the underlying technology. That way you can get the job done yourself without being forced to wrestle with the limitations of a particular tool.

TDataSource Basics

Class TDataSource is used as a conduit between TTable or TQuery and the data-aware controls such as TDBGrid, TDBEdit, and TDBComboBox. Under most circumstances, the only thing you will do with a TDataSource object is to set its DataSet property to an appropriate TTable or TQuery object. Then, on the other end, you will also want to set a data-aware control's DataSource property to the TDataSource object you are currently using.

 

 


NOTE: Visual tools such as TDBEdit or TDBGrid all have a DataSource property that connects to a TDataSource object. When reading this chapter, you need to distinguish between a visual control's DataSource property and the TDataSource object to which it is attached. In other words, the word DataSource can be a bit confusing. I try to refer to a TDataSource object as a data source, and to refer to the DataSource property as a DataSource. You should, however watch the context in which these words are used, and be aware of the rather subtle distinctions in meaning surrounding this issue.


A TDataSource also has an Enabled property, and this can be useful whenever you want to temporarily disconnect a table or query from its visual controls. This functionality might be desirable if you need to programmatically iterate through all the records in a table. For instance, if a TTable is connected to a data-aware control, each time you call TTable.Next, the visual control needs to be updated. If you are quickly going through two or three thousand records, it can take considerable time to perform the updates to the visual controls. In cases like this, the best thing to do is set the TDataSource object's Enabled field to False, which will enable you to iterate through the records without having to worry about screen updates. This single change can improve the speed of some routines by several thousand percent.

The AutoEdit property of TDataSource enables you to decide whether or not the data-aware controls attached to it will automatically enter edit mode when you start typing inside them. Many users prefer to keep AutoEdit set to True, but if you want to give a user more precise control over when the database can be edited, this is the property you need. In short, if you set AutoEdit to False, you have essentially made the table read-only.

Using TDataSource to Check the State of a Database

The events belonging to TDataSource can be extremely useful. To help illustrate them, you will find a program on the CD-ROM that comes with this book called Dsevents that responds to all three TDataSource events. This program shows an easy way to set up a "poor man's" data-aware edit control that automatically shows and posts data to and from a database at the appropriate time.

This example works with the COUNTRY database and it has a TTable, TDataSource, five edits, six labels, eight buttons, and a panel on it. The actual layout for the program is shown in Figure 9.10. Note that the sixth label appears on the panel located at the bottom of the main form.

 

FIGURE 9.10. The Dsevents program shows how to track the current state of a table.

TDataSource has three key events associated with it:

OnDataChange

 

OnStateChange

 

OnUpdateData

 

A TDataSource OnStateChange event occurs whenever the current state of the dataset changes. A dataset always knows what state it's in. If you call Edit, Append, or Insert, the table knows that it is now in edit mode, and that fact is reflected in the value held in the TDataSource State property. Similarly, after you Post a record, the database knows that it is no longer editing data, and it switches back into browse mode. If you want more control, the next section in this chapter explains that a dataset also sends out messages just before and just after you change states.

The dataset has nine possible states, each of which are captured in the following enumerated type:

enum TDataSetState { dsInactive, dsBrowse, dsEdit, dsInsert,

 

                     dsSetKey, dsCalcFields, dsUpdateNew,

 

                     dsUpdateOld, dsFilter };

 

During the course of a normal session, the database will frequently move back and forth between browse, edit, insert, or the other modes. If you want to track these changes, you can respond to them by writing code that looks like this:

void __fastcall TForm1::dsCountryStateChange(TObject *Sender)

 

{

 

  AnsiString S;

 

 

 

  switch (tblCountry->State)

 

  {

 

    case dsInactive:

 

      S = "Inactive";

 

      break;

 

    case dsBrowse:

 

      S = "Browse";

 

      break;

 

    case dsEdit:

 

      S = "Edit";

 

      break;

 

    case dsInsert:

 

      S = "Insert";

 

      break;

 

    case dsSetKey:

 

      S = "SetKey";

 

      break;

 

  }

 

  StatusBar1->Panels->Items[0]->Text = S;

 

}

 

In this code, I am using a TStatusBar object to report the state of the dataset to the user. To use a TStatusBar object, first drop one on a form and then double-click on the editor for the TStatusBar Panels property to bring up the Status Bar Panel Editor. Create a new panel in the Status Bar Panel Editor and set its text to Browse, or to some other string. You now have one panel the text of which you can change with the following line of code:

StatusBar1->Panels->Items[0]->Text = S;

 

It's time now to stop talking about the OnStateChange event and to move on to the OnDataChange event. OnDataChange occurs whenever you move on to a new record. In other words, if you call Next, Previous, Insert, or any other call that is likely to lead to a change in the data associated with the current record, an OnDataChange event will get fired. If someone begins editing the data in a data-aware control, an OnResync event occurs.

Dsevents has one small conceit that you need to understand if you want to learn how the program works. Because there are five separate edit controls on the main form, you need to have some way to refer to them quickly and easily. One simple method is to declare an array of edit controls:

TEdit *Edits[5];

 

To fill out the array, you can respond to the forms OnCreate event:

void __fastcall TForm1::FormCreate(TObject *Sender)

 

{

 

  int i;

 

  for (i = 0; i <= 4; i++)

 

    Edits[i] = (TEdit*)FindComponent("Edit" + IntToStr(i + 1));

 

  tblCountry->First();

 

}

 

The code shown here assumes that the first edit control you want to use is called Edit1, the second is called Edit2, and so on.

Given the existence of this array of controls, it is very simple to use the OnDataChange event to keep them in sync with the contents of the current record in a dataset:

void __fastcall TForm1::dsCountryDataChange(TObject *Sender, TField *Field)

 

{

 

  int i;

 

 

 

  if (FUpdating)

 

    return;

 

  for (i = 0; i <= 4; i++)

 

    Edits[i]->Text = tblCountry->Fields[i]->AsString;

 

}

 

This code iterates through each of the fields of the current record and puts its contents in the appropriate edit control. Whenever Table->Next is called, or whenever any of the other navigational methods are called, the function shown previously gets a chance to strut onto the stage. Its primary raison d'etre is to ensure that the edit controls always contain the data from the current record.

Whenever Post gets called, you will want to perform the opposite action. That is, you will want to snag the information from the edit controls and tuck them away inside the current record. To execute this chore, simply respond to TDataSource.OnUpdateData events, which are generated automatically whenever Post is called:

void __fastcall TForm1::dsCountryUpdateData(TObject *Sender)

 

{

 

  int i;

 

  for (i = 0; i <= 4; i++)

 

    tblCountry->Fields[i]->Value = Edits[i]->Text;

 

}

 

The Dsevents program switches into edit mode whenever you type anything in one of the edit controls. It manages this sleight of hand by responding to OnKeyDown events:

void __fastcall TForm1::Edit1KeyDown(TObject *Sender, Word &Key,

 

      TShiftState Shift)

 

{

 

  if (dsCountry->State != dsEdit)

 

    tblCountry->Edit();

 

}

 

This code demonstrates how to use the State variable of a TDataSource object to find out the current mode of the dataset.

Tracking the State of a Dataset

In the last section, you learned how to use TDataSource to keep tabs on the current state of a TDataSet and to respond just before certain events are about to take place.

Using a TDataSource object is the simplest way to perform all these functions. However, if you would like to track these events without using TDataSource, you can respond to the following rather intimidating list of events from TDataSet, all of which are naturally inherited by TTable or TQuery:

__property TDataSetNotifyEvent BeforeOpen;

 

__property TDataSetNotifyEvent AfterOpen;

 

__property TDataSetNotifyEvent BeforeClose;

 

__property TDataSetNotifyEvent AfterClose;

 

__property TDataSetNotifyEvent BeforeInsert;

 

__property TDataSetNotifyEvent AfterInsert;

 

__property TDataSetNotifyEvent BeforeEdit;

 

__property TDataSetNotifyEvent AfterEdit;

 

__property TDataSetNotifyEvent BeforePost;

 

__property TDataSetNotifyEvent AfterPost;

 

__property TDataSetNotifyEvent BeforeCancel;

 

__property TDataSetNotifyEvent AfterCancel;

 

__property TDataSetNotifyEvent BeforeDelete;

 

__property TDataSetNotifyEvent AfterDelete;

 

__property TDataSetNotifyEvent OnNewRecord;

 

__property TDataSetNotifyEvent OnCalcFields;

 

__property TFilterRecordEvent OnFilterRecord;

 

__property TOnServerYieldEvent OnServerYield;

 

__property TUpdateErrorEvent OnUpdateError;

 

__property TUpdateRecordEvent OnUpdateRecord;

 

__property TDataSetErrorEvent OnEditError;

 

__property TDataSetErrorEvent OnPostError;

 

__property TDataSetErrorEvent OnDeleteError;

 

Most of these properties are self-explanatory. The BeforePost event, for instance, is functionally similar to the TDataSource->OnUpdateData event that is explained and demonstrated previously. In other words, the Dsevents program would work the same if you responded to DataSource1->OnUpdateData or to Table->BeforePost. Of course, in one case you would not need to have a TDataSource on the form, while the other requires it.

All of these events are associated with methods that have a particular signature. For instance, most of them are of type TDataSetNotifyEvent. A TDataSetNotifyEvent is declared like this:

typedef void _C!astcall (__closure *TDataSetNotifyEvent)(TDataSet *DataSet);

 

Or, to make the matter more comprehensible, here is the code BCB generates for an AfterClose event:

void __fastcall TForm1::tblCountryAfterClose(TDataSet *DataSet)

 

{

 

}

 

To work with these properties, you should see the EventOrd program found on the CD-ROM that accompanies this book, and shown below in Figure 9.11. This program is better experienced in action, rather than explained in words here on the page. The main point of the program is to notify you when any major event associated with a table occurs. If you play with the program for some time, you will begin to get a good feeling for the events associated with a table, and for the order in which they occur.

 

 


NOTE: When I first wrote this program, I learned a number of things about VCL databases that had not been clear to me from prior experience. There is nothing like seeing the events being fired to start to get a sense of how the VCL really works! So I would strongly recommend spending some time with this program.



FIGURE 9.11. The EventOrd program tracks the events that occur in a TTable-based database application.


The EventOrd program responds to all the published events that can occur on a
TTable object. Each method response event does nothing more than report the event by posting a string to list box. Here, for instance, is the response to the NewRecord event:

void __fastcall TDataMod::tblCountryNewRecord(TDataSet *DataSet)

 

{

 

  HandleDataEvent("New Record");

 

}

 

The HandleDataEvent pops the string into the list box and makes sure that the most recently added items in the list box are always visible:

void __fastcall TDataMod::HandleDataEvent(char * S)

 

{

 

  Form1->ListBox1->Items->Add(S);

 

  Form1->ListBox1->ItemIndex = Form1->ListBox1->Items->Count - 1;

 

}

 

The main form for the application has a simple method that will write the contents of the list box to disk:

void __fastcall TForm1::WriteListBoxDataToDisk1Click(TObject *Sender)

 

{

 

  if (OpenDialog1->Execute())

 

    ListBox1->Items->SaveToFile(OpenDialog1->FileName);

 

}

 

Here, for instance, is a short run from the program:

Before Open

 

Filter Record

 

dsCountry: State Change

 

dsCountry: Data Change

 

After Open

 

Filter Record

 

dsCountry: Data Change

 

Navigator click: Next

 

Filter Record

 

dsCountry: Data Change

 

Navigator click: Next

 

Before Insert

 

dsCountry: State Change

 

New Record

 

dsCountry: Data Change

 

After Insert

 

Navigator click: Insert

 

Before Cancel

 

dsCountry: State Change

 

Filter Record

 

dsCountry: Data Change

 

After Cancel

 

Navigator click: Cancel

 

The preceding list records what happens when

  • A table is opened

 

  • The TDBNavigator Next button is pressed twice

 

  • The TDBNavigator Insert button is pressed

 

  • The TDBNavigator Cancel button is pressed

Unfortunately, the TDBNavigator OnClick method occurs after the changes are recorded in the dataset. For instance, the last six events in the record look like this:

Before Cancel

 

dsCountry: State Change

 

Filter Record

 

dsCountry: Data Change

 

After Cancel

 

Navigator click: Cancel

 

They actually occurred in the following order:

Navigator click: Cancel

 

Before Cancel

 

dsCountry: State Change

 

Filter Record

 

dsCountry: Data Change

 

After Cancel

 

The point is that I clicked the navigator Cancel button, and then the other events occurred, in the order shown. However, BCB gives priority to the database events, and then reports the OnClick event.

If you do a lot of database programming, you really need to know the order in which certain events occur. Outlining all that information in these pages is not likely to be particularly helpful to most readers. To learn about this information, it's best just to watch the events occur and to trace their history. So fire up the OrdEvents program and see what you can learn from it.

Working with TDatabase

While you are learning the basics of connecting to a table, you should also look at the TDatabase object, which exists primarily to give you a means of staying connected to a database even if you are continually opening and closing a series of tables. If you use TDatabase, you can be connected to Oracle, InterBase, or other servers without ever opening any tables. You can then begin opening and closing tables over and over without having to incur the overhead of connecting to the database each time you call Open.

The TDatabase object also enables you to start server-based applications without specifying a password, and it gives you access to transactions. Besides the information in this chapter, more information on TDataBase appears in the discussion of the Transacts program, which is covered at the end of Chapter 15, "Working with the Local InterBase Server."

To use the TDatabase object, first drop it on to a form or data module. Set the AliasName property to DBDEMOS. Create your own DatabaseName, such as MyDBDemos. Set Connected to True. Drop down a TTable object and set its DatabaseName property to MyDBDemos. The issue here is that you created a "new database" when you dropped the TDatabase object on the form and filled in its fields as shown previously. Therefore you can select the name of this database from the drop-down list for the TTable DatabaseName property.

After connecting the table to the TDatabase object, you can work with the TTable object exactly as you would under normal circumstances. For instance, you could connect it to the BioLife table, as shown in the DatabaseObject program found on the CD-ROM that accompanies this book.

If you double-click a TDatabase object, you bring up the component editor for this object, shown in Figure 9.12. Inside the component editor you can discover the default values for many of the database parameters passed in the BDE alias for this database.

FIGURE 9.12. The component editor for the TDataBase object displaying information about a connection to an InterBase table.


In this section your learned about the
TDatabase object, which can be used to optimize code that is continually connecting and disconnecting from tables that belong to one database. Throughout the rest of this chapter and the next, the TDatabase object will play only a minor role. However, these components come into their own when you are connecting to SQL data as described in Chapter 15.

Connecting to a Database without Visual Tools

Everything that you do in BCB with the visual tools can also be done in code. The visual tools are just an easy way to write code; they do not do anything special that you can't also do in code. In fact, the code came first, and then the visual tools were added to make it easier for you to create programs. The key to BCB is the way its language conforms to the metaphors of visual programming without causing you any undo overhead.

 

 


NOTE: It would not be true, of course, to say that BCB has no overhead as a result of its concessions to the world of visual programming. For instance, all objects that reside on the Component Palette inherit at least some code that makes them able to be manipulated visually. In other words, they descend not directly from TObject, but from TComponent.

The amount of this code is small compared to what you see in a product like PowerBuilder. In particular, it is nearly identical in size and kind to the code associated with an OWL or MFC object. In short, BCB components are really just normal objects with a few extra methods. As a result, they are much smaller than most ActiveX controls or than controls you see in other products. Furthermore, the principles of inheritance allow most of the code to be reused over and over by multiple objects. In other words, you get the hit the first time you bring up a form, but other forms, or subsequent objects dropped on a form, just reuse the same objects used by the
TForm object.

To help drive this point home, I should perhaps point out that a typical small BCB application with five or six components on it is about 200KB in size. Many of the ActiveX controls that I have used are at least that large. In other words, it would not be unusual to find that five ActiveX controls added over a megabyte to the size of your application's install image. BCB controls, on the other hand, frequently added no more than five or ten KB to the size of your executable.

An exception is the presence of the first database control you drop on a form, which usually adds about 200KB to your executable. You should note, however, that because of inheritance, subsequent database objects do not increase the size of your application by nearly so large a percentage. Big database applications with four or five forms and thirty or forty database controls are frequently some 500-700KB in size. That's big by the old standards of DOS, but not large by today's standards.


If you want to write code that is completely independent of the visual tools, you have to add some include directives to the header file for your form. For instance, to use a TTable object, you should add the following code to your header file:

#include <vcl\DBTables.hpp>

 

#include <vcl\DB.hpp>

 

Here is an example of how to connect to a TTable object without using any visual tools:

void __fastcall TForm1::Button1Click(TObject *Sender)

 

{

 

  TTable *MyTable = new TTable(this);

 

  MyTable->DatabaseName = "DBDEMOS";

 

  MyTable->TableName = "COUNTRY";

 

  MyTable->Open();

 

  ShowMessage(MyTable->Fields[0]->AsString);

 

  MyTable->Free();

 

}

 

Of course, this code would not work if I did not first drop down a TButton object and create an OnClick event for it with the visual tools. But I am not using any visual tools when working with the database.

In this example, the first line of code creates an instance of a TTable object. This is the equivalent in code of dropping a TTable object onto a form from the Component Palette. Notice that you pass in this to the object's constructor in order to give the object an owner. In particular, Form1 is being assigned as the owner of the object, and is therefore responsible for freeing the object at shutdown time. If you passed in NULL in this parameter, you would have to call Free or Delete in the last line of code. I chose to do this anyway, in large part because I am declaring TTable as a local variable that lives on the stack created for this one function. It therefore makes sense to destroy the object at the end of the function, though the VCL would in fact still clean up the memory for me when the form is destroyed. However, if I declared MyTable as a field of Form1, it would make sense just to let Form1 call Free on the object at program shutdown, which is what I do in the other examples included in this section.

The next three lines of code do what you would normally do with the Object Inspector. That is, you first fill in the DatabaseName and TableName properties, and then you call Open, which is the equivalent of setting Active to True.

After opening the table, you are free to access its code in any way you want. You could, for instance, iterate through the records in a table, as explained later in this chapter, or you could even connect the table to a data source, and the data source to a TDBGrid or TDBEdit control.

An example of this type of program is available on disk as a program called TABLESWITHOUTVISUALTOOLS.MAK, found in the NOVISUALTOOLS directory. The code for the program is shown in Listings 9.1 and 9.2.

Listing 9.1. The main source file for the TablesWithoutVisualTools program.

//--------------------------------------------------------------------------

 

#include <vcl\vcl.h>

 

#pragma hdrstop

 

#include "Main.h"

 

//--------------------------------------------------------------------------

 

#pragma resource "*.dfm"

 

TForm1 *Form1;

 

//--------------------------------------------------------------------------

 

__fastcall TForm1::TForm1(TComponent* Owner)

 

  : TForm(Owner)

 

{

 

}

 

//--------------------------------------------------------------------------

 

void __fastcall TForm1::Button1Click(TObject *Sender)

 

{

 

  TTable *MyTable = new TTable(this);

 

  MyTable->DatabaseName = "DBDEMOS";

 

  MyTable->TableName = "COUNTRY";

 

  MyTable->Open();

 

  ShowMessage(MyTable->Fields[0]->AsString);

 

  MyTable->Free();

 

}

 

 

 

Listing 9.2. The header file for the TablesWithoutVisualTools program.

//--------------------------------------------------------------------------

 

#ifndef MainH

 

#define MainH

 

//--------------------------------------------------------------------------

 

#include <vcl\Classes.hpp>

 

#include <vcl\Controls.hpp>

 

#include <vcl\StdCtrls.hpp>

 

#include <vcl\Forms.hpp>

 

#include <vcl\DBTables.hpp>

 

#include <vcl\DB.hpp>

 

//--------------------------------------------------------------------------

 

class TForm1 : public TForm

 

{

 

__published:    // IDE-managed Components

 

  TButton *Button1;

 

  void __fastcall Button1Click(TObject *Sender);

 

private:        // User declarations

 

public:         // User declarations

 

  virtual __fastcall TForm1(TComponent* Owner);

 

};

 

//--------------------------------------------------------------------------

 

extern TForm1 *Form1;

 

//--------------------------------------------------------------------------

 

#endif

 

If you also wanted to connect to TDatabase object without using any visual tools, you can do so as follows:

void __fastcall TForm1::bOpenTableClick(TObject *Sender)

 

{

 

  FMyDatabase = new TDatabase(this);

 

  FMyDatabase->AliasName = "DBDEMOS";

 

  FMyDatabase->DatabaseName = "MyDBDemos";

 

  FMyDatabase->Connected = True;

 

  FMyTable = new TTable(this);

 

  FMyTable->DatabaseName = FMyDatabase->DatabaseName;

 

  FMyTable->TableName = "COUNTRY";

 

  FMyTable->Open();

 

  DataSource1->DataSet = FMyTable;

 

}

 

For this code to work, you need to do several things. In particular, you would need to add DB.HPP and DBTABLES.HPP to your header file for your main form, and you would need to edit the declaration for your TForm1 class so that it looked like this:

class TForm1 : public TForm

 

{

 

__published:

 

  TButton *bOpenTable;

 

  TDBGrid *DBGrid1;

 

  TDataSource *DataSource1;

 

  void __fastcall bOpenTableClick(TObject *Sender);

 

private:                              // User declarations

 

  TDatabase *FMyDatabase;             // Add field for database

 

  TTable *FMyTable;                   // Add field for table

 

public:                               // User declarations

 

    virtual __fastcall TForm1(TComponent* Owner);

 

};

 

Notice the addition of the TDatabase and TTable data items. These are added so that the objects are available for use in the bOpenTableClick function.

I have also added TDBGrid and TDataSource to the project and hooked up my database to them. I do this in order to show that there is absolutely no difference between creating a TTable and TDatabase object in code and doing it with the visual tools. As you can see, you can hook up the visual tools to the code based objects with just a single line of code:

DataSource1->DataSet = FMyTable;

 

In particular, this line of code defines exactly what happens logically when you connect a data source to a table using the visual tools.

In this next example, absolutely everything is done from scratch. No visual database objects are used, and even the Alias is created on the fly. All the visual database controls are created on the fly, assigned locations on the form, attached to the database, and made visible:

void __fastcall TForm1::bCreateClick(TObject *Sender)

 

{

 

  FMyDatabase = new TDatabase(this);

 

  FMyDatabase->DatabaseName = "AnyName";

 

  FMyDatabase->DriverName = "STANDARD";

 

  FMyDatabase->Params->Add("path=g:\\cs\\examples\\data");

 

  FMyDatabase->Connected = True;

 

  FMyTable = new TTable(this);

 

  FMyTable->DatabaseName = FMyDatabase->DatabaseName;

 

  FMyTable->TableName = "BioLife";

 

  FMyTable->Open();

 

  FMyDataSource = new TDataSource(this);

 

  FMyDataSource->DataSet = FMyTable;

 

  FMyEdit = new TDBEdit(this);

 

  FMyEdit->DataSource = FMyDataSource;

 

  FMyEdit->Parent = this;

 

  FMyEdit->Visible = True;

 

  FMyEdit->DataField = "Common_Name";

 

  FMyImage = new TDBImage(this);

 

  FMyImage->Parent = this;

 

  FMyImage->BoundsRect = Bounds(0, 100, 300, 150);

 

  FMyImage->DataSource = FMyDataSource;

 

  FMyImage->DataField = "Graphic";

 

  FMyImage->Stretch = True;

 

  FMyImage->Visible = True;

 

  FMyNavigator = new TDBNavigator(this);

 

  FMyNavigator->Parent = this;

 

  FMyNavigator->BoundsRect = Bounds(0, 50, 100, 25);

 

  FMyNavigator->VisibleButtons =

 

    TButtonSet() << nbFirst << nbLast << nbNext << nbPrior;

 

  FMyNavigator->DataSource = FMyDataSource;

 

  FMyNavigator->Visible = True;

 

}

 

Needless to say, this code would not work unless the database objects were declared in the class declaration for TForm or in some other legal location:

class TForm1 : public TForm

 

{

 

__published:    // IDE-managed Components

 

    TButton *bCreate;

 

    void __fastcall bCreateClick(TObject *Sender);

 

private:        // User declarations

 

  TDatabase *FMyDatabase;

 

  TTable *FMyTable;

 

  TDataSource *FMyDataSource;

 

  TDBEdit *FMyEdit;

 

  TDBImage *FMyImage;

 

  TDBNavigator *FMyNavigator;

 

public:         // User declarations

 

    virtual __fastcall TForm1(TComponent* Owner);

 

};

 

In this case all variables declared in the private section are database objects used in the bCreateClick method shown previously.

The key lines to focus on are the ones that define the Alias for the TDatabase object:

FMyDatabase->DatabaseName = "AnyName";

 

FMyDatabase->DriverName = "STANDARD";

 

FMyDatabase->Params->Add("path=g:\\bcb\\examples\\data");

 

The first thing to notice about this code is that it hard-codes the path to my version of BCB into the application. You will, of course, probably have to edit this path before running the application.

You can see that the database object is given a DatabaseName, but no AliasName is declared. Instead, a DriverName is specified. This is an either/or proposition. You declare the AliasName, in which case the driver is specified inside the Alias, or else you explicitly declare the DriverName, which means that no externally defined Alias will be used.

Selecting the STANDARD driver is another way of saying you want to use a Paradox table. If you resort to the visual tools, you can see that this is a drop-down list of items, so you can see what other drivers are available on your system. As a rule, you will have available STANDARD drivers, dBASE drivers, and a list of SQL Links drivers such as Oracle, Sybase, and so on.

If no externally defined Alias will be used, how does the TDatabase object perform its function? How does it know which database you want to reference? How does it know the details about the database such as server name, user name, and database name? The solution to this quandary is supplied by the Params property, which can be used to define all the data that normally appears in an Alias. (This is the same property you explored earlier in this chapter when you double-clicked a TDataBase object to bring up the TDatabase component editor.)

In the case of Paradox tables, the only information that you need to fill concerns the path to the database. However, there are additional fields that will need to be covered if you want to connect to a client/server database. The Params property is of type TStrings, which means you can treat it more or less as you would any TStringList object, or as you would the Items property for a TListBox.

Notice that when you create visual database controls, you need to specify both the owner and the parent of the control:

FMyEdit = new TDBEdit(this);

 

FMyEdit->Parent = this;

 

In this case, both the owner and the parent are the main form. The Owner is responsible for deallocating the memory associated with the object, and the Parent is a field used by Windows so it will know where to draw the control.

If you are working in code rather than using the visual tools, you still have complete control over the object. For instance, I can define how many buttons will be visible on a TDBNavigator control:

FMyNavigator->VisibleButtons =

 

    TButtonSet() << nbFirst << nbLast << nbNext << nbPrior;

 

In this case, the VisibleButtons property is of type TButtonSet. In C++, sets are emulated through objects. If you want to add a member to the set, you use the overloaded << operator. In this case I am turning on the First, Last, Next, and Prior buttons, and leaving the other buttons uninitialized.

Before closing this section, I should make one final point. In all these examples, I am assuming that a TSession object has been created. This will indeed happen automatically, so long as you have a TApplication object initialized in your application. The DatabaseAlias application and all standard BCB applications initialize the TApplication object in WinMain:

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)

 

{

 

    Application->Initialize();

 

    Application->CreateForm(__classid(TForm1), &Form1);

 

    Application->Run();

 

    return 0;

 

}

 

If you don't have this kind of code in your application (for instance, if you have created a console application), you will have to explicitly create the TSession object yourself. Code for doing this is shown later in the book in the chapter on ISAPI programming, "Extending an Internet Server with ISAPI."

In this section you have learned something about creating database objects in code rather than with the visual tools. One drawback to this section is that it makes BCB database programming appear fairly difficult. The other side of the coin, of course, is that you don't have to manipulate the database tools this way. In fact, you will usually use the speedy visual tools to do in a few seconds what is done previously in code. It is, however, important to know that you can do all forms of database programming in code if you so desire.

Summary

In this chapter, you have learned how to use the TDataSet, TField, TDBDataSet, TTable, TDatabase, and TDataSource classes. This material is very much at the heart of the BCB database machinery, so you should be sure you understand how it works.

The key points to remember are as follows:

  • TDataSet encapsulates the basic actions you will perform on a table.

 

  • TField is a property of TDataSet that enables you to access the contents or name of each field in a record.

 

  • TDBDataSet gives you the capability to associate a dataset with a given table.

 

  • TTable encapsulates all the functionality of a dataset, but it also gives you access to table-specific chores such as setting indexes or creating linked cursors.

 

  • TDataSource forms a link between TTable or TQuery and any of the data-aware components such as TDBEdit or TDBGrid. TDataSource also contains three useful events that keep you informed about the current state of the database.

In the next chapter, you will learn about the TQuery object and SQL. SQL is especially useful when you want to access the advanced capabilities associated with servers and server data.

 

<<Back (Part 01)  

 

VMS Desenvolvimentos

Diversas Dicas, Apostilas, Arquivos Fontes, Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++ Builder.