Developing a Database Application:
The dB2K Tutorial

Ken Mayer, Senior SQA Engineer, dBASE Inc.
Michael Nuwer, dB2K user
Last Modified: October 13, 2001
Version 2.2.0

Phase III
Creating The DataModules

Goals and Objectives

The goals for Phase III of the tutorial project are:

Additional Readings


Before Starting This Phase. It is very important that before you begin working on this phase you make sure that the "Look in:" combobox is pointing to the "dB2KTutorial" directory, not to the "dB2KTutorial\Tables" directory (a step that was included toward the end of the previous phase, but you may have missed it ...). Please make sure you are pointing there now.

Data objects? What in the world are Data objects?

dBASE, Inc. has taken the world-class object model that was created for Visual dBASE 5.x, and moved it over to the tables and the databases. What this means is that you can now use tables, records, and fields as objects, with each having it's own set of properties, events, and methods. This gives the developer a lot of control over the code, and allows the developer a lot of the object oriented power and reusability that you can get from other objects in dB2K.

So, what are all these objects, and how are they related? Before we get into the "in-depth" discussion, let's step back and take a look at the bigger view of the data objects in dB2K.

Most of these objects are containers. This means that they can hold other objects. An example which you will see a lot of, is the query object. This object holds a rowset object, so it is a container for the rowset object. Unlike some containers, there can be only one rowset object for each query object. The rowset object is a container for a fields array. The fields array holds pointers to each field object that is contained in the rowset. Hence, you might draw out a diagram something like the following:

Query Object

The query object is a representation of an SQL statement that describes a query. It encapsulates the rowset object.

Rowset Object

The rowset object contains data that results from an SQL statement in a query object. It encapsulates the fields array object.

Fields Array Object [Field1, Field2]

The rowset’s fields array contains a Field object for each field in the row.

Field Object 1

Field Object 2

Each field object corresponds to one of the fields from a table. It contains properties that describe the field.

The rowset object would have a parent, which is the query, the fields array would have a parent, which is the rowset object, the field objects have a parent, which is the fields array.

