NOTE: This document was originally written for Visual dBASE 7.0/7.01, it has been updated for dB2K (release 1) to include information about new properties, events, etc., and any new controls since the document was first written. There have been very few changes for later versions of dBASE. If a control is new it will be noted. In updating this document, the images were left "as is" unless it was felt to be absolutely necessary to change them ...
In addition, this document refers to dB2K a lot, but unless it is about a dB2K specific aspect, the text can be used for Visual dBASE 7.0 through Visual dBASE 7.5.
The idea is that the student will follow along, and create a form that uses one of the sample tables that ships with dBASE. The form will be tested along the way, so that the student can see their progress ...
This document will NOT get into working with custom forms, custom controls, and so on. There are other HOW TO documents that cover these in detail, and when developing a real application, the student should examine those documents (available from the same location that this was downloaded from). There is also a Tutorial available from the same site that gets into creating a complete application using custom forms, custom controls, etc.
Suggested Reading
It is suggested that a student coming to dBASE from earlier versions of dBASE
(dBASE/DOS, Visual dBASE 5.x, etc.) examine "XBase to OODML" HOW TO in the Intermediate
section of the Knowledgebase to get a good feel for the differences between
XBase commands and the new OODML. It is also suggested that a student coming
to dBASE from ANY platform, or as a new developer, take a good look at
"Beginning Data Objects" -- a HOW TO document that takes a very detailed look
at how the data objects in dBASE work and how they are related to each other.
Before doing anything, load dBASE. You should see the Navigator window -- this has a variety of options. Before we do anything else, we need to set ourselves to a working folder. The simple version of doing this, for this project, is to set your working directory to the SAMPLES directory that ships with Visual dBASE.
To do this, click on the button with a folder on it, next to the "Look In" drop-down (combobox). A special window will appear that allows you to work in the directory/folder structure on your hard drive(s). Find the "Plus" folder, and under it (double-click on it), you should see the "Samples" folder. Double-click on that folder, and click the "OK" button.
You will see a variety of files in the navigator -- ignore them, as we will not be using them.
You should see a new form appear as shown in the image below:
With the blank form on the screen, you will also see a few "palettes" ... we will use these in a moment. However, next we need to put a table reference on the form, so that the form knows what table we are working with.
Click on the table you wish to use, and hold the mouse button down -- drag the icon to the form surface. This is called "Drag and Drop".
Now, let's examine what just happened ... you have placed an icon onto the form, and your form should now look like:
In addition, when this was done, the form's property "rowset" was set automatically. This is very important to note, as this property gets used a lot -- it refers to the rowset object.
To see that this was done, click on the Inspector -- this is one of the palletes on the screen. The inspector is very useful as it shows you what is going on with the objects, and allows you to change the objects. We will be using it a lot. If you do not see the inspector, right click on the form's surface, and in the popup menu that appears, select "Inspector". It should appear on the screen. You can also turn the inspector on/off by using the <F11> key.
Find the rowset property of the form in the Inspector. (This will be under the "Miscellaneous" heading, if you are using the categories -- if you see a "+" next to the word "Miscellaneous", click on the word, and it will expand to show a bunch of properties ...)
In the rowset property of the form, you should see something like: form.fish1.rowset ("fish1" may be different, depending on the table you dragged to the form's surface).
Now, it may not look like much, but you have actually done quite a bit ... the form now has a primary table to work with ... from here, you can do a lot, and we'll start looking at some of this.
The field palette contains a list of the fields in your table, and by default shows icons for each, the icons showing the type of control that will be placed on the form (entryfields, etc.).
For our purposes, drag a few fields -- it doesn't matter which -- to the form surface. Make sure you have at least one or two entryfields, as we need to be able to actually edit some of the data for testing later on.
The image shown as an example uses all of the fields in the "Fish" table ... note the colors -- this was done automatically because the table, when it was created by the "Borland Samples Group" has some custom properties for the fields -- including setting a "colorNormal" property to the colors shown here.
An important property that is used for all "datalinked" controls (controls that are linked to data in the rowset) is the dataLink property. To see this, click on one of the entryfield objects, and in the inspector click on the "DataLink" property (this is under the "Data Linkage" category). You will see something like: form.fish1.rowset.fields["Name"]. This was done for you by dragging the object from the Field Palette to the form's surface. You can also do this by hand, or by dragging an Entryfield from the Component Palette, and then setting the datalink property in the inspector yourself.
To see this, run the form -- you can do this by clicking on the button in the toolbar at the top of the screen that shows a lightning bolt. Before you can run the form the first time, you must name it -- in the dialog box, type "TutorialForm" and click the "Save" button. The form will now run.
With the form running, the toolbar at the top of the screen changes, and you will see some navigation buttons ... you can also put the cursor in an entryfield and change the data. There are other buttons in the toolbar that will change if you change the data, there's an "Add" button to add a row (record) to the table, and so on.
However, what we want is to be able to do this kind of stuff ourselves. In a real application, you may not want the user to see the IDE (Interactive Development Environment), so ...
To go back to development mode, click on the button next to the lightning button (the one that shows a ruler, a triangle, and a pencil) ...
These buttons can be done in quite a few ways, but we're going to do simple text, no images. If you want to do more, you can spend a bit of time tinkering ... but for our purposes, we want to keep it as simple as possible, as we are concentrating on getting these objects communicating with the rowset ... we're not as concerned with esthetics ...
We'll start with the "First" button. Set aside some space on your form for buttons (move the objects around, make the form later, whatever ...) so you have room for a few different buttons. In the example, I will place them down the right side of the form ... there are many ways to do this ...
You should see a "Component Palette" on the screen in the design mode. If not, right click on the form's surface, and select this -- it should now appear.
The "First" Button
On the "Standard" page of this palette, you will see a variety of controls.
If you are not sure what one is, place the mouse over it, and a speedtip will
appear, telling you what the object is. We want a pushbutton -- click on the
pushbutton object and drag it to the form's surface. You can move it around
to wherever you need it to be ...
The text of this button defaults to the name of the object, which is "Pushbutton1" -- we are going to change the name of the button as well as the text, because if you have a lot of pushbuttons, it gets confusing working with them if they are simply "Pushbutton1", "Pushbutton23", etc.
So, with the pushbutton having focus (if it doesn't, click on it), go to the inspector, and find the property "Name" -- it will be under the category "Identification". Click on the property, and on the right side, type "FirstButton" and press the <Enter> key.
Next, find the "text" property, which will be under the "Miscellaneous" category. Click on "text" and enter "First" and press <Enter>.
You should see the text on the pushbutton change to "First" on the form ...
Now, at this point in time, the pushbutton will not do anything when the user clicks on it ... so we must add some code to the button. The code for this button will be pretty simple ... but the question is, where do we assign the code?
Look at the inspector -- you should see an "Events" tab -- click on this. When you do, you should see a set of "Events" -- you can assign code to any of these events. It just so happens, with pushbuttons, that the first event listed is the one we want to use to assign code when the user clicks the button with their mouse.
Click on the "onClick" event. Where it says "Null" you should also see two small buttons, one shows the end of a wrench, the other shows a "T" for "Type". We want to use the wrench (or tool). Click on this, and you will see a small editor window, with the following:
function FIRSTBUTTON_onClick return
Any code you want to be executed when the user clicks on the button goes after the "function" statement, and before the "return" statement.
The code we want is quite simple. We want to go to the first row in the rowset when the user clicks on this button. To do this, we have to execute a method of the rowset, called "first". To do this, we type the code:
form.rowset.first()
What this does is it goes to the form object, and looks at the rowset property -- if no rowset is assigned to this property, this code will not work. However, we did that when we placed the table reference (the query object) on the form earlier. Assuming that a rowset is assigned, it then executes the "first" method (the parentheses are necessary to execute the code).
That's all there is to it. So you should see now in the editor window:
function FIRSTBUTTON_onClick form.rowset.first() return
To test this out, we need to run the form. So click on the lightning button. With the form running, we need to move to a different row (we start at the first one by default). Use the navigation buttons at the top of the screen in the toolbar, to move a few records into the rowset (use the 'next' button). Now, rather than using the button in the toolbar, use the button we just created on our form -- click on it, and you should find yourself back at the first row in the table.
Now that we have created one pushbutton, the steps should be easier for doing more ...
The "Next" Button
We need to add the "Next" button, so do the following steps:
Now we get to the more interesting part of this. The code can be very simple, and we will do the simple version first ...
Enter the code:
form.rowset.next()
In the editor, so that your onClick event code looks like:
function NEXTBUTTON_onClick form.rowset.next() return
Like before, let's test this ... run the form (click on the "lightning" button), and use the "Next" button to go to the next row. Keep doing this until you get to the end of the table (you'll know when it happens ... you'll get a a blank row!!).
Now, what happened here? You are pointing to the "end of rowset" pointer, and all the fields went blank. You don't really want your users to do that ... they might think this is a valid row, and try to enter data ... this won't do at ALL!
So, how do we deal with it? How do we, rather than display the "end of rowset" display an error message for the user?
We need to modify the code, obviously. Go back to design mode (the button in the toolbar next to the lightning button).
The next() method of the rowset object returns a logical value (true or false) -- if next() is successful, the method returns the value "true", if it wasn't we return the value "false". We can check for that in our code ... if we get a "false" value, we don't want to be at the end of rowset (endOfSet -- we'll look at this later), so we want to tell the user that they cannot continue, and display a message.
What we want to do is to try to navigate using the next() method of the rowset, and if the method returns "false", it means we hit the "end of rowset", so we need to back up one row (we can do this using the next() method, and use the optional parameter for the number of rows to navigate, in this case we will use "-1" -- the negative sign means to go backward). We will then display a message to the user that says we can't do this. The code is actually fairly simple:
if ( not form.rowset.next() ) form.rowset.next( -1 ) msgbox( "At end of rowset", "Can't Navigate", 64 ) endif
To do the above, change the code for the "NextButton" by clicking on the button in the inspector, click on the "Events" tab, click on the "onClick" event, and then the Tool button. Change the code in the editor so it looks like the following:
function NEXTBUTTON_onClick if ( not form.rowset.next() ) form.rowset.next( -1 ) msgbox( "At end of rowset", "Can't Navigate", 64 ) endif return
For details on the use of the msgbox() function, see online help -- it gives a lot of detail.
Now to test this, run the form again, and try navigating past the last row, and you should get the error message we created, and we will not see the end of rowset ...
The "Previous" Button
We need to add the "Previous" button, so do the following steps:
Enter code like the following:
function PREVIOUSBUTTON_onClick if ( not form.rowset.next(-1) ) form.rowset.next() msgbox( "At beginning of rowset", "Can't Navigate", 64 ) endif return
Note that this code is almost, but not quite, identical to that used for the "Next" button ... it works very much the same, except we are going in the opposite direction. The "if" statement checks to see if we can navigate "backward" (if you will) through the table, and so on ...
You can test this by running the form, clicking on it, and seeing the error message, navigating with the "next" button and then the "previous" button ... and so on. Then come back to the designer ...
The "Last" Button We need to add the "Last" button, so do the following steps:
Enter code like the following:
function LASTBUTTON_onClick form.rowset.last() return
One more time, you can test this by running the form, then clicking on the button -- it should take you to the last row in the rowset ... when done bring the form back to the designer ...
Your form may look something like this:
The first thing we need to note is that by default, a form's rowset is in "edit" mode -- meaning that when a user clicks on a field and types something, they are editing the data. This could mean that a user might accidentally change something that they didn't want to.
What's worse is that if they close the form at this point, the changes made to the row will be saved ... If they click on a navigation button, the changes will be saved. SO ... what can we do?
There is a property of the rowset object called autoEdit -- this defaults to true. It means that the row is automatically in edit mode. If you want to keep the user from automatically editing the row, the simple solution is to set this property to false.
To change this property, we need to modify it in the inspector, but to get there we must go through the query object ... so, click on the query object (the "SQL" icon on the form), and then go to the inspector. Click on the "rowset" property (it will say "Object"), and then click on the "I" button ("I" is for "Inspect"). You should see the autoEdit property (under the "Miscellaneous" category), and it should show as "true". Double-click on it, and it will change to "false".
To see if it works, run the form again. Click on one of the entryfields, and try to type something ... note that you cannot. This is because of the autoEdit property being set to false.
Of course, we will want to allow the user to edit a row ... otherwise what's the point? However, we'll start with a button to allow the user to add a new row to the table ...
Add Row Button
We need to add the "Add Row" button, so do the following steps:
Enter code like the following:
function ADDROWBUTTON_onClick form.rowset.beginAppend() returnNote that when your user clicks this button, the form will clear -- all objects that are "datalinked" will suddenly be empty, so that the user can add new data to a new row.
What you (or the user) have done at this point is created a blank row -- it is not real, however, until the user saves the row. Until they save it, the new row is in what is called a "buffer". The buffer is important, both here and in editing mode ... The user can save the row by using a "save" button (that calls the save() method of the rowset), or by navigating in the rowset (using the buttons we created earlier) ... either way, the row will be saved.
Another effect of the user clicking on the "Add Row" button is that there is a property of the rowset called state that gets changed. We will take a look at the state property later.
We can test it right now, but let's move on and add the other editing buttons first, and then we can test the series ...
Edit Row Button
We need to add the "Edit Row" button, so do the following steps:
Enter code like the following:
function EDITROWBUTTON_onClick form.rowset.beginEdit() return
The beginEdit() method of the rowset allows the user to modify the copy of the row -- the user is not editing the actual data -- as soon as the row is saved, the copy in the buffer is written back to the actual row making the changes in the table. This is important, because there is an abandon() method (which we will see soon) that allows the user to abandon their changes to the row.
Like with the "Add Row" button, clicking this button (or running the code) sets the rowset into a special "state" (again we'll look at the state property later). In addition, if the user changes the row in any way, we set a property of the rowset called modified to true (it defaults to false). This property can be quite useful, and we'll come back to it later ...
Delete Row Button
We need to add the "Delete Row" button, so do the following steps:
Enter code like the following:
The problem with deleting a row in a table in dBASE, using the OODML (as we are doing here) is that the row is not easily recoverable. To all intents and purposes, it is completely gone ... You may want to ask the user if they really want to delete the row by adding a quick dialog box that asks. If the user clicks the "Yes" pushbutton, then go ahead and delete the row.
function DELETEROWBUTTON_onClick if msgbox( "Delete this row?", "Delete Row?", 36 ) == 6 form.rowset.delete() endif return
Save Row Button
We need to add the "Save Row" button, so do the following steps:
Enter code like the following:
function SAVEROWBUTTON_onClick form.rowset.save() return
Abandon Changes Button
We need to add the "Abandon Changes" button, so do the following steps:
When abandoning changes, you may want to ask the user first ... if adding a new row, abandoning would release the new row in the buffer. If editing a row, abandoning changes the copy of the row in the buffer back to what the row looked like before editing the row. A person may have done some real work to get all the data just right, and accidentally hitting the abandon button could wipe all that out ...
Enter code like the following:
function ABANDONBUTTON_onClick if msgbox( "Abandon changes to this row?", "Abandon changes?", 36 ) == 6 form.rowset.abandon() endif return
At this point, your form may look like:
The modified property is used when a row is placed into edit mode, to determine if the row's buffer has been actually modified by the user. You can check the value of this property easily in your code by simply using:
if form.rowset.modified // do something endif
This can be quite useful in situations where you need to check it. For example, what if the user hits a navigation button after modifying a row? Do you want to just assume that the user wants to save the changes made? Wouldn't it be better to ask? You can do that by adding code to your navigation buttons to check for this. (We won't do it for the example here, but it's a good idea ...)
In addition, if you have code that executes when a user does some task that modifies the code, your code does not set the modified property to true. Therefore, you may need to set this property to have the rest of your form's code work properly. This is done by simply assigning a value:
form.rowset.modified := true // or false if you need it to be false
The state property is one that is constantly changed, based on what state the rowset is currently in. There are six different possible states: closed, browse, edit, append, filter and locate. By default, your rowset is in browse mode, but if your user modifies the data, they are in edit mode, if they are adding a new row, you are in append mode, and so on. "Closed" means that the query object's active property is false.
What does this mean? Well, it means that like the modified property, you can query it. However, the different modes are determined by numbers, starting at the number 0, and going to 5. So, the modes are:
0 | Closed |
1 | Browse |
2 | Edit |
3 | Append |
4 | Filter |
5 | Locate |
One of the problems with the way the form works when it is running is that it is difficult to tell what "state" the rowset is in. You might want to add a text object to the form that shows this state, or you might want to use a calculated field that updates an entryfield to show the state. The advantage to using a calculated field is that this is automatically updated, where a text field would need to be updated by your own code. The following is a simplified version of this, based on work by Gary White (one of dBASE, Inc.'s own dBVIPS). The steps are provided to make it as easy to create as possible.
// if we haven't already defined the calc field if type('form.rowset.fields["Rowstate"]') # "O" // do it here local f f = new field() // create it f.states = new array() // build an array with the // possible states f.states.add("Closed") f.states.add("View") // Gary's says "Browse" f.states.add("Edit") f.states.add("Append") f.states.add("Filter") f.states.add("Locate") f.fieldName := "Rowstate" f.beforegetvalue := ; {;return this.states[ this.parent.parent.state + 1 ]} form.rowset.fields.add( f ) endif this.datalink = form.rowset.fields["Rowstate"]
Now, after all that, try running the form ... click the "Add Row" button, and the word "View" should change to "Append". Click the "Abandon" button (select "Yes" to abandon). Click the "Edit Row" button, note that the rowstate control shows "Edit" ... Pretty spiffy. Click the "Abandon" button again (and select "Yes" again ...).
Once the criteria is entered, the user would need to then apply the filter, which could be done either with the same pushbutton, or with another ... this would tell dBASE to find all rows that match the condition(s) specified and only display those in the form -- the user could move through the table looking at those.
Finally, you would to have a method of clearing the filter condition ...
The following is based on code that is much more complicated in the dBASE Users' Function Library Project (dUFLP) -- this code was compiled from several sources and has had various developers put their touch on it ... I am simplifying it for this example ... All of the following is for a single pushbutton ...
Filter Button
We need to add the "Filter" button, so do the following steps:
function FILTERBUTTON_onClick do case // we are in "beginFilter" mode: case this.text == "Filter" // change Text this.text := "Run Filter" // save current bookMark so we can return // to it: form.bookMark = form.rowset.bookMark() // set form to "filter" mode form.rowset.beginFilter() // we are in "Run" mode (applyFilter() ) case this.text == "Run Filter" // don't update the controls until told otherwise form.rowset.notifyControls := false // try the filter -- if we don't find a match // we'll clear it out ... try // catch error about memo: if not form.rowset.applyFilter() // reset the text back to "Filter" this.text := "Filter" // clear the filter form.rowset.clearFilter() try //attempt to go back to the 'bookMark' // if we can't, we go to the first row // in the table form.rowset.goto( form.bookMark ) catch ( exception e ) form.rowset.first() endtry // let the user know that no match was found msgbox( "No match found", "Filter error", 48 ) else // otherwise the filter worked, // so now we want to change the image to // a "clear Filter" ... this.text := "Clear Filter" endif catch ( dbexception e ) msgbox( "Can't filter on a memo field", "Filter error", 48 ) form.rowset.goto( form.bookmark ) this.text := "Filter" endtry // turn this back on form.rowset.notifyControls := true // and actually refresh to show the first row // that matches the filter form.rowset.refreshControls() // clearFilter() case this.text == "Clear Filter" // reset text this.text := "Filter" // clear out the current filter ... form.rowset.clearFilter() endcase return
Before testing this, we need to do one more thing, and that is to set the rowset's filterOptions to allow as much flexibility as possible ... to do this, click on the query icon on the form, and in the inspector, click on the rowset object, then the "I" button. For the rowset object, then click on filterOptions, and select "3 - Match Partial and Ignore Case". If you do not do this, then the default for the filter is to match the length and match case, so the filter may not find a match easily.
Now that this is done, try it out ... try filtering on something you know exists, on something you know doesn't exist, etc. It works pretty well. Note that the Rowstate control automatically updates ...
Other Methods of Filtering
Data
You can get more specific and write code that will handle filtering the data,
and more. There are problems with "Filter-by-Form" mode, and they include the
fact that the filter assumes "equality" -- you cannot specify conditions such
as "less than" and so on, and you cannot use "and", "or" type of operations.
For that you would need to create a more complex form ...
Just like the filter button shown above, the code is a bit complex, but this is still simplified quite a bit from the code in the dUFLP custom code ... we'll discuss some of this later.
Locate Button
We need to add the "Locate" button, so do the following steps:
// here's where all the work starts: do case // we're now in "locate by form" mode: case this.text == "Locate" this.text := "Run Locate" form.bookmark = form.rowset.bookmark() form.rowset.beginLocate() // we're in "Run" mode ... case this.text == "Run Locate" // if user hit 'run' but didn't // change anything on the form: if not form.rowset.modified this.text := "Locate" form.rowset.abandon() return endif // problem with doing a locate on // memo fields: try // the first 'try' // here's the search: form.rowset.notifyControls := false if not form.rowset.applyLocate() this.text := "Locate" try // second/nested try form.rowset.goto( form.bookMark ) catch ( exception e ) form.rowset.first() endtry msgbox( "No rows match the condition", "Locate error", 48 ) else // we found a match ... // "Continue" text: this.text := "Continue Locate" // save *this* bookmark ... form.bookmark = form.rowset.bookmark() endif catch ( exception e ) msgbox( "Cannot search in memo", "Locate error", 48 ) form.rowset.goto( form.bookmark ) this.text := "Locate" endtry // end of catch for memo form.rowset.notifyControls := true form.rowset.refreshControls() // we're "searching again" case this.text == "Continue Locate" // look for the next match: if not form.rowset.locateNext() form.rowset.goto( form.bookmark ) msgbox( "No more rows match the condition", "Locate error", 48 ) this.text := "Locate" else form.bookmark = form.rowset.bookmark() endif endcase
Just like with the filter, the locateOptions property of the rowset will affect your ability to find a matching row or rows in the rowset. And, just like the filterOptions property, setting the locateOptions property for the rowset to "3 - Match Partial and Ignore Case" will solve this and make it possible to find most matches.
The locate option is rather handy, but it has similar limitations to what were mentioned above with filters -- if you need a more complex "locate" you would have to write your own code ...
Wow. After all that, hopefully you're starting to get a feel for how all this works ...
Close Button
We need to add the "Close" button, so do the following steps:
function CLOSEBUTTON_onClick form.close() return
You could check the state of the rowset, and if the user is in edit or append mode, ask if they want to save it ... there's a lot more that you might want to do. This is just a beginning ...
In addition to the HOW TO documents that are available in the Knowledgebase, you may also be interested in the dBASE Users' Function Library Project (dUFLP) -- this is a library of freeware code donated by many developers over the years. The code includes a set of pushbuttons that can be used with forms, and a series of custom controls, and a lot more.
DISCLAIMER: the author is an employee of dBASE, Inc., but has written this on his own time. If you have questions regarding this .HOW document, or about dBASE you can communicate directly with the author and dBVIPS in the appropriate newsgroups on the internet..HOW files are created as a free service by members of dBVIPS and dBASE, Inc. employees to help users learn to use dBASE more effectively. They are edited by both dBVIPS members and dBASE, Inc. Technical Support (to ensure quality). This .HOW file MAY NOT BE POSTED ELSEWHERE without the explicit permission of the author, who retains all rights to the document.
Copyright 2004, Kenneth J. Mayer. All rights reserved.
Information about dBASE, Inc. can be found at:
http://www.dbase.com
EoHT: OODMLForm.HTM -- January 22, 2004 -- KJM