5.7 Updating Data on the Server

One frequent question about Flash Remoting is, "How do I get the recordset back to the server?" The short answer is that you have to program your Flash movie to manually parse the data and send it to the server. For example, when using the DataGrid component, changes made to the data are not uploaded to the server automatically. In the next few sections, however, I'll show you a few techniques that can be used to ease the passing of data back to the server.

5.7.1 Passing a Record to the Service Manually

In Chapter 3, you saw a Products display using text fields in Examples Example 3-3 through Example 3-8. The examples added several new properties and methods to the RecordSet class. We'll expand on that example to show the updating, inserting, and deleting of data. I'll go through the server-side code first. The component is called ProductsAdmin.cfc and should be saved in the webroot\com\oreilly\frdg\admin folder. As you recall from the previous ColdFusion security discussion, this directory is protected by an Application.cfm file. Therefore, to access the remote methods in this directory, your Flash code must log into the application. For the purposes of the example, the authentication code is hardcoded into the Flash file.

5.7.1.1 The server-side code

We need these main services:

getSearchResult(search)

Gets a subset of the products, or all products

addProduct(record)

Adds a new product to the Products table

updateProduct(record)

Updates an existing product

deleteProducts(ids)

Deletes one or more records from the Products table

In addition to the main services, we need some utility services to feed two ComboBoxes in the Flash user interface:

getSuppliers( )

Gets a list of suppliers so that the SupplierID can be used as a foreign key in the Products table

getCategories( )

Gets a list of categories so that the CategoryID can be used as a foreign key in the Products table

The complete CFC for the required services is shown in Example 5-13. The SQL statements in the example are built up using the preceding-comma method, such as:

 INSERT INTO Products 
  (ProductName
  ,UnitPrice
  ,QuantityPerUnit
  ,CategoryID
  ,SupplierID)

The preceding commas might look funny, but when you are debugging complex SQL statements, this style of coding makes it easy to comment out individual lines of SQL code without having to reformat the rest of the SQL statement.

Example 5-13. The ProductsAdmin.cfc file
<cfcomponent displayname="Administer Products" 
 hint="Add, update, delete, and search Northwind product list">
  
<!--- Search the Products table in the Northwind database --->
  <cffunction name="getSearchResult" access="remote" 
   returnType="query" hint="Pass a search string to get a list of products, 
   or nothing to get all products">
    <cfargument name="search" type="string" default="">
    <cftry>
      <cfquery name="rsGetProducts" datasource="Northwind">
        SELECT ProductID, ProductName, UnitPrice, 
        QuantityPerUnit, CategoryID, SupplierID FROM Products
<!--- If no argument is passed, return all records --->
       <cfif search NEQ "">
        WHERE ProductName LIKE '%#search#%'
       </cfif> 
      </cfquery>
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry> 
    <cfreturn rsGetProducts />
  </cffunction>

<!--- Add a product to the Northwind Products table --->
  <cffunction name="addProduct" returntype="string" 
   access="remote" hint="Pass a record to add a product">
    <cfargument name="ProductName" type="string" required="true" />
    <cfargument name="UnitPrice" type="numeric" default=0 />
    <cfargument name="QuantityPerUnit" type="string" default="0" />
    <cfargument name="CategoryID" type="numeric" default=0 />
    <cfargument name="SupplierID" type="numeric" default=0 />
    <cftry>
      <cfquery name="rsSuppliers" datasource="Northwind">
       INSERT INTO Products 
       (ProductName
        ,UnitPrice
        ,QuantityPerUnit
        ,CategoryID
        ,SupplierID) 
       VALUES 
       ('#ProductName#'
        ,#UnitPrice#
        ,'#QuantityPerUnit#'
        ,#CategoryID#
        ,#SupplierID#)
      </cfquery>
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry>  
    <cfreturn "Record inserted" />
  </cffunction>

<!--- Update a product using a product record --->
  <cffunction name="updateProduct" 
   returntype="string" 
   access="remote"
   hint="Pass a record including the ProductID to update a product">
    <cfargument name="ProductName"     type="string"  required="true" />
    <cfargument name="UnitPrice"       type="numeric" default=0 />
    <cfargument name="QuantityPerUnit" type="string"  default="0" />
    <cfargument name="CategoryID"      type="numeric" default=0 />
    <cfargument name="SupplierID"      type="numeric" default=0 />
    <cfargument name="ProductID"       type="numeric" required="true" />
    <cftry>
      <cfquery name="rsSuppliers" datasource="Northwind">
       UPDATE Products 
       SET ProductName='#ProductName#'
          ,UnitPrice=#UnitPrice#
          ,QuantityPerUnit='#QuantityPerUnit#'
          ,CategoryID=#CategoryID#
          ,SupplierID=#SupplierID#
       WHERE ProductID = #ProductID#
      </cfquery>
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry>
    <cfreturn "Record updated" />
  </cffunction>

<!--- Delete products from a list of ProductIDs --->
  <cffunction name="deleteProducts" returntype="string" access="remote" 
   hint="Pass a ProductID or comma-separated list 
   of ProductIDs to delete records">
    <cfargument name="productids" type="string" default="0" />
    <cftry>
      <cfquery name="rsSuppliers" datasource="Northwind">
<!--- The following query will delete products. 
      The alternate query will merely set the Discontinued field to 1
       DELETE FROM Products WHERE ProductID IN (#productids#)--->
       UPDATE Products SET Discontinued = 1 WHERE ProductID in (#productids#)
      </cfquery>
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry>  
    <cfreturn "Record deleted" />
  </cffunction>
 
<!--- Get a list of suppliers to feed a dropdown list --->
  <cffunction name="getSuppliers" returntype="query" 
   access="remote" hint="Get a list of all suppliers">
    <cftry>  
      <cfquery name="rsSuppliers" datasource="Northwind"
       cachedwithin=#CreateTimespan(7,0,0,0)#>
       SELECT SupplierID, CompanyName FROM Suppliers
      </cfquery>
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry>  
    <cfreturn rsSuppliers />
  </cffunction>
     
<!--- Get a list of categories to feed a dropdown list --->
  <cffunction name="getCategories" returntype="query" 
   access="remote" hint="Get a list of product categories">
    <cftry>
      <cfquery name="rsCategories" datasource="Northwind"
       cachedwithin=#CreateTimespan(7,0,0,0)#>
       SELECT CategoryID, CategoryName FROM Categories
      </cfquery>  
      <cfcatch type="Any">
        <cfthrow message="There was a database error">
      </cfcatch>
    </cftry>
    <cfreturn rsCategories />
  </cffunction>

</cfcomponent>

The methods of the ProductsAdmin service are self-documenting using the hints of the <cffunction> tag and the inline comments. The methods each contain a basic error handler of a <cftry> and <cfcatch> block that simply throws an error message to the Flash movie upon any type of error. The getCategories( ) and getSuppliers( ) methods demonstrate the cachedwithin attribute of the <cfquery> tag? each query is executed only once every seven days, or upon a restart of the server. This improves the speed of the queries dramatically because they exist in the server's memory.

5.7.1.2 The client-side code

The ActionScript code for the ProductsAdmin.fla file is shown in Example 5-14.

Example 5-14. ProductsAdmin.fla
#include "NetServices.as"
#include "DataGlue.as"

// Set up the combo boxes to be able to pick a value
FComboBoxClass.prototype.pickValue = function (value) {
  for (var i=0; i<this.getLength( ); i++) {
    if (this.getItemAt(i).data == value) {
      this.setSelectedIndex(i);
      break;
    }
  }
};

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

// Responder objects
var SearchResult = new Object( );

SearchResult.onResult = function (result_rs) {
  Products_rs = result_rs;
  results_txt.text = "There were " + Products_rs.getLength( ) + 
   " records returned.";
  Products_rs.move("First");
  getRecord( );
};

SearchResult.onStatus = errorHandler;

// Set up a responder object to handle recordsets for ComboBoxes
function ComboBoxResponder (cbName) {
  this.cbName = cbName;
}
// The responder assumes that data is coming in with
// ID column in [0] position and description column
// in the [1] position
ComboBoxResponder.prototype.onResult = function (result_rs) {
  var fields = result_rs.getColumnNames( );
  var idField = '#' + fields[0] + '#';
  var descField = '#' + fields[1] + '#';
  DataGlue.bindFormatStrings(this.cbName, result_rs, descField,idField);
};
ComboBoxResponder.prototype.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;

// Init code
if (connected == null) {
  connected = true;
  NetServices.setDefaultGatewayUrl("http://localhost/flashservices/gateway");
  var my_conn = NetServices.createGatewayConnection( );
  my_conn.onStatus = errorHandler;
  my_conn.setCredentials("admin", "1234");   // hardcoded username and password
  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));

// Create new functionality for the RecordSet class
RecordSet.prototype.currentRecord = 0;
RecordSet.prototype.getCurrentRecordNum = function ( ) {
  return this.currentRecord
};

RecordSet.prototype.move = function (direction) { 
  direction = direction.toLowerCase( );
  switch (direction) {
    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; 
  }
};

Recordset.prototype.getCurrentRecord = function ( ) {
  return this.getItemAt(this.currentRecord-1);
};

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

moveFirst.setClickHandler("moveTo");
movePrevious.setClickHandler("moveTo");
moveNext.setClickHandler("moveTo");
moveLast.setClickHandler("moveTo");

insert_pb.setClickHandler("insertRecord");
update_pb.setClickHandler("updateRecord");
delete_pb.setClickHandler("deleteRecord");
// Event handlers for buttons
function getRecordset ( ) {
  myService.getSearchResult(SearchResult, search);
}

function moveTo (button) {
  Products_rs.move(button.label);
  getRecord( );
}

function updateRecord ( ) {
  myService.updateProduct(new MainServiceResponder( ), getUpdatedRecord( ));
  getRecordset( );
}

function insertRecord ( ) {
  if (insert_pb.getLabel( ) == "Add New Product") {
    Products_rs.addItem(getNewRecord( ));
    Products_rs.move("last");
    getRecord( );
    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( );
    insert_txt.text = "";
  }
}

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

// Display the current record
function getRecord ( ) {
  if (Products_rs.getLength( ) == 0) {
    ProductName_txt.text = "";
    UnitPrice_txt.text = "";
    QuantityPerUnit_txt.text = "";
    navStatus_txt.text = "No Records";
  } else {
    ProductName_txt.text = Products_rs.getCurrentRecord( ).ProductName;
    UnitPrice_txt.text = Products_rs.getCurrentRecord( ).UnitPrice;
    QuantityPerUnit_txt.text = Products_rs.getCurrentRecord( ).QuantityPerUnit;
    categories_cb.pickValue(Products_rs.getCurrentRecord( ).CategoryID);
    suppliers_cb.pickValue(Products_rs.getCurrentRecord( ).SupplierID);
    navStatus_txt.text = 
      "Rec. No. " + (Products_rs.getCurrentRecordNum( )) + " of " +
      Products_rs.getLength( );
  }
}

// Pack the updated record from the display into the RecordSet object
// and return the record to the caller
function getUpdatedRecord ( ) {
  var ProductName = ProductName_txt.text;
  var UnitPrice = UnitPrice_txt.text;
  var QuantityPerUnit = QuantityPerUnit_txt.text;
  var CategoryID = categories_cb.getSelectedItem( ).data;
  var SupplierID = suppliers_cb.getSelectedItem( ).data;
  var ProductID = Products_rs.getCurrentRecord( ).ProductID;
  var theRecord = { ProductName:ProductName
                   ,UnitPrice:UnitPrice
                   ,QuantityPerUnit:QuantityPerUnit
                   ,CategoryID:CategoryID
                   ,SupplierID:SupplierID
                   ,ProductID:ProductID
                  };
  Products_rs.replaceItemAt(Products_rs.getCurrentRecord, theRecord);
  return theRecord;
}

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

I'm not going to explain this code line by line, because much of it was explained in Chapter 3; however, several parts of the code warrant further explanation.

5.7.1.3 Enhancing the ComboBox component

When using ComboBoxes, there is no built-in method to pick a particular field in the box. Again, the flexibility of the UI components comes to our rescue?we can simply add new functionality to the ComboBox class. After including the required files, I add a custom method, pickValue( ), to the FComboBoxClass class:

// Set up the combo boxes to be able to pick a value
FComboBoxClass.prototype.pickValue = function (value) {
  for (var i=0; i<this.getLength( ); i++) {
    if (this.getItemAt(i).data == value) {
      this.setSelectedIndex(i);
      break;
    }
  }
};

This method allows you to pass a value to a ComboBox to display that particular record. Since there are two ComboBoxes in the file, I decided to build onto the FComboBoxClass class rather than call a generic function. This is useful in the display of the current record:

ProductName_txt.text = Products_rs.getCurrentRecord( ).ProductName;
UnitPrice_txt.text = Products_rs.getCurrentRecord( ).UnitPrice;
QuantityPerUnit_txt.text = Products_rs.getCurrentRecord( ).QuantityPerUnit;
categories_cb.pickValue(Products_rs.getCurrentRecord( ).CategoryID);
suppliers_cb.pickValue(Products_rs.getCurrentRecord( ).SupplierID);
5.7.1.4 Response handlers

Example 5-14 defines a generic error handler that is used for all of the responder objects for the remote methods being called:

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

We simply set the onStatus( ) methods of the responders equal to this function to be able to use one generic error handler for all of the remote calls. The NetConnection object my_conn uses this error handler as well.

Another aspect of the file that deserves a bit of explanation is the use of the responder objects. I've used three different responder objects for the six different services called in the example.

The getSearchResult( ) method uses a generic instance of the Object class with onResult( ) and onStatus( ) methods as a responder:

var SearchResult = new Object( );
SearchResult.onResult = function (result_rs) {
  Products_rs = result_rs;
  results_txt.text = "There were " + Products_rs.getLength( )+ 
   " records returned.";
  Products_rs.move("First");
  getRecord( );
};

SearchResult.onStatus = errorHandler;

The two utility methods, getCategories( ) and getSuppliers( ), both feed ComboBoxes, so I set up a responder class, ComboBoxResponder, that works with ComboBoxes:

function ComboBoxResponder (cbName) {
  this.cbName = cbName;
}
// The responder assumes that data is coming in with
// ID column in [0] position and description column
// in the [1] position
ComboBoxResponder.prototype.onResult = function (result_rs) {
  var fields = result_rs.getColumnNames( );
  var idField = '#' + fields[0] + '#';
  var descField = '#' + fields[1] + '#';
  DataGlue.bindFormatStrings(this.cbName, result_rs, descField,idField);
};
ComboBoxResponder.prototype.onStatus = errorHandler;

The ComboBoxResponder class accepts the ComboBox name in its constructor, which is packed with the recordset from the remote method. The services are called later in the code with inline statements:

// Set up the two ComboBoxes
myService.getCategories(new ComboBoxResponder(categories_cb));
myService.getSuppliers(new ComboBoxResponder(suppliers_cb));

The main service responder (for the update, insert, and delete functionality) simply displays the message from the server in the Output window:

// 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;
5.7.1.5 Calling the services

The three main service functions are called when the user clicks the corresponding button. The updateProduct( ) remote method in Example 5-13 takes the current record as an argument. It is called from the client-side updateRecord( ) function of Example 5-14, which is triggered by a click of the update_pb button:

function updateRecord ( ) {
  myService.updateProduct(new MainServiceResponder( ), getUpdatedRecord( ));
  getRecordset( );
}

The two arguments passed to the remote service are the responder object (stripped off by Flash before making the remote call) and the result of the getUpdatedRecord( ) function. The getUpdatedRecord( ) function updates the current client-side RecordSet object to match the currently displayed record, and it returns the current record to the caller:

// Pack the updated record from the display into the RecordSet object
// and return the record to the caller
function getUpdatedRecord ( ) {
  var ProductName = ProductName_txt.text;
  var UnitPrice = UnitPrice_txt.text;
  var QuantityPerUnit = QuantityPerUnit_txt.text;
  var CategoryID = categories_cb.getSelectedItem( ).data;
  var SupplierID = suppliers_cb.getSelectedItem( ).data;
  var ProductID = Products_rs.getCurrentRecord( ).ProductID;
  var theRecord = { ProductName:ProductName
                   ,UnitPrice:UnitPrice
                   ,QuantityPerUnit:QuantityPerUnit
                   ,CategoryID:CategoryID
                   ,SupplierID:SupplierID
                   ,ProductID:ProductID
                  };
  Products_rs.replaceItemAt(Products_rs.getCurrentRecord, theRecord);
  return theRecord;
}

For the sake of the example, we assume that the currently displayed record has been modified if (and only if) the user clicks the Update Product button. In a production situation, you can set "dirty" flags to indicate that a record needs to be updated (as shown in Example 5-16). One option is to check the current display against the client-side RecordSet object; if the displayed recordset differs in any way from the RecordSet object in memory, then you know it has been changed. You can also disable the Update button until the record has been changed by the user. Similarly, you should generally allow the user to confirm that she wants to submit changes by explicitly clicking an Update button; updating the server data automatically whenever the data changes on the client side may lead to unintentional database updates.

The record is sent to the server, which treats the fields of the record as named arguments. If you recall from our .cfc file in Example 5-13, all the fields for the database update were named as arguments in the function:

<cfargument name="ProductName" type="string" required="true" />
<cfargument name="UnitPrice" type="numeric" default=0 />
<cfargument name="QuantityPerUnit" type="string" default="0" />
<cfargument name="CategoryID" type="numeric" default=0 />
<cfargument name="SupplierID" type="numeric" default=0 />
<cfargument name="ProductID" type="numeric" required="true" />

We can simply pass an ActionScript object (the current record) to the remote service.

The insert functionality is similar, although the ProductID is generated by the database. It deserves a bit of explanation, though, because it has to be done in two parts:

function insertRecord ( ) {
  if (insert_pb.getLabel( ) == "Add New Product") {
    Products_rs.addItem(getNewRecord( ));
    Products_rs.move("last");
    getRecord( );
    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( );
    insert_txt.text = "";
  }
}

When the user clicks the Add New Product button, the display has to be cleared out and a new record needs to be inserted into the client-side RecordSet object. At this point, nothing has happened on the server. The display on the button changes to "Click again to insert into database." The user can type into the blank display to fill in the fields of the new record. When the user clicks the button again, the code adds the newly created record to the remote database.

The delete functionality is straightforward as well. The currently displayed record's ProductID field is sent to the server. The remote method deletes the record:

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

The completed code can be found at the online Code Depot. Keep in mind that any updates, inserts, and deletes will permanently change your database. Always make backups and keep clean copies of the Northwind database on hand for other examples.

5.7.2 Passing a Record to the Service Automatically

The DataGrid component is one of the commercial add-ons available from Macromedia in the first Developer Resource Kit (DRK), available from http://www.macromedia.com/go/drk. This section describes one way to update remote data from within a client-side DataGrid. The example uses the same remote services as the previous example?ProductsAdmin.cfc?with one additional method.

Because the DataGrid is a commercial product, licensing restrictions prevent distributing it from the online Code Depot. You must add your own DataGrid component to the movie for the example to work.

5.7.2.1 The updateProducts( ) method

The ProductsAdmin.cfc file from Example 5-13 contains an updateProduct( ) method for updating a single database record. Example 5-15 adds a new method, updateProducts( ), that allows batch updates of data.

Example 5-15. The updateProduct( ) method added to ProductsAdmin.cfc
<cffunction name="updateProducts"  returntype="string" 
   access="remote" hint="Batch update a group of products">
   <cfargument name="Products" type="array" required="true" />
   <cfloop index=i from="1" to=#ArrayLen(Products)#>
     <cfset temp = 
      updateProduct(Products[i].ProductName, 
                    Products[i].UnitPrice,
                    Products[i].QuantityPerUnit,
                    Products[i].CategoryID,
                    Products[i].SupplierID,
                    Products[i].ProductID)>
   </cfloop> 
   <cfreturn "Products updated" />
  </cffunction>

The updateProducts( ) method updates multiple records by calling the updateProduct( ) method within the same .cfc file (see Example 5-13) for each record passed in. This is typically how a batch update process is done. The client-side code is shown in Example 5-16.

5.7.2.2 The client-side ActionScript

Most of the client-side code remains the same as Example 5-14. The new code in Example 5-16 that is related to the DataGrid component is commented inline.

Example 5-16. ActionScript for ProductsAdminGrid.fla
#include "NetServices.as"
#include "DataGlue.as"

// Set up the combo boxes to be able to pick a value
FComboBoxClass.prototype.pickValue = function (value) {
  for (var i=0; i<this.getLength( ); i++) {
    if (this.getItemAt(i).data == value) {
      this.setSelectedIndex(i);
      break;
    }
  }
};

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

// Responder objects
var SearchResult = new Object( );
SearchResult.onResult = function (result_rs) {
  // Put the contents of the recordset into the DataGrid
  allProducts_dg.setDataProvider(result_rs);
  // Don't allow editing of the ProductID primary key
  allProducts_dg.getColumnAt(0).setEditable(false);
};

SearchResult.onStatus = errorHandler;

// Set up a responder object to handle recordsets for ComboBoxes
function ComboBoxResponder (cbName) {
  this.cbName = cbName;
}
// The responder assumes that data is coming in with
// ID column in [0] position and description column
// in the [1] position
ComboBoxResponder.prototype.onResult = function (result_rs) {
  var fields = result_rs.getColumnNames( );
  var idField = '#' + fields[0] + '#';
  var descField = '#' + fields[1] + '#';
  DataGlue.bindFormatStrings(this.cbName, result_rs, descField,idField);
}
ComboBoxResponder.prototype.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;
  my_conn.setCredentials("admin", "1234"); // hardcoded username and password
  var myService = my_conn.getService("com.oreilly.frdg.admin.ProductsAdmin");
}

// Set up the two combo boxes
myService.getCategories(new ComboBoxResponder(categories_cb));
myService.getSuppliers(new ComboBoxResponder(suppliers_cb));
// Set up change handlers for combo boxes
categories_cb.setChangeHandler("setCategory");
suppliers_cb.setChangeHandler("setSupplier");

// Set up the DataGrid
allProducts_dg.setEditable(true);
allProducts_dg.setSelectMultiple(true);

// Each time a row is edited, flag it for update
allProducts_dg.setEditHandler("flagForUpdate");
// Create an array to hold flagged product records
allProducts_toUpdate = new Array( );   // Records marked for update

// When the user selects a row, set the combo boxes to match the data
allProducts_dg.setChangeHandler("setCombos");

// Get the Product list
function getRecordset ( ) {
  myService.getSearchResult(SearchResult, '');
}
getRecordset( );

// Set up event handlers for buttons
insert_pb.setClickHandler("insertRecord");
update_pb.setClickHandler("updateRecords");
delete_pb.setClickHandler("deleteRecords");

// Event handlers for buttons

// Update a batch of records stored in the allProducts_toUpdate array
function updateRecords ( ) {
  myService.updateProducts(new MainServiceResponder( ), allProducts_toUpdate);
  getProductList( );
}

function insertRecord ( ) {
  if (insert_pb.getLabel( ) == "Add New Product") {
    allProducts_dg.addItem(getNewRecord( ));
    allProducts_dg.setSelectedCell(allProducts_dg.getLength( )-1,"ProductName");
    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( ),        
      allProducts_dg.getSelectedItem( ));
    getRecordset( );
    insert_txt.text = "";
  }
}

// Delete all selected records -- pass the ProductID numbers as a list
function deleteRecords ( ) {
  var deletedIndices = allProducts_dg.getSelectedIndices( );
  var deletedItems = new Array( );
  for (var i=0; i < deletedIndices.length; i++) {
    deletedItems.push(allProducts_dg.getItemAt(deletedIndices[i]).ProductID);
    allProducts_dg.removeItemAt(deletedIndices[i]);
  }
  myService.deleteProducts(new MainServiceResponder( ), deletedItems.join( ));
}

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

function flagForUpdate (grid_dg) {
  // This row has been modified; save it for update
  allProducts_toUpdate.push(grid_dg.getSelectedItem( ));
}

function setCombos ( ) {
  categories_cb.pickValue(allProducts_dg.getSelectedItem( ).ProductID);
  suppliers_cb.pickValue(allProducts_dg.getSelectedItem( ).SupplierID);
}

// Utility function to set the current CategoryID to the value in the combo
function setCategory (combo) {
  allProducts_dg.setCellData(allProducts_dg.getSelectedIndex( ), 
                             "CategoryID", combo.getValue( ));
}

// Utility function to set the current SupplierID to the value in combo
function setSupplier (combo) {
  allProducts_dg.setCellData(allProducts_dg.getSelectedIndex( ), 
                             "SupplierID", combo.getValue( ));
}

The main functional differences between Example 5-16 and Example 5-14 are:

  • Results can be seen for many records at once in a grid display.

  • Rows can be deleted in bulk by multiselecting the rows and pressing the Delete Selected Products button.

  • Updates are made in bulk as well; "dirty" (edited) records are stored in an array and passed to the server at once when the user clicks Update Products.

The DataGrid's changeHandler function, setCombos( ), is called whenever the user selects another row. This updates the combo boxes in the display. The DataGrid's editHandler function, flagForUpdate( ), adds the edited row to an array, which can then be passed to the server upon clicking the Update Products button.

The DataGrid is a highly versatile component that can be used by itself or in conjunction with other components, as shown here. You can also enhance the DataGrid so that the cells contain other components such as CheckBoxes, ComboBoxes, and other items, as described in Chapter 11.



    Part III: Advanced Flash Remoting
     
    ASPTreeView.com
     
    Evaluation has ѕВѕВАНГДёexpired.
    Info...