Flex by Example - Foundation ActionScript 3, Second Edition (2014)

Foundation ActionScript 3, Second Edition (2014)

Chapter 14. Flex by Example

In this chapter, you’re going to put into practice all that you’ve learned about Flex in the previous chapter and ActionScript 3.0 throughout this book. Here, you’ll develop the simple RSS reader application you built in Chapter 13 as a full-featured feed reader application. You’ll see that although the Flex framework and the MXML markup language are extraordinary and powerful tools on their own, you need to add ActionScript to create truly robust, complex applications.

This chapter demonstrates the following:

· How to conceptualize and plan a Flex project

· How to create a new Flex project in Flash Builder

· How to import and take advantage of external class libraries

· How to lay out a UI using MXML

· How to use controls and layout classes in the Flex framework

· How to use data binding to wire components together

· How to create pop-up dialog boxes

· How to create custom classes for use within a Flex application

· How to create and dispatch custom events

· How to use web services to load data

· How to save data between sessions using SharedObject

You’ll see how all of the ActionScript skills you’ve learned throughout this book can be applied to Flex development.

Planning the Application

Before you rush headlong into building this application (yes, I saw you reaching for your mouse), you need to identify exactly what you want it to do and roughly how you want it to look. Proper planning of a large application is an important step during your development process, helping you to identify potential problems, ensure consistent architecture, and provide better visibility (both for you and your clients) of what you will be building.

Because planning is so important to creating a good application, I’m going to dispense a few personal opinions about how a project should be developed. I’m not going to teach you the “right” way to do it (which is just as well because there is no right way to do it, whatever the fat-cat consultants or hip Extreme Programming gurus might tell you). The point is to get you thinking about how to develop an application—from an initial idea to the finished article.

Let’s walk through the five main steps of planning an application:

· Gathering the requirements

· Creating the functional specification

· Designing the UI

· Designing the interaction

· Designing the data and logic

Gathering the Requirements

The first step in planning an application is to identify the requirements. In a real-world project, this can mean anything from an informal chat with the people who will be using your application to a full-blown, official requirements-gathering process. The aim in both cases is the same: make sure that your application is useful to the people who will be using it every day. Now, that’s not to say that you must try to implement every little feature that each user says he wants. Part of the headache of the requirements-gathering process is that what your users think they want isn’t what they really want most of the time, and you need to work hard to decipher their requests into a feasible requirement. You also have a responsibility to include only the features that will be useful to a significant portion of your user base. This means that some features will not make the cut, even though they may be good ideas.

So, what are the requirements for the application you’ll build in this chapter? Because this is a practice application, I get to set the requirements. And because I’m being informal, I can just list what the application should do:

· Support multiple feeds, allowing the user to add and remove feeds

· Support the most common feed formats

· Allow the user to manually refresh feeds

· Automatically refresh feeds every five minutes

· Show the user the number of articles in a given feed

· Provide a way for the user to view the website associated with the feed

· Remember the user’s feed between sessions

Of course, this list could include even more features, but I want to keep version 1.0 of this application manageable, so these requirements will do just fine for now. At the end of chapter, I’ll leave you with a few ideas of where you can take this application if you feel like tinkering.

When deciding on what you want your application to do, it’s worth giving some thought to the things you won’t be able to do. In the case of the feed reader application, limitations are imposed by the Flash Player in which the application will run.

One limitation relates to storage. Although you want to store the user’s feed list, the only local storage medium available to a Flex application is a local SharedObject, which is limited in terms of how much content it can hold. This means that you won’t be able to store the list of articles fetched from each feed, as would be the case in a normal feed reader application, so when the application is restarted, the history of articles in the feed will be lost. (Note that you could use Adobe AIR, introduced in the previous chapter, to build an application that has direct access to the file system).

The other main limitation is imposed by the Flash Player’s security sandbox. The Flash Player can’t load XML data from another domain without express permission in the form of a crossdomain.xml file. This file, which is hosted on the server delivering the data, specifies other domains whose files, like SWFs, may access that data. This won’t be a problem during development because you’ll be running the application locally, which thankfully means it won’t be subject to this particular security restriction. However, it does mean that you won’t be able to upload this application and have it work for every feed.

You could host the application remotely by creating a server-side proxy script to fetch the feed data on behalf of the application. Such a proxy would be hosted on the same domain as your application SWF and would load the data from the remote domain. Because the application SWF would need to communicate with only the proxy script, hosted on the same domain, there would be no security restriction in the Flash Player. Adobe has examples of this available at http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_16520#proxy.

The Flash Player imposes other limitations, such as not being able to interface with the web browsers to have your application configured as the default feed reader, but these are relatively minor in comparison to the two just discussed.

Creating the Functional Specification

Now that you know what your application needs to do (and what it does not need to do), you can start thinking about how it should work. The essence of the process is to convert the requirements you’ve gathered into tangible, measurable features.

In a more formal setting, this would involve drawing up a functional specification document, which would be circulated to all interested parties so that everyone knows exactly what is being built. Such a document could run to hundreds or thousands of pages, depending on the complexity of your application, and it may need to go through several rounds of revisions before everyone is happy with it.

However, for the sample application, you’ll just flesh out the requirements gathered in the previous step into a list of features, with some basic details of how each feature will work.

· Feed reading:

· The users will be able to view a list of the feeds to which they have subscribed, with the list showing the title of the feed and the number of articles it contains.

· The users will be able to see the date and title of the articles for a given feed, and the title and excerpt for a given article.

· The users will be able to visit the website associated with a given feed, and they will be able to go to the page for an individual entry if they want to read more than the excerpt.

· Subscription management:

· The users will be able to manage their feed subscriptions from within the application. When subscribing to a feed, the users will be able to specify the URL from the feed. This URL will then be used to fetch the feed data, and the feed title will be extracted from the metadata that is part of the feed data.

· The system will support the following feed formats: RSS 1.0, RSS 2.0, and Atom 1.0.

· The format of a feed will be automatically detected so that the users do not need to know the type of a given feed when subscribing.

· Refreshing feeds:

· When the application is started, each feed in the user’s subscription list will be refreshed, fetching the latest articles. The feeds will be refreshed automatically every five minutes.

· A user will be able to manually refresh a given feed or all feeds in the subscription list.

· When refreshing a feed, any old articles that no longer appear in the feed data will be kept until the end of the session.

Designing the UI

Once you have the functional specification, you can start to think about the user experience. You want your application to be as easy to use as possible (in line with one of the application requirements), so you’ll stick to a single screen.

Now, you don’t need to go into a whole lot of detail here. You’re not worried about the look and feel of the application. You just need to know what the major UI elements are and roughly where they sit on the screen. With that in mind, and having looked at some other feed reader applications for inspiration, I came up with the layout shown in Figure 14-1.

image

Figure 14-1. The proposed layout for the RSS application

On the left is the list of feeds to which the user has subscribed, with the number of unread articles in brackets after the title of the feed. Below the list is a button to add a new subscription (labeled with a + sign) and another button to remove the selected subscription (labeled with a sign). Also included in this area are controls for refreshing the feeds, either manually or automatically at a specified interval.

In the top-right area is the list of articles in the selected feed, displayed in a scrollable grid showing the date and the title of the article. Beneath that is a label to tell the user when the feed was last checked for updates and a Visit website button, which will take the user to the websiteassociated with the feed.

Below the article list are the title and excerpt from the selected article, with a label telling the user when the article was posted and a button that will take the user to the full article.

Although you can probably guess which containers and controls from the Flex framework you might use to build this, it’s a little too early in the planning stage to be thinking about the implementation. There’s still one crucial step left that may have an impact on how you build the UI.

Designing the Interaction

Interaction design means deciding how the application will behave in response to user input. The user interaction for this application will be as simple as the interface.

For the sample application, the interaction is implied by the UI design. However, in a more formal project, interaction design is a crucial step to getting your application right.

If you’re going to be doing interaction design as part of your job, I suggest getting hold of a copy of About Face 2.0: The Essentials of Interaction Design by Alan Cooper and Robert M. Reimann (Wiley, 2003). This book will help you to determine what your potential users want from the UI and aid you in avoiding the most common pitfalls when designing a UI. This book was written with desktop application development in mind, but because RIA development is all about bringing desktop-style applications to the Web, most of the concepts discussed in this book apply equally well to our line of work.

Designing the Data and Logic