Note that you cannot define a rowset object independent of a query object, but you can define a field object independent of a fields array or rowset (but while you can use it outside of the rowset, it's kind of tricky unless you add it to the fields array of a rowset).

Now, to make things more interesting, a database object refers to a database or BDE Alias. A database, by its very nature, contains tables. In a way, the database object is a reference to an object -- the database itself. However, that is rather simplifying matters. You should normally use a BDE alias for reasons that will be discussed when we get to the database object ...

A datamodule can be used to contain database objects and query objects. Rowsets are contained automatically by queries, so while a datamodule contains the query, it doesn't directly contain the rowset.

So, how confused are you now? Hopefully not very ... All of this is leading to something ... wish I knew what it was ...

A Detailed Breakdown of Each Data Object

Each of the data objects, as noted elsewhere in this document, has properties, events and methods.

Properties
Properties are the attributes of controls and include among others identification, and other properties. A property setting can be changed programmatically as well as in the designer surfaces (form, report, datamodule). The setting is normally a single value, for example, a character string, number or reference to another object.

Events
Events are something that controls respond to, a mouse click, a control getting focus, a change in the control's value, etc. Events are always "firing" provided the event is occurring, however, in order to have something happen, you have to "hook" code to the event. (Windows itself is really just a big "event" handler -- it does not do anything until some event is fired ...)

Events whose name begin with ON, such as onOpen, occur after the action to which they refer. Thus, the onOpen event occurs after the query is opened. The second thing is that events that begin with CAN, such as canClose, must return a logical value that will determine whether the indicated action may proceed. So, a query's canClose event must return true before the query may close. Returning a value of false will prevent the query from closing. Failure to return a Boolean value may yield unpredictable results.

Methods
Methods are code that performs an action. Objects have a number of built in methods which are called through the control, i.e., form.rowset.next ().

One thing you should remember is that, if you use the built-in methods as a programming hook to insert your own code (this is called over-riding the method), you will nearly always want to call the original built-in method before, during, or after your code. (This would be achieved by entering: SUPER::methodname() into the code ...)

Database
The database object is used when you use BDE Aliases to handle your databases. An alias is absolutely required when working with SQL Server tables (such as Interbase, Oracle, etc.), and is optional but useful when using local tables.

We will use a database alias in this project. There are a number of good reasons why we will do this. Here a few:

Creating a Database Object Reference
You can create a database object reference easily enough. There are only a few things you must set:

   // for local tables
   d = new Database()
   d.databaseName := "MyAlias"
   d.active := true

Query
The Query object is the most often seen data object. The query object is used to reference the individual tables of your database, and is required if you are going to do any work with tables using the OODML.

As such, it is very important that you understand this one. It is the container of the rowset, which is where most of the data manipulation methods and events are, as well as the rowset being a container for the fields ...

How Do I Use a Query?
We will keep coming back to this topic, but at it's most basic, and assuming that you are starting with local tables, you need to instantiate the query, assign the SQL property, and then set the active property to true.

   q = new query()
   q.sql = "SELECT * FROM mytable"
   q.active = true

One thing that sometimes worries new users of dB2K is that they "have to know SQL". No, you don't. The above command is the most SQL you absolutely have to know to do some very detailed applications in dB2K. Most of what you might want to do is covered by properties, events, and methods of the rowset object, which we will get to presently.

The "*" in the SELECT statement is a wildcard, like it is in DOS -- it means "all fields" when used here.

The above three statements are all fine and dandy if you are creating a program that needs to manipulate data, but what if you want to use a form or a report?

It couldn't be more simple. Bring up a form in the forms designer, click on the navigator "Tables" tab, and drag the table you wish to use onto the form (or report) surface. You will see an icon used to represent the query object. This has the letters "SQL" on it. The designer surfaces automatically fill in the required information, although the code looks different if you examine the source code:

   this.CUSTOMER1 = new QUERY()
   this.CUSTOMER1.parent = this
   with (this.CUSTOMER1)
      left = 52.5714
      top = 2.5
      sql = "select * from customer"
      active = true
   endwith

The designers stream out the left and top properties so that every time you open the form, the query icon is in the same location. When you run the form, the query icon will not appear on the form -- it is there only during design mode.

The first statement creates an instance of a query object, but note that rather than "CUSTOMER1" you see "this.CUSTOMER1" -- "this" refers to the form. Note also that the parent property is being assigned for you, and again "this" refers to the form.

You could write the same code that was originally shown in a similar fashion in your program:

   q = new Query()
   with( q )
      sql = "select * from mytable"
      active = true
   endwith

This is just as valid as the previous way of writing the code.

SQL SELECT Statements
If you examine any book on SQL, you will find that there are a lot of options that you can use with the SQL Select statement.

As noted above, you do not really need to know a lot about SQL Select statements to use these data objects. However, you should know a few things -- particularly if you are familiar with SQL:

WHERE clauses work fine, but there are some caveats.

Read Only Queries
Local SQL (SQL statements used on local tables) will often generate read only queries. The following will generate read-only queries:

Next ...
Once you have the query set and it is active, what can you do with it? Well, not all that much. To work with the data, you have to work with the rowset and field objects ...

Rowset
The rowset object is the real work horse of the data objects in dB2K. A lot of the functionality found in the old XBase DML commands and functions is now in the rowset object's properties, events and methods. We will explore some of this functionality shortly.

Fields Array
This is a very simple topic. The fields array itself is an array that is contained by the rowset, and holds pointers to the field objects for the rowset.

What can you do with the fields array? Well, the fields array is how you address the fields. You can add or delete fields in the fields array (useful for calculated fields, a topic we'll get to later). You can find out how many fields are IN the fields array. There isn't much else.

As noted, this is fairly simple. The most important thing is that this is an associative array, which means that you can select a field either by a number (it's position in the list) or by it's name. You can do the following to find the value of a field:

   ? form.rowset.fields[ 1 ].value
   // or
   ? form.rowset.fields[ "fieldName" ].value

Rather than spend a lot of time here, let's look at the field object. There's a lot going on there ...

Field
The field object is used to allow access to individual fields in a table, and work with properties, events and methods to modify the behavior and value of the field. Each element in the Fields Array is an object of the Field class. And each field object corresponds to one of the fields returned by the query.

Some of the Field object's properties provide useful information -- like fieldName, length, or type. The value property reflects the current value of a field for the current rowset; assigning a value to the value property assigns that value to the row buffer.

The following code, for example, will loop through each of the fields in the query "q" and print the fieldName of those that are empty.

   for i=1 to q.rowset.fields.size
   cFieldName = q.rowset.fields[i].fieldname
      if empty( q.rowset.fields[cFieldName].value )
         ? cFieldName
      endif
   next

Getting Started with Data Objects

Now that we've examined the major data objects in dB2K let's take a look at using them a bit.

Most of the following is going to assume you are using forms (and/or reports, the ideas are the same for at least a lot of it), and so the syntax for the commands shown will assume forms, or in many cases the onClick event of pushbutton controls on forms. Keep in mind that the same types of things can be done programmatically.

Using a Table On a Form
After all that brouhaha, and detail, it sure sounds like this is going to be complicated, doesn't it? The following is true for reports and labels as well as forms ...

Actually, it couldn't be more simple. To use a table on a form, the simplest method is to open a new form in the designer, click on the navigator window in dB2K, click on the "Tables" tab of the navigator, and drag your table to the form designer surface.

What happens is that dB2K will place an icon on the form surface, which is used to represent a non-visual object (when the form is run, the user will never see this object). The icon will have the letters "SQL" on it -- this is a query object. In addition to placing the icon on the designer surface, dB2K will set the SQL property, and the active property will default to "true".

If you are using a BDE Alias, you will get, in addition to the query object, a database object as well. The form designer will automatically make the correct connections between the database object and the query object.

One thing that it is important to note is that the form itself has a property called "rowset". dB2K automatically stores a reference to the rowset of the first query object placed onto the form in this property of the form.

Navigating Through Tables
Once you have a table on a form, and presumably some controls for fields, you will, at the very least, need to allow your user to navigate through the table.

If you have set the indexName property of the rowset object, the table will be navigated based on whatever sequence is set in the expression of the index.

Navigation is done via methods of the rowset, which were discussed way up in the rowset section of this document. There are some things you should note:

When you write your own navigation code, here are some things to consider:

Editing
You can set a rowset to be not editable until the user explicitly decides to edit (I find this to be a good idea -- it's very easy to accidentally change something ...) by setting the rowset's autoEdit property to false (it defaults to true). This has the effect of disabling all controls that are dataLinked to the fields of the rowset.

If you decide to use the autoEdit property set to false, then you will need to provide a way for the user to edit the current row. This can be done with:

   form.rowset.beginEdit()

and you can place this code in the onClick event for a pushbutton.

You should probably provide a "save" option and a "cancel" option by calling the rowset's save() and abandon() methods.

Deleting Rows
The OODML does not support the XBase "soft delete" (basically this is the ability to continue to display a deleted row on the form) directly. It is possible to do this via the BDE's API, and there is an example of this in the dUFLP library.

The reason for this is that the .DBF is the only table format that allows this functionality -- all others basically assume that a deleted row is gone. Don't panic, however -- if your user decides to delete a row, it is actually still in the table, but you cannot show it to the user, and you cannot (using the OODML) allow the user to recall (undelete) that row.

Finding Rows in a Table
There are several ways to "find" rows in a table. You can use "findKey()" and/or "findKeyNearest()", you can use "beginLocate()" and "applyLocate()", and a lot more.

Filtering Rows in a Table
As with "Finding" rows, there are quite a few ways to filter rows in a table, including the "filter" property of the rowset, the "beginFilter()" and "applyFilter()" methods, the "setRange()" method, and the "canGetRow" event of the rowset.

What Is a DataModule?

A datamodule is a container for data objects. The purpose of the datamodule is to set-up your data objects in whatever way that you might need in one place, so that if you need to re-use the same set-up (for example, you might have a set of tables set on a form, and need the exact same set-up for a report, or maybe for another form) you can, with no extra effort.


Note: We are NOT using the SQL designer for this project. It is my opinion that the SQL generated by the SQL designer is unwieldy, and generally unnecessary for use with the data objects in dB2K. Most SQL statements you will need in most dB2K applications are pretty simple and it is not necessary to create joins and such (and indeed joins, as well as some other SQL selects, on local tables create read-only queries which is pretty much useless except for reports!).

Another useful feature of datamodules is that you can have a custom datamodule which can be used for some basic setup, which then can have that setup inherited by the datamodules that are subclassed from it. We will be using a custom datamodule in the application we are creating which will have the database object on it. That is all that will be there, but this will be used for all of the subsequent datamodules that we need for this application.

In the tutorial project we will use datamodules for all our data access objects. At this point I just want to say that in any situation where you are working with more than one table, or you are setting up some fairly complex code (perhaps in canAppend events and such) you are better off using a datamodule and encapsulating all of that, so that if you need it in another form, you can save yourself a lot of work. Use datamodules a lot. They will make your life as a developer much easier.

What is dQuery/Web?

dQuery/Web is the center piece of the dB2K information toolset. It offers visual tools for modeling, entering and filtering data. It also generates reports, desktop applications and web applications. Many of the dQuery/Web tools are designed so that you can interact with your data. But there are also tools provided for the developer.

In this Tutorial, we will use dQuery/Web to create our dataModules. We will not take advantage of the many filtering tools nor of the reporting tools. For those interested in these and other interactive tools you should read "The dB2K Guided Tour".

The dQuery/Web interface is divided into four sections. The upper portion is the Design Surface. This is where we create and use database and query objects. The lower portion contains a notebook with three pages. The Live Data tab displays a grid reflecting the selected query object. When a parent-child relationship exists between two queries, the grid in the Live Data area will reflect the constraints of the child query.

The second and third tabs in the dQuery/Web notebook can be used to create Custom Views of the data and to generate a report that reflects this data. The Tutorial Project will not need these two tabs.

There are may ways to add objects to the dQuery/Web Design Surface. In this Tutorial I will normally instruct you to drag and drop an object from the Navigator onto the Design Surface. But there are other ways of accomplishing the same task.

As an alternative you can right-click on the Design Surface and select the appropriate object from the list in the pop-up menu. You can alternatively use the Main Menu and select New | Query from Table; or click the Query button in the toolbar; or if you are using Easy Start, click Option 2: Query.

These are all valid methods for adding objects to the dQuery/Web Design Surface. The technique you choose to use is largely based on personal preference and the size and resolution of your monitor.

A Custom Datamodule

For our application we will be using a database alias, which means that we will need a database object. Rather than creating this database object on each datamodule we can put the definition in a custom datamodule and inherit the properties in the inherited datamodules.

Before we begin, we should change the current work folder. In the Navigator, click the yellow file folder icon to the right of the "Look in:" entryfield. In the Choose Directory, click the folder "MyCustom", then click OK. This is the folder where we will store our classes.

To create a custom datamodule, the easiest method is to go to the Navigator and select the "Datamodules" tab. You should now see two icons, both of which are named "Untitled". The first is a 'standard' Datamodule (this is mostly blue), the one next to it (which is yellow) is a custom Datamodule. (Note: if you right click on the "Untitled" icons in the Navigator, you will see "Create a new ..." with the type of whatever object -- you can select this little menu option if you wish, but you can also use this to determine what the icon is for if you are not sure.)

Double click the "Untitled" custom Datamodule. This will bring up the dQuery/Web design surface. We need to add a database object to the design surface. To do this you can select the database icon on the toolbar or you can right click the mouse and select Add Database Object from the popup menu.

The Select/Add Alias dialog form will appear.

Choose "dB2KTutorial" from the list box and click "OK." Save the datamodule (File | Save), give it a name: BASE.CDM, and, if needed, change the folder location to C:\dB2KTutorial\MyCustom.

If you open this custom datamodule in the source editor (press the F12 key), you will see code like:

   class BASEDATAMODULE of DATAMODULE
   with (this)
     left = -1
     top = -1
   endwith
   this.DB2KTUTORIAL1 = new DATABASE()
   this.DB2KTUTORIAL1.parent = this
   with (this.DB2KTUTORIAL1)
      left = 21
      top = 16
      width = 115
      height = 112
      databaseName = "dB2KTutorial"
      active = true
   endwith

 endclass

If you examined this in the source editor, close it (use the 'x' button in the titlebar, or press <Ctrl>+W ...).

We will come back to this code later in this document, but for now you can close the dQuery/Web datamodule designer.

Change the current working folder back to dB2KTutorial. In the Navigator's "Look in:" combobox, click the dropdown arrow. C:\dB2KTutorial should be one of the options in the list. Select it now. (Alternatively, you can type CD C:\dB2KTutorial in the Command Window.)

A DataModule for a "Customer Data Entry Form"

One of the many things we will need to do in our application is to create forms for interacting with the data. In order to do this, we need to set up some datamodules that handle a lot of the details (rather than re-creating them each time we need them) ...

One of the forms we will create is a form for interacting with the Customer table. This table will need to do some lookups on the State table and the Country table. Rather than creating the code to do this in the form, we will do it in a datamodule so that if we need to generate, say, a report that lists the customers we will not need to re-create the lookups.

In the Navigator, you should see two icons named "Untitled" in the the DataModule area. We want to create a "standard" datamodule, which we might do by double-clicking the first "Untitled" icon but there is slight problem.

At the time of this writing dQuery/Web does not create a datamodule that is subclassed from a custom datamodule (I'm using version 1014). If, however, you already have a datamodule that is derived from a custom class, dQuery/Web will work just fine. So for this tutorial we will work around this temporary limitation of the dQuery/Web designer.

To create the customer datamodule, go to the programs tab in the navigator and double click the 'Untitled" icon. This will bring up the Source Editor. Type the following two lines:

   class CustomerDataModule of BASEDATAMODULE from ":Tutorial:Base.cdm"
   endclass

The lines that you typed are called the class declaration. The name of this class is "CustomerDataModule"; "of BASEDATAMODULE" indicates that the class is derived from another class with the name "Basedatamodule"; "from :Tutorial:Base.cdm" specifies the file that contains the superclass called "Basedatamodule". In this case, we are using the source code alias :Tutorial: to point to the file named "Base.cdm". (You should have created this source code alias in Phase I of the Tutorial.)


Note: "Subclassing" is the technique of using a class as a "base" class -- creating a new class that inherits all of the properties, methods, and events of the base class.

An example of this is creating a form in dB2K (never mind "custom" forms) -- there is a "base" form class that is built in to dB2K. When you create a new form, it is a subclass of the base form. It has all the properties, events and methods of the standard form in dB2K.

Once you have subclassed an object you can modify its properties, events and even hook your own code into the methods of that class, without modifying the base class's definition.


Save this file as Customer.dmd. Be sure you type the file extension. Since we are working in the Source Editor the default extension is .PRG. However, there is nothing that prohibits us from entering other extensions.

Close the Source Editor.

Go to the Navigator and click the DataModules tab. In the list of files you should see Customer.dmd. If it is not in the list, click the View menu then click Refresh.

Double click the Customer.dmd icon. This will open the datamodule in dQuery/Web.

Now you are editing a datamodule that is subclassed from the custom one we created earlier. You should see the database object (dB2KTutorial1). This object is inherited from Base.cdm. We are not going to change any properties of this object, but we could (these changes would only affect this datamodule, not all datamodules in the application). If you wanted to affect all datamodules in an application you would want to make changes in the custom datamodule.

Next we need to add the Customer query to this datamodule, so that we can set it up for use in the data entry form we will be creating later.

Save your work so far by pressing <Ctrl>+S (save).

The listbox that represent the Customer1 query object contains the fields that are included in this query object. In the bottom pane of the dQuery/Web designer you see the query's rowset displayed in a grid. This query has selected all the field from the Customer table so the query's field list and the table's field list are the same. This is not, however, necessary when you create a query. In fact, a query can contain a subset of fields from one table or it can contain fields from multiple tables. This feature is part of what makes the query object such a powerful tool. However, in the current datamodule, the Customer1 query is a replica of the Customer table.

We want a specific index to be active, which will be the "Names" index. To use this click on the query object and right click the mouse. Select the Set Index option. In the Select Index dialog form, click "Names" from the list of index tags and then click the "Set Index" button. That is all that is necessary to set the index. Now Close the Set Index dialog form.

Next we need to set the datamodule's rowset property to point to the "customer1" rowset. Right click on the Design Surface and select "Inspect" from the popup menu (or press the F11 key). Be sure that the combobox at the top of the Inspector has only the word "Form". In the properties list, select "Rowset" and then select "customer1" from the dropdown list.

The rowset property of a datamodule defines it's controlling rowset. This will be useful when we create our forms. There are many coding statements that will reference the controlling rowset without needing to know the name of the query object.

Before we close this datamodule, let's review the code. With focus on the datamodule designer, press F12 to bring up the source code editor.

As you can see, creating datamodules isn't that difficult, except that it helps to have an idea what you need to do.

Create the Inventory Datamodule

The Inventory Datamodule will be similar in design to the Customer datamodule. Switch to the Programs tab in the Navigator and double click the untitled icon. Enter the following lines:

class InventoryDataModule of BASEDATAMODULE from ":Tutorial:base.cdm"
endclass

Save this as Inventory.dmd (and remember to type the file extension).

Now, you can double-click the Inventory.dmd icon on the DataModule tab in the navigator (you may need to switch back to this).

As we did with the Customer datamodule, once you have the new dQuery/Web datamodule design surface on the screen click on the navigator, select the "Tables" tab, and select the "Inventory" table. Drag it to the design surface.

Set the datamodule's rowset property to point to the "inventory1" rowset.

Click on the Inventory1 query object and right click the mouse. Select the "Set Index (Key)" option.

Set the indexName to "Description";

Save the datamodule and exit ...

Create the Supplier Datamodule

The Supplier datamodule will be set up nearly exactly like the customer table. Use the following code to create datamodule class:

class SupplierDataModule of BASEDATAMODULE from ":Tutorial:base.cdm"
endclass

and save this as Supplier.dmd

Open the new datamodule in the dQuery/Web designer (follow the steps given above). Drag the "Supplier" table to the design surface.

Set the datamodule's rowset property to point to the "supplier1" rowset.

Select the query object and Set the indexName to "Company"

Save and close the datamodule.

Create the Invoice Datamodule

The Invoice datamodule is going to be a bit more complicated and will require that you follow along carefully. We're going to use three tables for this one. The Customer table will be the primary table, and we will be using it as the "parent" table for the Invoice table, which will then be the "parent" table for the LineItems table (these are the individual items ordered by the customer), creating a parent/child/grandchild type of relationship.

Lets create the subclassed datamodule with the Source Code Editor. You should use the following code:

class InvoiceDataModule of BASEDATAMODULE from ":Tutorial:Base.cdm"
endclass

Save the datamodule as Invoice.dmd. Then open it into dQuery/Web.

So, to get started, drag the Customer table to the design surface.

Click on the datamodule surface and the rowset property in the Inspector. In the combobox, select "customer1". (This sets the controlling rowset ...)

Click on the customer query, right click the mouse and select "Set Index". Set the index to "Names".

Click the customer query again, right click the mouse and select "Inspect Rowset".

Set the autoEdit property to false.

Save the datamodule (<Ctrl>+S).

Next drag the Invoice table to the design surface, and set the Index to "Customer ID". It is very important that this be the index, because that is how a match will be found against the Customer table. Don't forget to open the Inspector and set autoEdit to false.

Next select the Customer1 query object and click the "Customer ID" field. Click the left mouse button and, while holding it down, drag the field onto the Invoice1 query object. Let up on the mouse button. You should see an arrow from the Customer1 query object and pointing to the Invoice1 query object. You have just used drag-and-drop to set a parent-child relationship between these two queries.

Next drag the LineItem table to the design surface. The LineItem table will be set such that we can have one or more line items per invoice. Some of that will have to be handled in code in the data forms we will use for adding data.

Set the Index to "Invoice ID" and remember to set autoEdit to false.

The Linking field between the Invoice1 query and the LineItem1 query is "Invoice ID". Select that field in the Invoice1 query object, than drag and drop it onto the LineItem1 query. Would you believe that this is all you have to do to set up a parent/child relationship?

When we use dQuery/Web to set the parent/child relationship between two queries, there are two properties of child rowset that are modified.

A child rowset object has a masterRowset property. The master rowset for Invoice1 is Customer1. The second property is the masterFields. This property identifies the key field in the parent rowset and is matched to controlling indexed of the child rowset.


Note: If using tables that are not local (.DBF or .DB) you would need to use the query object's masterSource property instead of the masterRowset and masterFields properties of the rowset object. This is discussed in detail in various of the Knowledgebase documents.

Data Validation

Now that we've created the basic datamodules that will be used in our application, let's consider a few additional elements that can be added to a datamodule. The first thing we must do is ensure that the user of our applications enters a customer's last name and postal code when a new row is added to our system. It is not uncommon for a user to start appending new information, but then get called way by the phone (or any number of other office distractions). In other words, we want the last name and postal code to be required fields.

If either field is blank, we want to alert the user that the data is missing and that it must be entered before the row can be saved. If the user does not have a last name or a postal code to enter they should abandon the data entry until that information is known.

There are two main types of data validation that can be added to a query object. The first is field-level validation. Each field object in the rowset's field array has a canChange event. Each time the field value changes, this event fires and validation code will run if its been coded. The canChange event fires when the user tries to move out of a field to another field or to another object (like a lookup list).

This event should be used with great caution, because there are several drawbacks with field-level validation. The primary drawback is that if the user doesn't know what to enter it is difficult to escape from the entryfield and close the form.

The most appropriate place to validate data entry is at the row level. We can use the rowset's canSave event to check the values of any field before the data is saved. The row-level canSave event fires only when the user tries to save the record. This event offers the developer greater flexibility for guiding the user, or gracefully abandoning the modifications.

We are not going to use extensive validation in this application. We will choose a few examples that illustrate how this is typically coded into an application.

To make the customer's last name and postal code require fields, we will begin by re-opening the customer.dmd Datamodule. Find this file in the Navigator and double click it.

dQuery/Web will open the datamodule in it's design surface and you should see a list box that represents the customer1 query object. Click your mouse inside that query object and click the right mouse button. In the popup menu, select "Inspect Rowset". This will call up the inspector.

Select the Event tab in the inspector and locate "canSave" in the list of events. Click this option and note that a wrench icon appears at the right. Click the wrench. The Source Editor comes up with the cursor positioned inside a function. The code that we will enter into this function executes each time the user attempts to save the customer1 rowset. This function returns a boolean value that controls whether dB2K will or will not save the row.

   function rowset_canSave
      local cErrors, bRetVal
      cErrors = ""     // String for errors
      if empty( this.fields[ "Last name" ].value )
         cErrors += "- LAST NAME cannot be blank" + chr(13)
      endif
      if empty( this.fields[ "postal" ].value )
         cErrors += "- ZIP CODE cannot be blank" + chr(13)
      endif
      if "" # cErrors
         msgbox( "Can't save current entry because:" + chr(13) + ;
	          cErrors, "Bad entry", 48 )
         bRetVal = false
      else
         bRetVal = true
      endif
      return bRetVal

Save and close the source editor (Ctrl+"W"). After creating the function for cansave, we can test to see if the code works in the dQuery/Web interface. On the toolbar click the "Add New Row" button. A blank row will appear in the dQuery/Web Live Data window. Tab to the "First Name" field, enter your first name and click the "Save Changes to Row" button on the toolbar. The "Bad entry" message box should popup. (If it donen't popup, you should check your code.)

If user mistakes are caught when the data is being entered, we will save a lot of time later when the data is being manipulated. If we do not validate data entry we might find that a customer's last name does not print on one of our reports. We must then go back and locate this customer's record, find the actual last name, enter it into the table, and reprint the report. Which can take a lot of time.

LookupSQL

This is an often touted feature that really confuses people.

What exactly is "lookupSQL" and why should you even care? How many times have you tried to use proper "normalization" for your data, to the point that you have a lookup table with a code field and some values associated with the code in other fields, and had to write the program code to get your primary table to interact with the lookup table?

Isn't that a lot of work? Loading arrays to display data in comboboxes, scanning through the data to find the proper code and storing that value in the primary table ... all of that can be a lot of code.

Not any more! LookupSQL is a feature that will do this automatically (even if it's not as complete as some of us would like).

In the sample database that we are using for this project, the customer table has a field called STATE ID, which is a two character field. This stores, as you might imagine, the standard US two character state codes (CA = California, etc.).

For your users, however, you may wish to display the full state name. To do this using the OODML of dB2K, what you need to do is use the lookupSQL property (the steps are below) and enter a simple SQL Select statement to select the fields you need in that table. (The lookupSQL property is a property of the field object.)

For this to work properly, you must select the code field in the lookup table as the first field (the type must match that of the field you will store it into), and then the field you want to display. If your lookup table is designed that way in the first place, you could simply use:

   select * from lookuptable

Let's add lookupSQL to our datamodules. First, open Customer.dmd into dQuery/Web. In this datamodule we need to set lookups for the "State ID" and "Country ID" for the customer, which will allow us to have dB2K automatically display the actual States and Countries associated with the two character codes that are stored in the Customer table.

Click on the Customer1 query object and right click the mouse. Select the "Inspect Query" option. Make sure that the customer query ("customer1") is what is displayed in the inspector (use the combobox at the top of the inspector to select it). Look down the left hand column of the Properties tab and click on rowset. To the right you should see the word "Object" and an "I" button -- this means that if you click it, you want to inspect the object that is referenced here. Do so. The inspector will now be viewing the properties of the rowset object.


Note: Throughout the tutorial I use the term "drill down" when I discuss using the inspector. What this means is to click on an object reference in the inspector (such as above, we selected rowset) and then click on the "Inspect" button (the button with the letter "I" on it). This moves you through the object hierarchy to get to an object that is contained by another object. (The query contains the rowset ...)

When I use the term "drill down" I am referring to this process. So if I suggest you "drill down to the fields array", it means click on the fields object in the inspector, and click on the "Inspect" button ...


Click on fields and the "Inspect" button. This will set you to inspecting the fields array.

Select the "State ID" field, and click on the "Inspect" button in the inspector. (You may need to click the "+" and expand the "Array Elements".) This will now bring the "State ID" field into the inspector and we are examining the properties of this field. Click on lookupSQL and type:

 select * from state order by state

and make sure you press the <Enter> key. If you set this property correctly the value property of the "State ID" field will show something much larger than two characters on the form (this is called "data morphing" -- using online help you may want to look up the terms "morphing" and "lookupSQL"). In addition we are ensuring that the display is alphabetical by the "state" field with the "order by" clause. (Normally, we will not be using the "order by" clause -- in some cases with local tables this creates a "read only" query. In the case of a lookup, this is not a problem as the lookup is, by it's very nature, read-only ...)

We need to do this again for the "Country ID" field. However, we have drilled down to the field object for "State ID". How do we move back up? Find the parent property, which says "object" -- click on the "Inspect" button, and we're back at the fields array. (You can also use the "Left Arrow" button at the top of the Inspector -- this takes you to the previous object inspected ...)

Select the "Country ID" field, click the "Inspect" button, select lookupSQL, enter:

 select * from country order by country
and press <Enter>.

If the data in the lower pane of the dQuery/Web datamodule Designer disappears, click the query object again to refresh the grid. Now scroll the Customer1 data grid to the right and locate the "State" and "Country" fields. You will notice that the data contains the full names rather then the two digit codes.

Save this datamodule and close dQuery/Web.

Next, open Supplier.dmd into dQuery/Web. Set the lookupSQL property for both the "State ID" and "Country ID" fields the same as the customer datamodule. (Drill down to the fields array, click in the individual field and drill down to it, click on the lookupSQL property, and enter the appropriate SQL select statement ...)

Save this datamodule and close dQuery/Web.

The next lookupSQL we need is in the Inventory datamodule. So open Inventory.dmd into dQuery/Web.

Click on the Inventory1 query object and right click the mouse. Select the "Inspect Query" option.

This particular lookup is selecting specific fields to use because if we didn't we would end up displaying the wrong field. We need the company name to appear on the Inventory form, not the Contact in the company, which is what would appear if we were not specific (the lookupSQL property uses the fields in the sequence of the table if you do not specify the fields -- the first field in the supplier table is the "supplier id" field, but the next field is "contact" -- we want to display the "company" field ...). In addition, we are specifying "supplier.'supplier id'" because the fieldname has spaces in it -- it appears this is the only way to get the correct fieldname to work using a SQL select statement.

The final lookupSQL we need is in the Invoice datamodule. Open Invoice.dmd into dQuery/web.

In this datamodule we need to set a lookup on the "Item ID" field to look in the inventory table so that we display the description field. Go to the fields array, and select the Item ID field, and drill down. Select the lookupSQL property and enter:

 select inventory.'item id', inventory.'description' from inventory
and press <Enter>.

Save the datamodule and exit dQuery/Web.

Calculated Fields

Calculated fields are rather important in a lot of applications. These are simply fields that display a calculation or a value not in the table for the user (some folk think of a calculated field as specifically being aimed at math, but combining two fields into one is considered to be a calculated field). The user cannot directly interact with them, because they are, by their nature, read-only -- they are not directly datalinked to a table. (The user can actually edit these unless you set the readOnly property on the field, but it won't make a difference to the table itself ...)

To create a calculated field, you must create an instance of a field object, set some properties, and then add the field to the fields array for the rowset. If set properly, as the user navigates through the table, this field will be calculated (using the beforeGetValue event), and the results will be displayed on the form (or report).

The following is a simple example of creating a calculated field to display a "full name" from first and last name fields in a table. Keep in mind that any valid dB2K expression will work. This example assumes that the field object is being added in a query's canOpen or onOpen event:

   // instantiate the field object
   f = new Field()
   f.fieldName := "Full Name"
   f.readOnly  := true
   // "this" in the following statement refers to the 
   // field object (important information)
   f.beforeGetValue := {|| trim( this.parent["first name"].value ) ;
         + " " +this.parent["last name"].value }
   // "this" in the following statement refers to the query:
   this.rowset.fields.add( f )

It is very important that you use the beforeGetValue event to actually perform the calculation, or the value will not update as you navigate through your table. If you assign the "value" property instead, it will display the value assigned for the first row seen, and not update as you navigate.

You can have multiple calculated fields for a rowset, you would just want to make sure that each had a unique fieldName property.

At this point in the tutorial we will use a simple calculated field to display the customer's "full name" from first and last name fields in the Customer table. Later in this tutorial we will need to create some additional calculated fields, so let's write a single method for creating these field objects. We will put this method in the custom datamodule called Base.cdm. This way, all our standard datamodules will have access to the method.

So switch to the Navigator and open Base.cdm into dQuery/Web. (You will need to use the "Look in:" field to change the current folder to c:\dB2KTutorial\MyCustom.) You will recall that this custom datamodule contains the database object. We are now going to add a method to this class.

Press F12 to bring up the source editor. Then move the cursor to the bottom of the class constructor code.


A Class is a definition of an object. As Alan Katz once pointed out: "Many beginners mistake a CLASS for a program or procedure. A CLASS is NOT executable code. It is a blueprint only. Just like a real blueprint, you can't live in the house represented by the blueprint until you build it." The Class Constructor Code is part of the "blueprint" for an object. It includes all the properties and their values. It does not include the methods (or functions) of the class. In the case of a container object, like the form or datamodule, the Constructor also includes the definition of all objects it contains -- like a database object or a query object. A method is the code that executes the behavior of an object. Methods are functions that belong to an object.

In the present case, there are no methods in our base datamodule class, so lines 2-16 are the class constructor code. You must move the cursor so that is below the last "endwith" and above "endclass". This is where we will insert a new method.

Go to the "Method" menu, and select "New Method ...". We are going to add a method into the datamodule that will be "unattached" to any specific event. Change the statement "function Method" to "function calcField(cName,nLength)", and add the code below (the "function" statement and the "Return" statement should already be there, but you will want to modify them so that they look like what is shown below):

   function calcField(cName,nLength)
      local oField
      oField = new field()
      ofield.fieldname := cName
      oField.length := nLength
      this.rowset.fields.add(oField)

      return

This method can be used to create a calculated field by any datamodule that is subclassed from Base.cdm.

We can now save and close the BASE datamodule. To be safe let's recompile this file before we move on. In the Navigator select BASE.CFM and right-click the mouse. In the pop-up menu select compile, then close the Status dialog form. Now we can return to the Customer datamodule and create the Full Name field.

Go to the Navigator and open Customer.dmd. (You will need to change the "Look in:" field so that you are looking in c:\dB2KTutorial.)

Select the Customer1 query object.

Right click the mouse and select Inspect Query.

The Inspector will come up. Click the Events tab (be sure "form.customer1" is the object being inspected).

Click the onOpen event and then click the wrench at the right.

Enter the following code into the customer1_onOpen function:

      class::calcField("FullName",30)

Bug: Be sure there are no spaces in the field name. A space in the field name can cause problems later when the beforeGetValue method is created. If you create a beforeGetValue method for a field with a space the designer will write:
   function Full Name_beforeGetValue

   return
Since a function name can not contain a space, the above method will cause an error. To avoid the problem use field names without spaces for your calculated fields.

This line will call the calcField method and pass "FullName" as the field name and 30 Characters as the field length.

Save this datamodule. We now will exit dQuery/Web and reopen the CUSTOMER.DMD datamodule in order to initialize the new field. You should do it now -- close dQuery/Web and reopen CUSTOMER.DMD.

Next we will use the beforeGetValue event to actually perform the calculation.

Select the CUSTOMER1 query object.

Right click the mouse and select Inspect Rowset.

We need to "drill-down" in the inspector to the new field object. So find "Fields" in the properties list and click the "I" (Inspect) button. In the Fields Array find the "FullName" object (this is our calculated field) and click the "I" button.

Next click the Events tab and then select the beforeGetValue event.

Click the wrench button and enter the following code:

   function FullName_beforeGetValue
      local a
	  a = trim( this.parent["first name"].value ) + " " ;
         +this.parent["last name"].value
      return a

For those who like to economize on their code, the above method could be alternatively written as:
   function FullName_beforeGetValue
      
      return trim( this.parent["first name"].value ) + " " ;
         +this.parent["last name"].value

You can now look at the results of the calculation in dQuery's lower pane. Scroll to the far right of the data grid. The last column should be labeled "FullName" and the data should read as first and last name combined.

Now save the Customer datamodule. We are done with creating our first calculated field.

After All That, So What?

Ok, what you just did was set up re-usable modules that can work for both forms and reports, and have, in some cases, lookups defined (which in earlier versions of dBASE took a lot of code). In one case you have a set of three related tables set up ... not too shabby for a small amount of work. The best part is the "reusable" part. Most of these datamodules will work for forms and reports (and labels), and while this application will not need them for this, we could use the same datamodule on different forms ...

The other feature that shows up through out the product (dB2K, that is) is the ability to create a datamodule (or form or report) and inherit from that, which gives you true object orientation. (You do not have to use custom datamodules, forms, or reports, that is just the main way of doing things ... All datamodules, forms, reports and other objects can be subclassed, not just custom ones.)


Proceed to the next part of the tutorial: Creating Custom Components
Go back to the tutorial menu

The Legal Stuff: This document is part of the dB2K Tutorial created by Ken Mayer. This material is copyright © 2001, by Ken Mayer. dB2K is copyrighted, trademarked, etc., by dBASE, Inc., the BDE (Borland Database Engine) and BDE Administrator are copyrighted, trademarked and all that by Borland, International. This document may not be posted elsewhere without the explicit permission of the author, who retains all rights to the document.