DekGenius.com
[ Team LiB ] Previous Section Next Section

27.3 Creating Service Modules

The My Page framework does not know or care which modules are being loaded, so you can add any kind of service module you want, using the three sample modules as exemplars. Remember to use relative addresses; avoid absolute references using _root. Because the service module movies are loaded into movie clips, _root refers to the framework's main timeline and not to the main timeline of the service .swf.

27.3.1 Making Notes

A common feature of My Page applications is the ability to make notes that are stored between sessions. This feature might be used for making daily goals lists, reminders, directions, or phone numbers. The notes module is instructional in that it uses a local shared object to store information—the user's notes—between sessions.

The notes module uses the ScrollBar component, so add the component symbol to the notes.fla document's Library by dragging an instance from the Components panel to the Stage (and then deleting the instance from the Stage).

The following example shows the code for notes.fla as it should appear on the first frame of the main timeline:

// Include MovieClip.as from Chapter 7 and TextField.as from Chapter 8.
#include "MovieClip.as"
#include "TextField.as"

// Create the form elements used.
function initNotes (  ) {

  // Create the text field used for accepting user input and displaying notes.
  this.createInputTextField("notes", this.getNewDepth(  ), 0, 0, 100, 200);
  notes.multiline = true;
  notes.wordWrap = true;

  // Create the scrollbar for the notes text field.
  this.attachMovie("FScrollBarSymbol", "sb", this.getNewDepth(  ));

  // Set the notes field width to 200 pixels, counting the scrollbar.
  notes._width = 200 - sb._width;

  // Tie the scrollbar to the notes field and position it at the field's right.
  sb._x = notes._width;
  sb.setSize(notes._height); 
  sb.setScrollTarget(notes);
}

// This function opens the LSO and reads and writes the user's notes.
function initLSO (  ) {

  // Open the local shared object named "notes".
  notesLSO = SharedObject.getLocal("notes");

  // Create a temporary property that references the notes field.
  notesLSO.notesRef = notes;

  // If the shared object has any stored values, display them in the notes field.
  notes.text = notesLSO.data.notes;

  // Set up notesLSO to listen for changes made to the notes field.
  notes.addListener(notesLSO);
  notesLSO.onChanged = function (  ) {
    this.data.notes = this.notesRef.text;
    this.flush(  );
  };
}

// Call initNotes(  ) and initLSO(  ) to start the movie.
initNotes(  );
initLSO(  );

The notes module is a relatively short application, and it uses much of the same logic as the framework if you look closely. There are only two functions in the entire application. The first one, initNotes( ), performs the straightforward task of creating the TextField and ScrollBar objects that are used to accept user input and display the notes.

The initLSO( ) function is a little more complex, so let's examine it more closely. The function first opens an LSO named "notes", if one exists; otherwise, a new LSO is created. Next, a reference to the notes text field is assigned to a custom property of the shared object named notesRef. This is so that within the onChanged( ) method of the shared object, it can address the notes text field in a relative fashion. The relative addressing is extremely important in the case of a movie such as this one that will be loaded into a movie clip within another movie. Because SharedObject and other non-MovieClip objects do not have timelines, it is not possible to make relative addresses using _parent, so creating a reference property is a great way to achieve the desired results. The shared object is added as a listener to the notes field, and an onChanged( ) method—which is invoked automatically when the contents of the notes field change—is defined.

27.3.2 Keeping Track of Addresses

Another useful service module is one that keeps track of email addresses and names.

The address book module uses the PushButton and ListBox components. Therefore, you should add the corresponding component symbols to the addressBook.fla document's Library by dragging instances from the Components panel to the Stage (and deleting the instances).

The following example shows the code that you should place on the first frame of addressBook.fla to implement an address book. This module uses a local shared object to store the addressees' information.

// Include MovieClip.as from Chapter 7 and TextField.as from Chapter 8.
#include "MovieClip.as"
#include "TextField.as"
// Include Table.as from Chapter 11
#include "Table.as"