At this stage, you would normally look at designing the data entities (using UML or entity relationship diagrams) and the logic of the application (using UML or flowcharts), but this isn’t a book on software design, and that topic is way too big for me to do it any justice in a page or two. Take a look at the UML website (http://www.uml.org) for the UML specification and an introduction to its use, plus its benefits for application development.

Setting up the Project

So now we get down to the fun stuff: using the Flex framework to build the application. In order to get a feel for MXML, you’ll use the Flash Builder IDE to set up and manage the project to save the bother of messing too much with the command line. If you are using the free Flex SDK, the ActionScript and the MXML covered here will be exactly the same. You will just need to manually create the directories and files, and compile through the command line (consult Adobe’s documentation for instructions on compiling through the command line).

You’ll build the application in an incremental fashion, testing new features as you go. This means that you’ll get the basics working first and then build on them step by step until you have the finished application. Building an application in this way means that you can spot any problems early instead of waiting until the very end to compile and test, only to find that the most basic things aren’t working.

The first step in building the application is setting up the project. With Flash Builder open, select File image New image Flex Project from the menus to open the New Flex Project dialog box, as shown in Figure 14-2.

image

Figure 14-2. The New Flex Project dialog box in Flash Builder

Enter IrisReader as the project name. Uncheck Use default location and create a project directory specifically for this chapter, following the same convention as you have for the examples in previous chapters. Leave the Application type radio button selected as web application. ClickFinish to create the new project.

At this point, you have a project structure set up with the root directory containing your main MXML file, IrisReader.mxml, and a number of properties files, which you will not edit. In addition, four subdirectories have been created:

· bin-debug: This directory contains all the files needed to deploy your application, including HTML for embedding the SWF; JavaScript for player detection, writing the SWF to the page, and history management; and the SWFs themselves, both normal and debug versions.

· html-template: This directory contains the files that are used to produce the output in the bin directory, with tokens that can be replaced, based on your application. For instance, if you set the background color or width and height in your MXML, these values can be written into your HTML in the bin directory using the template in the html-template directory.

· libs: This directory can be used to store external or third-party libraries or resources that are used in your Flex project.

· src: This directory contains all source code files that a developer has created for the given project.

Creating the Basic UI

Now that you have set up the project, you can start to build the UI. In Flex, this generally means starting with the container components that will control the layout of your application. Let’s take the wireframe from Figure 14-1 and break that down into a hierarchy of Flex containers, as shown in Figure 14-3.

image

Figure 14-3. The container components in the application

As you can see, Panel containers (the boxed elements with title bars) separate the different elements of the UI, encapsulated in a combination of HDividedBox and VDividedBox containers. These containers position items horizontally or vertically, respectively, and provide a means to drag to resize the internal elements, giving the UI maximum flexibility.

Creating the Containers

You should be in Source view of the IrisReader.mxml file. Begin by changing the layout attribute of the <mx:Application> element to vertical. Then add an HDividedBox container with both width and height attributes set to 100%.

<mx:Application xmlns:mx=image
"http://www.adobe.com/2006/mxml" layout="vertical">
<mx:HDividedBox width="100%" height="100%">
</mx:HDividedBox>
</mx:Application>

Within the HDividedBox, add a Panel container with a title of Subscriptions, a width of 40%, and a height of 100%.

<mx:Application xmlns:mx=image
"http://www.adobe.com/2006/mxml" layout="vertical">
<mx:HDividedBox width="100%" height="100%">
<mx:Panel title="Subscriptions" width="40%" height="100%">
</mx:Panel>
</mx:HDividedBox>
</mx:Application>

As a sibling of the Panel, add a VDividedBox with a width of 60% (so it occupies the remainder of the width of the HDividedBox parent) and a height of 100%.

<mx:Application xmlns:mx=image
"http://www.adobe.com/2006/mxml" layout="vertical">
<mx:HDividedBox width="100%" height="100%">
<mx:Panel title="Subscriptions" width="40%" height="100%">
</mx:Panel>
<mx:VDividedBox width="60%" height="100%">
</mx:VDividedBox>
</mx:HDividedBox>
</mx:Application>

Finally, add two Panel containers as children of the VDividedBox, with titles of Articles and Article from top to bottom, and both with 100% width and 50% height (so they take up half the VDividedBox each).

<mx:Application xmlns:mx=image
"http://www.adobe.com/2006/mxml" layout="vertical">
<mx:HDividedBox width="100%" height="100%">
<mx:Panel title="Subscriptions" width="40%" height="100%">
</mx:Panel>
<mx:VDividedBox width="60%" height="100%">
<mx:Panel title="Articles" width="100%" height="50%">
</mx:Panel>
<mx:Panel title="Article" width="100%" height="50%">
</mx:Panel>
</mx:VDividedBox>
</mx:HDividedBox>
</mx:Application>

Creating Basic Feed Integration

With the very basic UI created, you can turn your attention to loading feeds. One of the requirements for the application is support for RSS 1.0, RSS 2.0, and Atom 1.0 syndication formats.

The proper approach for feed integration is to abstract the common functionality from all three feed formats into a set of classes and specialize those classes for each individual feed format. Creating these classes would be a lot of work and wouldn’t necessarily teach you anything about building Flex applications (the whole point of this chapter). Thankfully, Adobe has already done the hard work for you with its XML syndication library, which is available from Adobe Labs.

Installing the XML Syndication Library

The XML syndication library provides code that parses the XML feed formats into ActionScript objects that you can handle natively in your application. All you need to do is download this library and integrate it into your project:

1. The XML syndication library is available through Google’s code repository. Visit http://code.google.com/p/as3syndicationlib/ and download the featured .zip file.

2. Extract the .zip file to your working directory. The file of interest is xmlsyndication.swc, located in the bin directory of the extracted files.

In the doc directory, you’ll find API documentation for the XML syndication library, which is worth reviewing, as you’re going to be using several of these classes and interfaces in the feed reader application.

3. In the Flex Navigator panel, right-click (Control-click for Mac OS X) the libs folder and select Import. Select File system under the General folder in the Select screen of the Import window, as shown in Figure 14-4, and then click Next.

image

Figure 14-4. Choosing to import resources from the file system

4. Click the Browse button at the top right of the File system screen. Find and select the bin directory from the files extracted in step 2 and click OK (Windows) or Choose (Mac OS X).

5. Select the bin directory on the left side of the File system screen of the Import window, and check the check box next to the xmlsyndication.swc entry on the right side, as shown in Figure 14-5.

image

Figure 14-5. Choosing to import the xmlsyndication.swc file

6. Click the Finish button in the Import window to import the xmlsyndication.swc file into your project.

7. Now you need to tell Flash Builder that you want this file compiled as part of your project. Right-click (Windows) or Control-click (Mac OS X) the IrisReader project in the Navigator panel and select Properties from the context menu.

8. Select Flex Build Path from the left side of the Properties for IrisReader window, and then select the Library path tab on the right side, as shown in Figure 14-6.

image

Figure 14-6. Setting the Flex build path

9. Click the Add SWC button and then click the Browse button in the Add SWC dialog box. Select the xmlsyndication.swc file within the libs directory of your main project directory. Then click OK (Windows) or Choose (Mac OS X).

10.Your Library path tab should look like Figure 14-7. Click OK in the Properties for IrisReader window to close it.

image

Figure 14-7. Use the Add SWC button to have the xmlsyndication.swc file compile with your project

If you’re using Flex SDK 4, you can just create a lib directory in your chosen project directory and copy the xmlsyndication.swc file into that directory. Then all you need to do is add the xmlsyndication.swc file to the library-path option of the mxmlc compiler, which you can do as part of your compile command. In Windows, type this:

mxmlc -library-path+=lib\xmlsyndication.swc -output bin\
IrisReader.swf src\IrisReader.mxml

In Mac OS, type this:

mxmlc -library-path+=lib/xmlsyndication.swc -output bin/
IrisReader.swf src/IrisReader.mxml

Creating the Subscription Class

You’re going to encapsulate all the information about a subscribed feed in a class named Subscription. The following is the bare minimum information that your feed class will need to contain:

· The URL of the feed

· The title of the feed (derived from the feed data)

· The URL of the feed’s website (derived from the feed data)

· A list of articles for the feed

· The date the feed was last checked

Adding a New Class to Your Project

Armed with this information, you can sketch out the skeleton of your Subscription class:

1. Right-click (Windows) or Control-click (Mac OS X) the IrisReader project in the Navigator panel and select New image ActionScript Class to open the New ActionScript Class wizard.

2. Enter com.foundationAS3.ch14.irisreader in the Package text box and Subscription in the Name text box, as shown in Figure 14-8.

image

Figure 14-8. Creating the new Subscription class

3. Click Finish to create the class file and necessary package directories, as shown in Figure 14-9. Flash Builder will automatically open the file, ready for editing.

image

Figure 14-9. The project directory structure in the Navigator panel showing the new class

Fleshing out the Subscription Class

Now you can start to add the properties you need, beginning with feedURL, which is obviously going to be a String. At the moment, I can’t see any reason why the URL of the feed will need to be changed from outside the Subscription class, so let’s make it private with a public getter function and have the initial value passed through in the constructor:

package com.foundationAS3.ch14.irisreader {

public class Subscription {

private var _feedURL:String;

public function Subscription(feedURL:String) {
_feedURL = feedURL;
}

public function get feedURL():String {
return _feedURL;
}

}

}

The other properties—title, url, articles, and lastChecked—will need to be visible from outside the Subscription class, but only for reading. With that in mind, let’s add them as private variables, with each having a simple getter function:

package com.foundationAS3.ch14.irisreader {

import mx.collections.ArrayCollection;

public class Subscription {

private var _feedURL:String;
private var _title:String;
private var _url:String;
private var _articles:ArrayCollection;
private var _lastChecked:Date;

public function Subscription(feedURL:String) {
_feedURL = feedURL;
_articles = new ArrayCollection();
}

public function get feedURL():String {
return _feedURL;
}

public function get title():String {
return _title;
}

public function get url():String {
return _url;
}

public function get articles():ArrayCollection {
return _articles;
}

public function get lastChecked():Date {
return _lastChecked;
}

}

}

Notice that you’re using an ArrayCollection for the articles property instead of a simple Array. This means that you can take advantage of data binding, which is available in the Flex framework’s ArrayCollection class, when dealing with the articles list (if you need to), and maybe save yourself a few lines of code later on. You’re also initializing the articles property in the constructor so that it’s ready to be filled with articles from the feed.

Testing the Subscription Class

You have a way to go with the Subscription class, but I get nervous if I go too long without at least testing to check that a class compiles without errors. With that in mind, let’s add some script to the IrisReader.mxml file that imports the Subscription class and creates a new instance of it.

Switch to the IrisReader.mxml document in Flash Builder and add an <mx:Script> element after the <mx:Application> tag. This element will allow you to write ActionScript directly into your MXML file.

Add an import statement for the Subscription class and then create a private variable to hold your test instance. Because you’re here, you may as well make that variable an ArrayCollection that will hold all the Subscription objects for your application—there’s no sense in adding extra work for yourself, now is there?

<mx:Application xmlns:mx=image
"http://www.adobe.com/2006/mxml" layout="vertical">
<mx:Script>
<![CDATA[
import com.foundationAS3.ch14.irisreader.Subscription;
import mx.collections.ArrayCollection;

private var _subscriptions:ArrayCollection;
]]>
</mx:Script>
<mx:HDividedBox width="100%" height="100%">
<mx:Panel title="Subscriptions" width="40%" height="100%">
</mx:Panel>
<mx:VDividedBox width="60%" height="100%">
<mx:Panel title="Articles" width="100%" height="50%">
</mx:Panel>
<mx:Panel title="Article" width="100%" height="50%">
</mx:Panel>
</mx:VDividedBox>
</mx:HDividedBox>
</mx:Application>

Again, you used an ArrayCollection, instead of a simple Array, for its data-binding benefits.

Finally, you need to write the code to initialize the _subscriptions variable and feed it an instance of the Subscription class. This function needs to run once the application is ready to go, so you’ll tie it into the creationComplete event of the Application class. You can add a listener for this event directly within the MXML tag for the Application. The handler for the event will be passed an instance of FlexEvent, so you’ll import that class as well:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" image
layout="vertical" creationComplete="onCreationComplete(event)">
<mx:Script>
<![CDATA[
import com.foundationAS3.ch14.irisreader.Subscription;
import mx.collections.ArrayCollection;
import mx.events.FlexEvent;

private var _subscriptions:ArrayCollection;

private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
_subscriptions.addItem(image
new Subscription("http://foundationas3.org/feed"));
}
]]>
</mx:Script>
<mx:HDividedBox width="100%" height="100%">
<mx:Panel title="Subscriptions" width="40%" height="100%">
</mx:Panel>
<mx:VDividedBox width="60%" height="100%">
<mx:Panel title="Articles" width="100%" height="50%">
</mx:Panel>
<mx:Panel title="Article" width="100%" height="50%">
</mx:Panel>
</mx:VDividedBox>
</mx:HDividedBox>
</mx:Application>

