Build A Web 2.0 Voting Widget With Flex: Part II
Building practical Flash widgets, like ones that record users votes and display the results, is easy in Flex. In my previous article I demonstrated how to set up Flex Builder and create vote tally widgets. In this second article, I’ll show you how we can upgrade that widget to not only read votes, but post them as well. And I’ll demonstrate how to configure Flex projects to build lightweight Flash applications, which are perfect for deployment as widgets.
Download the code archive for this article if you’d like to play along at home.
Pay attention! Once again, there will be a quiz at the end of the article. The first 100 people to complete the quiz will receive a copy of my book, Getting Started With Flex 3, for FREE, delivered to your door. For a limited time, the book is also available as a FREE PDF, thanks to Adobe.
Building the Voting Service
Upgrading the widget from one that reads vote tallies to one that can post votes means upgrading the server backend. To do this, we’ll create a very simple MySQL database that stores the vote topics and the options for each voting topic along with their vote count.
This is the MySQL code we use to create the tables and to preload it with a question:
DROP TABLE IF EXISTS topics;
CREATE TABLE topics (
id INT NOT NULL AUTO_INCREMENT,
text VARCHAR( 255 ) NOT NULL,
PRIMARY KEY ( id ) );
DROP TABLE IF EXISTS options;
CREATE TABLE options (
id INT NOT NULL AUTO_INCREMENT,
topic_id INT NOT NULL,
text VARCHAR( 255 ),
votes INT NOT NULL,
PRIMARY KEY ( id ) );
INSERT INTO topics VALUES ( 0, 'Which is the best Star Wars movie?' );
INSERT INTO options VALUES ( 0, 1, 'Episode I', 10 );
INSERT INTO options VALUES ( 0, 1, 'Episode II', 20 );
INSERT INTO options VALUES ( 0, 1, 'Episode III', 50 );
INSERT INTO options VALUES ( 0, 1, 'Episode IV', 150 );
INSERT INTO options VALUES ( 0, 1, 'Episode V', 250 );
INSERT INTO options VALUES ( 0, 1, 'Episode VI', 30 );
It’s necessary to use mysqladmin
to create the database, like so:
% mysqladmin --user=root create votes
… and then load the schema into it using the mysql
command:
% mysql --user=root votes < vote.sql
If you’re using PHPMyAdmin
to manage your database, you can create the database using PHPMyAdmin
, and then use the database restore function to load these tables into the database.
The next step is to upgrade the AMFPHP
service code for the vote system. The complete new code is shown here:
<?php
require_once("MDB2.php");
include_once(AMFPHP_BASE . "shared/util/MethodTable.php");
class VoteService2
{
function getTopics()
{
$dsn = 'mysql://root@localhost/votes';
$mdb2 =& MDB2::factory($dsn);
$sth =& $mdb2->prepare( "SELECT * FROM topics" );
$res = $sth->execute();
$rows = array();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) { $rows []= $row; }
return $rows;
}
function getOptions( $topic )
{
$dsn = 'mysql://root@localhost/votes';
$mdb2 =& MDB2::factory($dsn);
$sth =& $mdb2->prepare( "SELECT * FROM options WHERE topic_id=?" );
$res = $sth->execute( $topic );
$rows = array();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) { $rows []= $row; }
return $rows;
}
function addVote( $option )
{
$dsn = 'mysql://root@localhost/votes';
$mdb2 =& MDB2::factory($dsn);
$sth =& $mdb2->prepare( "SELECT * FROM options WHERE id=?" );
$res = $sth->execute( $option );
$rows = array();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
$sth2 =& $mdb2->prepare( "UPDATE options SET votes=? WHERE id=?" );
$row['votes'] = $row['votes'] + 1;
$res = $sth2->execute( array( $row['votes'], $option ) );
return $row;
}
return null;
}
}
I won’t get into the weeds on this code, because it’s beyond the focus of this article, but I will point out that the script uses the MDB2 extension to access the MySQL database for the votes. The MDB2 library is the new standard for database-agnostic access with PHP.
The three methods are getTopics
, which returns the list of available vote topics, then getOptions
, which returns an array of options for the specified topic. Each option includes the option text and the count of votes. Finally, addVote
adds a single vote to a particular option.
To test these functions, let’s bring up the AMF service browser that comes with the AMFPHP project and use it to inspect the service, as shown here.
This screenshot shows what happens when we run the getTopics
function. It returns an array of objects, each of which has the id
of the topic
and the text of the topic.
Next we can check the getOptions
function, as shown.
We see that this function returns another array of objects where each object has the text
of the option, the unique id
of the option, the associated topic’s id
, and the current count of votes
.
The final service function to test is addVote
. Here it is in action.
In this screenshot we specify that we’re adding a vote for the option with the id
of 1
, which is “Episode I” (seriously). The return is a single object with the new values for the option record with the id
of 1
.
That’s about all we need concern ourselves with for the web server side. Now comes the fun part: wrapping our application in a nice Flex interface.
Building the Voting Interface
The first step in building the voting application is to get the list of topics and to pick one to vote on. The code below implements just that portion of the functionality, but it defines the entire web server API in the RemoteObject
object:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
creationComplete="voteRO.getTopics.send()">
<mx:Script>
<![CDATA[
private var topicID:int = 0;
private function onGetTopics() : void {
var topicind:int = Math.round( Math.random() * voteRO.getTopics.lastResult.length );
if ( topicind >= voteRO.getTopics.lastResult.length )
topicind = voteRO.getTopics.lastResult.length - 1;
var topic:Object = voteRO.getTopics.lastResult[ topicind ];
topicID = topic.id;
topicText.text = topic.text;
}
]]>
</mx:Script>
<mx:RemoteObject id="voteRO"
endpoint="http://localhost/amfphp/gateway.php"
source="votes2.VoteService2" destination="votes2.VoteService2"
showBusyCursor="true">
<mx:method name="getTopics" result="onGetTopics()" />
<mx:method name="getOptions">
<mx:arguments><mx:Topic /></mx:arguments>
</mx:method>
<mx:method name="addVote">
<mx:arguments><mx:Topic /></mx:arguments>
</mx:method>
</mx:RemoteObject>
<mx:Label fontSize="20" id="topicText" />
</mx:Application>
There’s a lot here that we’ve seen from the original examples in the first article, but it does contain a lot more ActionScript code than any of the previous examples. Let’s start with what’s familiar.
The <mx:RemoteObject>
is exactly the same as the one we had in the first article, with the exception that I’ve added a few more methods, and two of those methods have arguments. The <mx:Label>
tag at the bottom will present the text of the question.
Now, the ActionScript code may look daunting, but it’s relatively straightforward. The onGetTopics
method is called in response to a successful call to the getTopics
function on the web server. The method takes the topics that were returned and picks one at random using the Math.random
method. The code then sets the text of the table to the text of the topic.
The relationship between the getTopics
method on the RemoteObject
and the onGetTopics
method in the application is worth our attention. Web services calls in Flex are asynchronous. In fact, just about everything in Flex is asynchronous and event-based. When you call the send method on getTopics
, it returns immediately. Flex then asynchronously makes the call to the web server, gathers the response, and fires the result method which, in this case, calls the onGetTopics
method. There’s also a callback for a web services fault, which I don’t specify in this example.
Back to the example, though. Running this should result in something that looks like this.
If you have added more topics to the database, you should see one of those topics chosen at random. The data set that I put in the MySQL code just had one topic, and we see that in the figure above.
Now, the next trick is for us to build an interface that allows the user to select one of the options. A conventional interface would consist of a set of radio buttons followed by a Submit button. But for a casual voting system like this, it may be just as easy to present a bunch of buttons, where clicking on one registers a vote. We’ll take that approach here.
The MXML code below, all of which is additional to the previous example, dynamically creates buttons for each of the options and adds them to the Application’s container:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application ...>
<mx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.controls.Button;
private var topicID:int = 0;
private function onGetTopics() : void {
// ...
voteRO.getOptions.send( topicID );
}
private function onGetOptions() : void {
for each( var opt:Object in voteRO.getOptions.lastResult ) {
var optBut:Button = new Button();
optBut.label = opt.text;
optBut.data = opt.id;
optBut.percentWidth = 100;
optBut.addEventListener( MouseEvent.CLICK, onOptionButtonClick );
addChild( optBut );
}
}
private function onOptionButtonClick( event:Event ) : void {
voteRO.addVote.send( event.currentTarget.data );
mx.controls.Alert.show( 'You successfully voted for ' + event.currentTarget.label );
}
]]>
</mx:Script>
<mx:RemoteObject id="voteRO" ...>
...
</mx:RemoteObject>
<mx:Label fontSize="20" id="topicText" />
</mx:Application>
Our original code remains unchanged, except for the addition of the call to getOptions
in the onGetTopics
method. When getOptions
is finished successfully it calls onGetOptions
, which creates a Button
class for each of the options and adds it to the Application’s container. It also registers the onOptionButtonClick
method to respond to mouse clicks on the newly created button.
These two methods, addEventListener
and addChild
, are fundamental to Flex. Almost every object in the Flex framework posts messages when it changes state. To listen for these messages, call addEventListener
and specify the message for which you want to listen, and the method to call when that message is fired.
NOTE: Use the EventDispatcher
to Publish Events
The messaging model discussed above is implemented by the EventDispatcher
base class, and uses the dispatchEvent
method. If you wish to create an object that publishes events, you’ll need to ensure that your object inherits from the EventDispatcher
class to do so.
The addChild
method is the cornerstone method of the Flex framework’s visual hierarchy. Just like an HTML document, the Flex application’s visual hierarchy can be represented by a tree. The root container is almost always the Application object. Within that, you can have containers and controls and so on, and in turn, almost every container or control supports child controls or containers. You can add child controls (as I’ve done in the code above) using addChild
, and you can remove children with the removeChild
and removeAllChildren
methods.
Okay, with that mini-primer on Flex out of the way, let’s take a look at what happens when we launch this code from Flex Builder – check out this figure.
The figure shows that the application picked a question and put up buttons for each of the different options. The user can then respond to the poll by clicking one of the buttons, as demonstrated in this next figure.
Clicking one of these buttons calls the onOptionButtonClick
, which in turn calls the addVote
function on the server. The application also displays a nifty message box to inform the voter that the vote has been counted.
The final change I’ll make is to display the pie chart of the results of the poll instead of just the alert box. This final version of the code is shown here:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application ...>
<mx:Script>
<![CDATA[
import mx.controls.Button;
private var topicID:int = 0;
private var voted:Boolean = false;
private function onGetTopics() : void {
// ...
}
private function onGetOptions() : void {
if ( voted )
resultPie.dataProvider = voteRO.getOptions.lastResult;
else {
for each( var opt:Object in voteRO.getOptions.lastResult ) {
var optBut:Button = new Button();
optBut.label = opt.text;
optBut.data = opt.id;
optBut.percentWidth = 100;
optBut.addEventListener( MouseEvent.CLICK, onOptionButtonClick );
optionsBox.addChild( optBut );
}
}
}
private function onOptionButtonClick( event:Event ) : void {
voted = true;
voteRO.addVote.send( event.currentTarget.data );
}
private function onVoteResult() : void {
contentStack.selectedChild = pieBox;
voteRO.getOptions.send( topicID );
}
]]>
</mx:Script>
<mx:RemoteObject ...>
...
</mx:RemoteObject>
<mx:Label fontSize="20" id="topicText" />
<mx:ViewStack id="contentStack" creationPolicy="all" width="100%" height="100%">
<mx:VBox width="100%" height="100%" id="optionsBox">
</mx:VBox>
<mx:VBox width="100%" height="100%" id="pieBox">
<mx:PieChart width="100%" height="100%" id="resultPie" showAllDataTips="true">
<mx:series>
<mx:PieSeries field="votes" nameField="text" explodeRadius="0.1" />
</mx:series>
</mx:PieChart>
</mx:VBox>
</mx:ViewStack>
</mx:Application>
The big change here comes at the end of the file where I’ve added a <mx:ViewStack>
object that contains two <mx:VBox>
containers. A ViewStack
is a lot like a deck of cards. You can pick one card to be visible on top, while all the rest remain hidden below. In this case the first VBox
, the optionsBox
, will hold the buttons for the options. The second VBox
, the pieBox
, will hold the PieChart
. The PieChart
is exactly like the one from the first article.
The onGetOptions
method in the ActionScript
has also changed. It now checks to see if we have voted or not. If we have voted, the code sets the dataProvider
on the PieChart
to the updated response from getOptions
. If we have not voted, it adds the buttons to the optionsBox
using addChild
.
The onVoteResult
method, where we used to put up the alert box, now simply flips the ViewStack
card over to the pieBox
to present the pie chart. The pie chart updates once the onGetOptions
method is fired again.
When we bring this up in the browser, and proceed to vote, we can see the resulting pie chart.
Not bad! We now have a widget that talks to a web service, picks a question at random, presents the question, then displays the voting results using a nifty pie chart if the user chooses to vote.
The only problem is that the Flash application itself is fairly heavy. In fact, on my machine at the time of this writing the SWF files are about 350–400K. That’s a little too heavy for a lightweight widget. What to do?
Moving to Runtime Shared Libraries
Thankfully, Adobe has a fix for the fat SWF problem – and it doesn’t even require any coding changes. All you have to do is tweak your project a little. The trick is to use Runtime Shared Libraries.
The idea is simple. When you compile your SWF file instead of including the library in the SWF itself, you tell the Flash player to go fetch the library at a specific URL. The Flash player then downloads the library automatically for you and caches it, so the next time it looks for the library it will just use this stored version.
To make the change, right click on the “voter” project and select “Properties.” Then select “Flex Build Path” from the left-hand column. The result is shown below.
From there, select the framework.swc
file as shown here.
Double-click on the Link Type. This will bring up a dialog where you can uncheck the “Use same linkage as framework” option and select “Runtime shared library (RSL)” from the dropdown, as shown.
The final step is to click on the Add button. This brings up the dialog shown below.
Now, we enter the URL where the swz
file for the framework will be located. This file must be copied from the Flex Builder installation directory to your web server.
Because this particular example uses the charting classes, we’ll also follow the same process with the datavisualization.swc
library.
Where to Go from Here
These two articles should set you up well to get started with with Flex. Small applications, such as this voting application, are the perfect place from which to branch out.
If I had to define the three greatest benefits of Flex, it would be these:
- Flex makes building Flash applications easy for developers.
- Flash applications run well practically anywhere, and work just as well as their Ajax counterparts.
- Flex 3 can be used to build Flash applications small enough to create Web 2.0 widgets.
I hope these two articles will leave you pointed in that same direction (be sure to download the code archive for this article).
Quiz Yourself!
Test your understanding of this article with a short quiz – if you’re quick, you’ll receive a FREE copy of my book, Getting Started With Flex 3 for a limited time, thanks to Adobe.