How to Work With
Multiple Page Forms in dBASE

Last Modified: January 22, 2004
Ken Mayer, Senior SQA Engineer
dBASE, Inc.

Example files available in mpforms.zip


The purpose of this document is to discuss multiple page forms and how to use them. This is a fairly simple topic, but is one that for a new developer using dB2K can be a bit confusing.


NOTE: This document was originally written for Visual dBASE 7.0/7.01, it has been updated for dB2K and dBASE Plus to include information about new properties, events, etc., and any new controls since the document was first written. If a control is new it will be noted.

In addition, this document refers to dBASE a lot, but unless it is about a dBASE Plus specific aspect, the text can be used for Visual dBASE 7.0 through Visual dBASE 7.5 and dB2K.


What Is a Multiple Page Form?

A Multiple Page Form in dBASE is a single form that can have more than one page of information. In the dBASE/DOS world, this was done with multiple forms. In dBASE, this can be done with one form. (Although creating a 10 page form may not be a good idea in dBASE due to Windows resource exhaustion ...)

There are at least three methods that can be used to create and work with Multiple Page Forms (using Tabs, using Pushbuttons, other programmatic means -- we will not be discussing the latter here), this HOW TO document will discuss two of them. We will not be discussing the use of the Notebook control, which is a visual control that can be used in addition to the discussions here to show multiple pages of a container (not of a form). Do not let this document limit your imagination. dBASE is a powerful developer's tool, and the chances are good that there are more ways than those discussed here to accomplish your task.


Some Basics

dBASE forms and objects have a property that did not exist in dBASE 5.0 for Windows or the dBASE/DOS products: PageNo. This property is vital to the task we have at hand.

The Page Number (PageNo) property defines for a form what the current page number is. This can be changed programmatically with the statement:

    form.PageNo := 2

for example.

Objects have a PageNo property, which defines what page they should appear on. As an example, an entryfield that was assigned to Page 2 would have its page number property defined along the following lines:

class ENTRYFIELD1(parentObj, name) of ENTRYFIELD(parentObj, name)
   with (this)
      height = 1
      left = 1.2857
      top = 0.7273
      width = 10.7143
      metric = 0	// Chars
      pageno = 2
   endwith
endclass

Importantly: since the default page of any form is page 1, any object placed on Page 1 normally will not stream out a "pageno" property in dBASE for page 1.

The PageNo property can be manipulated as any other property. However, in most cases, you will want to leave a specific object on a specific page.

In some cases, you may wish an object to appear on all pages of a form. This, as it turns out, is just as easy as the rest -- if you set the PageNo property to 0 (the number zero), that object will appear on all pages of the form.


Changing Pages -- Method 1 -- TabBox

Changing Pages is where the developer can have a lot of fun. Somehow, during the time that the form is executing, the user must be able to change from one page to another.

The TabBox Control is a dBASE control, and is designed to allow you to create and manipulate multi-page forms easily.


NOTE: The Tabbox control is not limited to working with multiple page forms. It is possible to have multiple tabbox controls on the same form, with various code associated to do things ... Once again, dBASE is a powerful language/software package, and if you can think of a use for the Tabbox control the chances are you can use it for that ...

In the "Standard" page of the Component Palette when designing a form, you will find an icon for the TabBox. If you click on it and drag it to a form, a single-tab tabbox will be placed on the bottom of your form.

The TabBox should default to having a PageNo property of 0. If it does not have this number, please set it now. You can assign a name to the TabBox if you want. The default is usually TabBox1, which is what this document will assume.

You need to set the tabs for this yourself. If you look at the DataSource property, you should see: ARRAY {"TabBox1"}.

This brings up a topic of interest that we will briefly examine (arrays are covered in more detail in other HOW TO documents):

Literal Arrays The default array type for a TabBox is what is called a Literal Array. This is an array that may be defined with the visual designer. The one thing about it that is a little disconcerting is that it has no name. There is no way to refer to the contents of this array. In most cases, when using a TabBox, this is absolutely fine! However, you don't have to use a literal array if you don't want to. You can use a more standard dBASE Array instead (one with a name) -- the only reason I can think of for doing this would be if you needed to know the text of the tab the cursor is currently on, but there could easily be other reasons.
The visual designers allow you to build your array through a really spiffy dialog box. To get to it, click on the tool icon next to the current array definition. You can add and remove and rearrange items quite easily with this dialog, and when you're done the "OK" button will place the contents of the array on the form. The number of tabs on your tabbox is the same as the number of elements in the array.

The OnSelChange Event

In the inspector, if you click on the events page, you should see this event listed. Click on it. You can add code to be executed here in a code block, or you can get a bit more fancy. I recommend you get a bit more fancy, and call a procedure. This is easiest done by simply clicking on the tool button in the inspector for this event. The form designer will bring up the source editor, and place a heading for the event, and a place to put your own code:

   PROCEDURE Tabbox1_onSelChange

In this procedure, you will want to set the form's PageNo to the current tab number. The current tab number property is: CurSel. The procedure might look like:

  PROCEDURE Tabbox1_onSelChange
     form.PageNo := this.CurSel