// Set up the address book form, including input fields, buttons, and a list box to
// display the results of a search.
function initForm (  ) {

  // Create the text field for the column headings (name and email address).
  this.createTextField("nameLabel", this.getNewDepth(  ), 0, 0, 75, 20);
  nameLabel.text = "Name";

  // Position the email label dynamically to the right of the name label.
  this.createTextField("emailLabel", this.getNewDepth(  ), 0, 0, 75, 20);
  emailLabel.text = "Email";

  // Create the text field for the addressee's name and email address.
  this.createInputTextField("name", this.getNewDepth(  ), 0, 0, 75, 20);
  this.createInputTextField("email", this.getNewDepth(  ), 0, 0, 75, 20);

  // Create an Add button used to add a new addressee to the address book.
  this.attachMovie("FPushButtonSymbol", "addBtn", this.getNewDepth(  ));
  addBtn.setLabel("add entry");
  addBtn.setClickHandler("addEntry");

  // Add a search field.
  this.createTextField("searchLabel", this.getNewDepth(  ), 0, 0, 75, 20);
  searchLabel.text = "Search";
  this.createInputTextField("search", this.getNewDepth(  ), 0, 0, 75, 20);

  // Add a Search button for submitting the entry search.
  this.attachMovie("FPushButtonSymbol", "searchBtn", this.getNewDepth(  ));
  searchBtn.setLabel("search for entry");

  // Create the callback for the Search button.
  searchBtn.setClickHandler("searchEntries");

  // Create the list box that displays search results.
  this.attachMovie("FListBoxSymbol", "output", this.getNewDepth(  ));
  output.setSize(180, 40);

  // Create a button that removes entries from the address book.
  this.attachMovie("FPushButtonSymbol", "removeBtn", this.getNewDepth(  ));
  removeBtn.setLabel("remove entry");
  removeBtn.setClickHandler("removeEntry");

  // Use a table to position all the elements.
  tr0 = new TableRow(5, new TableColumn(5, nameLabel, name), 
                     new TableColumn(5, emailLabel, email));
  tr1 = new TableRow(5, new TableColumn(5, addBtn));
  tr2 = new TableRow(5, new TableColumn(5, searchLabel, search, searchBtn));
  tr3 = new TableRow(5, new TableColumn(5, output, removeBtn));
  t = new Table(5, 0, 0, tr0, tr1, tr2, tr3);
}

// Open the shared object and, if it is new, initialize a blank address array.
function openSharedObject (  ) {
  addressLSO = SharedObject.getLocal("addressBook");
  if (addressLSO.data.entries == undefined) {
    addressLSO.data.entries = new Array(  );
  }
}

// Add an entry to the shared object (called from the Add button).
function addEntry (  ) {
  var entries = addressLSO.data.entries;
  entries.push(name.text + " [" + email.text + "]");
  entries.sort(  );
  addressLSO.flush(  );
  name.text = "";
  email.text = "";
}

// Search the entries array for any entries containing the specified substring.
function searchEntries (  ) {
  var entries = addressLSO.data.entries;
  var matches = new Array(  );
  for (var i = 0; i < entries.length; i++) {
    // Keep a record of all matching entries.
    if (entries[i].indexOf(search.text) != -1)
      matches.push(entries[i]);
  }
  // If there are matches, display them.
  if (matches.length > 0) {
    output.setDataProvider(matches);
  } else {
    // Otherwise, tell the user that none were found.
    output.removeAll(  );
    output.addItem("no matches found");
  }
}

// Remove the selected entry from the entries array (triggered by the Remove button).
function removeEntry (  ) {
  var entries = addressLSO.data.entries;
  var val = output.getValue(  );
  // Remove the item from the ListBox.
  output.removeItemAt(output.getSelectedIndex(  ));
  for (var i = 0; i < entries.length; i++) {
    if (entries[i] == val) {
      // Remove the addressee and update the stored data.
      entries.splice(i, 1);
      addressLSO.flush(  );
      break;
    }
  }
}

// Call initForm(  ) and openSharedObject(  ) to start the movie.
initForm(  );
openSharedObject(  );

The address book module consists of five functions. Two of the functions, initForm( ) and openSharedObject( ), handle initialization actions for the application. The other three functions are callback functions for the form buttons.

Although initForm( ) is a relatively long function, no single aspect is too complicated. The function is responsible for creating the form using functions such as createTextField( ) and attachMovie( ); it then sets properties for the created objects.

