BT

From Tags to Riches: Going from Web 1.0 to Flex

Posted by James Ward & Shashank Tiwari on Feb 10, 2008 |

Introduction

The success of the iPhone has made it clear that users want more interactivity from their software experiences. Better interactivity leads to effective utilization of application features and better efficiency and that is why it is extremely important not only in personal information management applications but in enterprise business applications as well. Among business applications, the ones that involve data visualization gain the most from better interactivity, because better effectiveness and efficiency in these cases translates directly to better decision making and therefore immediate business gains. Dashboards are the prototypical example of data visualization applications. Interestingly, most dashboards today lack the interactivity to create efficient user experiences. So we decided to spice up a typical Web 1.0 dashboard to a more interactive and rich dashboard. We did not reinvent the wheel and create the entire application from scratch. Instead we re-mastered the interface and plumbed it into the existing server side infrastructure. In doing so, we realized a quick yet useful transformation.

The dashboard we used for this exercise is part of the open source Pentaho BI suite. The data and views are part of sample applications in the distribution.

Although our example is a dashboard application, the concepts can be applied to any project that needs to migrate from Web 1.0 to RIA. Our RIA toolkit of choice is Adobe Flex and all our discussion here is in the context of this framework, the Flash VM and the supporting libraries.

If you would like to follow along you should install the following software applications:

Remember to start the Pentaho server and log into the dashboard before running the Flex interface. The Flex interface assumes you are already logged in and authenticated. The source code assumes the server is listening on port 8080 for HTTP requests. Please modify the source code appropriately if your Pentaho HTTP server is listening on some other port. A dummy data set version of the Flex interface is also provided in the source code download bundle as a convenience for those who want to skip installing the Pentaho server altogether.

Now, that you are setup, its time to dive into our example application.

Pimp My Pentaho Dashboard

First a disclaimer - This article was created to provide a "get your feet wet experience". It shouldn't be necessary bu if you would like to have some background experience with Flex before you dive into this you may want to check out learn.adobe.com or flex.org. The code in this article does not attempt to convey the best practices for maintainable code architecture or user interface design practices. For instance we chose not to use Ely Greenfield's charts drill down components [demo] in this example because even though it would have improved the user experience it would have made it slightly more work for you to get this example up and running. We wanted to provide a single copy and paste experience for those who want to play with this code. Adding a third party component or a true MVC architecture into the mix would have complicated that experience. If you are interested in delving further into those topics there are numerous article you will find useful across the web and on the Adobe Developer Connection.

Let's first start by looking at the end result so you can understand the experience we are trying to create. View Demo.

The original Pentaho Dashboard looked similar:

As indicated earlier, we used Adobe Flex to create the new Dashboard. The code for the Flex Based dashboard was written with the declarative MXML language and the procedural ActionScript language and compiled to a SWF file with the free Flex SDK. That SWF file is the bytecode for the Flash Player VM. You can run a SWF file in the browser or as a desktop application with the new Adobe Integrated Runtime, AIR. Lets take a look at the source code that is used create the new dashboard.

