This tutorial demonstrates how a developer can easily extend the prepackaged Flex library classes to achieve superior user interface results.
In this case, we'll be creating an advanced datagrid which can make insanely long lists into manageable chunks and display them as paged lists. This tutorial deals with primarily AS3, but has a dose of php and mySQL to support back end functionality. The application will be able to store and recall the list's shown columns, current page, sort orders and sort fields. Let's go..
Introduction
The datagrid and advanced datagrid classes packaged with Flex are rather limited in functionality. They are only good for very short lists in a real world application. Lists can be millions of lines long and XML documents can't be sent representing the full list efficiently. Also, if the XML returned only represents one section of the list, then that is the only section the user will ever see.
This tutorial is the first stage of my own experimentation trying to make advanced datagrids act as the user would intend. Sections of this could be used for a regular DataGrid, but the AdvancedDataGrid is much more usable. The AdvancedDataGrid has a watermark unless you purchase the Professional Edition or if you're a student and pick up the education version of FlexBuilder. You will need FlexBuilder, a place to host your php code and a mySQL server for the database to follow this tutorial. I'm also assuming you're competent in basic FlexBuilder functionality like building projects.
Step 1: Requirements Gathering
The first step is always to figure out what you want to do. Since all applications are different in their use, your own lists may have different functionality. Based on my user requirements I determined that I needed an application that could:
- Show data from a list that could be 1 - ∞ records long.
- Allow paging of the data to enhance UI and load times.
- Remember the fields being shown.
- Remember the fields the list is being sorted on.
- Remember the direction those fields are being sorted on (ascending or descending).
- Allow the user to change the field order of the grid.
- Allow the user to change the page that they are viewing.
Step 2: Determine List Controls
The user will have all controls built into the AdvancedDataGrid such as multi column sorting and column reordering through dragging the headers. Additionally, the user will have to be able to navigate through the pages of the list. For this, we'll need to use a stepper, as well as buttons to allow the user to go to the first page, go to the previous page, reload, go to the next page and go to the last page. Since this cannot be built into the AdvancedDataGrid class itself we must have some kind of container that we create which holds the list we'll be working with and all of the controls.
Step 3: What Are My Events?
We're trying to make the front end as "dumb" as we can. This means that for every action the user makes we must record their actions on the backend. I usually write out a list of everything that must be communicated to my server to help with flow. It also helps for agile development so we can focus on one piece of functionality at a time. For this experiment I determined the following events will need to take place based on the requirements gathered. I've broken this down into two groups; events fired by the list itself and events fired by the controls outside of the list
Outside Events:
- Page is visited for the first time this session.
- Stepper value is changed using the stepper item.
- Stepper value is changed using the navigation buttons.
List Events:
- User changes the grid's column sorting.
- User changes the grid's column display order.
- Columns are loaded.
- List items are loaded.
The Database
Long lists must come from databases. While there are numerous ways to do this, I have chosen something simple just to get data back to for this experiment. We'll need to create a database and two tables. One table for the data we are going to list, and one table as an advanced session variable storage that will make it possible for a list to remember its state as long as the session is valid.
Step 4: Create Your Tables
Create two tables. The first is the actual data that we'll be retrieving. To make things simple you can simply copy the following to get an exact replica of my test data.
CREATE TABLE IF NOT EXISTS `ExampleTable` ( `RecID` int(10) unsigned NOT NULL auto_increment, `Name` varchar(100) NOT NULL, `Age` int(11) NOT NULL, `School` varchar(100) NOT NULL, PRIMARY KEY (`RecID`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=16 ; INSERT INTO `ExampleTable` (`RecID`, `Name`, `Age`, `School`) VALUES (1, 'Bugs Bunny', 57, 'Acme U'), (2, 'Mickey Mouse', 72, 'Disney Community College'), (3, 'Bart Simpson', 12, 'Springfield Grade'), (4, 'Peter Griffin', 38, 'Cohog High'), (5, 'Speedy Gonzalez', 20, 'Univesity of Andale'), (6, 'Pepe Lepew', 25, 'La Conservatoir National'), (7, 'Homer Simpson', 39, 'Springfield University'), (8, 'Raphael', 18, 'Splinter Special Studies'), (9, 'Splinter', 34, 'The Technodrome'), (11, 'Donald Duck', 34, 'Naval Academy'), (12, 'Scrooge McDuck', 65, 'McDougal University'), (13, 'Papa Smurf', 72, 'Smurf School'), (14, 'Jabberjaws', 15, 'School Under the Sea'), (15, 'Quicky Koala', 41, 'Australian State');
Now we must create the table to store session information. This is the table that makes this whole thing work.
CREATE TABLE IF NOT EXISTS `Session` ( `RecID` int(10) unsigned NOT NULL auto_increment, `SessionID` varchar(32) NOT NULL, `Name` varchar(200) NOT NULL, `Value` varchar(40960) NOT NULL, `LastActiveDTS` timestamp NOT NULL default '0000-00-00 00:00:00' on update CURRENT_TIMESTAMP, PRIMARY KEY (`RecID`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=37 ;
The Session Table has a unique key id, a "SessionID" which will be stored as a cookie in PHP, the "Name" of the type of information being stored, the "Value" stored and a "LastActiveDTS" to help with future cleanup to purge old expired sessions. This cleanup is not currently implemented.
PHP Event Code
As we determined in Step 3 there are several events that must happen. The following steps go through the php scripts that allow these events to occur on the server. There are many ways to do this, what I have coded works, but even I would not use this in a production environment.
Step 5: Tag.php
I created a helper class to help with creating any kind of structured tagged output. This can be used to create html, but in this case it will be the basis for XML. I won't go into this code much as I use it as an accessory, more than the point of this tutorial.
/** * @author Jay Leffue * @version 1.0 */ /** * This class is the tag of XML. */ class Tag { /** * The name of this tag. */ private $_name; /** * the array of properties to this tag. */ private $_props; /** * Array of Tags that are direct chilren of this Tag. */ private $_children; /** * The string content inside this tag. */ private $_content; /** * class constructor * * @param string $TagName the name of this tag * @param array $Attributes an array of attributes this tag will have * @param string $Content content that goes inside this tag. * @return void */ public function __construct($TagName, $Props = null, $Content = null) { $this->_name = $TagName; $this->_props = array(); if(isset($Props)) { $this->setProps($Props); } if(isset($Content)) { $this->_content = $Content; } $this->_children = array(); } /** * Returns the name used by this Tag. * * @return string */ public function getName() { return $this->_name; } /** * Replaces the current properties for this tag and returns a reference to self. * * @param array $props An array of properties to set for this element. * * @throws Exception * @return Tag */ public function setProps($Props) { if (!is_array($Props)) throw new Exception('Invalid Properties'); $this->_props = $Props; return $this; } /** * Adds a specific property for this tag and returns a reference to self. * If the property already exists it will replace it. * * @param mixed $Props An array of properties or if just setting one property a string indicating the name of the property to set. * @param mixed $Value If $props is a string this will be the value to set for that property. * * @throws Exception * @return HTMLElement */ public function addProps($Props,$Value=null) { if (is_array($Props)) { $this->_props = array_merge($this->_props,$Props); } elseif(isset($Value)) { $this->_props[$Props] = $Value; } else { unset($this->_props[$Props]); } return $this; } /** * Gets a specific property for this tag. * * @param string $Name The name of the property to return. * @return mixed */ public function getProp($Name=null) { if (!isset($Name)) { return $this->_props; } elseif(isset($this->_props[$Name])) { return $this->_props[$Name]; } return null; } /** * Returns true if the specified property exists. * * @param string $Name the name of the property we are searching for. * * @return boolean */ public function hasProp($Name) { return array_key_exists($Name,$this->_props); } /** * Adds child tags to this tag and returns the tag it added. * * @param Tag $Child The tag to make a child of this Tag. * @param mixed $Key Optional value of the array key to assign to this array element * * @return Tag */ public function addChild(Tag $Child,$Key=null) { if(isset($Key)) { $this->_children[$Key] = $Child; } else { $this->_children[] = $Child; } return $Child; } /** * Looks to see if the tag has an immediate child with the specified name. * * @param string $Name The Name we are looking for. * * @return boolean */ public function hasChild($Name) { foreach($this->_children as $Child) { if($Child->getName() == $Name) { return true; } } return false; } /** * Returns the children. * * @param none. * * @return array */ public function getChildren() { return $this->_children; } /** * Returns the Child Tag with the name specified * * @param $string $Name the name of the tag we hope to return * * @return Tag */ public function getChild($Name) { foreach($this->_children as $Child) { if($Child->getName() == $Name) { return $Child; } } } public function getChildCount() { return count($this->_children); } /** * Returns the xml of this tag, if it has children tags, they will be called as well * * @param none * * @return string The resulting XML */ public function getTagXML() { $string = ''; //if there are no children then just return the XML tag with embedded '/' at the end if(empty($this->_children)) { if(isset($this->_content)) { $string .= "<".$this->_name; foreach($this->_props as $name=>$prop) { $prop = ereg_replace("'",'',$prop); // Remove single quotes as these appear to be causing issues with fusion charts. $string .= " ".$name."='".$prop."'"; } $string .=">".$this->_content."</".$this->_name.">"; } else { $string .= "<".$this->_name; foreach($this->_props as $name=>$prop) { $prop = ereg_replace("'",'',$prop); // Remove single quotes as these appear to be causing issues with fusion charts. $string .= " ".$name."='".$prop."'"; } $string .=" />"; } } //otherwise we'll need to dig deeper before closing our tag else { $string.= "<".$this->_name; foreach($this->_props as $name=>$prop) { $prop = ereg_replace("'",'',$prop); // Remove single quotes as these appear to be causing issues with fusion charts. $string .= " ".$name."='".$prop."'"; } $string .=">"; //now go through its children foreach($this->_children as $Child) { $string .= $Child->getTagXML(); } $string .= "</".$this->_name.">"; } return $string; } }
Step 6: GetListColumns.php
This script initializes the session if needed, then returns the columns in the display order and what their sorts are.
First, we make a connection to the database and include our Tag class.
require_once('Tag.php'); //Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con);
Logically, this is the first event that will be fired when the user first visits the page with the list. Columns must be known before they can be retrieved. With this assumption, the first part of the code deals with initializing the default list options in the Session table and assigning a cookie to this user.
//There will be no cookie for session if this is their first time on the page. if (!array_key_exists('session',$_COOKIE)) { //Since they do not have a cookie set yet, and this is the first php call, we'll set up some session information for them. $_COOKIE['session'] = md5(time()); setcookie('session',$_COOKIE['session']); //There are several items I will set up as defaults here for the list. //create the default column order mysql_query(sprintf("INSERT INTO Session (SessionID, Name, Value) VALUES ('%s', '%s', '%s')",$_COOKIE['session'],'ColumnOrder','Name,Age,School')); //create the default column sort, this is a comma delimited string that is one to one with column order mysql_query(sprintf("INSERT INTO Session (SessionID, Name, Value) VALUES ('%s', '%s', '%s')",$_COOKIE['session'],'SortOrders','Name')); mysql_query(sprintf("INSERT INTO Session (SessionID, Name, Value) VALUES ('%s', '%s', '%s')",$_COOKIE['session'],'SortDirections','asc')); //create the default page for the pager mysql_query(sprintf("INSERT INTO Session (SessionID, Name, Value) VALUES ('%s', '%s', '%s')",$_COOKIE['session'],'ListPage','1')); }
Now we get the columns to show. We know at this point that we have a session and session variables for this list.
//Now it is guaranteed that the user will have a session cookie, as well as the default list at least, so we use the database to //determine the column order. $dbResult = mysql_query(sprintf("SELECT Value FROM Session WHERE Name = 'ColumnOrder' and SessionID = '%s'",$_COOKIE['session'])); $return = new Tag('node'); $columns = $return->addChild(new Tag('columns')); //generic loop while($row = mysql_fetch_array($dbResult)) { $columnA = explode(',',$row['Value']); foreach($columnA as $column) { $columns->addChild(new Tag('column',array('header'=>$column,'data'=>$column,'width'=>'200'))); } } mysql_close($con); print('<?xml version="1.0" encoding="UTF-8"?>'.$return->getTagXML());
Step 7: listLoad.php
This script is what will return the actual data for the DataGrid.
First, we make a connection to the database and include our Tag class.
require_once('Tag.php'); try { $head = new Tag('items'); //Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con);
We create the query based on values from the session table. Namely, the current page and the sort paramenters needed.
//set up how to query based on stored session variables //get the start record, assuming 5 records per page. $dbResult = mysql_query(sprintf("SELECT Value FROM Session WHERE Name = 'ListPage' and SessionID = '%s'",$_COOKIE['session'])); $page = (int)mysql_result($dbResult, 0); $startRecord = $page*5 - 5; //get the sort columns and associate to their direction $dbResult = mysql_query(sprintf("SELECT Value FROM Session WHERE Name = 'SortOrders' and SessionID = '%s'",$_COOKIE['session'])); $order = explode(',',mysql_result($dbResult, 0)); $dbResult = mysql_query(sprintf("SELECT Value FROM Session WHERE Name = 'SortDirections' and SessionID = '%s'",$_COOKIE['session'])); $directions = explode(',',mysql_result($dbResult, 0)); $orderBy = array(); for($i=0;$i < sizeof($order);$i++) { $orderBy[] = $order[$i].' '.$directions[$i]; } $query = sprintf('SELECT * FROM ExampleTable ORDER BY %s LIMIT %d,5',implode(',',$orderBy),$startRecord);
Now that we have created our desired query string, we get the data and return it in XML form. We must also return the ORDER BY parameters so that it can be displayed to the user on the front end.
//query the table $dbResult = mysql_query($query); while($row = mysql_fetch_array($dbResult)) { $item = $head->addChild(new Tag('item')); $item->addChild(new Tag('id',null,$row['RecID'])); $item->addChild(new Tag('Name',null,$row['Name'])); $item->addChild(new Tag('Age',null,$row['Age'])); $item->addChild(new Tag('School',null,$row['School'])); } $order = $head->addChild(new Tag('sorts',null,implode(',',$orderBy))); mysql_close($con); print('<?xml version="1.0" encoding="UTF-8"?>'.$head->getTagXML()); } catch(Exception $e) { print('There was an error that prevented the process from completing.'); }
Step 8: stepperMax.php
This script is needed to determine the maximum number of pages possible for the list. It returns the max pages, as well as the current page for display purposes. For simplicity, the result is a string formatted like "MAX, CURRENT".
Complete script:
//Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con); //calculate the total number of pages assuming 5 per page. $query = sprintf("SELECT COUNT(*) FROM ExampleTable"); $dbResult = mysql_query($query); $totalRecs = (float)mysql_result($dbResult, 0); $maxPage = ceil($totalRecs / 5); $page=1; $dbResult = mysql_query(sprintf("SELECT Value FROM Session WHERE Name = 'ListPage' and SessionID = '%s'",$_COOKIE['session'])); if($dbResult) { while($row = mysql_fetch_assoc($dbResult)) { $page = $row['Value']; } } mysql_close($con); print($maxPage.','.$page);
Breather
This wraps up our scripts needed to make the list visible at all. The next php files are solely for updating session information based on user interaction.
Step 9: stepperUpdate.php
This is the simple script that changes the value of the "ListPage" session variable. This script is called whenever the stepper of the list is modified by the user. It also assumes there will be a post parameter called "page" sent from the front end application.
//Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con); //create the default column order $query = sprintf("UPDATE Session SET Value = '%s' WHERE SessionID = '%s' AND Name = 'ListPage'",$_POST['page'],$_COOKIE['session']); if(mysql_query($query)) { print("1"); } else { print("0"); } mysql_close($con);
Step 10: SetColumnOrder.php
This is the simple script that changes the value of the "ColumnOrder" session variable. This script is called whenever the user changes the columns around on the front end and it assumes a Post variable named "order".
//Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con); //create the default column order $query = sprintf("UPDATE Session SET Value = '%s' WHERE SessionID = '%s' AND Name = 'ColumnOrder'",$_POST['order'],$_COOKIE['session']); if(mysql_query($query)) { print("1"); } else { print "0"; } mysql_close($con);
Step 11: SetSort.php
This is the simple script that changes the values of the "SortOrders" and "SortDirections" session variables. This script is called whenever the user changes the sorts around on the front end. It assumes a Post variable named "order" and a Post variable named "directions".
//Open communication to the database and table for sessions. $con = mysql_connect("localhost","Your Login Name","Your Password"); if (!$con) { die('Could not connect: ' . mysql_error()); } mysql_select_db("Your Database", $con); //create the default column order $query = sprintf("UPDATE Session SET Value = '%s' WHERE SessionID = '%s' AND Name = 'SortOrders'",$_POST['order'],$_COOKIE['session']); $query2 = sprintf("UPDATE Session SET Value = '%s' WHERE SessionID = '%s' AND Name = 'SortDirections'",$_POST['directions'],$_COOKIE['session']); if(mysql_query($query) and mysql_query($query2)) { print("1"); } else { print "0"; } mysql_close($con);
/PHP
That wraps up all the PHP scripts needed to provide for the functionality we want. For ease of use, these files should reside in the same directory as the page that will hold the .swf and the .swf itself. I didn't go into too much detail as there are many other ways to access a database through php. My final result will most likely only have one php script that is called that can interpret the type of request and has a sort of factory class that determines what should be set or returned. For this example that may have just been overkill. Just remember, we are trying to keep the front end as dumb as we can. The less logic it has to do the better.
Now, finally, we have the backend ready and we can start with Flex.
Step 12: Set Up Folders in FlexBuilder
In the src folder of your project create a folder called "css" and a folder called "images".
Step 13: Create Images For Stepper Buttons
You could just use generic buttons, but since vectortuts gave us such a nice selection of icons here : https://vector.tutsplus.com/articles/web-roundups/60-free-vector-icon-packs-for-design-professionals/ we can utilize some of these. I used for my example the milky icon set to get my arrow buttons. I created the mouse over icons by simply applying a grey screen over the green and saving it as such. The names I used are in the CSS file we'll discuss now. Just remember, we need a total of ten icons; five for the icon, and five matching icons for mouseover events.
Step 14: longList.css
This is my css file for visualization of the list I will be creating.
/* CSS file */ .nextButton { up-skin: Embed("images/next.png"); down-skin: Embed("images/nextover.png"); over-skin: Embed("images/nextover.png"); disabled-skin: Embed("images/next.png"); } .previousButton { up-skin: Embed("images/previous.png"); down-skin: Embed("images/previousover.png"); over-skin: Embed("images/previousover.png"); disabled-skin: Embed("images/previous.png"); } .lastButton { up-skin: Embed("images/last.png"); down-skin: Embed("images/lastover.png"); over-skin: Embed("images/lastover.png"); disabled-skin: Embed("images/last.png"); } .firstButton { up-skin: Embed("images/first.png"); down-skin: Embed("images/firstover.png"); over-skin: Embed("images/firstover.png"); disabled-skin: Embed("images/first.png"); } .reloadButton { up-skin: Embed("images/reload.png"); down-skin: Embed("images/reloadover.png"); over-skin: Embed("images/reloadover.png"); disabled-skin: Embed("images/reload.png"); }
Step 15: LongList.as
Create a new ActionScript class. This class will be our list itself so it extends the AdvancedDataGridClass. I have tried to comment the code as needed so as not to break up the source. By keeping the files at the root we don't have to worry about package paths. In a real world setting I highly advise against doing this as all your script files will need to be organized in different directories.
The key to remember about this class is that we're not really adding much to the library supplied with Flex. All we have to do is give an AdvancedDataGrid the ability to fire events to the server to load information, or change session information. Actual user interface is not changed at all.
package { //Files to include. import flash.events.Event; import mx.collections.ArrayCollection; import mx.collections.Sort; import mx.collections.SortField; import mx.collections.XMLListCollection; import mx.controls.AdvancedDataGrid; import mx.controls.Alert; import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn; import mx.events.AdvancedDataGridEvent; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.rpc.http.mxml.HTTPService; //Class for lists whos data is very long and can last for several pages of data. public class LongList extends AdvancedDataGrid { //The service that loads the columns private var columnService:HTTPService; //The service that loads the list data private var listService:HTTPService; //The service that sets the columns order. private var columnOrderService:HTTPService; //The service that sets the sorts. private var columnSortService:HTTPService; //Names of script pages for each of the services. private var columnLoadURL:String = "GetListColumns.php"; private var listLoadURL:String = "listLoad.php" private var columnOrderURL:String = "SetColumnOrder.php" private var columnSortURL:String = "SetSort.php" /* Class constructor. */ public function LongList() { //Call the parent constructor super(); this.percentWidth = 100; this.height = 150; this.horizontalScrollPolicy="on"; this.verticalScrollPolicy="on"; //Set up the different service events that can happen. columnService = new HTTPService(); columnService.addEventListener(ResultEvent.RESULT,columnResultHandler); columnService.addEventListener(FaultEvent.FAULT ,defaultResultFault); columnService.useProxy = false; columnService.resultFormat = "e4x"; listService = new HTTPService(); listService.addEventListener(ResultEvent.RESULT,listResultHandler); listService.addEventListener(FaultEvent.FAULT ,defaultResultFault); listService.useProxy = false; listService.resultFormat = "e4x"; listService.url = this.listLoadURL; columnOrderService = new HTTPService(); columnOrderService.addEventListener(ResultEvent.RESULT,columnOrderResult); columnOrderService.addEventListener(FaultEvent.FAULT ,defaultResultFault); columnOrderService.useProxy = false; columnOrderService.resultFormat = "text"; columnOrderService.method = "Post"; columnOrderService.url = this.columnOrderURL; columnSortService = new HTTPService(); columnSortService.addEventListener(ResultEvent.RESULT,columnSortResult); columnSortService.addEventListener(FaultEvent.FAULT ,defaultResultFault); columnSortService.useProxy = false; columnSortService.resultFormat = "text"; columnSortService.method = "Post"; columnSortService.url = this.columnSortURL; } /* Function that sends the service to load the columns for the list. */ public function loadColumns():void { columnService.url = this.columnLoadURL+'?rand='+Math.random(); columnService.send(); } /* Function that sends the service to load the list items. This MUST be done after columns have been loaded. */ public function loadList():void { listService.url = this.listLoadURL+'?rand='+Math.random(); listService.send(); } /* The handler for when a result comes back from requesting the columns. I assume the structure of the XML. */ private function columnResultHandler(event:ResultEvent):void { this.columns = []; var ac:ArrayCollection = new ArrayCollection(); for each(var item:XML in event.result.columns.children()) { var column:AdvancedDataGridColumn = new AdvancedDataGridColumn(); column.headerText = item.attribute("header"); column.dataField = item.attribute("data"); column.width = item.attribute("width"); ac.addItem(column); } this.columns = ac.toArray(); //now that we know the columns load the list to match. this.loadList(); } /* Result handler when a response is returned from the server for a successful column reordering. Nothing needs to be done on the front end unless the response is not a "1" meaning success. */ private function columnOrderResult(event:ResultEvent):void { if(event.result.toString() != "1") { Alert.show("There was an error updating the session variable for this table."); } } /* Result handler when a response is returned from the server when the user wants to sort differently. */ private function columnSortResult(event:ResultEvent):void { if(event.result.toString() == "1") { this.loadList(); } else { Alert.show("There was an error updating the session variable for this table."); } } /* The handler for when a response (XML) is returned from the server when the current page's list is returned. I assume XML structure based on the PHP code written. */ private function listResultHandler(event:ResultEvent):void { var temp:XMLList = event.result.item as XMLList; var resultList:XMLListCollection = new XMLListCollection(); resultList.source = temp; resultList.sort = new Sort(); resultList.sort.fields = new Array; //now figure out the sorting. //The sort tag will be like "Column Direction, Column2 Direction2, etc" var sorter:Sort = new Sort(); var sorts:String = event.result.sorts; //split into an array where each item is the column and that column's direction var sArray:Array = sorts.split(','); for each(var sortString:String in sArray) { var iArray:Array = sortString.split(' '); var desc:Boolean; if(iArray[1] == "desc") { desc = true; } else { desc = false; } resultList.sort.fields.push(new SortField(iArray[0],true,desc)); } this.dataProvider = resultList; } /* Handler for when the user clicks on a column header to change the sort parameters for the list. This sends off the service to store the new sorts. */ protected override function headerReleaseHandler(event:AdvancedDataGridEvent):void { super.headerReleaseHandler(event); var colArray:Array = new Array(); var dirArray:Array = new Array(); var sortFields:ArrayCollection = new ArrayCollection(Sort(this.dataProvider.sort).fields); for(var i:int;i < sortFields.length;i++) { colArray.push(sortFields[i].name); if(sortFields[i].descending == true) { dirArray.push("desc"); } else { dirArray.push("asc"); } } var parameter:Object = new Object(); parameter.order = colArray.toString(); parameter.directions = dirArray.toString(); this.columnSortService.request = parameter; this.columnSortService.send(); } /* Handler for when the user lets up the mouse button when they are rearranging the columns. This sends off the service to save the new column order. */ protected override function columnDraggingMouseUpHandler(event:Event):void { super.columnDraggingMouseUpHandler(event); var columnOrder:Array = new Array(); for(var i:int;i < this.columns.length;i++) { columnOrder.push(this.columns[i].dataField as String); } this.columnOrderService.request = {order: columnOrder.toString()}; this.columnOrderService.send(); } private function defaultResultFault(event:FaultEvent):void { Alert.show(event.fault.faultString, "Error"); } } }
Step 16: LongListArea.as
This is the display container that will have the LongList as well as its controls. For ease I just extended the Panel class and laid everything out inside of it. This class is only concerned with layout of its elements, and what should happen when the user changes the stepper's value be it through the stepper itself or the buttons. Again, to keep it simple I have tried to comment in the code to keep the code all together.
package { //Necessary files for import. import flash.events.MouseEvent; import mx.containers.ControlBar; import mx.containers.Panel; import mx.controls.Alert; import mx.controls.Button; import mx.controls.NumericStepper; import mx.controls.Text; import mx.controls.VRule; import mx.events.NumericStepperEvent; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.rpc.http.mxml.HTTPService; /* This object is the entire control area. Based on the standard Panel Control. This object contains an instance of a LongList, as well as various controls for viewing data of the list. */ public class LongListArea extends Panel { //This is the stepper for viewing pages of data private var stepper:NumericStepper; //A string letting the user know what page they are currently on. "Page x of XX" private var displayed:Text; //A button to take the user back to the first page of data private var firstButton:Button; //A button to take the user to the last page of data. private var lastButton:Button; //A button to take the user back one page of data. private var previousButton:Button; //A button to take the user forward one page of data. private var nextButton:Button; //A button to reload the current page of data. private var reloadButton:Button; //The list itself. private var list:LongList; //A service to update the current page of the list on the server. private var stepperService:HTTPService; /* Constructor that sets up the different variables associated to the list area */ public function LongListArea() { super(); this.height = 250; this.percentWidth = 80; //initialize all the private variables displayed = new Text; stepperService = new HTTPService(); stepperService.addEventListener(ResultEvent.RESULT,stepperResultHandler); stepperService.addEventListener(FaultEvent.FAULT ,stepperResultFault); stepperService.useProxy = false; stepperService.resultFormat = "text"; stepperService.method = "POST"; list = new LongList(); stepper = new NumericStepper(); stepper.minimum = 1; stepper.addEventListener(NumericStepperEvent.CHANGE,handleStepper); firstButton = new Button(); firstButton.styleName="firstButton"; firstButton.toolTip = "Go to the first page." firstButton.id = 'first'; firstButton.addEventListener(MouseEvent.CLICK,handleButtonClick); lastButton = new Button(); lastButton.styleName = "lastButton"; lastButton.toolTip = "Go to the last page."; lastButton.id = 'last'; lastButton.addEventListener(MouseEvent.CLICK,handleButtonClick); reloadButton = new Button(); reloadButton.styleName = "reloadButton"; reloadButton.toolTip = "Reload the current page." reloadButton.id = 'reload'; reloadButton.addEventListener(MouseEvent.CLICK,handleButtonClick); previousButton = new Button(); previousButton.styleName="previousButton"; previousButton.toolTip="Go to the previous page"; previousButton.id = 'previous'; previousButton.addEventListener(MouseEvent.CLICK,handleButtonClick); nextButton = new Button(); nextButton.styleName = "nextButton"; nextButton.toolTip = "Go to the next page."; nextButton.id = 'next'; nextButton.addEventListener(MouseEvent.CLICK,handleButtonClick); //now set up the layout of the Panel. addChild(list); var control:ControlBar = new ControlBar; control.addChild(stepper); var ruler:VRule = new VRule(); ruler.height = 32; control.addChild(ruler); control.addChild(firstButton); control.addChild(previousButton); control.addChild(reloadButton); control.addChild(nextButton); control.addChild(lastButton); control.addChild(this.displayed); addChild(control); //Now we fire off the services to load information about the list and setup the stepper's max and current value. //These can be done sequentially this as they are independent of each other. loadColumns(); setStepper(); } /* Sets up and sends the service to load the stepper's max value and current value. */ private function setStepper():void { var stepperInit:HTTPService = new HTTPService; stepperInit.url = "stepperMax.php?random="+Math.random(); stepperInit.addEventListener(ResultEvent.RESULT,stepperInitResultHandler); stepperInit.addEventListener(FaultEvent.FAULT ,stepperResultFault); stepperInit.useProxy = false; stepperInit.resultFormat = "text"; stepperInit.send(); } /* Event Handler for a result from initializing the stepper. Result should be "X,Y" where X=maximum number of pages and Y=Current page. */ private function stepperInitResultHandler(event:ResultEvent):void { var temp:Array = (event.result.valueOf() as String).split(','); stepper.value = temp[1].valueOf(); stepper.maximum = temp[0].valueOf(); //Now we can set the string that shows what page the user is on currently. displayed.text = 'Page ' + stepper.value + ' of ' + stepper.maximum; } /* Event Handlers when the user clicks one of the buttons. */ private function handleButtonClick(event:MouseEvent):void { switch(event.target.id) { case "first": stepper.value = 1; break; case "previous": if(stepper.value != stepper.minimum) { stepper.value = stepper.value - 1; } break; case "next": if(stepper.value != stepper.maximum) { stepper.value = stepper.value + 1; } break; case "last": stepper.value = stepper.maximum; break; default: break; } //now that the stepper has been updated, call the function to set it on the backend. sendStepperUpdate(); } /* Function that sends the service to update the current page. */ private function sendStepperUpdate():void { stepperService.url = "stepperUpdate.php"; stepperService.request = {page: stepper.value}; stepperService.send(); } /* Event for when the stepper value is changed using the stepper itself. Pass through function to sendStepperUpdate */ private function handleStepper(event:NumericStepperEvent):void { sendStepperUpdate(); } /* Handler when a result comes back from a stepper request. A response of "1" means the session variable was successfully saved. Anything else is Considered a fault. */ private function stepperResultHandler(event:ResultEvent):void { if(event.result == "1") { loadList(); displayed.text = 'Page ' + stepper.value + ' of ' + stepper.maximum; } else { Alert.show("There was an error accessing the server."); } } /* Standard fault handler for the stepper. */ private function stepperResultFault(event:FaultEvent):void { Alert.show(event.fault.faultString, "Error"); } /* Pass through function to element's list. This will load the contents of the list. */ public function loadList():void { list.loadList(); } /* Pass through function to element's list. This will load the columns of the list. */ public function loadColumns():void { list.loadColumns(); } /* Returns the element's list. */ public function getList():LongList { return list; } } }
Step 17: GridExample.mxml
Since we did all the work in ActionScript the actual mxml markup is super simple. I did it this way to follow OOP principles and to program for reusability. There really is nothing to this file, so here it is.
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*"> <mx:Style source="css/longList.css"/> <local:LongListArea/> </mx:Application>
Step 18: Wrap Up
Build your project and put the created files in the same directory as all the php scripts you created. For simplicity, I have them all living at my web root. Try it out, navigate away from the page after you have changed around the page, or the sort, and then come back to it. Hit refresh in your browser. You'll see that the application remembers what you have done!
Conclusion
This is just an example of how by simply extending what Flex already has you can achieve very nice results for the user interface. By customizing events you are able to tell Flex what it should be doing and you can use this same methodology to make any list perform as you wish.
I'll be expanding on this hopefully, with added features like a Quick Search, the ability to choose which columns to show, adding columns, removing columns, letting the user Save their selections through the use of a login system, predefined search parameters, predefined list layouts etc. Since Flex is made to be completely extendable there's really no need to have to reinvent the wheel, just make the wheel better! I hope you enjoyed this tutorial.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post