The openSharedObject( ) function simply opens the shared object and, if it is a new object, initializes the entries array as a new property.

The addEntry( ) function adds a new entry to the array stored in the shared object. It adds the text from the name and email text fields to the array, and then sorts the array alphabetically before saving it using the flush( ) method. For more information, see Recipe 16.4.

You are not strictly required to use the flush( ) method on the LSO to save the data. However, there are several reasons why you should do so here. First of all, as in any situation, there is always the possibility that the data may not save if the user's settings are not correct and you leave it to the automatic save feature. Another reason is that calling flush( ) saves the data immediately; otherwise, the attempt to save data occurs when the movie is closed.

function addEntry (  ) {
  var entries = addressLSO.data.entries;
  entries.push(name.text + " [" + email.text + "]");
  entries.sort(  );
  addressLSO.flush(  );
  name.text = "";
  email.text = "";
}

When the user clicks on searchBtn, the searchEntries( ) function is called. This function loops through each of the values stored in the entries array of the shared object. It uses the indexOf( ) method to determine if each entry contains the value entered into the search text field. If so, the entry is added to an array which is then used to populate the output list box. The need to manually implement a search function is typical, and searches are usually accomplished using indexOf( ). Because indexOf( ) searches strings and not arrays, we search each element of the array separately. Beware, however, that for large text fields, or when searching very large numbers of array elements, indexOf( ) may prove to be slow.

function searchEntries (  ) {
  var entries = addressLSO.data.entries;
  var matches = new Array(  );
  for (var i = 0; i < entries.length; i++) {
    if (entries[i].indexOf(search.text) != -1)
      matches.push(entries[i]);
  }
  if (matches.length > 0) {
    output.setDataProvider(matches);
  } else {
    output.removeAll(  );
    output.addItem("no matches found");
  }
}

The removeEntry( ) function removes the selected value from the list box and from the shared object. You should remove the entry from both so that not only is the display updated to reflect the deletion but so is the actual data store. The function removes the item from the list box using the removeItemAt( ) method. The function loops through all the entries stored in the shared object until it finds the one that matches the selected entry, and then removes it with the splice( ) function and saves the updated shared object. In this example, a break statement is used after a matching entry is found and removed. This is a good practice because it ensures that the program does not needlessly loop through the remaining values. However, if you want the function to remove all entries (even duplicates) that match the value selected, remove the break statement.

function removeEntry (  ) {
  var entries = addressLSO.data.entries;
  var val = output.getValue(  );
  // Remove the item from the ListBox
  output.removeItemAt(output.getSelectedIndex(  ));
  for (var i = 0; i < entries.length; i++) {
    if (entries[i] == val) {
      // Remove the addressee and update the stored data
      entries.splice(i, 1);
      addressLSO.flush(  );
      break;
    }
  }
}

27.3.3 Searching the Web Using Google

Another common and useful service is one that enables the user to search the Web from his My Page application. This service module uses Flash Remoting for ColdFusion MX. Alternatively, if you are using Flash Remoting for .NET, you can use the same code and adjust the gateway URL to point to an .aspx page. The rest of the code remains the same. No matter which server platform you are using, however, you must obtain a developer's key, which is free, from Google by registering at http://www.google.com/apis/.

Note that the gateway URL depends on your Flash Remoting set up, such as ColdFusion, .NET, J2EE, or PHP. (Refer to Chapter 20 for more details on Flash Remoting.) On the other hand, the URL of the Google web service used in the call to getService( ) is always the location of the .wsdl file provided by Google (this is the same URL used by everything that calls the Google web service). You, as a Google developer accessing its web service, are identified by the unique Google key—specified in the params object created within doSearch( )—and not via a unique URL.

The search service requires both the ScrollBar and PushButton components. So the first thing you should do is add the corresponding component symbols to the search.fla document's Library by dragging instances from the Components panel to the Stage.

The following example shows the code that you should place on the first frame of the main timeline for search.fla:

// Include NetServices.as for the Flash Remoting API.
#include "NetServices.as"
// Include MovieClip.as from Chapter 7 and TextField.as from Chapter 8.
#include "MovieClip.as"
#include "TextField.as"
//Include Table.as from Chapter 11.
#include "Table.as"