<?xml version="1.0" encoding="utf-8"?>
 <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="initApp()" layout="horizontal">
  <mx:Script>
 <![CDATA[
 // import classes used in ActionScript and not used in the MXML
 // ActionScript is code generated from MXML by the free Flex SDK's mxmlc compiler
 // The generated ActionScript can be viewed if you pass mxmlc the -compiler.keep-generated-actionscript argument
 import mx.charts.HitData;
 import mx.collections.ArrayCollection;
 import mx.rpc.events.ResultEvent;

 // Bindable is a metadata / annotation which generates event code so that the variable
 // can be used in an MXML Binding Expression, ie. dataProvider="{territoryRevenue}"
 [Bindable] public var territoryRevenue:ArrayCollection;
 [Bindable] public var productlineRevenue:Object;
 [Bindable] public var topTenCustomersRevenue:Object;

  // Variables in ActionScript have namespaces like in Java.  You can use the typical public,
 // private, and protected namespaces as well as create your own namespaces
 // Object types are specified with a colon character and are optional but recommended.
 private var _selectedTerritory:String = "*";
 private var _selectedProductLine:String = "*";

 // the initApp function is called when the Application's creationComplete event is fired (see the mx:Application tag above
 private function initApp():void
 {
 // initializes our data caches
 productlineRevenue = new Object();
 topTenCustomersRevenue = new Object();

 // initiates the request to the server to get the Sales by Territory data
 // tSrv is defined in MXML below.  It could also have been defined in ActionScript but would have been slightly more verbose tSrv.send();

 // Since the Sales by Product Line and Top Ten Customer Sales depend on the selected territory
 // we make a call to the functions that will fetch the data based on the selected territory (or pull it from cache)
 // in this case the selected territory is "*" or our indicator for all.  When the users selects a new territory
 // these methods will be called again but the selected territory will be different
 updateProductLineRevenue();
 updateTopTenCustomersRevenue();
 }

 // Setter method that causes the data stores for the Sales by Product Line and Top Ten Customer Sales
 // to be updated and the charts to be updated accordingly
 public function set selectedTerritory(territory:String):void
 {
 // update the private backing variable
 _selectedTerritory = territory;

 updateProductLineRevenue();
 updateTopTenCustomersRevenue();
 }

 // Getter method that returns the selected Territory
 // This method has the Bindable metadata / annotation on it so that the selectedTerritory property can
 // be used in a binding expression
 [Bindable] public function get selectedTerritory():String
 {
 return _selectedTerritory;
 }

 // Setter method similar to selectedTerritory but for the selected product line
 public function set selectedProductLine(productLine:String):void
 {
 _selectedProductLine = productLine;

 updateTopTenCustomersRevenue();
 }

 [Bindable] public function get selectedProductLine():String
 {
 return _selectedProductLine;
 }

 // If the data is in cache then just directly update the chart based on the selected territory.
 // If the data is not in cache then assemble the name value pairs that are needed by the
 // web service request, then make the request.
 private function updateProductLineRevenue():void
 {
 if (productlineRevenue[_selectedTerritory] == undefined)
 {
 productlineRevenue[_selectedTerritory] = new ArrayCollection();

 var p:Object = new Object();

 if (_selectedTerritory != "*")
 {
 p.territory = _selectedTerritory;
 }

 plSrv.send(p);
 }
 else
 {
 plPie.dataProvider = productlineRevenue[_selectedTerritory];
 }
 }

 // Similar to updateProductLineRevenue except that both the selected territory and
 // the selected product line determine the data set.
 private function updateTopTenCustomersRevenue():void
 {
 if (topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine] == undefined)
 {
 topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine] = new ArrayCollection();

 var p:Object = new Object();

 if (_selectedTerritory != "*")
 {
 p.territory = _selectedTerritory;
 }

 if (_selectedProductLine != "*")
 {
 p.productline = _selectedProductLine;
 }

 ttcSrv.send(p);
 }
 else
 {
 ttcBar.dataProvider = topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine];
 }
 }

 // This function handles a response from the server to get the Sales by territory.  It reorganizes that data
 // into a format that the chart wants it in.  The tPie chart notices changes to the underlying ArrayCollection
 // that happen inside the for each loop.  When it sees changes it updates it's view of the data.
 private function handleTResult(event:ResultEvent):void
 {
 territoryRevenue = new ArrayCollection();
 tPie.dataProvider = territoryRevenue;

 var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN-HDR-ROW']['COLUMN-HDR-ITEM'];
 for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])
 {
 var spl:Object = new Object();
 spl[hdr[0]] = pl['DATA-ITEM'][0];
 spl[hdr[1]] = pl['DATA-ITEM'][1];
 territoryRevenue.addItem(spl);
 }
 }

 // Similar to handleTResult except that it handles the data for Sales by Product Line
 private function handlePLResult(event:ResultEvent):void
 {
 var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN-HDR-ROW']['COLUMN-HDR-ITEM'];
 
 for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])
 {
 var spl:Object = new Object();
 spl[hdr[0]] = pl['DATA-ITEM'][0];
 spl[hdr[1]] = pl['DATA-ITEM'][1];
 productlineRevenue[_selectedTerritory].addItem(spl);
 }

 plPie.dataProvider = productlineRevenue[_selectedTerritory];
 }

 // Similar to handleTResult except that it handles the data for Top Ten Customer Sales
 private function handleTTCResult(event:ResultEvent):void
 {
 var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['ROW-HDR-ROW'];
 var pl:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'];

 for (var i:int = 0; i < pl.length; i++)
 {
 var spl:Object = new Object();
 spl.name = hdr[i]['ROW-HDR-ITEM'][0];
 spl.sales = pl[i]['DATA-ITEM'];
 topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine].addItemAt(spl,0);
 }

 ttcBar.dataProvider = topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine];
 }

 // This function is called to format the dataToolTips on the tPie chart.
 private function formatTPieDataTip(hitdata:HitData):String
 {
 return "<b>" + hitdata.item.TERRITORY + "</b><br>" + cf.format(hitdata.item.SOLD_PRICE);
 }
 ]]>
 </mx:Script>

 <!-- These HTTP Services communicate via HTTP to a server -->
 <mx:HTTPService id="tSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=samples&path=steel-wheels/homeDashboard&action=Sales_by_Territory.xaction" result="handleTResult(event)"/>
  <mx:HTTPService id="plSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=samples&path=steel-wheels/homeDashboard&action=Sales_by_Productline.xaction" result="handlePLResult(event)"/>
  <mx:HTTPService id="ttcSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=samples&path=steel-wheels/homeDashboard&action=topnmdxquery.xaction" result="handleTTCResult(event)"/>
  <!-- Non-visual component to format currency's correctly.  Used in the formatTPieDataTip function -->
 <mx:CurrencyFormatter id="cf" precision="0"/>

 <!-- Effects used to make the charts more interactive -->
 <mx:SeriesInterpolate id="plEffect"/>
 <mx:SeriesSlide id="ttcSlide" direction="right"/>

 <!-- Stacked vertical layout container -->
 <mx:VBox height="100%" width="40%">
 <!-- Nice box with optional drop shadows, title bars, and control bars -->
 <mx:Panel width="100%" height="100%" title="Revenue By Territory">
 <!-- Pie Chart -->
 <mx:PieChart id="tPie" width="100%" height="100%" showDataTips="true" dataTipFunction="formatTPieDataTip">
 <!-- Sets the itemClick property on the PieChart to the embedded ActionScript code.  We could have also called a function defined above. -->
 <mx:itemClick>
 // calls the appropriate setter method
 selectedTerritory = event.hitData.item.TERRITORY;

 // tells the pie chart to explode the pie wedge the user click on
 var explodeData:Array = [];
 explodeData[territoryRevenue.getItemIndex(event.hitData.item)] = 0.15;
 tPie.series[0].perWedgeExplodeRadius = explodeData;
 </mx:itemClick>
 <!-- Sets the series property on the Pie Chart. -->
 <mx:series>
 <!-- The Pie Series defines how the Pie Chart displays it's data. -->
 <mx:PieSeries nameField="TERRITORY" field="SOLD_PRICE" labelPosition="insideWithCallout" labelField="TERRITORY"/>
 </mx:series>
 </mx:PieChart>
 </mx:Panel>

 <!-- A Binding Expression in the title bar of the Panel uses the Bindable getter for the selectedTerritory property -->
 <mx:Panel width="100%" height="100%" title="Revenue By Product Line (Territory = {selectedTerritory})">
 <mx:PieChart id="plPie" width="100%" height="100%" showDataTips="true">
 <mx:itemClick>
 selectedProductLine = event.hitData.item.PRODUCTLINE;

 var explodeData:Array = [];
 explodeData[productlineRevenue[_selectedTerritory].getItemIndex(event.hitData.item)] = 0.15;
 plPie.series[0].perWedgeExplodeRadius = explodeData;
 </mx:itemClick>
 <mx:series>
 <!-- The showDataEffect on the Series uses Binding to (re)use an effect defined above -->
 <mx:PieSeries nameField="PRODUCTLINE" field="REVENUE" labelPosition="insideWithCallout" showDataEffect="{plEffect}" labelField="PRODUCTLINE"/>
 </mx:series>
 </mx:PieChart>
 </mx:Panel>
 </mx:VBox>
  <mx:Panel width="100%" height="100%" title="Top 10 Customers (Territory = {selectedTerritory} | Product Line = {selectedProductLine})">
 <mx:BarChart id="ttcBar" width="100%" height="100%" showDataTips="true">
 <mx:series>
 <mx:BarSeries xField="sales" showDataEffect="{ttcSlide}"/>
 </mx:series>
 <mx:verticalAxis>
 <mx:CategoryAxis categoryField="name"/>
 </mx:verticalAxis>
 </mx:BarChart>
 </mx:Panel>

 </mx:Application>

