Sponsored Link •
|
Summary
As Joe Nuxoll of JBuilder and JavaPosse fame will tell you (given the slightest provocation), one place where the Java designers completely dropped the ball is in Java's component model. This becomes especially clear when comparing it with a system like Flex which has full language support for components.
Advertisement
|
Not only does Flex have built-in support for properties and events, it includes a higher-level abstraction language (MXML) for laying out and configuring components, which greatly simplifies the development process. You can even create new components directly in MXML, although that's more commonly done using ActionScript. This article shows how to create components in Flex and how clean the resulting applications can be that use those components.
You'll see that ActionScript is quite similar to Java in many ways, but I hope you'll also see that ActionScript includes time-saving features that don't appear in Java.
Although you can write all your Flex code by hand and compile it using mxmlc (the free command-line compiler), it's much easier to use FlexBuilder which is built on top of Eclipse and has command completion, context help, built-in debugging and more. Some of the examples in this article use AIR, a beta feature that allows you to create desktop applications that access files and other aspects of your local machine (rather than being limited to the web sandbox). You can download a beta of FlexBuilder including AIR support here. (These examples can also be built using the command-line compiler).
The easiest way to create a component is using MXML. First, create a new application: from the FlexBuilder menu, choose File | New | Flex Project, and fill out the wizard. Then, from the FlexBuilder menu, choose File | New | MXML Component; this produces a little wizard that guides you through the process, including the choice of a base component to inherit from. I chose "Button" and called my new component "RedButton." The result is a file called RedButton.mxml (the name of the file determines the name of the component) containing the following:
<?xml version="1.0" encoding="utf-8"?> <mx:Button xmlns:mx="http://www.adobe.com/2006/mxml"> </mx:Button>
This new component is based on Button and can be used in an application like this:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*"> <local:RedButton label="Not so Red"/> </mx:Application>
All the original capabilities of a button (including the ability to set a label, used here) are preserved.
We can set the background color of the button to make the name of the component honest, add a tooltip and a default click action:
<?xml version="1.0" encoding="utf-8"?> <mx:Button xmlns:mx="http://www.adobe.com/2006/mxml" fillColors="['red', 'blue']" toolTip="A Red Button" click="clicked()"> <mx:Script> <![CDATA[ private function clicked():void { label = "Quite Red"; setStyle("fillColors", ['red', 'red']); } ]]> </mx:Script> </mx:Button>
The clicked() function is defined inside a Script tag, within a CDATA block (inserted automatically by FlexBuilder) because you're in XML. Note that the function definition includes a return type, void -- optional static typing is an Actionscript feature that allows FlexBuilder to do a better job of context help and command completion.
You can continue to add more sophistication to components this way, but MXML components are best used for simple things. More complex MXML components can become tedious and you're better off creating them directly in ActionScript (the compiler turns MXML into ActionScript anyway).
FlexBuilder also has a wizard to help you create ActionScript components, found by selecting File | New | ActionScript Class. Because all classes must be in packages (like Java), you have the option of selecting a package name (if you don't, you get the default unnamed package). The wizard also allows you to select a superclass. In the following example I've told the wizard to automatically generate the framework for the constructor:
package SimpleComponents { import mx.controls.Button; public class ASRedButton extends Button { public function ASRedButton() { super(); } } }
The import statement was automatically created by FlexBuilder, and the file name is ASRedButton.as. Although the component is created in ActionScript, it is available as MXML:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*" xmlns:SimpleComponents="SimpleComponents.*"> <SimpleComponents:ASRedButton label="Not so Red"/> </mx:Application>
When I began typing ASRedButton in FlexBuilder, it performed completion on the name, then added the XML namespace before the tag, and also included the xmlns attribute in the Application tag. Even if you ultimately plan to use the free command-line compiler, it's worth starting with the free download of FlexBuilder just to get initial help with details like this.
With ActionScript it's easier to create more sophisticated components. We can give ASRedButton the same behavior as RedButton:
package SimpleComponents { import mx.controls.Button; import flash.events.MouseEvent; public class ASRedButton extends Button { public function ASRedButton() { super(); label = "Reddish"; setStyle("fillColors", ['red', 'blue']); toolTip="A Red Button"; } override protected function clickHandler(event:MouseEvent):void { label = "Quite Red"; setStyle("fillColors", ['red', 'red']); } } }
There are a number of ways to respond to events, but in an ActionScript component the easiest way is usually just to override a method. Note that the override keyword ensures that you don't accidentally create a new method (which would not produce the desired result).
The import statements were automatically included by FlexBuilder.
You can create an attribute that is readable and writable inside MXML by simply creating a public field, but ActionScript also contains get and set keywords to indicate methods for reading and writing a property. Here's a Label subclass where you see both approaches:
package SimpleComponents { import mx.controls.Label; import flash.events.Event; public class MyLabel extends Label { public var labelStates:Array = ["Ontological", "Epistemological", "Ideological"]; private var state:uint = 0; // unsigned integer public function set textValue(newValue:String):void { text = newValue; } public function get textValue():String { return text; } public function onClick(event:Event = null):void { text = labelStates[state++ % labelStates.length]; } } }
Here you see the use of optional static typing on labelStates, which is an Array. There are a number of ways to create and populate an Array, the simplest being the use of square brackets. Arrays are not typed; they simply hold Objects. However, you are not forced to downcast when pulling items out of an Array if the dynamic typing mechanism can handle the result.
labelStates is public, so it can be accessed by setting an MXML attribute. textValue has both a get and set method, making it a property and allowing you to execute code when that property is changed; here I've just assigned it to the text field for demonstration purposes.
I've also added the onClick() method which cycles the text field through the elements in the labelStates array. onClick() also accepts an event argument, which defaults to null so that it's optional -- this allows it to be used as an event listener, as you'll see shortly. Here's an example that makes use of the component:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:SimpleComponents="SimpleComponents.*" > <SimpleComponents:MyLabel fontSize="24" id="display" textValue="Hello, World!" click="display.onClick()" /> <mx:Button click="if(display.labelStates.indexOf('Logical') == -1) display.labelStates.push('Logical')" /> </mx:Application>
Note that I still have access to properties like fontSize in MyLabel.
The assignment to textValue results in "Hello, World!" appearing as the label text. In order for click to be assigned to onClick(), the MyLabel instance must have an object identifier, so is given an id of display.
The code assigned to click in the Button shows that you aren't required to just reference a function; you can define the code inline (although this can rapidly become messy). Here we add 'Logical' to the list if it isn't already there.
Everything in Flash is event-based, and as a programmer you can hook into either the framework-generated events or user-generated events. If you want to insert some code at a particular point in the life cycle of an application, you find the appropriate framework event, such as creationComplete, which happens after the application has been constructed (there are many other framework events which you can look up in the online help system).
As a simple example, we can drive MyLabel using a timer:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:SimpleComponents="SimpleComponents.*" creationComplete="init()"> <SimpleComponents:MyLabel fontSize="24" id="display" textValue="Hello, World!" click="timer.stop()" /> <mx:Script> <![CDATA[ private var timer:Timer = new Timer(1000); private function init():void { timer.addEventListener(TimerEvent.TIMER, display.onClick); timer.start(); } ]]> </mx:Script> </mx:Application>
This demonstrates three different events:
In general, event handling is exactly this simple; note the succinctness of code when compared with Java event handling.
Flex also provides data binding, which allows one component to respond to a change in the data of another component. This was seen as a common activity which would ordinarily require a lot of event-handling code, so the Flex designers decided to do the work for the programmer. Here's a counter that uses data binding:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()"> <mx:Label fontSize="24" text="{count}"/> <mx:Script> <![CDATA[ private var timer:Timer = new Timer(1000); [Bindable] public var count:uint = 0; private function init():void { timer.addEventListener(TimerEvent.TIMER, function(event:Event):void {count++}); timer.start(); } ]]> </mx:Script> </mx:Application>
In text="{count}", the curly braces bind the text field to the count var, which has been modified with the [Bindable] annotation so that the compiler wires up all the necessary event generation and handling for you. Every time the timer fires, count changes and the text field in the Label changes in response.
This example also shows the use of an inline function (a.k.a. a lambda) as the target of the event listener.
An aside: when you create inline code inside MXML like this, the compiler actually creates a class to hold the code, and an instance of that class. Although this is convenient for small blocks of code it can become confusing if you try to make that code too large or complex, in which case you should move it to a separate ActionScript file.
I'll finish with a more sophisticated component that will also demonstrate the use of AIR to read local files from the disk. In addition, we'll explore more ActionScript syntax including regular expressions.
This component was created to support the Flex Jams; in particular, for people who are using Flex for the first time and need step-by-step instructions. Programmed Learning is a fairly old practice; it focuses on learning through exercises and, instead of giving you the answer all at once, it guides you through by giving a series of hints and answers so that you get the sense of discovery and learning at every step.
Flex includes the perfect component for programmed learning, the Accordion. This contains a set of sliding "windows" so that you can move each one up and expose more information as you go. Because I'm creating lots of exercises (and undoubtedly making lots of changes), it's far too much work to set them up as static MXML files; instead, I'll inherit a new component from Accordion and teach it to build itself by reading text files (which is where AIR comes in).
First, I've created a little language that allows me to describe the exercise, hints, intermediate solutions and steps. Any line that begins with a '.' contains a command. Here's an example, Exercise1.txt in the directory 1. Basics:
.ex Install Flexbuilder. Test it by creating "Hello world" in both AIR and Flex. .h1 Go to http://labs.adobe.com/ to get the public beta of FlexBuilder 3 (This includes support for AIR). .h2 Select File|New|Flex Project and follow the instructions to create a new Flex app. The second page allows you to specify either Flex or AIR. .h3 Place a Text component in the application. Type a '<' and begin typing "Text" and you'll see the context help pop up the possibilities. Choose and press Return to insert it, and close the tag with />. .s3 <mx:Text /> .h4 Now set the text property to "Hello, World!". With your cursor inside the tag, begin typing "text" and use command completion. .s4 <mx:Text text="Hello, World!" /> .h5 You may also want to choose the Design button that you'll see in the application area, then click and drag a Text component to add it to your application Panel. Use Flex Properties to add text "Hello World." If you use design mode, be sure to switch back to Source mode and look at the generated MXML for your program. .final <?xml version="1.0" encoding="utf-8"?> <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" > <mx:Text text="Hello, World!" /> </mx:WindowedApplication>
The .ex tag indicates the exercise description. Anything starting with .h is a hint followed by a hint number. A .s is an intermediate hint solution, which also has a number. The .final tag indicates the completed solution.
Although I could certainly have written a Python program to translate this language into something (like XML) that would be easy for the Flex app to consume, the intermediate step would have reduced the interactivity of the solution. In the ExercisePresenter component, I use AIR to find and read the text file, and regular expressions to break it up into directives:
package ProgrammedLearning { import mx.containers.Accordion; import mx.containers.VBox; import mx.controls.TextArea; import mx.controls.Alert; import flash.filesystem.File; import flash.filesystem.FileStream; import flash.filesystem.FileMode; import flash.net.FileFilter; import flash.events.Event; public class ExercisePresenter extends Accordion { public var dataDirectory:String; public var chapter:String; public var fileName:String; private var file:File = File.documentsDirectory; private var pathsResolved:Boolean = false; // Called when properties are set: protected override function commitProperties():void { super.commitProperties(); // Prevent multiple calls: if(!pathsResolved) { pathsResolved = true; resolvePaths([dataDirectory, chapter, fileName]); } } private function resolvePaths(paths:Array):void { for each(var path:String in paths) { var successfullyResolved:File = file; // Store as far as we've gotten file = file.resolve(path); if(file.exists) continue; else { // Files installed elsewhere var exFilter:FileFilter = new FileFilter("Exercise", "Exercise*.txt"); file = successfullyResolved; file.browseForOpen("File Not Found; Select File to Open", [exFilter]); file.addEventListener(Event.SELECT, parseFile); } return; // Wait for user to choose new path } parseFile(); } private function parseFile(event:Event=null): void { var stream:FileStream = new FileStream(); stream.open(file, FileMode.READ); var str:String = stream.readUTFBytes(stream.bytesAvailable); stream.close(); var entries:Array = []; // Each entry is delimited by a '.' at the start of a line: for each(var s:String in str.split(new RegExp("\\n\\.", "s"))) if(s.length > 0) entries.push(s); for each(s in entries) { // Split on first newline: // (Could do this with a regular expression, too!) var brk:uint = s.indexOf("\n"); var tag:String = s.substring(0, brk); var contents:String = s.substr(brk); // Strip leading newlines: while(contents.charAt(0) == '\n') contents = contents.substr(1); switch(tag.charAt(0)) { case 'e': addStep("Exercise", contents); break; case 'h': addStep("Hint " + tag.substr(1), contents); break; case 's': addStep("Hint solution " + tag.substr(1), contents); break case 'f': addStep("Completed solution", contents); break; default: } } } public function addStep(label:String, contents:String):void { // Create a new "fold" in the Accordion: var vbox:VBox = new VBox(); vbox.percentHeight = vbox.percentWidth = 100; vbox.label = label; var text:TextArea = new TextArea(); text.percentHeight = text.percentWidth = 100; text.text = contents; vbox.addChild(text); addChild(vbox); // Add to self (Accordion object) } } }
The dataDirectory, chapter, and fileName fields tell the component where to find the example. Because these are public fields and we need to wait until they are set before trying to use them (otherwise they will contain incorrect data), we override the commitProperties() method which is automatically called by the framework. commitProperties() is typically called more than once, so the standard practice is to use a flag to keep track of whether you have performed your task; in this case we only want to call resolvePaths() once.
resolvePaths() iterates through an array of sequential directories and verifies that each one is correct. If it gets all the way through the array, it calls parseFile() directly, but if it fails -- which means the path information is incorrect, possibly because the user hasn't installed the files in the expected location -- it allows the user to choose the directories and the file. browseForOpen() opens a file browser window in the native OS; note that the last successfully found directory is kept and used so the user doesn't have to start from scratch.
Notice that calling browseForOpen() is almost like starting a separate thread (although Flex and Flash don't support programmer threads -- a good thing, since thread programming is virtually impossible to get right -- the new betas of the Flash VM use threads internally for speed and utilization of multiple cores). Once the user selects the new file, we still want to call parseFile(). This is accomplished by setting up an event listener, which effectively establishes a callback. You see this kind of programming -- passing control, then using an event listener as a callback -- quite a bit in Flex code, especially in network programming.
The first few lines of parseFile() show the standard way to open and read a text file, which only works in an AIR application. Note that it looks slightly similar to Java code but is less verbose because the decorator pattern is not (mis)used as it is in Java.
Once the file is read, a regular expression breaks it into "entries," each of which includes a dot command and the text that follows it (blank entries are discarded). Note that the regular expression uses similar syntax as Java does, in particular the double backslash when you actually want a single backslash. Discovering how to use ActionScript regular expressions took me a bit of time because the documentation and examples are lacking (or at least, I couldn't find them); you might have more luck using the regular expression section in the Strings chapter of Thinking in Java.
Each entry is broken into its dot command and body text, then a switch statement adds each step as a window in the Accordion component by calling addStep().
To test the component, we can create and configure it in MXML:
<?xml version="1.0" encoding="utf-8"?> <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:ProgrammedLearning="ProgrammedLearning.*"> <ProgrammedLearning:ExercisePresenter width="100%" height="100%" dataDirectory="FlexJamProgrammedLearning" chapter="1. Basics" fileName="Exercise1.txt"/> </mx:WindowedApplication>
However, this isn't the whole solution. The enclosing application (which I will eventually write) will construct a main page consisting of all the exercises, each of which you can select to bring up the programmed learning accordion. To accomplish this, I must be able to manipulate the component at runtime -- which I can, through both my own design choices and the structure of Flex:
package ProgrammedLearning { public class DynamicTest extends ExercisePresenter { function DynamicTest() { percentHeight = percentWidth = 100; dataDirectory = "FlexJamProgrammedLearning"; chapter = "1. Basics"; fileName = "Exercise1.txt"; } } }
One thing I'd really like to see in a future version of Flex is better string manipulation tools. A perfect model for this is the Python string library, which is tried and tested, and you even have the source code so creating an ActionScript string library is a matter of translation.
Have an opinion? Readers have already posted 29 comments about this weblog entry. Why not add yours?
If you'd like to be notified whenever Bruce Eckel adds a new entry to his weblog, subscribe to his RSS feed.
Bruce Eckel (www.BruceEckel.com) provides development assistance in Python with user interfaces in Flex. He is the author of Thinking in Java (Prentice-Hall, 1998, 2nd Edition, 2000, 3rd Edition, 2003, 4th Edition, 2005), the Hands-On Java Seminar CD ROM (available on the Web site), Thinking in C++ (PH 1995; 2nd edition 2000, Volume 2 with Chuck Allison, 2003), C++ Inside & Out (Osborne/McGraw-Hill 1993), among others. He's given hundreds of presentations throughout the world, published over 150 articles in numerous magazines, was a founding member of the ANSI/ISO C++ committee and speaks regularly at conferences. |
Sponsored Links
|