// The initForm(  ) function creates the form elements used by the search service.
function initForm (  ) {

  // Create the input text field that allows the user to enter a search string.
  this.createInputTextField("searchString", this.getNewDepth(  ), 0, 0, 150, 20);
  searchString.text = "<type search string here>";

  // When search field gains focus, remove any existing text (such as the initial
  // <type search string here> value).
  searchString.onSetFocus = function (  ) {
    this.text = "";
  };

  // Create a Search button to initiate the search.
  this.attachMovie("FPushButtonSymbol", "searchBtn", this.getNewDepth(  ));
  searchBtn.setLabel("Search");
  searchBtn.setClickHandler("doSearch");

  // Create a text field to display the search results.
  this.createTextField("searchResults", this.getNewDepth(  ), 0, 0, 300, 100);
  searchResults.multiline = true;
  searchResults.wordWrap = true;
  searchResults.html = true;
  searchResults.border = true;

  // Add a scrollbar for the search results.
  this.attachMovie("FScrollBarSymbol", "sb", this.getNewDepth(  ));
  sb.setScrollTarget(searchResults);
  sb.setSize(searchResults._height);

  // Add a Previous button to get the previous 10 search results.
  this.attachMovie("FPushButtonSymbol", "prevBtn", this.getNewDepth(  ));
  prevBtn.setLabel("Previous 10");
  prevBtn.enabled = false;
  prevBtn.setClickHandler("doSearch");

  // Add a Next button to get the next 10 search results.
  this.attachMovie("FPushButtonSymbol", "nextBtn", this.getNewDepth(  ));
  nextBtn.setLabel("Next 10");
  nextBtn.enabled = false;
  nextBtn.setClickHandler("doSearch");

  // Create a table to properly align the searchResults text field 
  // and the scrollbar to one another.
  scrollerTr0 = new TableRow(0, new TableColumn(0, searchResults), 
                                new TableColumn(0, sb));
  scrollerTable = new Table(5, 0, 0, scrollerTr0);

  // Create a table to position the form elements, including the scrollerTable 
  // subtable.
  tr0 = new TableRow(5, new TableColumn(5, searchString), 
                     new TableColumn(5, searchBtn));
  tr1 = new TableRow(5, new TableColumn(5, scrollerTable));
  tr2 = new TableRow(5, new TableColumn(5, prevBtn), new TableColumn(5, nextBtn));
  t = new Table(5, 0, 0, tr0, tr1, tr2);
}

// The initNetServices(  ) function initializes the Flash Remoting objects.
function initNetServices (  ) {

  // This uses the default localhost standalone CFMX installation URL. If you use a
  // different one, change this code appropriately. See Chapter 20 for details on how
  // to make a connection and create a service object.
  gwUrl = "http://localhost:8500/flashservices/gateway/";
  NetServices.setDefaultGatewayURL(gwUrl);
  conn = NetServices.createGatewayConnection(  );

  // This is the service object for the Google Web service WSDL.
  srv = conn.getService("http://api.google.com/GoogleSearch.wsdl");
}

// Create the response object.
res = new Object(  );

// When the result is returned, format the contents and display them in the
// searchResults text field as HTML.
res.onResult = function (result) {
  var r = searchResults;
  r.html = true;
  r.htmlText = "";
  var re = result.resultElements;

  // Loop through the search results and add them as HTML to the searchResults field.
  for (var i = 0; i < re.length; i++) {
    r.htmlText += "<font color=\"#FF\"><a href=\"" + re[i].url + 
                  "\">" + re[i].title + 
                  "</a></font>" + "<br>";
    r.htmlText += re[i].snippet + "<br>";
    if (re[i].summary != "") {
      r.htmlText += re[i].summary + "<br>";
    }
    r.htmlText += "<br><br>";
  }

  // Reset the vertical and horizontal scroll positions.
  vsb.setScrollPosition(0);
  hsb.setScrollPosition(0);

  // Get the estimated total number of results.
  var count = result.estimatedTotalResultsCount;

  // Enable the Previous 10 results button if we're 
  // not already viewing the first page.
  if (result.startIndex > 0) {
    prevBtn.enabled = true;
  } else {
    prevBtn.enabled = false;
  }

  // If the ending index is less than the estimated total number of results, 
  // enable the Next 10 results button, but otherwise disable it 
  // so that users cannot try to load results past the last page.
  if (result.endIndex < count) {
    nextBtn.enabled = true;
  } else {
    nextBtn.enabled = false;
  }
  
  // Set the next and previous starting indexes.
  nextBtn.start = result.endIndex;
  if (result.startIndex - 10 < 0) {
    prevBtn.start = 0;
  } else {
    prevBtn.start = result.startIndex - 11;
  }
};