Download or view the source code. Note that there are two versions of the application. The pentaho_dashboard_demo.mxml uses a fake dataset. The pentaho_dashboard.mxml connects to an actual Pentaho server to get it's data.

The Porting Process

The process of porting to RIA begins with designing a new interface. In parallel you may need to figure out how to expose the data and services for your application as, out to your new RIA. With Pentaho the data and services have already been exposed for us so the hardest part is to parse the data into a format that the Flex Charting components expect. At some point you can hook the back end services to the new interface. Lets walk through these steps in more detail.

Designing a New Interface

The best way to create a new RIA interface is to not think about how to make your existing interface richer, but to rethink how the user wants to visualize and interact with information. Designers can be very helpful in this process. If possible let them be very creative. As a developer you can begin building a prototype. To do this efficiently you might need to create a mock data set. You can see the data set we used in the Sales_by_Productline.xaction, Sales_by_Territory.xaction, and topnmdxquery.xaction files. In this case I made the mock data sets look like the web services which Pentaho exposes. This wasn't necessary but made it easier to hook up to the real services once we decided we liked the design. By having a mock data set we were able to iterate much faster on the design.

The design we created actually looks almost exactly like the original dashboard. If we had a designer helping us -- which we didn't -- we could have come up with something more creative. But even though we are developers we were able to create a much more interactive dashboard. We could have gone quite a bit further creating a richer interface but for this example we wanted to keep the code easy to read and understand.