The reason to put this into its own event handling code, is that you may wish to get more fancy (such things as perhaps checking to see which page is now the current one, and setting specific values for objects on the form, maybe changing the form's rowset to a new one ... lots of possibilities spring to mind ...).


Changing Pages -- Method 2 -- PushButtons

While TabBoxes are the method of choice for most new applications, there are other methods of changing pages. Another method that can be quite effective is to use PushButtons.

If you drop a couple of pushbuttons on a form, you will need to set the OnClick events to handle changing page numbers. This is simple enough (example below).

You should set the OnClick event for both of these buttons to handle changing the page number, but since there are some other things that may need to be done as well, it is recommended that you call an event, rather than using a codeblock.

One suggestion is to simply create an event for each that handles incrementing and decrementing the page number, and then calls another routine to handle anything else that may need to be accomplished when you change pages (such as setting the focus to the appropriate object on the appropriate page).

The following is suggested code (it assumes your pushbuttons are named "NextPage" and "PreviousPage"):

   PROCEDURE NextPage_OnClick
      form.PageNo++                // increment the page number
      class::MyPageChange()        // procedure to handle
                                    // other items necessary

   PROCEDURE PreviousPage_OnClick
      form.PageNo--                 // decrement the page number
      class::MyPageChange()         // same same

The OnClick description for the pushbuttons would then be (in the Property Inspector, Methods page): class::NextPage_OnClick or: class::PreviousPage_OnClick.

When navigating through a form with several pages, you might have a problem if you simply left the pushbutton code alone. What if you are on Page 1, and the user clicks on the "Previous Page" button? There is no Page 0 (well, there is, but it's usually pretty ugly, because it contains all objects on the form). What if you are on the last page, and the user clicks on the "Next Page" button? If there is no page defined, the form will display a blank page.

This can be handled in the MyPageChange procedure by enabling or disabling the pushbuttons as needed:

   PROCEDURE MyPageChange
      if form.PageNo == 1
         form.PreviousPage.Enabled := false
      else
         form.PreviousPage.Enabled := true
      endif

      if form.PageNo == 3  // assume page three is the last number
         form.NextPage.Enabled = false
      else
         form.NextPage.Enabled = true
      endif

These two if/endif statements could be refined a bit using the dBASE IIF() function, but I wanted to show the longer version of the code for clarity's sake.

Here's the short-hand version:

       form.PreviousPage.Enabled := iif(form.PageNo==1,false,true)
       form.NextPage.Enabled     := iif(form.PageNo==3,false,true)

SUGGESTION: In order to avoid hard-coding a constant value like the last page number in your code directly, is to use a method of the form itself, used to display the highest page number that has any components on it:

       form.pageCount()
   

To use this in the code described above, try:

       form.NextPage.Enabled := iif(form.PageNo==form.pageCount(),false,true)
   

The advantage to doing this is that if you add pages to the form later (or remove them) you do not have to update the code to handle it.


The MyPageChange Procedure

The MyPageChange procedure is where you tell dBASE what to do when the page number changes. For example, you might want to set the focus to the first object on each page:

  PROCEDURE MyPageChange
     // if using pushbuttons to move between pages:
     form.PreviousPage.Enabled := iif(form.PageNo==1,false,true)
     form.NextPage.Enabled     := iif(form.PageNo==form.pageCount(),false,true)

     // handle setting focus and anything else that might be
     // needed when the page number changes.
     do case
        case form.PageNo == 1
             form.entryfield1.setfocus()
        case form.PageNo == 2
             form.entryfield43.setfocus()
        case form.PageNo == 3
             form.entryfield71.setfocus()
     endcase

VALIDATION

You may wish to process validation of your entryfields and other objects on a page, when you issue a page change. This could be handled here in the PageChange procedure as well.

The code may look something like:

    if form.Tabbox.Cursel # 1 and empty(form.entryfield1.value)
       MsgBox('Must Enter Client # First!')
       This.CurSel := 1
       form.entryfield1.SetFocus()
    else
       form.pageno := this.cursel
    endif

Potential Problems

It is probably not a good idea to add data in other tables on other pages if a master record on the first page has not been saved. One solution is to prevent users from changing pages until the master record on the first page (or where-ever) has been saved by turning off the enabled property of a tabbox (or if using pushbuttons to navigate, the pushbuttons involved) until the record has been saved. Note that with the OODML database and query objects in dBASE, this may not be as much of a problem as it was in earlier (5.x) versions of Visual dBASE.

When saving a form that uses multiple pages in the forms designer, you may find that if you were working on a page other than page 1 when you saved the form, that the page number you were working on got streamed out to your .WFM. When you run the form the next time, you will not start on Page 1. The fix for this is to ensure that no matter what page you were working on, when the form opens, it starts on page 1. This is best done in an OnOpen method for the form:

   PROCEDURE form_onOpen
     form.Tabbox1.curSel := 1 // this will fire the onSelChange
                              // event of the tabbox
     // any other code you need 

Summary

Briefly, the flexibility of dBL is such that you can probably find all kinds of reasons to use multiple page forms. You can do it using techniques shown here, you can programmatically change to a "hidden page" to show the user information under specific circumstances, and so on ... like always, if you can think of it, chances are pretty good that you can do it in dBASE.


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
    


Sample Code

In the .ZIP file that this came in there should be two simple forms that can be examined that show some of the techniques mentioned in this document. These are "MULTIPG1.WFM" and "MULTIPG2.WFM". These are simple, but the concepts shown here are covered in these forms.

EoHT: MULTPAGE.HTM -- January 22, 2004 -- KJM