res.onStatus = function (status) {
  trace(status.description);
};

// Perform the search.
function doSearch (cmpnt) {
  var start = 0;

  // If the calling component is the next or previous button, 
  // set the start value appropriately.
  if (cmpnt._name != "searchBtn") {
    start = cmpnt.start;
  }

  // Create the parameter object that the
  // doGoogleSearch(  ) web service method expects.
  var params = new Object(  );
  params.key = "xxxxxxxxxxxxxxxx"; // Your Google registration key goes here.
  params.q = searchString.text;
  params.start = start;
  params.maxResults = 10;
  params.filter = true;
  params.restrict = "";
  params.safeSearch = false;
  params.lr = "";
  params.ie = "";
  params.oe = ""; 
  srv.doGoogleSearch(res, params);
}

// Call initNetServices(  ) and initForm(  ) to start the movie.
initNetServices(  );
initForm(  );

The search service is the longest of the three modules we've implemented, and quite a bit of its code warrants further explanation.

The initNetServices( ) function uses the NetServices object's methods in the standard ways. The gateway URL used in the example code points to the Flash Remoting gateway for the localhost, as it would be installed for a standard installation of ColdFusion MX as a standalone server on port 8500. The service object is opened using the URL to the Google web service .wsdl file as the service name. This is the standard way in which web services are consumed directly from a Flash movie using Flash Remoting (for ColdFusion MX), as discussed in Recipe 20.24.

function initNetServices (  ) {
  gwUrl = "http://localhost:8500/flashservices/gateway/";
  NetServices.setDefaultGatewayURL(gwUrl);
  conn = NetServices.createGatewayConnection(  );
  srv = conn.getService("http://api.google.com/GoogleSearch.wsdl");
}

Web services can also be consumed directly from Flash movies if you are using Flash Remoting for .NET, in which case only the gateway URL differs. In that case, point the gateway to an .aspx page that exists within the root directory of the IIS web application in which Flash Remoting is installed.

Thus, when using ASP.NET, change gwURL, as follows:

function initNetServices (  ) {
  gwUrl = "http://yourservername/yourWebApplication/gateway.aspx";
  ...
}

To perform a Google search, attach the required search parameters as properties of a generic object and pass the object to the Google web service, as shown in the doSearch( ) method. For example, the q property specifies the search string and the start property specifies the starting record requested from the search.

The response object's onResult( ) method receives the search results returned from the Google web service. The return value is an object containing several properties, including an array containing the matches for the search.

The names and datatypes of the properties of both the submission and return objects are determined by the web service's .wsdl file (open the .wsdl file in a text editor to explore them further). The return properties that we are most interested in with regards to this example are the following:

estimatedTotalResultsCount

The total number of results from the search (estimated).

startIndex

The index of the first returned result relative to the total results (not just the one page of results).

endIndex

The index of the last returned result relative to the total results.

resultElements

An array of results. Each element in the array is an object whose properties are:

url

The URL to the web page associated with a search result

title

The title of the web page

snippet

A text snippet from the web page

summary

A textual summary of the web page

The onResult( ) method loops through each of the elements in the resultElements array and displays the values in the searchResults text field. The code uses the searchResults field's htmlText property so that HTML tags such as <a href> can be used to create links, and the <font> and <u> HTML tags can be used to create formatting easily. I have chosen to display each search result's title, snippet, and summary. I made this decision in keeping with the conventions for how search results are displayed on the Google web page; however, you are free to experiment with how the results are displayed. In this example, the result's title is made into a hyperlink to the URL associated with the page. This is accomplished using an <a href> HTML tag. Also, because links do not automatically have formatting applied to them in Flash (as they do in most HTML browsers), I have added a <font> tag to color the title blue and a <u> tag to underline the title.

res.onResult = function (result) {
  var r = searchResults;
  r.html = true;
  r.htmlText = "";
  var re = result.resultElements;
  for (var i = 0; i < re.length; i++) {
    r.htmlText += "<font color=\"#0000FF\"><u><a href=\"" + re[i].url + 
                  "\">" + re[i].title + "</a></u></font><br>";
    r.htmlText += re[i].snippet + "<br>";
    if (re[i].summary != "") {
      r.htmlText += re[i].summary + "<br>";
    }
    r.htmlText += "<br><br>";
  }
   . . . 
};

The onResult( ) method also ensures that the user cannot browse to results that do not exist (prior to the first page or after the last page). The example enables and disables the prevBtn and nextBtn instances, as appropriate, when the user is on the first or last page of results. If the result object's startIndex is not greater than 0, it means that the first page of results is being displayed, so the prevBtn is disabled. Likewise, if the endIndex is not less than the estimatedTotalResultsCount property, the nextBtn is disabled. Additionally, both the nextBtn and prevBtn objects store a value in a custom property named start. I define the start property here so that the program can keep track of the starting index of the next and previous set of search results. Each time a new page of results is returned to the onResult( ) method, the start property for nextBtn and prevBtn is updated to reflect the change. The next page of results always has a starting index that is one more than the endIndex value of the current set. Likewise, the previous page of results will always be the startIndex value of the current page minus 11 (assuming that there are 10 results per page) because Google adds 1 to the start parameter passed as part of the search request. Just in case the indexing is skewed at some point in browsing next and previous pages, you should make sure that the start property for the prevBtn is never less than 0. In the event that it is, simply reset it to 0.

  var count = result.estimatedTotalResultsCount;
  if (result.startIndex > 0) {
    prevBtn.enabled = true;
  } else {
    prevBtn.enabled = false;
  }
  if (result.endIndex < count) {
    nextBtn.enabled = true;
  } else {
    nextBtn.enabled = false;
  }
  nextBtn.start = result.endIndex;
  if (result.startIndex - 10 < 0) {
    prevBtn.start = 0;
  } else {
    prevBtn.start = result.startIndex - 11;
  }

Finally, there is the doSearch( ) function, which calls the Google web service. This function, for all its importance, is surprisingly short. The function is set as the callback function for the Search button (searchBtn) instance and also for the Previous and Next buttons (prevBtn and nextBtn). This is because each time the user wants to view another page (next or previous) of search results, the Flash movie needs to call the web service method again. While the actions taken are the same no matter which button invokes the function, the start value sent to the web service differs. If searchBtn invokes the function, the starting index is 0, and the web service returns the first page of results. However, if prevBtn or nextBtn invokes the function, the starting index is passed in as a property of the calling component (cmpnt.start). In this way, the pages of results returned have different starting indexes instead of always beginning with 0.

Other than the if statement that handles the starting index, the rest of the function is the same whether initiated by the Search, Previous, or Next button. It creates the params object containing the search parameters and sends it to the web service method. Each of the properties of the params object are required and defined by the .wsdl file for the web service. The only property that changes dynamically within this example is the start property. In the example, the maxResults property is set to a fixed value of 10 so that the number of results per page is always 10. The value 10 is the maximum value allowed by Google. However, you could also set the value to any number between 1 and 9. You could also allow the user to select the number of results to return per page. One possible approach to this would be to add a ComboBox instance with values from 1 to 10, and to dynamically set the maxResults property according to the selected value.

You must specify your Google registration key as the params.key property.

The srv object on which the Google service is called is the one returned by the earlier call to getService( ). The res parameter passed to doGoogleSearch( ) is the results text field defined earlier (and used here as the result object).

function doSearch (cmpnt) {
  var start = 0;
  if (cmpnt._name != "searchBtn") {
    start = cmpnt.start;
  }
  var params = new Object(  );
  params.key = "XXXXXXXXXXXXXXXXX"; // Your Google registration key goes here.
  params.q = searchString.text;
  params.start = start;
  params.maxResults = 10;
  params.filter = true;
  params.restrict = "";
  params.safeSearch = false;
  params.lr = "";
  params.ie = "";
  params.oe = ""; 
  srv.doGoogleSearch(res, params);
}
    [ Team LiB ] Previous Section Next Section