With that done (and once you save any changes to the project files), you’ll see any compile errors in the Problems panel. If you do see errors, don’t fret. Just double-click each error to go to the appropriate line in the source code and see if you can figure out what’s wrong.

Loading the Data

Let’s continue adding functionality to the Subscription class. You still need some way of retrieving the feed data, and you need to make use of the Adobe XML syndication library to process that feed data into something you can use in your application.

Don’t forget to keep saving your files as you proceed so that Flash Builder can compile your application after each step. Not only will that prevent you from losing too much work should Flash Builder crash but it will also detect any errors in your code early, so you can fix them right away.

You’ll use the HTTPService class of the Flex framework to load the feed data, so the first thing you need is a private variable of this type that gets initialized with the feed URL in the constructor function:

package com.foundationAS3.ch14.irisreader {

import mx.collections.ArrayCollection;
import mx.rpc.http.HTTPService;

public class Subscription {

private var _feedURL:String;
private var _title:String;
private var _url:String;
private var _articles:ArrayCollection;
private var _lastChecked:Date;
private var _service:HTTPService;

public function Subscription(feedURL:String) {
_feedURL = feedURL;
_articles = new ArrayCollection();

_service = new HTTPService();
_service.url = _feedURL;
}
...
}

By default, the HTTPService class returns its data as an Object, but the XML syndication library will deal only with either an XML object or a string containing the XML data. With that in mind, you need to change the resultFormat property of the _service object toHTTPService.RESULT_FORMAT_E4X so you get the data back in the desired format:

...
public function Subscription(feedURL:String) {
_feedURL = feedURL;
_articles = new ArrayCollection();

_service = new HTTPService();
_service.url = _feedURL;
_service.resultFormat = HTTPService.RESULT_FORMAT_E4X;
}
...

Next, you need to add event listeners to the _service object to detect when a response is received from the server and to handle any errors that might occur:

package com.foundationAS3.ch14.irisreader {

import mx.collections.ArrayCollection;
import mx.rpc.http.HTTPService;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
...
public function Subscription(feedURL:String) {
_feedURL = feedURL;
_articles = new ArrayCollection();

_service = new HTTPService();
_service.url = _feedURL;
_service.resultFormat = HTTPService.RESULT_FORMAT_E4X;
_service.addEventListener(ResultEvent.RESULT, onServiceResult);
_service.addEventListener(FaultEvent.FAULT, onServiceFault);
}

private function onServiceResult(event:ResultEvent):void {
}

private function onServiceFault(event:FaultEvent):void {
}
...
}

With that done, you can turn your attention to fleshing out the onServiceResult() event handler method of the Subscription class. Here is where you use the XML syndication library you went to all that bother to integrate into your project.

Within the generic package of the library, the FeedFactory class analyzes the feed data and parses it into the appropriate object. This class has two static methods: getFeedByString(), for dealing with a String containing XML data, and getFeedbyXML(), for dealing with an XML object. Both functions return an object that implements the IFeed interface (which is also part of the generic package).

Interfaces are constructs in object-oriented languages that specify a group of methods that a class must implement. This is often described as a “contract” for a class that the class must fulfill. The reason to do this is so that objects of different classes that have different superclasses can all implement a common known interface. This allows other objects to call methods that are guaranteed to be defined on those classes implementing the interface. The methods in an interface do not contain any body, so therefore have no functionality. The methods exist only to specify which methods a class implementing the interface must define.

For example, suppose that you have a Farmer class, and a Farmer instance must feed both its Chicken instances and its Pig instances. Chicken extends Bird, while Pig extends Mammal. In the case of this farm, the inheritance stops there, without a grand Animal class from which all the animals extend. How can the farmer be guaranteed that both the chickens and the pigs will act the same way? An interface solves this problem.

You define an interface named IFeedable (interfaces usually have a capital I prefix) that specifies an eat() method. Pig and Chicken implement IFeedable, which will require them to define an eat() method. Then, if the farmer had an array of animals, and that array contained only classes that implemented IFeedable, it would not matter whether an index contained a pig or a chicken or whatever—the interface could be used to allow for the eat() method to be called without a compile-time error, as in the following code:

for each (animal:IFeedable in animals) {
animal.eat();
}

Because an interface can be used for a data type just as a class can be used, the animal variable can be cast to the interface, so eat() can be called successfully.

Consider needing to check whether an animal actually had an eat() method, and having to use loose typing with Object, and you will begin to appreciate the value of interfaces:

for each (animal:Object in animals) {
if (animal.hasOwnProperty("eat")) {
animal.eat();
}
}

Import all classes from the com.adobe.xml.syndication.generic package, and add a call to the FeedFactory.getFeedByXML() method in the onServiceResult event handler to parse the resulting XML into a local IFeed object:

package com.foundationAS3.ch14.irisreader {

import mx.collections.ArrayCollection;
import mx.rpc.http.HTTPService;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import com.adobe.xml.syndication.generic.*;
...
private function onServiceResult(event:ResultEvent):void {
var feed:IFeed = FeedFactory.getFeedByXML(event.result as XML);
}
...

You can now expand the onServiceResult() handler to use the information contained in your feed object, starting with pulling the feed title and URL out of the object’s metadata property:

...
private function onServiceResult(event:ResultEvent):void {
var feed:IFeed = FeedFactory.getFeedByXML(event.result as XML);

_title = feed.metadata.title;
_url = feed.metadata.link;
}
...

Next, you need to loop through the items array of the feed and add any new items to the articles collection of our Subscription object.

Each element of the items array is an object that implements the IItem interface. This interface has a date property, which is a Date object representing the time the article was posted. All you need to do is compare this date to the lastChecked property of your Subscriptionobject to see whether the article is new:

...
private function onServiceResult(event:ResultEvent):void {
var feed:IFeed = FeedFactory.getFeedByXML(event.result as XML);

_title = feed.metadata.title;
_url = feed.metadata.link;
for each (var item:IItem in feed.items) {
if (lastChecked == null ||image
item.date.getTime() > lastChecked.getTime()) {
articles.addItem(item);
}
}
}
...

Notice that you add an extra check to the conditional to ensure that lastChecked exists before you call the getTime() method. If lastChecked didn’t exist, which would happen when the class is first initialized, then calling getTime() would throw an error.

For this event handler, you need to set the lastChecked property to the current date, so that the next time you refresh the feed, you deal only with articles that have subsequently been added to the feed:

...
private function onServiceResult(event:ResultEvent):void {
var feed:IFeed = FeedFactory.getFeedByXML(event.result as XML);

_title = feed.metadata.title;
_url = feed.metadata.link;
for each (var item:IItem in feed.items) {
if (lastChecked == null ||image
item.date.getTime() > lastChecked.getTime()) {
articles.addItem(item);
}
}

_lastChecked = new Date();
}
...

Now you need to add a public refresh function that can be called to refresh the feed by calling the send() method of the _service object. You also need to call this function internally from the constructor function to fetch the initial feed data.

...
public function Subscription(feedURL:String) {
_feedURL = feedURL;
_articles = new ArrayCollection();

_service = new HTTPService();
_service.url = _feedURL;
_service.resultFormat = HTTPService.RESULT_FORMAT_E4X;
_service.addEventListener(ResultEvent.RESULT, onServiceResult);
_service.addEventListener(FaultEvent.FAULT, onServiceFault);

refresh();
}
...
private function onServiceFault(event:FaultEvent):void {
}

public function refresh():void {
_service.send();
}

public function get feedURL():String {
return _feedURL;
}
...

Allowing Subscription Instances to be used for Data Binding

The last thing you need to do with the Subscription class is to prepare it so that it can be used as the source for data binding. This involves marking the relevant parts of the class for data binding and dispatching appropriate events when you change the values.

Because the properties of the Subscription class are read-only, you’ll need to manually dispatch propertyChange events to enable them to be used as the source of data-binding expressions. This also means that you need your class to extend EventDispatcher so you can dispatch the necessary events.

Import the EventDispatcher class from the flash.events package and change the class definition so that the Subscription class extends EventDispatcher.

...
import mx.rpc.events.ResultEvent;
import com.adobe.xml.syndication.generic.*;
import flash.events.EventDispatcher;

public class Subscription extends EventDispatcher {
...

Add the [Bindable] metadata tag just before the class definition. This tag informs the Flex compiler that the class will broadcast a change event that can be used for data binding.

...
import mx.rpc.events.ResultEvent;
import com.adobe.xml.syndication.generic.*;
import flash.events.EventDispatcher;

[Bindable]
public class Subscription extends EventDispatcher {
...

Because you have a lot of properties that need to dispatch the propertyChange event, let’s create a helper function so you don’t need to keep repeating the event-dispatching code. This function needs the name of the property that changed, the old value, and the new value, so it can create the PropertyChangeEvent object.

...
import mx.rpc.events.ResultEvent;
import mx.events.PropertyChangeEvent;
import com.adobe.xml.syndication.generic.*;
import flash.events.EventDispatcher;

[Bindable]
public class Subscription extends EventDispatcher {
...
public function refresh():void {
_service.send();
}

private function notifyPropertyChange(name:String, image
oldValue:Object, value:Object):void {
if (value !== oldValue) {
dispatchEvent(PropertyChangeEvent.createUpdateEvent(image
this, name, oldValue, value));
}
}

public function get feedURL():String {
...

Notice that the value parameters for this function are of type Object so that they can be passed data of any type. Within the function, you’re comparing the old and new values to make sure they are not the same before creating and dispatching the event, because you don’t want to invoke the data-binding mechanism if nothing has changed.

Now you need to give each read-only property a private function that you can use to update the value and then call the notifyPropertyChange() function with the appropriate values. Let’s start with feedURL:

...
public function get feedURL():String {
return _feedURL;
}
private function setFeedURL(value:String):void {
var oldValue:Object = _feedURL;
_feedURL = value;
notifyPropertyChange("feedURL", oldValue, value);
}
...

Here, the setFeedURL() function first stores the old value of the _feedURL property before overwriting it with the value passed it. It then passes the name of the public property along with both these values to the notifypropertyChange function to invoke the data-binding mechanism.

Now add the remaining public properties:

...
public function get title():String {
return _title;
}
private function setTitle(value:String):void {
var oldValue:Object = _title;
_title = value;
notifyPropertyChange("title", oldValue, value);
}

public function get url():String {
return _url;
}
private function setURL(value:String):void {
var oldValue:Object = _url;
_url = value;
notifyPropertyChange("url", oldValue, value);
}

public function get articles():ArrayCollection {
return this._articles;
}
private function setArticles(value:ArrayCollection):void {
var oldValue:Object = _articles;
_articles = value;
notifyPropertyChange("articles", oldValue, value);
}

public function get lastChecked():Date {
return this._lastChecked;
}
private function setLastChecked(value:Date):void {
var oldValue:Object = _lastChecked;
_lastChecked = value;
notifyPropertyChange("lastChecked", oldValue, value);
}
...

Finally, change the code in the onServiceResult() event handler to use these new functions instead of directly setting the private variable values. This will mean that the appropriate data-binding events will be dispatched and any associated UI controls will be updated.

...
private function onServiceResult(event:ResultEvent):void {
var feed:IFeed = FeedFactory.getFeedByXML(event.result as XML);

setTitle(feed.metadata.title);
setURL(feed.metadata.link);
for each (var item:IItem in feed.items) {
if (lastChecked == null ||image
item.date.getTime() > lastChecked.getTime()) {
articles.addItem(item);
}
}

setLastChecked(new Date());
}
...

That’s it. You’re finished with the Subscription class. It’s time to turn your attention to hooking up the UI.

Creating the Subscriptions List

The list of feeds that a user has subscribed to will be displayed in a simple List control. You already have a private _subscriptions property of the IrisReader.mxml file that will hold the user’s Subscription objects, so all you need to do is to create a List control within theSubscriptions panel and bind it to the _subscriptions ArrayCollection.

Add a [Bindable] metatag just before the definition of the _subscriptions variable in the code block in the IrisReader.mxml file. While you’re there, add a few more Subscription objects to the _subscriptions collection so that you can see more than one feed.

...
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import com.foundationas3.irisreader.Subscription;
import mx.events.FlexEvent;

[Bindable]
private var _subscriptions:ArrayCollection;

private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
_subscriptions.addItemimage
(new Subscription("http://blogs.adobe.com/labs/feed"));
}
]]>
</mx:Script>
...

Within the Subscriptions <mx:Panel> element, add a List control with its dataProvider property bound to the _subscriptions variable. Use title as the labelField, and set its width and height to 100%.

...
<mx:Panel title="Subscriptions" width="40%" height="100%">
<mx:List id="subscriptionsList" dataProvider="{_subscriptions}" image
labelField="title" width="100%" height="100%"/>
</mx:Panel>
...

You’ve given the List control an id of subscriptionsList here because at some point in the future, you’ll need to get the currently selected entry from that control in order to populate the Articles panel’s data grid (shown in Figure 14-1). Any controls you need to refer to from other parts of the code should have id attributes. It’s possible to refer to them without using IDs, but that’s a painful process.

If you test your application now, you should see a list of feeds in the left panel, as shown in Figure 14-10. Once they have loaded, you’ll see their titles.

image

Figure 14-10. The list of feeds appears in the Subscriptions panel

Before you get drunk on the power of data binding, you should know that a couple of things are amiss here: before the feed data has loaded, each Subscription object is shown as [object Subscription] (yuck), and you’re not displaying the total number of articles in each feed.

To set these misdemeanors right, you will need to tell the List control how you want the label for each item to be rendered, by using a custom label function.

Remove the labelField attribute from the List control and replace it with a labelFunction attribute with a value of renderSubscriptionsListLabel:

...
<mx:Panel title="Subscriptions" width="40%" height="100%">
<mx:List id="subscriptionsList" dataProvider="{_subscriptions}"
labelFunction="renderSubscriptionsListLabel"
width="100%" height="100%"/>
</mx:Panel>
...

Now you need to create the renderSubscriptionsListLabel() function in the <mx:Script> block. Label-renderer functions get passed the object for a given item in the list and should return a String of the label to be displayed. In this case, you want to test to see whether the title of the Subscription object passed in is null, and if it is, display the feedURL property instead of the title. In any case, you also want to add the number of items in the feed after the URL or title in brackets. That all translates into a function that looks like this:

...
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import com.foundationas3.irisreader.Subscription;

[Bindable]
private var _subscriptions:ArrayCollection;

private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
_subscriptions.addItemimage
(new Subscription("http://foundationas3.com/feed"));
_subscriptions.addItemimage
(new Subscription("http://dynamicflash.com/feed"));
_subscriptions.addItem(image
new Subscription("http://weblogs.macromedia.com/mxna/xml/rss.cfm"));
}

private function renderSubscriptionsListLabel(image
subscription:Subscription):String {
var title:String = subscription.title;
if (title == null) {
title = subscription.feedURL;
}
title += " (" + subscription.articles.length + ")";
return title;
}
]]>
</mx:Script>
...