The code for creating the UI is pretty straight forward. To create a PieChart you just add some MXML like:

<mx:PieChart width="100%" height="100%">
    <mx:series>
       <mx:PieSeries nameField="TERRITORY" field="SOLD_PRICE" labelPosition="insideWithCallout" labelField="TERRITORY"/>
    </mx:series>
 </mx:PieChart>

To add interactivity you can add an itemClick event handler to the PieChart:

<mx:itemClick>
 selectedTerritory = event.hitData.item.TERRITORY;

  var explodeData:Array = [];
 explodeData[territoryRevenue.getItemIndex(event.hitData.item)] = 0.15;
 tPie.series[0].perWedgeExplodeRadius = explodeData;
 </mx:itemClick>

Since Flex has support for properties you cause something to happen when you set the selectedTerritory like we did in the itemClick event handler. In this case we want to refresh the data sets which some of the pie charts are bound to:

public function set selectedTerritory(territory:String):void
 {
  _selectedTerritory = territory;

   updateProductLineRevenue();
  updateTopTenCustomersRevenue();
 }

We can also very easily add smooth transitions to the charts which have changing data sets. First thing to do is to add an instance of the effect we want to apply:

<mx:SeriesInterpolate id="plEffect"/>

Then we can tell one of our Pie Charts to use that effect when the data changes. To do this we bind the showDataEffect to the instance of the effect we want to use:

<mx:PieSeries nameField="PRODUCTLINE" field="REVENUE" labelPosition="insideWithCallout" showDataEffect="{plEffect}" labelField="PRODUCTLINE"/>

We also may want to add custom mouse over data tips:

<mx:PieChart id="tPie" width="100%" height="100%" showDataTips="true" dataTipFunction="formatTPieDataTip">

The dataTipFunction, named formatTPieDataTip, returns a custom string to use in the data tip:

private function formatTPieDataTip(hitdata:HitData):String
 {
     return "<b>" + hitdata.item.TERRITORY + "<b><br>" + cf.format(hitdata.item.SOLD_PRICE);
 }

We could specify a number of other styles to customize the look and feel of the application. These styles can be specified inline as properties or in an external CSS file.

Exposing the Back End

With Pentaho it was easy to find and interact with the web services which expose the data we need for the dashboard. The URL which returns the Sales by Territory is (when running locally): HERE

Some of the URLs take parameters to determine which data set to return. For instance the Top Ten Customers web service can take two optional parameters, territory and productline, which determine how to return subsets of the information.

Hook the Front to the Back

To hook the front end to the back end you first start by creating an HTTPService (WebService if you are using SOAP):

<mx:HTTPService id="tSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=
samples&path=steel-wheels/homeDashboard&action=Sales_by_Territory.xaction"
result="handleTResult(event)"/>

The result property specifies code to run when a response returns from the server. In this case we just call a function named handleTResult passing it a single Event parameter. That function is responsible for transforming the data we received into the form that the Charts need. Whether we are using Charts or other components like the DataGrid we might have to tweak it somewhat. In this case we do some minor tweaking of the data before we hand it off to the Chart:

private function handleTResult(event:ResultEvent):void
 {
   territoryRevenue = new ArrayCollection();
   tPie.dataProvider = territoryRevenue;

    var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN-HDR-ROW']['COLUMN-HDR-ITEM'];

   for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])
  {
   var spl:Object = new Object();
   spl[hdr[0]] = pl['DATA-ITEM'][0];
   spl[hdr[1]] = pl['DATA-ITEM'][1];\\\\\\\\
   territoryRevenue.addItem(spl);
  }
 }

