Project Notes


Project: dbSwing: Customized JdbNavToolBar Sample
Author: JBuilder Team
Company: borland.com
Description:
A JdbNavToolBar is customized with buttons that "drill down" (show detail records for the current row) and "roll up" (return from the details to the master dataset). A single JdbTable is used. It displays customers, orders for the selected customer, or line items for the selected order, depending on the "drill down" or "roll up" selection.


What this sample demonstrates

Customizing a JdbNavToolBar

Master-detail relationships

Working with JdbTable

JdbNavField component and other dbSwing tools

Setup

This sample requires that you have JDataStore installed and read/write access to the sample IntlDemo.jds JDataStore database in the samples/JDataStore/datastores directory. The sample tries to dynamically determine the proper URL to IntlDemo.jds, but since the location is dependent upon the location of your JBuilder installation, it might not always succeed. It writes a message indicating the URL which it is trying to use. If you are unable to run or design the sample, you should explicitly specify the URL to IntlDemo.jds correct for your installation in CustomToolBarFrame.java.

If you have not installed JDataStore, please install it before running this sample. Follow the instructions on Installing JDataStore in Developing Database Applications.

This sample borrows IntlDemo's data but it doesn't enforce IntlDemo's data consistency rules. If you add unknown product codes (in the SKU column) or product colors, insert orders without line items, or change quantities or prices without changing the corresponing order totals, you might affect how IntlDemo works. To prevent changes that might cause trouble when the IntlDemo application is run, we recommend that you insert new customer, order, or line items records, experiment with them, and delete them when you're through.

Customizing a JdbNavToolBar

In this sample we demonstrate a variety of ways that the behavior of dbSwing's JdbNavToolBar can be customized. We: Please read the section on master-detail relationships below along with the following explanations. Here we tell how we did what we did; there, we tell why we did it -- what master-detail functionality we wanted. Read both sections to get the full picture.

Hiding a button: To hide a button, just set the appropriate buttonState.. property of the toolbar to JdbNavToolBar.HIDDEN. The Ditto button isn't very useful in this sample, so we used setButtonStateDitto() to hide it throughout the application. When a button is hidden, the toolbar's layout does not allocate space for it.

Enabling/disabling a button programmatically: Instead of setting a button's state for the entire application, you might want to vary its state while the application is running. This sample doesn't do that, but you can imagine that we might hide the Ditto button for one data set but not for the others. Just decide in your code when to change the button's state and set it using the appropriate constant, such as JdbNavToolBar.ENABLED or JdbNavToolBar.DISABLED. To restore a button to its default state, use JdbNavToolBar.AUTO_ENABLED.

You might notice that the toolbar's Insert and Delete buttons are disabled for the Customer data set and enabled for the Orders and LineItems data sets. This is not achieved by setting toolbar properties. Instead, we set the Customer data set's enableInsert and enableDelete properties false. The toolbar automatically responds to these settings by disabling its Insert and Delete buttons when the Customer data set is its focused data set. If you want to prevent edits to existing rows as well, you can set the data set's editable property false, or set it's readOnly property true; again, the toolbar will prohibit inserts and deletes.

You shouldn't try to prohibit inserts and deletes by setting the state of toolbar buttons, for two reasons:

  1. Row operations that cannont be performed from the toolbar can still be performed through keystrokes or from JdbTable's pop-up menu.
  2. Other components that display columns from the same data set may still be able to insert or delete rows.
Setting these properties on the data set ensures that they're always enforced.

Overriding a button's operation: JdbNavToolBar provides a get..Button method for each of its buttons. To change the behavior of an existing toolbar button, get the button, remove the toolbar from the list of listeners to the button's actionPerformed event, and add your event listener in its place. In this sample, we have overridden the default behavior of the Save and Refresh buttons. Their new behavior is described below.

Adding a button: When you add a JButton to the toolbar, the button takes space in the toolbar's layout, but none of the toolbar's state or event handling applies to it. You treat it just like any other button, setting properties as desired and providing an actionPerformed event handler. Buttons you add to the toolbar might appear at the left of the toolbar in the UI Designer, but will be to the right of the toolbar's buttons at runtime.

We've added expand ("drill down") and collapse ("roll up") buttons. In their event handlers, they use two data set methods that provide information about master-detail relationships, getDetails() and getMasterLink(), to determine which data set should be displayed in the frame's JdbTable. (The collapse button's event handler also demonstrates one form of the little-known but very useful copyTo() method that DataSet inherits from ReadRow.)

When we change the JdbTable's data set, we cause the toolbar to send a change event for its focusedDataSet property. We listen for this event, check if the new data set has a master and/or details, and enable or disable the expand and collapse buttons appropriately. Here too, we examine the master-detail relationship using getDetails() and getMasterLink(); we never reference our specific data sets. As a result, this code is fully general.

Master-detail relationships

When two tables are linked by a master-detail relationship, only detail records that match the current master record are accessible. As you navigate the master data set, the accessible contents of the detail data set changes. It is as though the detail data set is refiltered each time you navigate to a different master.

One common UI design for a master-detail relationship displays the contents of a single master row in JdbTextFields and all its detail rows in a JdbTable. It is also possible to use two JdbTables, one for the master data set and one for the details. In this sample, we have chosen to use just a single JdbTable. Initially, it displays the master table, Customers. When you click the Expand button on the toolbar, the contents of the table changes to display the details -- from the Orders data set, in this case -- for the current customer.

More than two data sets can be linked together by master-detail relationships. For example, a master data set can have two or more detail data sets, or a detail data set can also function as a master for another set of details. The IntlDemo sample data contains a relationship of this second type: each row in Customers can have multiple details rows in Orders, and each row in Orders can have multiple detail rows in LineItems. One advantage of our UI design is that it is not limited to two data sets: when the table displays orders for a customer, you can click the Expand button again to display the details in the LineItems data set for the current order. However, this design won't handle a master that has several detail sets.

JBuilder offers two options for fetching detail sets. When the MasterLinkDescriptor's fetchAsNeeded option is false, all detail sets are fetched immediately. When it is true, a detail set is fetched only if and when its master is navigated to. If the detail data set is large and many master rows are never navigated to, fetchAsNeeded can save space and time. On the other hand, turning fetchAsNeeded off ensures that all data, for both master and detail, is consistent because it's all fetched in the same transaction. In this sample, fetchAsNeeded is true.

Save button: By default, the toolbar's Save button saves changes for the data set the toolbar is bound to (or "is currently bound to", if the toolbar is allowed to auto-detect its data set). When editing tables in a master-detail relationship, it is better to save changes to all data sets at once. This allows new rows in the master data sets to be inserted before their details are inserted, while details to be deleted are removed before their masters are deleted. To accomplish this, we override the default action of the Save buttons and call the form of Database's saveChanges() method that takes an array of data sets. This form of saveChanges() correctly orders changes to data sets in master-detail relationships. The Save operation has no visible effect, so you have to examine the application's code to know that we customized its behavior.

Refresh button: Our change to the Refresh button's behavior is more visible. Before refreshing, we check for unsaved changes to the current data set by calling its StorageDataSet's getDeletedRowCount(), getInsertedRowCount(), and getUpdatedRowCount() methods. We also check for unposted edits to the current row. If there are unsaved changes, we use a Swing OptionPane to ask for confirmation that they should be discarded.

Let's look at refresh behavior in a little more detail. When the fetchAsNeeded option is false for a MasterLinkDescriptor, detail sets for all master rows are fetched when the master data set is opened. Refresh logically does the same thing: it refetches all detail sets. When fetchAsNeeded is true, however, detail sets are not fetched when the master data set is opened. Each detail set is fetched individually, when its master row is first navigated to - or is never fetched, if its master is never navigated to. In this case, refresh refetches only the current detail set. So the meaning of refresh is very different with fetchAsNeeded true, but consistent with the way details are originally fetched. In both cases, refresh works on a single data set. The meaning of Save, by the way, does not vary according to fetchAsNeeded; it always saves all changes to the named data set or data sets.

Our code to check for unsaved changes is correct for the case where fetchAsNeeded is false. But in this sample, it's set to true. So our code is stronger than it needs to be: it tests for any changes to the data set when it only needs to test for changes to the current set of details. That would be a bit more work. Instead of getting a count of deleted rows, we'd have to get the deleted rows themselves in a data set, use locate to see if any row in this data set contained the linking column values of the current detail set, and repeat this test for inserted and updated rows.

Working with JdbTable

Multi-line headers, custom header font and color, hidden cols, column order, custom column widths?

Change one query to select all rows, use hidden cols to hide some; doc that we do this to demonstrate (e.eg, if you need hidden cols for computation or other programmatic work).

JdbNavField component and other dbSwing tools

JdbNavField: JdbNavField, labeled "Locate" in this sample, makes it easy to locate a value in a JdbTable. Tab to the column of the table to be searched, click in the JdbNavField or type Alt+L to set focus to it, and enter the value to search for. In a String column, JdbNavField does an incremental search, so it will begin to show possible matches immediately. You can continue typing to narrow the search or use Up and Down to move to other partial matches. In columns of other data types, enter a complete value, then press Enter to perform the search.

DBPasswordPrompter: When you're developing an application that accesses data on a server, you should include the user name and password to connect to the server in your ConnectionDescriptor's definition. However, when you distribute an applicaition, you want this information to be supplied by the user.

DBPasswordPrompter requests a user name and password, attempts to open a connection to its database using them, and returns a boolean result. Call DBPasswordPrompter's showDialog() method early, before visual components bound to data sets are displayed. We call it in CustomTooolBarFrame's constructor, just after jbInit(). If it fails, we exit; otherwise, the sample runs normally.

Other tools: The dbSwing samples demonstrate three valuable and easy-to-use dbSwing components: