11.4 Enhancing the RecordSet Class for Interactivity

Chapter 3 showed an enhancement to the RecordSet class that facilitated a user interface showing only one record at a time?a common way of displaying resultset data to the end user. The enhancement added the concept of a current record and provided methods to move to specific records (first, previous, next, last, and record number). To augment this functionality, let's implement a feature to associate a field in a recordset with user interface controls and other elements (DataGlue style).

11.4.1 The Current Record Functionality

The current record functionality was described in Chapter 3, but we'll add a few new methods to implement the gluing of components to individual RecordSet fields. The basic functionality that we will begin with is shown in Example 11-7.

Example 11-7. Adding current record functionality to a RecordSet
// Initialize the current record number
RecordSet.prototype.currentRecord = 0;

// Return the current record
RecordSet.prototype.getCurrentRecord = function ( ) {
  return this.getItemAt(this.currentRecord-1);
};

// Return the current record number
RecordSet.prototype.getCurrentRecordNum = function ( ) {
  return this.currentRecord
};

RecordSet.prototype.move = function (direction) {
  switch (direction.toLowerCase( )) {
    case "first":
      this.currentRecord = 1;
      break;
    case "previous":
      if (--this.currentRecord < 1) this.currentRecord = 1;
      break;
    case "next":
      if (++this.currentRecord > this.getLength( ))
        this.currentRecord = this.getLength( );
      break;
    case "last":
      this.currentRecord = this.getLength( );
      break;
    default:
      // Not a direction: must be a number
      this.currentRecord = direction;
  }
  this.recordChanged( );
};

The code is identical to what was shown in Chapter 3 under Section 3.6.3, with one exception: we added a call to this.recordChanged( ) in the move( ) method. The recordChanged( ) method will be described in the next few sections.

Remember, the currentRecord property contains numbers from 1 to the length of the recordset. However, recordsets use a zero-relative index, so we'll add or replace records based on the currentRecord - 1.

11.4.2 Adding the glue( ) and recordChanged( ) Functionality

DataGlue effectively binds a RecordSet object to a ComboBox or a ListBox. In those cases, you are populating the component with the entire recordset (or specific fields of a recordset). This is possible because ComboBoxes and ListBoxes display items in rows. But what about components or objects that don't support multiple individual items, such as a CheckBox or a text field? These types of objects come in handy when you're displaying only one record from a resultset. The recordChanged( ) method that we will create will change the components that are bound to the RecordSet object, but first we have to bind the fields. We'll add another custom method to the RecordSet class, called glue( ). This is the method that effectively binds a recordset field to a component or text field. The glue( ) method is shown in Example 11-8, along with the uiFields property that holds the fields to be glued from the RecordSet.

Example 11-8. The glue( ) method binds the component to the field
// Set up an array of UI components to bind to fields
RecordSet.prototype.uiFields = new Array( );

// The glue( ) method binds a control of controlType to a field
// controlTypes supported:
  //  "text"
  //  "combobox"
  //  "checkbox"
  //  "radiobutton"

RecordSet.prototype.glue = function (control, field, controlType) {
  // Create the uiField member as an object
  var controlObj = {};
  controlObj.control = control;
  controlObj.field = field;
  controlObj.controlType = controlType;
  // Replace the field if it is already defined.
  for (var i=0; i<this.uiFields.length; i++) {
    if (this.uiFields[i].control == controlObj.control) {
      uiFields[i] = controlObj;
      return;
    }
  }
  // If the field is not bound yet, add it to the array
  this.uiFields.push(controlObj);
};

You call the glue( ) method like this, passing the component (or text field), the name of the database field you want to glue to the component, and the component type ("text", "combobox", or "checkbox"):

RecordSetname.glue(component, databaseField, componentType);

Typical calls to glue( ) look like this:

// Glue the CategoryID field to the categories_cb ComboBox.
Products_rs.glue(categories_cb, "CategoryID", "combobox");
// Glue the ProductName field to the ProductName_txt TextField.
Products_rs.glue(ProductName_txt, "ProductName", "text");

The glue( ) method is one piece of the puzzle. After the field is glued to the component, you have to be able to update the component in the UI. This is handled by a recordChanged( ) method, shown in Example 11-9.

Example 11-9. The recordChanged( ) method
RecordSet.prototype.recordChanged = function ( ) {
  // Define variables to hold current field, component, and type of component
  var theField, theControl, theControlType;
  // The current record to be changed
  var record = this.getCurrentRecord( );
  // The uiFields property is an array of controlObj objects set up in glue( )
  var tempLength = this.uiFields.length;
  for (var i=0;i < tempLength; i++) {
    theField = this.uiFields[i].field;
    theControl = this.uiFields[i].control;
    theControlType = this.uiFields[i].controlType;
    switch (theControlType) {     // What kind of control is it?
      case "text":                // Text fields have the text property set
        theControl.text = record[theField];
        break;
      case "combobox":            // ComboBoxes use the custom pickValue( ) method
        theControl.pickValue(record[theField]);
        break;
      case "checkbox":            // CheckBoxes have the value set true or false
        theControl.setValue(record[theField]);
        break;
      default:                    // Other components not supported at this time
        trace(theControlType + " not supported")
    }
  }
};

The recordChanged( ) method cycles through the uiFields array, updating each UI component to display the data that is in the glued field of the current record. The methods have been implemented for TextFields, ComboBoxes, and CheckBoxes, but you can add functionality for any type of UI component.

Now, components and text fields can be glued to a field in a recordset, and the field will change when the current record changes. Next, the setCurrentRecord( ) method will update the current record in the recordset, based on what is in the fields that are glued to it.

11.4.3 The setCurrentRecord( ) Method

The UI is now capable of being updated as the user pages through the recordset. But TextFields, ComboBoxes, and other UI components might be changed by the user as well. We need a method to update the current displayed record directly from the fields that are glued in the recordset. The setCurrentRecord( ) method, shown in Example 11-10, accomplishes this.

Example 11-10. The setCurrentRecord( ) method updates the recordset
// Update the current record based on values in the glued components
RecordSet.prototype.setCurrentRecord = function ( ) {
  // Define variables to hold the current field, component, and type of component
  var theField, theControl, theControlType;
  // The current record to be changed
  var record = this.getCurrentRecord( );
  // The uiFields property is an array of controlObj objects set up in glue( )
  var tempLength = this.uiFields.length;
  for (var i=0;i < tempLength; i++) {
    theField = this.uiFields[i].field;
    theControl = this.uiFields[i].control;
    theControlType = this.uiFields[i].controlType;
    switch (theControlType) { // What kind of control is it?
      case "text":            // The TextField uses the text property
        record[theField] = theControl.text;
        break;
      case "combobox": /      // ComboBoxes use the data value of the selected item
        record[theField] = theControl.getSelectedItem( ).data;
        break;
      case "checkbox":        // CheckBoxes use the value true or false
        record[theField] = theControl.getValue( );
        break;
      default:               // Other components not supported at this time
        trace(theControlType + " not supported")
    }
  }
};

The setCurrentRecord( ) method operates in place on each component or TextField that is glued to the recordset. Whatever is currently displayed in the UI is written to the client-side recordset that the UI component or text field is glued to.

11.4.4 Putting It Together

With the glue( ) functionality in place, you can now simplify the process of building a rich interface. The ProductsAdmin.fla code from Example 5-14 can be simplified, including using a checkbox to display the Discontinued status of the product. The interface fields that are glued include TextFields, ComboBoxes, and a CheckBox.

The administrative interface that is bound to a recordset is shown in Figure 11-4.

Figure 11-4. The administrative interface for the Products table of Northwind
figs/frdg_1104.gif

The code is shown in Example 11-11. The completed .fla file showing the interface can be downloaded from the online Code Depot.

Example 11-11. Using the glue( ) functionality simplifies the ActionScript code
#include "NetServices.as"
#include "DataGlue.as"
#include "NetDebug.as"
#include "com/oreilly/frdg/DataFriendlyCombo.as"
#include "com/oreilly/frdg/RecordSetPlus.as"

// General error handler for authoring environment
function errorHandler(error) {
  trace(error.description);
}

// Responder objects

// SearchResult( ) takes one optional argument: recNum
// When inserting or updating a record, the recNum can be specified
// to move the user interface to that record; otherwise, use "first"
function SearchResult(recNum) {
  if (recNum) {
    this.recNum = recNum;
  } else {
    this.recNum = "first";
  }
}

// The SearchResult responder object handles the gluing of the UI
SearchResult.prototype.onResult = function (result_rs) {
  Products_rs = result_rs;
  // Use the glue( ) method to bind the UI to the recordset
  Products_rs.glue(ProductName_txt, "ProductName", "text");
  Products_rs.glue(categories_cb, "CategoryID", "combobox");
  Products_rs.glue(UnitPrice_txt, "UnitPrice", "text");
  Products_rs.glue(QuantityPerUnit_txt, "QuantityPerUnit", "text");
  Products_rs.glue(suppliers_cb, "SupplierID", "combobox");
  Products_rs.glue(test_ch, "Discontinued", "checkbox");

  results_txt.text = "There were " + Products_rs.getLength( )+ " records returned.";
  Products_rs.move(this.recNum);
};

SearchResult.prototype.onStatus = errorHandler;

// Set up a responder object to handle recordsets for ComboBoxes
// This responder assumes that data is coming in with
// ID column in [0] position and description column
// in the [1] position
function ComboBoxResponder (cbName) {
  this.onResult = function (result_rs) {
    var fields = result_rs.getColumnNames( );
    var idField = '#' + fields[0] + '#';
    var descField = '#' + fields[1] + '#';
    DataGlue.bindFormatStrings(cbName, result_rs, descField,idField);
    cbName.setDescriptor("--Choose One--", 0);
    cbName.setDefaultValue(0);
  };
  this.onStatus = errorHandler;
}

// Main responder for the Update, Insert, and Delete functions
// Display is to the Output window only
function MainServiceResponder( ) {}
MainServiceResponder.prototype.onResult = function (result) {
  trace(result);
};
MainServiceResponder.prototype.onStatus = errorHandler;

// Initialization code
if (connected == null) {
  connected = true;
  NetServices.setDefaultGatewayUrl("http://localhost/flashservices/gateway");
  var my_conn = NetServices.createGatewayConnection( );
  my_conn.onStatus = errorHandler;
  var myService = my_conn.getService("com.oreilly.frdg.admin.ProductsAdmin");
  var Products_rs = null;     // Main RecordSet object for product list
  // Set up the two ComboBoxes
  myService.getCategories(new ComboBoxResponder(categories_cb));
  myService.getSuppliers(new ComboBoxResponder(suppliers_cb));
}

// Set up event handlers for buttons
submit_pb.setClickHandler("searchProducts");

// Move buttons
moveFirst.setClickHandler("moveTo");
movePrevious.setClickHandler("moveTo");
moveNext.setClickHandler("moveTo");
moveLast.setClickHandler("moveTo");

// Insert, Update, and Delete buttons
insert_pb.setClickHandler("insertRecord");
update_pb.setClickHandler("updateRecord");
delete_pb.setClickHandler("deleteRecord");

// Event handlers for buttons

// submit_pb click handler
function searchProducts ( ) {
  getRecordset( );
}

// moveFirst( ), movePrevious( ), moveNext( ), and moveLast( ) click handler
function moveTo (button) {
  // The label of the button indicates the direction to move the recordset:
  // "first", "previous", "next", "last"
  Products_rs.move(button.label);
  navStatus_txt.text =
      "Rec. No. " + (Products_rs.getCurrentRecordNum( )) + " of " +
      Products_rs.getLength( );
}

// update_pb click handler
function updateRecord ( ) {
  myService.updateProduct(new MainServiceResponder( ), getUpdatedRecord( ));
  var tempRec = Products_rs.getCurrentRecordNum( );
  getRecordset(tempRec);
}

// insert_pb click handler
function insertRecord ( ) {
  if (insert_pb.getLabel( ) == "Add New Product") {
    Products_rs.addItem(getNewRecord( ));
  Products_rs.move("last");
    insert_pb.setLabel("Insert To Database");
    insert_txt.text = "Click again to insert to database";
  } else {
    insert_pb.setLabel("Add New Product");
    myService.addProduct(new MainServiceResponder( ), getUpdatedRecord( ));
    getRecordset("last");
    insert_txt.text = "";
  }
}

// delete_pb click handler
function deleteRecord ( ) {
  var productID = Products_rs.getCurrentRecord( ).ProductID;
  myService.deleteProducts(new MainServiceResponder( ), ProductID);
  getRecordset( );
}

// Utility functions
function getRecordset (recNum) {
  // Call the remote method using a responder object with optional record number
  //   to move the recordset to
  myService.getSearchResult(new SearchResult(recNum), search);
}

// Pack the updated record from the display into the RecordSet object
// and return the record to the caller
function getUpdatedRecord ( ) {
  Products_rs.setCurrentRecord( );
  return Products_rs.getCurrentRecord( );
}

// Get a blank record
function getNewRecord ( ) {
  var theRecord = { ProductName:''
                   ,UnitPrice:''
                   ,QuantityPerUnit:''
                   ,CategoryID:0
                   ,SupplierID:0
                   ,ProductID:''
                  };
  return theRecord;
}

In Example 11-11, the onResult( ) method updates the UI as the record changes and updates the client-side recordset if the user changes the UI:

  // Use the glue( ) method to bind the UI to the recordset
  Products_rs.glue(ProductName_txt, "ProductName", "text");
  Products_rs.glue(categories_cb, "CategoryID", "combobox");
  Products_rs.glue(UnitPrice_txt, "UnitPrice", "text");
  Products_rs.glue(QuantityPerUnit_txt, "QuantityPerUnit", "text");
  Products_rs.glue(suppliers_cb, "SupplierID", "combobox");
  Products_rs.glue(test_ch, "Discontinued", "checkbox");


    Part III: Advanced Flash Remoting