If you test the application now, you should see the feed URLs displayed in lieu of titles before the feed data is loaded, and each entry shows the number of items in that feed in brackets, as shown in Figure 14-11.

image

Figure 14-11. The list of feeds fixed to show URLs before data is loaded and the number of items in each feed

You’ll come back to the Subscriptions panel later on to add the additional controls shown in Figure 14-1. Now let’s get the basics of the other panels working.

Creating the Articles Data Grid

From the wireframe, you can see that the articles for a feed are displayed in a DataGrid control with two columns, showing the date and title of each article. You’ll use data binding to display the articles from the Subscription object currently selected in the subscriptions list.

Within the Articles <mx:Panel> element, add a 100% width and height DataGrid control with two columns displaying the date and title properties of each item in the articles collection:

...
<mx:Panel title="Articles" width="100%" height="50%">
<mx:DataGrid id="articlesGrid" width="100%" height="100%">
<mx:columns>
<mx:DataGridColumn dataField="date" headerText="Date"/>
<mx:DataGridColumn dataField="title" headerText="Title"/>
</mx:columns>
</mx:DataGrid>
</mx:Panel>
...

Once again, you gave this control an id attribute so you can refer to it later in the code (specifically, for populating the Article panel with information about the currently selected entry in the data grid).

Next, add a dataProvider property to the DataGrid object with the value shown:

...
<mx:Panel title="Articles" width="100%" height="50%">
<mx:DataGrid id="articlesGrid" width="100%" height="100%"
dataProvider="{subscriptionsList.selectedItem.articles}" >
<mx:columns>
<mx:DataGridColumn dataField="date" headerText="Date"/>
<mx:DataGridColumn dataField="title" headerText="Title"/>
</mx:columns>
</mx:DataGrid>
</mx:Panel>
...

Go ahead and test your application now. Once the feeds have loaded, select each one in turn and marvel once again at the magic of data binding. Figure 14-12 shows an example.

image

Figure 14-12. The list of feed articles in the Articles panel

But look again, and you’ll see that something is not quite right with the way the application is working. According to the wireframe shown in Figure 14-1, the title of the Articles panel should change to be the title of the currently selected item in the subscriptions list. Luckily, you can use data binding to get around that, too.

Change the value of the title attribute of the Articles Panel component to match the following:

...
<mx:Panel title="{subscriptionsList.selectedItem.title}"
width="100%" height="50%">
<mx:DataGrid id="articlesGrid" width="100%" height="100%"
dataProvider="{subscriptionsList.selectedItem.articles}" >
<mx:columns>
<mx:DataGridColumn dataField="date" headerText="Date"/>
<mx:DataGridColumn dataField="title" headerText="Title"/>
</mx:columns>
</mx:DataGrid>
</mx:Panel>
...

Test the application again, and the results should look something like Figure 14-13.

image

Figure 14-13. The Articles panel shows the title of the feed selected in the subscriptions list

You still have a little work to do on the Articles panel. The date format looks a little funky, and you’re missing the last updated timestamp and a button to take the user to the website. But for now, let’s move on to populating the Article panel.

Populating the Article Panel

The majority of the Article panel is taken up by a TextArea control that displays the excerpt of the currently selected item in the articlesGrid control. Let’s add that now.

Add a 100% width and height TextArea control as a child of the Article <mx:Panel> container, and set the editable property to false (because there’s no sense in allowing the user to change what’s displayed in this control):

...
<mx:Panel title="Article" width="100%" height="50%">
<mx:TextArea id="excerptTextArea" editable="false"
width="100%" height="100%"/>
</mx:Panel>
...

Yet again, you’ve added an id attribute, and you should know why by now.

Set the htmlText property of the new TextArea control to be bound to the excerpt.value property of the currently selected item in the articlesGrid control:

...
<mx:Panel title="Article" width="100%" height="50%">
<mx:TextArea id="excerptTextArea" editable="false"
width="100%" height="100%"
htmlText="{articlesGrid.selectedItem.excerpt.value}" />
</mx:Panel>
...

You used excerpt.value because excerpt is an object with several properties. In this case, you’re interested in only the value property, so you read that directly. Flash Builder will warn you that it won’t be able to detect assignments to the value property, and there’s nothing you can do about that because it’s an internal part of the XML syndication library, which doesn’t seem to have been built with data binding in mind. However, it doesn’t affect the functionality of the completed application.

You know that the title of the Article panel should be the title of the currently selected item from the articlesGrid control, so let’s set that up next. Change the value of the title attribute of the Article panel to be bound to the title property of the currently selected item from thearticlesGrid control:

...
<mx:Panel title="{articlesGrid.selectedItem.title}"
width="100%" height="50%">
<mx:TextArea id="excerptTextArea" editable="false"
width="100%" height="100%"
htmlText="{articlesGrid.selectedItem.excerpt.value}" />
</mx:Panel>
...

Now test your application’s newfound capability to display the excerpt from the selected item in the Article panel. The result should look something like Figure 14-14.

image

Figure 14-14. The Article panel shows an excerpt of the article selected in the Articles list

Go ahead and take a break. When you’re ready, you’ll start fine-tuning the panels.

Completing the Subscriptions Panel

Although your application is technically working, you still have a way to go to satisfy the requirements drawn up earlier in the chapter. The most glaring omission at the moment is that the subscriptions are hard-coded, which is no good unless you fancy recompiling the application every time you want to add or remove a feed. Let’s fix that now.

Allowing Users to Subscribe to a Feed

If you glance back at the application’s UI in Figure 14-1, you’ll see add/remove buttons below the list of subscribed feeds, which allow the users to manage their subscriptions. The section on interaction design mentioned that a dialog box should be shown when the user clicks the add button, but the dialog box wasn’t part of the original wireframe. Figure 14-15 shows how that dialog box should appear.

image

Figure 14-15. The dialog box for adding a feed

This is a very simple dialog box with a Label, a TextArea, and a couple of Button controls, all neatly wrapped in a TitleWindow container. You need to create this as a component that you can then reference in your application to be opened as a modal window.

Laying out the SubscribeDialog Component

Let’s begin by creating the new component and setting up its basic layout.

Right-click (Windows) or Control-click (Mac OS X) the IrisReader project in the Navigator panel of Flash Builder and select New image MXML Component from the context menu. In the New MXML Component dialog box, enter SubscribeDialog in the Filename text box and pickTitleWindow from the Based on drop-down list. Select vertical from the Layout drop-down list, and enter 300 in the Width text box. Clear the value from the Height text box (so that the height of the dialog box will be based on its content). Your dialog box should look like Figure 14-16. Click Finish to create the component.

image

Figure 14-16. Creating the SubscribeDialog component

Now you have a new MXML file containing the values you selected for the TitleWindow container. In that file, set the title property of the TitleWindow container to "Subscribe to feed". Then add a 100% width HBox container containing Label and TextInputcontrols with the following configurations:

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"
title="Subscribe to feed" layout="vertical" width="300">
<mx:HBox width="100%" verticalAlign="middle">
<mx:Label text="Feed URL"/>
<mx:TextInput id="feedURL" width="100%"/>
</mx:HBox>
</mx:TitleWindow>

Add another HBox as a sibling of the previous one. It contains the two Button controls for the dialog box.

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"
title="Subscribe to feed" layout="vertical" width="300">
<mx:HBox width="100%" verticalAlign="middle">
<mx:Label text="Feed URL"/>
<mx:TextInput id="feedURL" width="100%"/>
</mx:HBox>
<mx:HBox width="100%" horizontalAlign="right">
<mx:Button id="okButton" label="OK"/>
<mx:Button id="cancelButton" label="Cancel"/>
</mx:HBox>
</mx:TitleWindow>

Now the main layout for the SubscribeDialog component is complete.

Wiring up the Buttons

Next, you need to add the necessary buttons to the Subscriptions panel and wire up the subscribe button so that it opens the SubscribeDialog component you just created.

Switch back to the IrisReader.mxml file and add a ControlBar container below the List control in the Subscriptions <mx:Panel> container, containing the subscribe and unsubscribe buttons:

...
<mx:Panel title="Subscriptions" width="40%" height="100%">
<mx:List id="subscriptionsList" dataProvider="{_subscriptions}"
labelFunction="renderSubscriptionsListLabel"
width="100%" height="100%"/>
<mx:ControlBar>
<mx:Button id="subscribeButton" label="+"/>
<mx:Button id="unsubscribeButton" label="-"/>
</mx:ControlBar>
</mx:Panel>
...

To make the subscribe button open the dialog box, you need to add a click event handler for the ­ subscribeButton component. While you’re there, you may as well do the same for the ­unsubscribeButton component. Have them call functions named subscribe andunsubscribe, respectively. You’ll create those functions next.

...
<mx:Panel title="Subscriptions" width="40%" height="100%">
<mx:List id="subscriptionsList" dataProvider="{_subscriptions}"
labelFunction="renderSubscriptionsListLabel"
width="100%" height="100%"/>
<mx:ControlBar>
<mx:Button id="subscribeButton" label="+"
click="subscribe(event)"/>
<mx:Button id="unsubscribeButton" label="-"
click="unsubscribe(event)"/>
</mx:ControlBar>
</mx:Panel>
...

Create the subscribe() and unsubscribe() functions, adding them to the bottom of the <mx:Script/> block in the IrisReader.mxml file:

...
return title;
}

private function subscribe(event:MouseEvent):void {
}

private function unsubscribe(event:MouseEvent):void {
}
]]>
</mx:Script>
...

Import the PopUpManager class from the mx.managers package and use it in the subscribe() function to create a modal window from your SubscribeDialog component. You’ll also call the PopUpManager’s centerPopUp() method to center the dialog box within the application window:

...
import mx.events.FlexEvent;
import mx.managers.PopUpManager;
...
private function subscribe(event:MouseEvent):void {
var dialog:SubscribeDialog = PopUpManager.createPopUp(this, image
SubscribeDialog, true) as SubscribeDialog;
PopUpManager.centerPopUp(dialog);
}
...

If you test the application now and click the new subscribe button in the Subscriptions panel, you should see the dialog box you just created pop up in the center of the screen, as shown in Figure 14-17.

image

Figure 14-17. The Subscribe to feed dialog box

This dialog box doesn’t actually do much at the moment. You can’t even close it once it has been opened. But it’s a start.

Next, you need to wire up the OK and Cancel buttons so they at least close the dialog box. You can do that by using various methods of the PopUpManager class in response to the button clicks.

Switch back to the SubscribeDialog.mxml file in Flash Builder and add in an <mx:Script> element at the top. Then import the PopUpManager class from the mx.managers package.

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"
title="Subscribe to feed" layout="vertical" width="300">
<mx:Script>
<![CDATA[
import mx.managers.PopUpManager;

]]>
</mx:Script>
<mx:HBox width="100%" verticalAlign="middle">

To close the dialog box, you need to use the PopUpManager.removePopUp() method, passing a reference to this so it knows which window to close. You’ll do this in click event handlers for the two buttons.

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"
title="Subscribe to feed" layout="vertical" width="300"
<mx:Script>
<![CDATA[
import mx.managers.PopUpManager;

private function onOKButtonClick(event:MouseEvent):void {
PopUpManager.removePopUp(this);
}

private function onCancelButtonClick(event:MouseEvent):void {
PopUpManager.removePopUp(this);
}
]]>
</mx;Script>
<mx:HBox width="100%" verticalAlign="middle">
...
<mx:HBox width="100%" horizontalAlign="right">
<mx:Button id="okButton" label="OK"
click="onOKButtonClick(event)"/>
<mx:Button id="cancelButton" label="Cancel"
click="onCancelButtonClick(event)"/>
</mx:HBox>
</mx:TitleWindow>

With that done, you should now be able to close the dialog box by clicking either the OK or Cancel button.

That’s not the end of the story, however. You still need to be able to get the feed URL from the dialog box and create a new Subscription object for it when the user clicks the OK button.

Getting the Feed

To get the new feed, you’ll create an Event class to hold the feed URL and dispatch this event when the dialog box is being closed. Then you will have the main IrisReader class listen for this event being dispatched and act accordingly.

Right-click (Windows) or Control-click (Mac OS X) the IrisReader project in the Navigator panel and select New image ActionScript Class from the context menu. In the New ActionScript Class dialog box, enter details for a class named SubscribeDialogEvent in thecom.foundationAS3.ch14.irisreader.events package, with Event as the superclass, as shown in Figure 14-18. Click Finish to create the new class.

image

Figure 14-18. Creating the SubscribeDialogEvent

In the newly created SubscribeDialogEvent.as file, add a constructor function that accepts the event type and feed URL, both as a String, as well as the bubbles and cancelable properties, and passes them to the superclass Event constructor:

package com.foundationAS3.ch14.irisreader.events {

import flash.events.Event;

public class SubscribeDialogEvent extends Event {

public function SubscribeDialogEvent(
type:String,
feedURL:String,
bubbles:Boolean=false,
cancelable:Boolean=false
) {
super(type, bubbles, cancelable);
}

}

}

Create a read-only feedURL property and modify the constructor to store the feedURL parameter in this property:

package com.foundationAS3.ch14.irisreader.events {

import flash.events.Event;

public class SubscribeDialogEvent extends Event {

private var _feedURL:String;

public function SubscribeDialogEvent(
type:String,
feedURL:String,
bubbles:Boolean=false,
cancelable:Boolean=false
) {
super(type, bubbles, cancelable);
_feedURL = feedURL;
}

public function get feedURL():String {
return _feedURL;
}

}

}

Finally, add a public static constant to represent the event type. The constant name is completely arbitrary but is generally a verb, so let’s call it SUBSCRIBE. You also need to override Event’s clone() method, which should be done for any child class of Event.

package com.foundationAS3.ch14.irisreader.events {

import flash.events.Event;

public class SubscribeDialogEvent extends Event {

public static const SUBSCRIBE:String = "subscribe";
private var _feedURL:String;

public function SubscribeDialogEvent(
type:String,
feedURL:String,
bubbles:Boolean=false,
cancelable:Boolean=false
) {
super(type, bubbles, cancelable);
_feedURL = feedURL;
}

override public function clone():Event {
return new SubscribeDialogEventimage
(type, feedURL, bubbles, cancelable);
}

public function get feedURL():String {
return _feedURL;
}

}

}

Next, modify the onOKButtonClick() method of the SubscribeDialog.mxml to dispatch your new event before the dialog box is closed, passing the text from the feedURL control as the feedURL parameter:

...
import mx.managers.PopUpManager;
import image
com.foundationAS3.ch14.irisreader.events.SubscribeDialogEvent;

private function onOKButtonClick(event:MouseEvent):void {
dispatchEvent(new SubscribeDialogEvent(image
SubscribeDialogEvent.SUBSCRIBE, feedURL.text));
PopUpManager.removePopUp(this);
}
...

In the subscribe() method in the IrisReader.mxml file, you need to listen for the subscribe event of the SubscribeDialog, and when it has fired, add a new Subscription object to the _subscriptions array using the feedURL that is included as part of the event data:

...
import mx.managers.PopUpManager;
import image
com.foundationAS3.ch14.irisreader.events.SubscribeDialogEvent;

[Bindable]
private var _subscriptions:ArrayCollection;
...
private function subscribe(event:MouseEvent):void {
var dialog:SubscribeDialog =image
PopUpManager.createPopUp(this, image
SubscribeDialog, true) as SubscribeDialog;
PopUpManager.centerPopUp(dialog);
dialog.addEventListener(SubscribeDialogEvent.SUBSCRIBE, image
onSubscribeDialogSubscribe);
}

private function unsubscribe(event:MouseEvent):void {

}

private function onSubscribeDialogSubscribe(image
event:SubscribeDialogEvent):void {
_subscriptions.addItem(new Subscription(event.feedURL));
}
...

That’s all that needs to be done to allow users to add their own subscriptions. Give it a try using feed URLs from your favorite blogs or news sites. Figures 14-19 and 14-20 show an example of adding a subscription.

image

Figure 14-19. Subscribing to Yahoo! News feed

image

Figure 14-20. Yahoo! News has been added to the subscriptions list

Before moving on to the unsubscribe functionality, you need to tidy up a couple of subscription areas. First, a number of hard-coded subscriptions are in the application, and now that users can add their own subscriptions, you can get rid of the hard-coded ones. Change the init()method of the IrisReader.mxml file to remove the creation of the three Subscription objects:

...
private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
}
...

Another slight problem is that the user can click the OK button in the Subscribe to feed dialog box without having entered a feed URL. You could solve that by implementing a regular expression to check that what the user has entered is a valid URL, but for the sake of simplicity, you’ll just disable the OK button if the feedURL control is empty.

If this were a production-quality application being developed for a client, I would definitely make sure the URL entered was a valid URL. Depending on the requirements, I might even go so far as loading the contents of the URL to check that it is a valid feed before allowing the user to click the OK button.

In the SubscribeDialog.mxml file, bind the enabled property of the OK button using a data-binding expression to check that the length of the feedURL control’s text property is greater than zero:

<mx:Button id="okButton" label="OK" image
click="onOKButtonClick(event)" image
enabled="{feedURL.text.length > 0}"/>

Allowing Users to Unsubscribe from a Feed

Now that users can subscribe to a feed, they’ll need some way of unsubscribing if the feed becomes boring (or, like mine, dormant for long periods of time). You already have a button in the Subscriptions panel for this purpose, and it’s wired up to the unsubscribe method. So, all you need to do is to remove the selected item in the list from the _subscriptions collection when this button is clicked.

In the IrisReader.mxml file, modify the unsubscribe() method to remove the currently selected item in the subscriptionsList control from the _subscriptions collection:

...
private function unsubscribe(event:MouseEvent):void {
_subscriptions.removeItemAt(subscriptionsList.selectedIndex);
}
...

You’re removing the selected item by index in the list because the ArrayCollection class doesn’t have a removeItem method; it has only a removeItemAt method.

To tidy up, you want the unsubscribe button enabled only if there is something selected in the _subscriptions list. To handle this, bind the enabled property of the unsubscribeButton using a data-binding expression that checks the selectedItem property of thesubscriptionsList control to make sure it’s not null.

...
<mx:Button id="unsubscribeButton" label="-"click="unsubscribe(event)"
enabled="{subscriptionsList.selectedItem !== null}"/>
...

Users can now add and remove subscriptions at their leisure. However, if they restart the application, all of their subscriptions are lost. Because one of the requirements was that the application remember the subscriptions list between sessions, let’s tackle that now.

Saving the Subscriptions List Between Sessions

As a Flex developer, you have basically two choices for data storage: store the data on the client in a local SharedObject or store it on the server using one of the variety of remote communication protocols that Flex supports. Because this section of the book is about Flex application development, you’ll go with the former approach. Local SharedObjects aren’t without their limitations, though. They’re the Flash equivalent of browser cookies, and like cookies, they can store only a limited amount of information. With this in mind, the plan is to store only a list of URLs for the user’s subscriptions list instead of the Subscription objects themselves and all the articles they contain.

Let’s start by creating a function to save the subscriptions list to a local SharedObject.

In the IrisReader.mxml file, import the SharedObject class from the flash.net package, and then create a new function named saveData() that initially loads the local SharedObject named IrisReader:

import mx.managers.PopUpManager;
import com.foundationAS3.ch14.irisreader.image
events.SubscribeDialogEvent;
import flash.net.SharedObject;
...
private function unsubscribe(event:MouseEvent):void {
_subscriptions.removeItemAt(subscriptionsList.selectedIndex);
}

private function saveData():void {
var so:SharedObject = SharedObject.getLocal("IrisReader");
}
...

Create a new feedURLs array, and then loop through all the Subscription objects in the _subscriptions collection and add the feedURL property of each to the feedURLs array:

...
private function saveData():void {
var so:SharedObject = SharedObject.getLocal("IrisReader");
var feedURLs:Array = new Array();
for each (var subscription:Subscription in _subscriptions) {
feedURLs.push(subscription.feedURL);
}
}
...

Finally, add the feedURLs array to the data property of the so object and call the flush() method to write the data to disk:

...
private function saveData():void {
var so:SharedObject = SharedObject.getLocal("IrisReader");
var feedURLs:Array = new Array();
for each (var subscription:Subscription in _subscriptions) {
feedURLs.push(subscription.feedURL);
}
so.data.feedURLs = feedURLs;
so.flush();
}
...

With that done, you need an equivalent function to get the feed URLs from the SharedObject and re-create the _subscriptions collection. Create a function named loadData() that initially clears the _subscriptions collection and then loads the local SharedObjectnamed IrisReader:

...
so.data.feedURLs = feedURLs;
so.flush();
}

private function loadData():void {
_subscriptions.removeAll();
var so:SharedObject = SharedObject.getLocal("IrisReader");
}
...

After checking to make sure the feedURLs array exists as part of the SharedObject’s data (which is necessary because it won’t exist the first time the user uses the application), loop through all the entries and create a Subscription object for each one, adding it to the_subscriptions collection:

...
private function loadData():void {
_subscriptions.removeAll();
var so:SharedObject = SharedObject.getLocal("IrisReader");
if (so.data.feedURLs) {
for each (var feedURL:String in so.data.feedURLs) {
_subscriptions.addItem(new Subscription(feedURL));
}
}
}
...

With these functions ready to go, you’ll load the data when the application starts and save the data whenever it changes. Loading the subscriptions list when the application starts can be taken care of in the handler for the application’s creationComplete event. Saving the data can be done whenever a new feed is added or removed in the onSubscribeDialogSubscribe() and unsubscribe() methods.

Add a call to the loadData() method just after the initialization of the _subscriptions collection in the onCreationComplete() method of the IrisReader.mxml file:

...
private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
loadData();
}
...

Next, add calls to saveData() in both the onSubscribeDialogSubscribe() and unsubscribe() methods:

...
private function unsubscribe(event:MouseEvent):void {
_subscriptions.removeItemAt(subscriptionsList.selectedIndex);
saveData();
}
...
private function onSubscribeDialogSubscribe(image
event:SubscribeDialogEvent):void {
_subscriptions.addItem(new Subscription(event.feedURL));
saveData();
}
...

And that’s another feature you can check off your list. If you run the application now, subscribe to a few feeds, and then restart the application, you should see your feed list reloaded. Nice.

At this point, you’re almost finished with the Subscriptions panel, with one last feature to add.

Refreshing the Subscriptions List

Of course, the users will want to see updates to their feeds. Let’s take care of that now.

Automatically Refreshing the Subscriptions List

The big feature that the feed reader application is missing is automatic checking of the subscriptions list and fetching of new articles. Users won’t want to need to repeatedly click a refresh button just to see if there have been any updates to their favorite feeds. Let’s get the feed to automatically refresh itself every five minutes.

Create a function named refreshAll() in the IrisReader.mxml file that loops through all the Subscription objects in the _subscriptions collection and calls their refresh methods:

...
}
}

private function refreshAll(event:Event):void {
for each (var subscription:Subscription in _subscriptions) {
subscription.refresh();
}
}

private function onSubscribeDialogSubscribe(image
event:SubscribeDialogEvent):void {
_subscriptions.addItem(new Subscription(event.feedURL));
saveData();
}
...

Now create a new private variable named refreshTimer to hold the Timer object and initialize the timer in the onCreationComplete() function. While you’re there, add an event listener for the TimerEvent.TIMER event.

...
import com.foundationAS3.ch14.irisreader.image
events.SubscribeDialogEvent;
import flash.net.SharedObject;
import flash.utils.Timer;
import flash.events.TimerEvent;

[Bindable]
private var _subscriptions:ArrayCollection;
private var _refreshTimer:Timer;

private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
loadData();

_refreshTimer = new Timer(5 * 60 * 1000);
_refreshTimer.addEventListener(TimerEvent.TIMER, image
onRefreshTimer);
_refreshTimer.start();
}
...

Now add the onRefreshTimer() event handler function, which just needs to call the refreshAll() method you wrote earlier:

...
private function refreshAll(event:Event):void {
for each (var subscription:Subscription in _subscriptions) {
subscription.refresh();
}
}

private function onRefreshTimer(event:TimerEvent):void {
refreshAll(event);
}
...

That handles automatic refreshing. Now let’s allow for manual updates.

Manually Refreshing the Subscriptions List

You can finish the Subscriptions panel by adding some buttons to allow the users to manually refresh the selected feed or all feeds in their subscriptions list.

In the IrisReader.mxml file, add a Label and two Button controls to the ControlBar container within the Subscriptions Panel component and give them appropriate labels. Add a 100% width Spacer to push the subscription buttons over to the right side of the control bar:

<mx:Panel title="Subscriptions" width="40%" height="100%">
<mx:List id="subscriptionsList" dataProvider="{_subscriptions}"
labelFunction="renderSubscriptionsListLabel"
width="100%" height="100%"/>
<mx:ControlBar>
<mx:Label text="Refresh: "/>
<mx:Button label="Selected"/>
<mx:Button label="All"/>
<mx:Spacer width="100%"/>
<mx:Button id="subscribeButton" label="+"
click="subscribe(event)"/>
<mx:Button id="unsubscribeButton" label="-"
click="unsubscribe(event)"
enabled="{subscriptionsList.selectedItem !== null}"/>
</mx:ControlBar>
</mx:Panel>

Add a click handler to the All button, which calls the refreshAll() function you created earlier:

<mx:Button label="All" click="refreshAll(event)"/>

Add a click handler to the Selected button, which calls a function named refreshSelected() (you’ll create that in a moment). While you’re there, bind the enabled property of this button using a data-binding expression to check that the selectedItem property of thesubscriptionsList control isn’t null.

<mx:Button label="Selected" click="refreshSelected(event)"
enabled="{subscriptionsList.selectedItem !== null}"/>

Create the refreshSelected() function, which needs to cast the selectedItem property of the subscriptionsList control as a Subscription object and then call its refresh method:

private function refreshAll(event:Event):void {
for each (var subscription:Subscription in _subscriptions) {
subscription.refresh();
}
}

private function refreshSelected(event:Event):void {
(subscriptionsList.selectedItem as Subscription).refresh();
}

If you test your application, you should be able to refresh all subscriptions or just the selected feeds using the buttons you’ve just added, which are shown in Figure 14-21.

image

Figure 14-21. The Selected and All buttons added to the Subscriptions panel

You’ll be glad to know that you’re now finished with the Subscriptions panel. However, you could make some improvements, as I’ll suggest at the end of the chapter.

Completing the Articles and Article Panels

The hard work is complete. Now you just need to add some finishing touches to the Articles and Article panels.

Finishing the Articles Panel

The Articles panel is actually very nearly complete. If you look at the original UI design (refer to Figure 14-1), all that’s missing is a label showing when the feed was last checked and a button to take users to the website for the feed.

However, before you get to those items, you have a little housework to do. The display formats for the dates in the application are not what you would call human-friendly. To make the dates prettier, you need to create a DateFormatter object and use this to render all the dates in the application.

Add a new private DateFormatter variable to the IrisReader.mxml file and initialize this variable in the init method to use DD MMM YYYY HH:MM as its format. This will display dates in a format such as 25 Dec 2007 18:47.

...
import flash.utils.Timer;
import flash.events.TimerEvent;
import mx.formatters.DateFormatter;

[Bindable]
private var _subscriptions:ArrayCollection;
private var _refreshTimer:Timer;
private var _dateFormatter:DateFormatter;

private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
_loadData();

_dateFormatter = new DateFormatter();
_dateFormatter.formatString = "DD MMM YYYY HH:NN";

_refreshTimer = new Timer(5 * 60 * 1000);
_refreshTimer.addEventListenerimage
(TimerEvent.TIMER, onRefreshTimer);
_refreshTimer.start();
}
...

Now create a new private function that accepts a string and returns that string formatted using your DateFormatter instance:

...
private function onCreationComplete(event:FlexEvent):void {
_subscriptions = new ArrayCollection();
_loadData();

_dateFormatter = new DateFormatter();
_dateFormatter.formatString = "DD MMM YYYY HH:NN";

_refreshTimer = new Timer(5 * 60 * 1000);
_refreshTimer.addEventListenerimage
(TimerEvent.TIMER, onRefreshTimer);
_refreshTimer.start();
}

private function formatDate(date:String):String {
return _dateFormatter.format(date);
}
...

Add a ControlBar container to the Articles panel containing a Label and a Text control. Bind the text property of the Text control to the lastChecked property of the currently selected Subscription object in the subscriptionsList control, formatting it by callingyour new formatDate() method.

...
<mx:Panel title="{subscriptionsList.selectedItem.title}"
width="100%" height="50%">
<mx:DataGrid id="articlesGrid" width="100%" height="100%"
dataProvider="{subscriptionsList.selectedItem.articles}" >
<mx:columns>
<mx:DataGridColumn dataField="date" headerText="Date"/>
<mx:DataGridColumn dataField="title" headerText="Title"/>
</mx:columns>
</mx:DataGrid>
<mx:ControlBar>
<mx:Label text="Last checked:"/>
<mx:Text text= image
"{formatDate(subscriptionsList.selectedItem.lastChecked)}"/>
</mx:ControlBar>
</mx:Panel>
...

That takes care of the last-checked date display and format, as shown in Figure 14-22, but what about the values in the Date column of the articlesGrid control? You can use a custom label function to render these dates using the same DateFormatter object.

image

Figure 14-22. The last-checked date properly formatted

Set the labelFunction property of the DataGridColumn instance representing the Date column of the articlesGrid control to renderArticlesGridDate(). While you’re there, set the width of the column to 150, because you won’t need so much space for the date once it has been property formatted.

...
<mx:DataGrid id="articlesGrid" width="100%" height="100%"
dataProvider="{subscriptionsList.selectedItem.articles}">
<mx:columns>
<mx:DataGridColumn dataField="date" headerText="Date"
labelFunction="renderArticlesGridDate" width="150"/>
<mx:DataGridColumn dataField="title" headerText="Title"/>
</mx:columns>
</mx:DataGrid>
...

Now create the renderArticlesGridDate() function to return the formatted date. Label-renderer functions for DataGrid controls receive references to the item currently being rendered and the DataGridColumn in question. In this case. the current item will be an IItemobject from the com.adobe.xml.syndication.generic.IItem package, which has a date property that you need to format. You need to feed this date to the format method of your DateFormatter object and return the resultant string.

...
import flash.events.TimerEvent;
import mx.formatters.DateFormatter;
import com.adobe.xml.syndication.generic.IItem;
...
private function formatDate(date:String):String {
return _dateFormatter.format(date);
}

private function renderArticlesGridDate(
item:IItem,
column:DataGridColumn
):String {
return _dateFormatter.format(item.date);
}
...

Test the application again to see the prettier dates in all their glory, as shown in Figure 14-23.

image

Figure 14-23. The Articles panel shows formatted dates

Now that you have pretty dates all round, you just need to add the Visit site button. In the ControlBar for the Articles panel, add a 100% width Spacer and a Button control with a label of "Visit site" and click handler of visitSelectedSite(). Bind the enabledproperty of the button using a data-binding expression to check that there is something selected in the subscriptionsList control.

...
<mx:ControlBar>
<mx:Label text="Last checked:"/>
<mx:Text text= image
"{formatDate(subscriptionsList.selectedItem.lastChecked)}"/>
<mx:Spacer width="100%"/>
<mx:Button label="Visit site"
click="visitSelectedSite(event)"
enabled="{subscriptionsList.selectedItem !== null}"/>
</mx:ControlBar>
...

Now create the visitSelectedSite() function, which should use the url property of the selected Subscription object in the subscriptionsList control to open a new window to the site using the built-in navigateToURL() function:

...
private function refreshSelected(event:Event):void {
(subscriptionsList.selectedItem as Subscription).refresh();
}

private function visitSelectedSite(event:Event):void {
var subscription:Subscription = image
(subscriptionsList.selectedItem as Subscription);
navigateToURL(new URLRequest(subscription.url));
}
...

That’s all there is to it. If you test the project now, you’ll see the Visit site button, as shown in Figure 14-24. You can click it to visit the site referenced in the metadata of your subscribed feeds.

image

Figure 14-24. The Visit site button added to the Articles panel

With that, you’ve finished your work on the Articles panel and can turn your attention to the Article panel.

Finishing the Article Panel

As with the Articles panel, the Article panel needs very little to satisfy the application’s requirements. It’s missing a label to show the date of the currently selected item in the Articles panel and a button to take users to the URL associated with the item so they can read the rest of the article.

Let’s start with the article date. Add a ControlBar to the Articles panel in the IrisReader.mxml file, containing a Label and a Text control. Bind the text property of the Text control to the date property of the currently selected item in the articlesGrid control, passing it through the DateFormatter object so it’s nice and pretty:

...
<mx:Panel title="{articlesGrid.selectedItem.title}"
width="100%" height="50%">
<mx:TextArea id="excerptTextArea" editable="false"
width="100%" height="100%"
htmlText="{articlesGrid.selectedItem.excerpt.value}" />
<mx:ControlBar>
<mx:Label text="Posted:"/>
<mx:Text text= image
"{formatDate(articlesGrid.selectedItem.date)}"/>
</mx:ControlBar>
</mx:Panel>
...

Figure 14-25 shows the date added to the Article panel.

image

Figure 14-25. The date appears at the bottom of the Article panel

Well, that was easy. Now you just need to add in and wire up the Read more button.

In the ControlBar for the Article panel, add a 100% width Spacer and a Button control with a label of "Read more" and click handler of readSelectedArticle(). Bind the enabled property of the button using a data-binding expression to check that there is something selected in the articlesGrid control.

...
<mx:ControlBar>
<mx:Label text="Posted:"/>
<mx:Text text= image
"{formatDate(articlesGrid.selectedItem.date)}"/>
<mx:Spacer width="100%"/>
<mx:Button label="Read more"
click="readSelectedArticle(event)"
enabled="{articlesGrid.selectedItem !== null}"/>
</mx:ControlBar>
...

Now create the readSelectedArticle() function, which should use the link property of the selected IItem object in the articlesGrid control to open a new window to the site using the built-in ­navigateToURL() function:

...
private function visitSelectedSite(event:Event):void {
var subscription:Subscription = image
(subscriptionsList.selectedItem as Subscription);
navigateToURL(new URLRequest(subscription.url));
}

private function readSelectedArticle(event:Event):void {
var item:IItem = (articlesGrid.selectedItem as IItem);
navigateToURL(new URLRequest(item.link));
}

And now you can relax. You have finally nailed down the final feature of our feed reader application, as shown in Figure 14-26. Save your changes, run the project, and bask in the reflective glow of your achievement.

image

Figure 14-26. The feed reader application with all its features

Improving the Feed Reader

While putting this chapter together, I made a conscious decision to omit some features that you would want to have in a high-quality feed reader application. That’s not to say that the application you’ve built is useless. In fact, I’m using this application every day to keep an eye on the feeds that I subscribe to in a simple and unobtrusive way. However, the application would be even more useful if it had some of the following features:

· User-specified refresh interval: Currently, the feeds are refreshed every five minutes. This may be too frequent (or not frequent enough) for some users, so allowing them to specify their own interval (using a Slider or a NumberStepper, for example) would be a good idea. You would need to save their interval along with the subscriptions list in the local SharedObject; otherwise, the users would need to reset this value every time they started the application, which wouldn’t be ideal.

· Control columns in the panel: The Articles panel could contain more information about an item. Some extra fields may be important to users, so allowing them to specify which fields are visible (and in what order) would be a good idea.

· The ability to organize subscriptions into folders: Subscriptions can get a bit overwhelming if you’ve subscribed to hundreds of feeds, so the ability to add folders to the subscriptions list would be ideal. This would involve swapping the List control for a Treecontrol, and you would need to tweak how you get the data into it in the first place.

· Reading list support: You could add the ability to read Outline Processor Markup Language (OPML) files, which contain lists of feeds supplied by someone else. This would involve extending the Adobe XML syndication library to add support for OPML files. OPML files can contain folders, so the feature suggested in the previous item would be good to go with this one.

Summary

I hope that this chapter has been a bit of an eye-opener about the power of Flex framework when coupled with your own ActionScript for rapidly building high-quality, complex web applications. We started from the conceptualization stage and went all the way through full implementation, utilizing the built-in UI, utility, and management classes in Flex to wire together a set of common widgets to display remote data in an intuitive, straightforward manner that is both easy to use and aesthetically pleasing.

Flex in itself is a fantastic tool, but it can go only so far. It takes extra knowledge of the underlying ActionScript 3.0 language to truly bring out its full potential. The topics presented in this book—from the basic constructs of ActionScript all the way through event handling, graphic creation, and remote communication—can all be applied when developing Flex applications.

And remember that the Flex framework is built on ActionScript. The source code is available to peruse to gain insight into how some of its powerful features were implemented. Use it to learn more. Build your own classes that suit your own needs. The beauty of ActionScript is that it can be used to accomplish so many varied tasks, ranging from scripting animation, to building games, to creating full-blown e-commerce and social media sites.

Take advantage of ActionScript’s potential and create what you want to create.