In this example all the code is in a single file. In a real world application we would probably break all that out into different files by following the Model View Controller architecture. We will discuss the topic of structuring large applications using some proven design patterns at another time. For now, we are done as far as the example illustration goes. From here on till the end of the article we will discuss a few interesting aspects of RIA with Adobe Flex and provide pointers to what can be done with this example application to make it a production ready solution.

What is great about the example?

The example quickly demonstrates the importance of richer and more interactive user interfaces for traditional old school web applications. The beauty of the application is that it provides a great interface to an existing application without the need of substantial refactoring. Part of this convenience can be attributed to Pentaho's exposure of its server side calls as web service endpoints and part of it can be attributed to Flex's ability to consume these services gracefully within its component and data binding model.

The example also demonstrates that its prudent to mix and match technologies. A good and robust Java server application can be hooked effectively with a rich Flex interface to create compelling applications. This is a great value proposition and this example hopefully brings it to life.

Another aspect of the application is that it shows through real and working code how easy it is to build Flex applications using ActionScript and MXML. For most people familiar with Java and XML the syntax and semantics are extremely familiar. Isn't it!

What is lacking and how to bridge that gap?

The example is not a production ready piece of software. It is more concerned being a simple prototype. However, a little effort of abstracting the Pentaho web services calls as an ActionScript library may be a good step towards building a robust application. There are open source efforts underway to build such a library. The example is procedural in its approach and uses very little of the object oriented constructs. Use of the Model View Controller architectural pattern, programming to interfaces and use of inheritance and polymorphism appropriately would create a maintainable and a clean application. In the example application we copied the look-an-feel. Ideally we should have created a completely new interface based on interaction requirements and then bound the visual components to the web service components. For creating a production ready application we should invest time and energy in such an effort.

Further Reading

This example was really just the tip of the ice berg for what you can do with Rich Internet Applications. One enhancement we could make is to give a more meaningful transition to the chart drilldowns. Ely Greenfield has created a great demo with instructions and code showing how to implement a drill down with the Flex Charts. See his blog for details and the demo. We used Flex 2 for building this demo but there are quite a few new features in the Flex 3 Charting componets. You can read more about those features here and here. If you would like to learn more about programming in Flex check out flex.org and learn.adobe.com.

About the Authors

James Ward is a Technical Evangelist for Flex at Adobe and Adobe's JCP representative to JSR 286, 299, and 301. Much like his love for climbing mountains he enjoys programming because it provides endless new discoveries, elegant workarounds, summits and valleys. His adventures in climbing have taken him many places. Likewise, technology has brought him many adventures, including: Pascal and Assembly back in the early 90's; Perl, HTML, and JavaScript in the mid 90's; then Java and many of it's frameworks beginning in the late 90's. Today he primarily uses Flex to build beautiful front-ends for Java based back-ends. Prior to Adobe, James built a rich marketing and customer service portal for Pillar Data Systems. You can learn more about James on his blog: www.jamesward.org.

Shashank Tiwari is the Chief Technologist at Saven Technologies, a Chicago based company that provides cutting edge technology driven business solutions for banking and financial service companies. Shashank is a prolific developer, author and speaker who is active in the JCP as an expert group member on the following JSRs - 274, 283, 299, 301 and 312. He actively programs in at least a dozen of languages, including Java, ActionScript, Python, Perl, PHP, C++, Groovy, JavaScript, Ruby and Matlab. He is a popular blogger on the O'Reilly Network. These days he is busy building web 2.0 applications using Flex and Java. More information about him can be obtained at www.shanky.org.

Hello stranger!

You need to Register an InfoQ account or to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Flex Datasource is still too complicated by Joshua Partogi

One reason why people is not using Flex yet because it is still too difficult to connect Flex and Datasource besides there are no real examples and step-by-step article on connecting Flex and Datasource. IMHO that's the most important issue before anyone decide to use Flex in their apps. Even in this article, connecting to a datasource example is not given.

Re: Flex Datasource is still too complicated by Wolfgang Loder

Actually it is shown in the paragraph "Hook the Front to the Back". The supposed architecture for a (web) Flex application is to get data from the backend - nothing complicated. It's a good idea though to use a proxy to 'translate' the returned data into objects, when a generic web service is used.

Where in pentaho should I put the flex files? by Herve Ramangalahy

I follow this tutorial and my questions are: how do I change the pentaho dashboard in the home page with the new one(flex)?
Should I put *.html and *.swf file together in the jsp directory or put them separately(put the *.swf in homeDashboard directory)?

I'm new to pentaho and flex technologies.
In advance thankx a lot.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

3 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2013 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT