Relate Two ComboBoxes In Flex: My Example and Your Suggestions For Improvement

I'm working on an internal Flex application that requires two ComboBoxes. The user's selection in one ComboBox determines the values displayed in the other ComboBox. This post describes my initial solution and seeks any comments improving how I relate the two ComboBoxes. To demonstrate my solution go to: http://www.stfm.org/flex/relateComboBoxes/bin/relateComboBoxes.html (right click to view source).

Step 1: Get The Data For The ComboBoxes

The data used are approximately 800 companies grouped by state. The first ComboBox displays the states. After the user selects a state, the second ComboBox displays the companies located in that state. The data originates in two tables in a database. I have no control over these tables and cannot alter their structure. Companies are changed/added/removed frequently.

Below is the CFC function I call from within Flex to get the data.

<cffunction name="getCompanies" access="public" returntype="struct" output="no">
   
      <cfset Var companies = " ">
      <cfset Var stCompInfo = "">
      <cfset Var t1 = "">
      
      <!---get companies - used for company drop down box--->
      <cfquery name="companies" datasource="OMITTED">
         
         SELECT tblCompanies.[Company Name] AS company_name, tblCompanies.[Company ID] AS company_id,
         tblCompanyAddresses.State
         FROM tblCompanies INNER JOIN tblCompanyAddresses
         ON tblCompanies.[Company ID] = tblCompanyAddresses.[Company ID]
         ORDER BY tblCompanyAddresses.State, tblCompanies.[Company Name]
         
      </cfquery>
      
    <!---Let's create a stucture that holds an array of structures
    set up a structure of companies by state
key is state abbrev. - value is an array of structures that
      includes company id and company name--->
      <cfset stCompInfo = structnew()>
      
      <cfloop query="companies">
         
         <!---if the state is not already a key value, make it one and
         assign an array as its value--->

         <cfif NOT structkeyexists(stCompInfo, ucase(state))>
            
            <cfset stCompInfo[ucase(state)] = arraynew(1)>
            
         </cfif>
         <!---create a temporary structure to hold this company's information--->
         <cfset t1 = structnew()>
         <cfset t1.company_name = company_name>
         <cfset t1.company_id = company_id>
         
         <!---append this structure to the array--->
       <cfset arrayappend(stCompInfo[ucase(state)], t1)>
         
      </cfloop>
      
      <cfreturn stCompInfo>
   
   </cffunction>

The above function returns a complex structure. The structure has a key for each state (AL, AK, AR, etc). The value for each key is an array. Each element of the array is another structure. The structure that is an array element has a key for company_name (whose value is the company_name from the query) and a key for company_id (whose value is the company_id from the query). Please note that I learned this code technique from Dave Ross of ColdSpring fame. As you can imagine, Dave is one of the smartest CF programmers I know.

Step 2: Process The Data In Flex

Once I get the structure into Flex, I treat it as a generic Object (Flex doesn't have a structure data type). The function below from my Flex application handles the result returned from calling the above CFC method.

/*Handles the result returned by calling the getCompanies method
   of the CFC
   */

   public function handleGetCompanies(event:ResultEvent):void{
      
      oResult = event.result as Object; //CFC method returns a structure                                //which in Flex is treated                                //as a generic Object       
      //result returned is an Object containing a field for each       //state (eg AL, AK, AR, etc)       //Each state field is an array       //For example oResult.AL is an array       //Each array element is an Object with a       //COMPANY_NAME and COMPANY_ID field       
      //set the companyAryCol to the oResult.AL array       //companyAryCol now holds all the Objects stored       //in the oResult.AL array       //Each object in the array has a COMPANY_NAME and COMPANY_ID field       
      companyAryCol = new ArrayCollection( oResult.AL );
      
      /*
      var akAryCol:ArrayCollection = new ArrayCollection( oResult.CA ) ;
      
      Alert.show( akAryCol[1].COMPANY_NAME );   
      
      Alert.show("Done");
      */

         
   } //end function

In Flex, the oResult Object now has a field for each state (AL, AK, AR, etc). Stored in that field is an array. Each element of that array is an Object that has two fields: COMPANY_NAME and COMPANY_ID (note all capital letters). In the above function I create an ArrayCollection (companyAryCol) using the oResult.AL array. This array stores all the objects that represent the companies in Alabama. companyAryCol is the dataprovider for my second ComboBox. So when the application first runs, the top state is AL in the state ComboBox and the companies in the company ComboBox are the companies in Alabama.

Step 3: Relate The Two ComboBoxes

When the user changes the selected state in the state ComboBox, my Flex application calls the below function.

private function changeEvt(event:Event):void {
            
/*
   Alert.show(event.currentTarget.selectedItem.label + " " +
   event.currentTarget.selectedIndex );
*/


   //update the companyAryCol which is the data provider for the    //company ComboBox    switch( event.currentTarget.selectedLabel ) {
      
      case "AL": companyAryCol = new ArrayCollection( oResult.AL );
            break;
      case "AK": companyAryCol = new ArrayCollection( oResult.AK );
            break;
      case "AR": companyAryCol = new ArrayCollection( oResult.AR );
            break;
      case "AZ": companyAryCol = new ArrayCollection( oResult.AZ );
            break;
      case "CA": companyAryCol = new ArrayCollection( oResult.CA );
            break;
      case "CO": companyAryCol = new ArrayCollection( oResult.CO );
            break;
      case "CT": companyAryCol = new ArrayCollection( oResult.CT );
            break;
      
   }//end switch    
   /*
   Another possible solution is to call a CFC method passing that method the
   state selected and getting back the companies for that state
   Then I would not need this switch statement
   */

   
}//end function changeEvt

This function uses a switch statement (note to Java programmers new to ActionScript 3.0, in ActionScript the switch statement can be used to compare strings) to compare the label value of what the user selected in the state ComboBox with the state strings ("AL", "AK", etc). When there is a match, the companyAryCol is recreated using the field of the oResult object that matches that state. Thus, if the user selected AZ in the state ComboBox, the companyAryCol is recreated and now holds all the objects stored in the oResult.AZ array. Since the companyAryCol is the dataprovider for the second ComboBox, the second ComboBox is automatically updated and now shows the companies located in Arizona. (Note that I left out most of the 50 some case statements in the switch statement above)

Overall, the test seems to work well. It is a hassle to write the 50 some case statements in the switch statement. I considered just calling another CFC method when the user selects a state, sending the CFC method the state value the user selected, and having it return the companies for that state. I could then use that data to recreate the ArrayCollection that is the data provider for the second ComboBox. However, I felt that this solution might cause a noticeable delay after the user selects the state until the second ComboBox is updated with all the companies for that state. This solution I tested above seems to be very responsive to the user both in the initial load (800 complex structures come into the Flex application quickly) and when the user selects a state.

If you have some suggestions to improve how I am relating these two ComboBoxes, please post a comment.

Comments
Hey Bruce,

I havent actually tried this but...

Have you tried setting the dataprovider of the companyCB to....
oResult[stateCB.selectedItem] ?

You may have to change your stateCB dataprovider items from mx:Object to mx:String

Could save you lots of switch statements

Or if you could modify your struct returned from CF to return an array of structs something like
ArrayofStates
item:
StateName - VA
CompaniesArray
item:
StateName - another
CompaniesArray

Then you could do your comboboxes like
StatesCB dataProvider="{oResult}" labelField="StateName"

CompanyCB dataProvider="{StatesCB.selectedItem.CompaniesArray}" labelField="COMPANY_NAME"

Hope this helps,
Scotty
# Posted By Scotty | 12/13/06 5:12 PM
Hi Bruce, there is a much simpler way to do what you want. ArrayCollection supports a nifty property called filterFunction which does what the name implies. You create a function that accepts a single parameter (of type object if memory serves) and returns a boolean value.

So what you should do is simply dump each of your tables into its own ArrayCollection. The filterFunction property on your companyListAC would look something like this:

private function filterByState(obj:Object):Boolean
{
return obj == stateDropDown.selectedValue;
}

Then in the change handler for your stateDropDown you will simply need to call companyListAC.refresh(); Something else that may come in handy, which I just learned recently, is that ComboBoxes support a property called prompt which is the text they display before any selection is made. So for your companyDropDown you may assign something like "Select state first..." to the prompt property.

HTH,
Ben
# Posted By Ben | 12/13/06 7:36 PM
Scotty: You may be on to something with changing my return from the CFC function to an array of Structures. I will try that. I could not do oResult[stateCB.selectedItem], as that caused a syntax error.

Ben - I had not thought of filtering the ArrayCollection so it only holds the Object that matches the state the user selected. I have done some filtering in other projects. I'll give that a shot.

Thanks for the feedback.
# Posted By Bruce | 12/13/06 8:48 PM
Bruce:

You can dynamically set which array from your object to use rather than a switch statement.

private function changeEvt(event:Event):void {
companyAryCol = new ArrayCollection( oResult[event.currentTarget.selectedLabel] );
}
# Posted By Sam Shrefler | 12/14/06 7:21 AM
Sam:

Awesome! That worked. You just save me over 50 lines of code.

Brilliant!


Bruce
# Posted By Bruce | 12/14/06 9:45 AM
I've updated the source code view with Sam's suggestion. I commented out the old code in function changeEvt so you can see what I first tried to do. Visit:

http://www.stfm.org/flex/relatecomboboxes/bin/rela...

and right click to view the source.
# Posted By Bruce | 12/14/06 9:50 AM
Hi Bruce,
thanks for a great tutorial. I've got a problem. I am loading xml file with the structure:
<catalog>
<author name="1vva >ax‚€uav" authorName="1vva >ax‚€uav" authorID="1" hometown="Berd" age="4" clubName="Pound">
<photo name="?aratb ~asa|elk}" description="?aratb ~asa|elk}" photoID="1" source="anna_tsaturyan_a6.jpg" authorName="1vva >ax‚€uav" authorID="1" thumbSource="anna_tsaturyan_a6.jpg"/>
</author>
</catalog>

Then:
<mx:HTTPService url="http://localhost:8500/xmlgallery/assets/shrjanak.x..." id="galleryRPC" resultFormat="e4x" result="photoHandler(event)" />
<mx:XMLListCollection id="authorsColl" source="{galleryRPC.lastResult.author}"/>

<mx:Script>
   <![CDATA[
   import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import flash.events.Event;
   import mx.events.*;
import mx.collections.ArrayCollection;
import mx.collections.XMLListCollection;

private var oResult:Object;
   [Bindable]
public var photoArrayColl:XMLListCollection;


public function photoHandler(event:ResultEvent):void{
    oResult = event.result..photo as Object;
   
}
private function changeEvt(event:Event):void{
   
   photoArrayColl = new XMLListCollection(event.currentTarget.selectedItem..photo) ;
   trace(photoArrayColl);
}
         
   ]]>
</mx:Script>

and in the rest i am getting 2 interralted combos:
<mx:ComboBox id="authors" dataProvider="{authorsColl}" labelField="@name" change="changeEvt(event)"/>
<mx:Label text="{authors.selectedItem.@name}"/>
<mx:ComboBox dataProvider="{photoArrayColl}" id="images" labelField="@thumbSource"/>
   
</mx:HBox>

At this point everything works fine.
Then i want to use same staments to generate tile list with images, which are using their @thumbSource attribute for the source.

That doesnt work, getting "unable to bind thumbSource" error.
Could you help to understand what may cause the error.

Thank you in advance,
Mika
# Posted By Mka | 12/25/06 5:31 PM
Mka:

Take a look at my examples of using the TileList component. You may need to convert your XML to an ArrayCollection of Objects.
# Posted By Bruce | 12/26/06 6:58 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.1.004.