AS3: Button with Text and Basic Styling

It’s been a while since I had the opportunity to work on a pure AS3 project. I recently had a need for a simple AS3 button that displays text and decided to customize the SimpleButton control. As I quickly discovered, the SimpleButton control isn’t really setup to support text or a lot of other features, and hacking these features into it doesn’t make much sense. So I built a quick little class that emulates the behavior of SimpleButton but could display text and offer a few other features for styling the button dynamically.



SimpleButtonProject.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package
{
    import com.components.CustomButton;
    import flash.display.Sprite;
    import flash.events.MouseEvent;
   
    [SWF( width = '200', height = '200', backgroundColor = '#FFFFFF', frameRate = '20')]
    public class SimpleButtonProject extends Sprite
    {
        private var button:CustomButton;
       
        public function SimpleButtonProject()
        {
            button = new CustomButton("Welcome");
            button.x = 0;
            button.addEventListener(MouseEvent.CLICK, handleMouseClick);
            addChild(button);
        }
       
        private function handleMouseClick(event:MouseEvent):void
        {
            button.label = "Clicked"
           
            /*
                 // remove the button and mark it for garbage collection
                 button.removeEventListener(MouseEvent.CLICK, handleMouseClick);
                 this.removeChild(button);
                 button = null;
            */

        }
    }
}

CustomButton.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package com.components
{
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFormat;
    import flash.text.TextFormatAlign;
   
    public class CustomButton extends Sprite
    {
        // ---- Properties -----
       
        protected var __button:CustomSimpleButton;
        protected var __txtFormat:TextFormat;
        protected var __txtField:TextField;
        protected var __background:Shape;
        protected var __hitarea:Sprite;
       
        protected var _label:String = "";
        protected var _font:String = "Arial";
        protected var _fontSize:int = 12;
        protected var _fontColor:uint = 0x000000;
        protected var _upColor:uint = 0xFFCC00;
        protected var _overColor:uint = 0xCCFF00;
        protected var _downColor:uint = 0x00CCFF;
        protected var _buttonWidth:int;
        protected var _buttonHeight:int;
        protected var _buttonStrokeColor:int = 0x000000;
       
        public function get label():String
        {
            return _label;
        }
        public function set label(value:String):void
        {
            _label = value;
            updateDisplayList();
        }
       
        public function get buttonStrokeColor():uint
        {
            return _buttonStrokeColor;
        }
        public function set buttonStrokeColor(value:uint):void
        {
            _buttonStrokeColor = value;
            updateDisplayList();
        }
       
        public function get font():String
        {
            return _font;
        }
        public function set font(value:String):void
        {
            _font = value;
            updateDisplayList();
        }
       
        public function get fontSize():int
        {
            return _fontSize;
        }
        public function set fontSize(value:int):void
        {
            _fontSize= value;
            updateDisplayList();
        }
       
        public function get fontColor():uint
        {
            return _fontColor;
        }
        public function set fontColor(value:uint):void
        {
            _fontColor= value;
            updateDisplayList();
        }
       
        public function get upColor():uint
        {
            return _upColor;
        }
        public function set upColor(value:uint):void
        {
            _upColor = value;
            updateDisplayList();
        }
       
        public function get overColor():uint
        {
            return _overColor;
        }
        public function set overColor(value:uint):void
        {
            _overColor = value;
        }
       
        public function get downColor():uint
        {
            return _downColor;
        }
        public function set downColor(value:uint):void
        {
            _downColor = value;
        }
       
        // ---- Constructor -----
       
        public function CustomButton(label:String = "", w:int = 80, h:int = 22)
        {
            super();
           
            if(label != "") this.label = label;
           
            this.addEventListener(Event.REMOVED_FROM_STAGE, destroy);
           
            _buttonWidth = w;
            _buttonHeight = h;
           
            createChildren();
            updateDisplayList();
        }
       
        // ---- Public Methods -----
       
        public function destroy(event:Event = null):void
        {
            this.removeEventListener(Event.REMOVED_FROM_STAGE, destroy);
           
            if(__background) {
                __background.graphics.clear();
                __background = null;
            }
           
            if(__hitarea){
                __hitarea.removeEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
                __hitarea.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
                __hitarea.removeEventListener(MouseEvent.MOUSE_UP, onMouseOver);
                __hitarea.removeEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
                __hitarea.graphics.clear();
                __hitarea = null;
            }
           
            if(__txtField) {
                __txtField.text = "";
                __txtField = null;
                __txtFormat = null;
            }
        }
       
        // ---- Protected Methods -----
       
        protected function createChildren():void
        {
            if(!__background) {
                __background = new Shape();
                addChild(__background);
            }
           
            if(!__txtField) {
                __txtField = new TextField();
                __txtField.selectable = false
                addChild(__txtField);
            }
           
            if(!__hitarea) {
                __hitarea = new Sprite();
                __hitarea.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver);
                __hitarea.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
                __hitarea.addEventListener(MouseEvent.MOUSE_UP, onMouseOver);
                __hitarea.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
                addChild(__hitarea);
            }
        }
       
        protected function drawButtonBackground(color:uint):void
        {
            if(__background){
                __background.graphics.beginFill(color);
                __background.graphics.lineStyle(1, buttonStrokeColor);
                __background.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
                __background.graphics.endFill();
            }
        }
       
        protected function updateDisplayList():void
        {
            if(__txtField){
                __txtFormat = new TextFormat(font, fontSize, fontColor, false, null, null, null, null, TextFormatAlign.CENTER);
                __txtField.width = _buttonWidth;
                __txtField.defaultTextFormat = __txtFormat;
                __txtField.text = _label;
                __txtField.y = (_buttonHeight - __txtField.textHeight)/2;
            }
           
            if(__background) {
                drawButtonBackground(upColor);
            }
           
            if(__hitarea){
                __hitarea.graphics.beginFill(0x000000, 0);
                __hitarea.graphics.drawRect(0, 0, _buttonWidth, _buttonHeight);
                __hitarea.graphics.endFill();
            }
        }
       
        protected function onMouseOver(e:Event):void
        {
            e.stopPropagation();
            drawButtonBackground(overColor);
        }
       
        protected function onMouseDown(e:Event):void
        {
            e.stopPropagation();
            drawButtonBackground(downColor);
        }
       
        protected function onMouseUp(e:Event):void
        {
            e.stopPropagation();
            drawButtonBackground(overColor);
        }
       
        protected function onMouseOut(e:Event):void
        {
            e.stopPropagation();
            drawButtonBackground(upColor);
        }
    }
}

So there you have it, a very simple button that displays text and has a few other features. I realize its not so simple ( # lines of code), but I added a lot of public properties to change the style and label dynamically, as well as logic for garbage collection. If you don’t need those extra features, you can easily be customize it to fit any your needs.

-Mister

Posted in AS3, Flash | Tagged , , , | Leave a comment

AIR for Android: Phoenix Traffic Application

I developed a simple Flex 4 application targeting AIR for Android. For this application, I used Flex to build a somewhat smaller version of a previous application that uses XML data to display a list Phoenix traffic cameras locations and images. The images are updated on an set interval and locations are selected from a list of freeways and intersections for each traffic camera.

Initially, I built the application in Flash, but ran into some issues and annoyances with creating a proper list that worked with the Multitouch events for capturing user gestures on multitouch devices like Android.

The Flex 4 list has support for TouchEvent. However, it’s still difficult to differentiate between the different TouchEvent’s being fired (TouchEvent.TOUCH_BEGIN, TouchEvent.TOUCH_MOVE, TouchEvent.TOUCH_END, TouchEvent.TAP). As a result, the list has a tendency to select items while scrolling the list.

Here are some screen shots of the application running on my NexusOne:

The Flex preloader was replaced with a simple splash screen graphic.

Simple list of freeways.

Once you select a freeway, you see list of camera locations for that freeway.

Selecting a camera takes displays the live camera image accompanied by two still images for direction.

This was my first run at doing Android and I can see some potential. Deploying AIR application from Flex 4 to your Android device is pretty easy, though nothing like the experience publishing from Flash CS5. The applications tend to be a little robust, hopefully this will be resolved with the release of the Halo components which are designed specifically for mobile devices.

To build your own AIR for Android applications, you need an Android device running Android 2.2 which supports Flash Player 10.1 and AIR. you also need to sign up for the AIR for Android Prerelease program. There are some good example applications with code starting to appear, including a scrolling list. Here are the posts I referenced used building this application:

Flex 4 List Scrolling on Android with Flash Player 10.1
“VoiceNotes for Android”: Sample App using Flex, AIR, and the Microphone API
AIR on Android: TweetrApp Video walk-through
Employee Directory Sample Application Using Flex and AIR for Android – Updated for Froyo
Flex 4 Application Handling Touch Events on Android with Flash Player 10.1

- Mister

Posted in AIR 2, Android, Flex, Flex 4, Flex Builder 4 | Tagged , , , , , | Leave a comment

Blog Refresh

I just moved my blog from my previous web host to a new one, expect some bumps along the way along with broken links and examples. I am slowly working through fixing them, but feel free to report them to me if you find them.

What prompted me to move my blog was price for one, but also having WordPress installed by the host as opposed to me installing my own and being responsible for upgrades. I was starting to have issues with posting new messages and getting plug-ins to work properly.

I can say this though, migrating your MySQL database and all the custom parts of my blog were a huge pain and time suck. I couldn’t recommend it to anyone, much better to find a host that already has the WordPress application.

-Mister

Posted in Flex | Tagged | Leave a comment

Screen Capture with AIR 2 NativeProcess

Prior to AIR 2 the only way to capture the screen was to use something like Marapi Java Bridge that acts as a conduit between your application and the native system. However, AIR 2 now lets us call

With AIR 2 Beta and the upcoming AIR 2 release, you can now use the command-line tool “screencapture” that comes with Mac OS. Nothing extra needs to be bundled with the application for this to work on a Mac, but on Windows, you need an additional piece of code to execute the screen capture process.

The process of doing a screen capture is pretty easy. You first create a File object that points to the location of the “screencapture” command line on the Mac:

1
2
var file:File = File.applicationDirectory;
file = file.resolvePath("usr/sbin/screencapture");

Then create a NativeProcessStartupInfo object that will be used when we start the NativeProcess.The arguments property of the NativeProcessStartupInfo takes a list of arguments that will be passed to the command line tool when its started. For a complete list of arguments used by the screencapture command line tool, type “screencapture –help” in the terminal window. For this example I wanted to push out a file named “screencap.png” to the desktop from a selection of the screen. Optionally, you could capture the image to the clipboard and paste it directly.

1
2
3
4
5
6
7
var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
var args:Vector. = new Vector.();
args[0] = "-i";
args[1] = "screencapture.png";

nativeProcessStartupInfo.arguments = args;
nativeProcessStartupInfo.executable = file;

Now we can start the native process:

1
2
process = new NativeProcess();
process.start(nativeProcessStartupInfo);

The screencapture command tool will be launched and you will see the selection cursor on your screen. After you make a selection, the file will automatically be saved to the desktop as “screencapture.png”.

Here is the complete project code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" creationComplete="init()">

<![CDATA[

private var process:NativeProcess;

private function init():void
{
launchNativeProcess();
}

private function launchNativeProcess():void
{
if(NativeProcess.isSupported) {

var file:File = File.applicationDirectory;

if (Capabilities.os.toLowerCase().indexOf("win") > -1) {
//file = file.resolvePath("bin/mylocalexe.exe");
} else if (Capabilities.os.toLowerCase().indexOf("mac") > -1) {
file = file.resolvePath("/usr/sbin/screencapture");
}

var args:Vector.<String> = new Vector.<String>();
args[0] = "-i";
args[1] = "screencapture.png";

var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
nativeProcessStartupInfo.arguments = args;
nativeProcessStartupInfo.executable = file;
nativeProcessStartupInfo.workingDirectory = File.desktopDirectory;

process = new NativeProcess();
process.start(nativeProcessStartupInfo);

}
}
]]>

To get something like this to work on Windows, you need to know a little bit of C or C# so you can call your own service and launch the screen print functionality on Windows. For more about using the NativeProcess in AIR 2 for both Windows and Mac, you check out this Adobe article.

Reblog this post [with Zemanta]
Posted in AIR, Component, Flex | Tagged , , , | Leave a comment

AS3 Yammer Library

For the past few months I have been working on an AS3 library that wraps the Yammer API for use in Flash, Flex, and AIR projects. The biggest hurdle was making the application work for the web. As it is now, for Flash projects, you still need to be targeting Flash player 10 so that you can upload the ByteArray information file attachments. The second hurdle was creating an easy Oauth flow that doesn’t add items to the header that would violate the Flash player security, something you don’t have to worry about when creating AIR applications.

With that work out of the way, I would like to introduce an AS3 Yammer API library that communicates with the Yammer service. The as3yammerlib is hosted on GitHub and the source code is publicly available. To use the library you will need your own consumer key and secret for the Yammer API available from Yammer’s developer site. The library is also dependent on the as3corelib for doing JSON parsing and as3-signals for broadcasting events.

I put together a Flex web-based example using Flex Builder 4 that walks you through the Oauth process and retrieves messages for the logged in user. To use the code, just create a new Flex project and paste the Main.mxml file in your main application file and create another mxml for the MessageItemRenderer file within the same directory:

Main.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo"
               xmlns:ns="library://ns.adobe.com/flex/mx"
               creationComplete="init()"
               minWidth="1024" minHeight="768">
   
   
    <fx:Script>
        <![CDATA[
            import com.yammer.api.Yammer;
            import com.yammer.api.events.YammerEvent;
            import com.yammer.api.vo.Oauth;
            import com.yammer.api.vo.YammerCurrentUser;
            import com.yammer.api.vo.YammerError;
            import com.yammer.api.vo.YammerMessageList;
            import com.yammer.api.vo.YammerTab;
           
            import mx.collections.ArrayCollection;
            import mx.controls.Alert;
           
            private var oauthToken:String;
            private var oauthSecret:String;
       
            // your consumer key and secret go here
            private var consumerKey:String = "your consumer key";
            private var consumerSecret:String = "your consumer secret";
           
            private var service:Yammer;
           
            [Bindable] private var currentUser:YammerCurrentUser;
           
            private function init():void
            {
                this.service = new Yammer(this.consumerKey, this.consumerSecret);
                this.service.errorReceived.add(errorReceived);
               
                // we have tokens stored
                if(oauthToken && oauthSecret){
                    service.setOuthTokens(oauthToken, oauthSecret);
                    this.getCurrentUser();
                }
            }
           
            // get request token from service.
            private function getRequestToken():void
            {
                service.resultsReceived.add(requestTokenReceived);
                service.requestToken();
            }
           
            // submit the virify pin from the web site
            private function getAccessToken(verify_pin:String):void
            {
                service.resultsReceived.add(accessTokenReceived);
                service.accessToken(verify_pin);
            }
           
            // sends user to browser page to retrieve verification pin.
            private function sendAuthorizationRequest():void
            {
                service.sendAuthorizationRequest(); // open browser window
            }
           
            private function getCurrentUser():void
            {
                service.resultsReceived.add(currentUserReceived);
                service.getCurrentUser();
                return;
            }
           
            public function getMessages(tab:YammerTab):void
            {
                service.resultsReceived.add(messageListReceived);
                service.getMessages(tab.url);
                return;
            }
           
            private function requestTokenReceived(value:Oauth):void
            {
                service.resultsReceived.remove(requestTokenReceived);  
                Alert.show("Request tokens received: " + value.oauth_token);
            }
           
            private function accessTokenReceived(value:Oauth):void
            {
                service.resultsReceived.remove(accessTokenReceived);
                Alert.show("Access tokens received:: token: " + value.oauth_token + " secret: " + value.oauth_token_secret);
                this.getCurrentUser();
            }
           
            private function currentUserReceived(value:YammerCurrentUser):void
            {
                service.resultsReceived.remove(currentUserReceived);
                currentUser = value;
                var tab:YammerTab = currentUser.tabs[0]; // get first user tab, usually My Feed
                this.getMessages(tab);
            }
           
            private function messageListReceived(value:YammerMessageList):void
            {
                service.resultsReceived.remove(messageListReceived);
                var messagelist:YammerMessageList = value;
                var messages:ArrayCollection = new ArrayCollection(messagelist.getMessages());
                messageList.dataProvider = messages;
            }
           
            private function errorReceived(error:YammerError):void
            {
                Alert.show("Yammer Service Error: " + error.errorMessage);
            }
           
        ]]>
    </fx:Script>
   
    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>
   
    <s:Button label="1. Request Token" click="getRequestToken()" width="120" x="10" y="10"/>
   
    <s:Button label="2. Open Browser" click="sendAuthorizationRequest()" width="120" x="138" y="10"/>
   
    <s:TextInput id="pinInput" text="3. Paste Pin" width="120" textAlign="center" focusIn="pinInput.text = ''" x="266" y="10"/>
   
    <s:Button label="4. Access Token" click="getAccessToken(pinInput.text)" width="120" x="394" y="10"/>
   
    <s:List id="messageList" x="10" y="113" width="563" height="337" itemRenderer="MessageItemRenderer"/>
   
    <ns:Image source="{currentUser.mugshot_url}" width="48" height="48" x="11" y="45"/>
   
    <s:Label text="{currentUser.full_name}" x="67" y="50"  fontSize="14"/>
   
    <s:Label text="{currentUser.job_title}" x="67" y="72"  fontSize="11"/>
   
</s:Application>

MessageItemRenderer.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer width="100%"
           xmlns:fx="http://ns.adobe.com/mxml/2009"
           xmlns:s="library://ns.adobe.com/flex/spark"
           xmlns:mx="library://ns.adobe.com/flex/halo" xmlns:ns="library://ns.adobe.com/flex/mx">
   
    <fx:Script>
        <![CDATA[
            import com.yammer.api.utils.MessageUtilities;
            import com.yammer.api.vo.YammerMessage;
           
            [Bindable] private var message:YammerMessage;
           
            override public function set data(value:Object):void
            {
                super.data = value;
                message = data as YammerMessage;
            }
           
            /**
             *  override set data to set
             */

            override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
            {
                super.updateDisplayList(unscaledWidth,unscaledHeight);    
            }
        ]]>
    </fx:Script>
   
    <s:Rect left="0" bottom="1" right="0" height="1">
        <s:fill>
            <s:SolidColor color="0x000000" alpha=".2"/>
        </s:fill>
    </s:Rect>

    <s:HGroup width="100%" height="100%" paddingTop="5" paddingBottom="5" paddingRight="5" paddingLeft="5">
       
        <ns:Image source="{message.sender.mugshot_url}" width="48" height="48"/>
       
        <s:VGroup width="100%" height="100%">
   
            <s:Label  text="{message.sender.full_name}" maxDisplayedLines="1" left="10" top="10" right="26"  fontWeight="bold" color="#000000"/>
           
            <s:RichEditableText id="richEdTxt" text="{message.body_plain}" width="100%" editable="false" selectable="false" focusEnabled="false" />
               
            <s:HGroup width="100%" paddingTop="2">
                <s:Label text="{message.created_at}"/>
            </s:HGroup>
           
        </s:VGroup>
       
    </s:HGroup>
   
</s:ItemRenderer>

I am sure there are lots of ways to improve the library, but this one is a pretty simple start with lots of functionality. Please let me know if you find any errors and feel free to fork this version and add any improvements to the code.

-Mr

Posted in AIR, API, Flash Builder 4, Flex, Flex 4 | Tagged , , , | 1 Comment

AIR: Migrating Expired Certificates using AIR 1.5.3

If you renew your certificate and you are running an earlier version of Adobe AIR than 1.5.3, you are in a world of hurt. The process of updating and renewing your certificate is enough of a pain, but trying to update your AIR application after your certificate expires prior to 1.5.3 required you to just uninstall your previous version and reinstall a new version of your application.

Adobe AIR 1.5.3 Update

Luckily with AIR 1.5.3 you now have a grace period of 6 months to renew your certificate and still be able to publish an update to your application that users can install over the previous application. The down side is that it’s is not easy to update your certificate once you update to 1.5.3. You can read more about the Adobe AIR 1.5.3 update and changes to certificate migration here:

Release notes for Adobe AIR developers
Adobe AIR 1.5.3 Now Available

Updating Flex Builder 3 for AIR 1.5.3

First off, grab and install the latest and greatest AIR 1.5.3 from Adobe’s download site. You will also need to download the Adobe AIR 1.5.3 SDK to install within your Flex Builder 3. If you just change the descriptor file and not update the SDK to use AIR 1.5.3, you will receive the following error when compiling the project from the IDE:

invalid application descriptor: descriptor version does not match runtime version

or

EncryptedLocalStore may not use publisher IDs passed in from ADL

The instructions for updating Flex Builder 3 to use AIR 1.5.3 are not really clear, at least not for me. The best way to upgrade on the MAC is to download the AIR 1.5.3 SDK and manually drag the files to your Flex Builder 3 SDK folder. When you download the AIR 1.5.3 SDK and expand it, you can see the following folder structure:

AIR SDK 1.5.3

You can just drag the folders one-by-one ( or the contents of the folder to be more careful ) to your Flex Builder 3 application directory inside the sdks folder. Now restart your Flex Builder 3 and create a new dummy project. If all goes well, your new project will have the 1.5.3 namespace already in the descriptor file. Now for existing Flex Builder 3 projects, you need to manually update your application’s namespace to 1.5.3 and add a new descriptor tag for publisherID (you don’t need to do this for a new AIR application, only if you are updating an project to AIR 1.5.3):

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://ns.adobe.com/air/application/1.5.3">

    <!-- The application identifier string, unique to this application. Required. -->
    <id>MyApplication</id>
   
    <publisherID>3782AD3EDB99182DA9E106898998986691F7E39C8DBA6A3.1</publisherID>
        ...

The publisherID is a combination of your application name and the publish id for the application. You can find the publisherID manually by looking at your currently installed application directory on MAC (Windows users can find it in the release notes):

1
MaciontoshHD > Users > [username] > Library > Application Support > Adobe > AIR > ELS > MyApplication.3782AD3EDB99182DA9E106898998986691F7E39C8DBA6A3.1

The long number after the application name is the publisherID, copy this and add it to your application descriptor file. You can also get the application publisherID from the NativeApplication:

1
trace(NativeApplication.nativeApplication.publisherID);

It should be noted that bye adding the …. to the descriptor file will create an error when compiling your application using Flex Builder 3:

invocation forwarded to primary instance

So this must be commented out of the descriptor file until you are ready to publish. If you leave this commented out when you publish the application, you won’t be able to migrate the application. So I am not sure how tracing the publisherID is going to be a help to you from the IDE when testing. You should also update the debug version of your Flash Player to the latest (10.0.42.34) before compiling your application. Now you can test your application from the IDE and it should work.

Migrating Expired Certificate

Now your application should run from the IDE and you can prepare to publish the application using your renewed certificate. Publish the application as normal using the renewed certificate to create the AIR compiled application. Now you will need to migrate your previous expired certificate using the ADT migration tools. The instructions for migrating certificates using the ADT migration tools can be found here:

http://www.insideria.com/2009/04/migrating-air-application-cert.html

Once you successfully migrated your previous expired certificate, your compiled AIR application is ready to update your previous version of the installed application on your system. If things didn’t go correctly, you will get an error that an application already exists with the same name, and you need to install the application in a different directory. That would be bad, and you probably missed one of the delicate steps and need to go through it all again. If you do get an update option to replace the existing application, you can no celebrate.

My Opinion about Certificates

In my opinion, using 3rd party certificate vendors (and paying for them) like Thawte, Verisign, ect. is total nonsense and just way to cumbersome. Adobe could easily provide a way to authenticate the authors of AIR applications with minimal cost to developers. This is especially important when you just want to publish an application for free and distribute it through the Adobe market place.

Adobe could borrow some ideas from Android Market, especially since Adobe plans on allowing developers to create more and more Flash/Flex content for mobile devices. Developers don’t need some complicated and expensive system in place to review code and authorize updates (i.e. iPhone app store), we simply need a way to verify to the end user that the author is who she says she is and the application is protected from someone else writing an application with identical name.

The renewal process for a certificate and the above steps to migrate certificates is a total pain, and I think an unnecessary barrier for Adobe AIR application developers. Anything Adobe could to do to lower the barrier for the average AIR developer to create certified applications would be greatly appreciated. There are also other ways to freely publish and distribute your application freely as described on this post at polyGeek.com:

http://polygeek.com/862_flex_deploying-trusted-air-apps-without-certification-for-free

-Mister

Posted in AIR, Flex 3 | Tagged , , | Leave a comment

Preventing scrolling and mouse events on out of focus AIR application

This has been a little bit of annoyance for me for some time. When an AIR application (like any Twitter application) is out of focus and you click on the application activate it, you inadvertently click a link or button within the application. You are also able to scroll the application when the application has no focus, which may or may not be your desired behavior. I wanted a way to prevent mouse events and scrolling until the application has focus. I tried to find away around this annoyance by capturing if the application has focus by listening for Event.ACTIVATE or Event.DEACTIVATE events on the NativeApplication.nativeApplication:

1
2
3
4
5
6
7
8
9
10
11
12
NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, handleAppActivate);
NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, handleAppDiactivate);

private function handleAppActivate(event:Event):void
{
    // set a flag that application has focus so now links and buttons work
}
           
private function handleAppDiactivate(event:Event):void
{
       // set some flag to let the application has no focus so disable links and buttons
}

This solution has a couple of problems. First the application has focus the instance you click on any link within the application window. So fine, the way around that is to use some kind of timer so that your flag that says application is now active is not set until some time after the application receives focus. This leads to the second problem, at least for larger scale applications. You have to pipe the flag that says the application has no focus to every button and link through the entire application, this is a huge pain.

Today I just happened to be playing with the new FramerateThrottler class created by Grant Skinner to reduce CPU usage for AIR applications running on Mac (this is yet another gripe of mine). So on his bog there are some comments about using mouseChildren and mouseEnabled on all open AIR windows to reduce CPU. Anyways, when I was messing around with this suggestion, I happened to notice my application no longer scrolled when the application was in the background, but mouse events were still active.

So I then thought of combining my previous attempt at preventing links and buttons from being clicked on with mouseChildren and mouseEnabled. I ended up only setting mouseChildren and mouseEnabled to true if the application has focus, and after a small timer dalay of 500 miliseconds. That gives me just enough time to click on the application any where without also clicking on a link or button and activating it. So now I can click on the application to give it focus any place I like without firing off click events:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NativeApplication.nativeApplication.addEventListener(Event.ACTIVATE, handleAppActivate);
NativeApplication.nativeApplication.addEventListener(Event.DEACTIVATE, handleAppDiactivate);

private var timer:Timer;
private function handleAppActivate(event:Event):void
{
    // set timer to enable mouse events after short delay on app activation
        timer = new Timer(500);
        timer.addEventListener(TimerEvent.COMPLETE, enableAppEvents);
        timer.start();
}
           
private function handleAppDiactivate(event:Event):void
{
       this.mouseChildren = false;
       this.mouseEnabled = false;
}

private function enableAppEvents(event:TimerEvent):void
{
       timer.stop();
       timer.removeEventListener(TimerEvent.COMPLETE, enableAppEvents);
   
       // now scrolling and mouse events work
       this.mouseChildren = true;
       this.mouseEnabled = true;
}

That’s it, a simple solution that was probably sitting under my nose the whole time but I never thought of it.

UPDATE

Jonnie Hallman from Adobe posted a great article that expands the idea of handling throttling and events (mouse wheel for example).

http://www.adobe.com/devnet/air/flex/articles/framerate_throttling.html

- Mister

Posted in AIR | Tagged , , , , | 1 Comment

Custom AIR updater interface using ApplicationUpdater

To tell you the truth, I never gave the ApplicationUpdaterUI much thought in terms of memory use before this post. I just thought it was a great way to add a complete update system to your AIR applications. I had not thought it could be so memory intensive until a recent conversation with Jesse Warden on Twitter:

Tweet Conversation

* Please note that Jesse is dude that looks about 17 and I am the older more distinguished gentleman.

Problem with ApplicationUpdaterUI

Indeed, that “mofo eats mad RAM” is putting it lightly. It turns out that the ApplicationUpdaterUI consumes about 14MB of RAM within the application. Furthermore, for the ApplicationUpdaterUI to even check for available updates, it must loads the entire UI in memory even if not displayed. So just by using the framework, you end adding 14MB to your application that won’t be garbage collected. Granted, in most cases 14MB of RAM is not that big of deal. However, for smaller applications you might want to consider an alternative approach.

There happens to another class file called ApplicationUpdater. This class basically gives you all the benefits of the ApplicationUpdaterUI framework without any of the visible elements. This means you have to build your own user interface. This might actually be a benefit in the long run as you may want to customize your application updater to match your application. The other benefit is that you can use the ApplicationUpdater class to check for updates without loading any user interface. The interface only needs to be loaded if there is an actual update available.

Custom Updater Interface with ApplicationUpdater

In building my own custom updater interface, I thought it might be nice to have the same look and feel of the ApplicationUpdaterUI along with some of the more polite features (like postpone update until restart). Building your own updater using the ApplicationUpdater turns out to be a little more involved than I had originally thought. However, when you start attacking a problem you persevere until the end, no matter what the pain.

I did find some available information in the Adobe AIR 1.5 Cookbook, which has a basic custom updater example. This helped with some of the basic stuff, but I still needed a little more detail to make something similar to the ApplicationUpdaterUI. I found another good example by Jens Krause, but this example is for Flex 4. I needed something that would work for Flex 3. So here is what I built based on these examples:

Check Update
Download Update
Install Update

The application consists of one class file and one MXML dialog for displaying all the user interface elements. I borrowed some of the icons from the ApplicationUpdaterUI which is available in the AIR SDK (I hope Adobe doesn’t mind). The update dialog UI is only loaded when needed, all checks for application updates happen using the ApplicationUpdater. The update dialog is only displayed when the user wants to manually update or the periodic update check finds a new application update.

The Good and the Bad

The good news is the updater only consumes about 2MB of RAM when you load the update dialog box, part of that is the ApplicationUpdater itself running in the background. The other good news is that you can yank out or customize any part of the this example to meet your own needs. The bad news is that even though I tried my best to fully remove the update dialog and have garbage collection clean it up, it doesn’t fully unload from memory (go figure). I don’t feel this is a huge issue as the application is most likely restarted after you install an update. Below is the code followed by a zip file containing the full application.

UpdateManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
package com.thanksmister
{
    import air.update.ApplicationUpdater;
    import air.update.events.DownloadErrorEvent;
    import air.update.events.StatusFileUpdateErrorEvent;
    import air.update.events.StatusFileUpdateEvent;
    import air.update.events.StatusUpdateErrorEvent;
    import air.update.events.StatusUpdateEvent;
    import air.update.events.UpdateEvent;
   
    import flash.desktop.NativeApplication;
    import flash.events.ErrorEvent;
    import flash.events.Event;
    import flash.events.ProgressEvent;
    import flash.filesystem.File;
   
    public class UpdateManager
    {
        private var appUpdater:ApplicationUpdater;
        private var appVersion:String;
        private var baseURL:String;
        private var updaterDialog:UpdaterDialog;
        private var configurationFile:File;
        private var isFirstRun:String;
        private var upateVersion:String;
        private var applicationName:String;
        private var installedVersion:String;
        private var description:String;
       
        private var initializeCheckNow:Boolean = false;
        private var isInstallPostponed:Boolean = false;
        private var showCheckState:Boolean = true;
       
        /**
         * Constructer for UpdateManager Class
         *
         * @param showCheckState Boolean value to show the Check Now dialog box
         * @param initializeCheckNow Boolean value to initialize application and run check on instantiation of the Class
         * */

        public function UpdateManager(showCheckState:Boolean = true, initializeCheckNow:Boolean = false)
        {
            this.showCheckState = showCheckState;
            this.configurationFile = new File("app:/config/update.xml");
            this.initializeCheckNow = initializeCheckNow;
            initialize();
        }
       
        public function checkNow():void
        {
            //trace("checkNow");
            isInstallPostponed = false;
            if(showCheckState) {
                createDialog(UpdaterDialog.CHECK_UPDATE);
            } else {
                appUpdater.checkNow();
            }
        }

        //----------  ApplicationUpdater ----------------//
       
        private function initialize():void
        {
            //trace("initialize");
            if(!appUpdater){
                appUpdater = new ApplicationUpdater();
                appUpdater.configurationFile = configurationFile;
                appUpdater.addEventListener(UpdateEvent.INITIALIZED, updaterInitialized);
                appUpdater.addEventListener(StatusUpdateEvent.UPDATE_STATUS, statusUpdate);
                appUpdater.addEventListener(UpdateEvent.BEFORE_INSTALL, beforeInstall);
                appUpdater.addEventListener(StatusUpdateErrorEvent.UPDATE_ERROR, statusUpdateError);
                appUpdater.addEventListener(UpdateEvent.DOWNLOAD_START, downloadStarted);
                appUpdater.addEventListener(ProgressEvent.PROGRESS, downloadProgress);
                appUpdater.addEventListener(UpdateEvent.DOWNLOAD_COMPLETE, downloadComplete);
                appUpdater.addEventListener(DownloadErrorEvent.DOWNLOAD_ERROR, downloadError);
                appUpdater.addEventListener(ErrorEvent.ERROR, updaterError);
                appUpdater.initialize();
            }
        }
       
        private function beforeInstall(event:UpdateEvent):void
        {
            //trace("beforeInstall");
            if (isInstallPostponed) {
                event.preventDefault();
                isInstallPostponed = false;
            }
        }
       
        private function updaterInitialized(event:UpdateEvent):void
        {
            //trace("updaterInitialized");
            this.isFirstRun = event.target.isFirstRun;
            this.applicationName = getApplicationName();
            this.installedVersion = getApplicationVersion();
       
            if(showCheckState && initializeCheckNow) {
                createDialog(UpdaterDialog.CHECK_UPDATE);
            } else if (initializeCheckNow) {
                appUpdater.checkNow();
            }
        }
       
        private function statusUpdate(event:StatusUpdateEvent):void
        {
            //trace("statusUpdate");
             event.preventDefault();
             if(event.available){
                this.description = getUpdateDescription(event.details);
                this.upateVersion = event.version;
               
                if(!showCheckState) {
                    createDialog(UpdaterDialog.UPDATE_AVAILABLE);
                } else if (updaterDialog) {
                    updaterDialog.applicationName = this.applicationName;
                    updaterDialog.installedVersion = this.installedVersion;
                    updaterDialog.upateVersion = this.upateVersion;
                    updaterDialog.description = this.description
                    updaterDialog.updateState = UpdaterDialog.UPDATE_AVAILABLE;
                }
             } else {
                if (showCheckState) createDialog(UpdaterDialog.NO_UPDATE);
             }
        }
       
        private function statusUpdateError(event:StatusUpdateErrorEvent):void
        {
            event.preventDefault();
            if(!updaterDialog){
                createDialog(UpdaterDialog.UPDATE_ERROR);
            } else {
                updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
            }
        }
       
        private function statusFileUpdate(event:StatusFileUpdateEvent):void
        {
            event.preventDefault();
            if(event.available) {
                updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
                appUpdater.downloadUpdate();
            } else {
                updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
            }
        }
       
        private function statusFileUpdateError(event:StatusFileUpdateErrorEvent):void
        {
            event.preventDefault();
            updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;;
        }
       
        private function downloadStarted(event:UpdateEvent):void
        {
            updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
        }
       
        private function downloadProgress(event:ProgressEvent):void
        {
            updaterDialog.updateState = UpdaterDialog.UPDATE_DOWNLOADING;
            var num:Number = (event.bytesLoaded/event.bytesTotal)*100;
            updaterDialog.downloadProgress(num);
        }
       
        private function downloadComplete(event:UpdateEvent):void
        {
            event.preventDefault(); // prevent default install
            updaterDialog.updateState = UpdaterDialog.INSTALL_UPDATE;
        }
       
        private function downloadError(event:DownloadErrorEvent):void
        {
            event.preventDefault();
            updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
        }
       
        private function updaterError(event:ErrorEvent):void
        {
            updaterDialog.errorText = event.text;
            updaterDialog.updateState = UpdaterDialog.UPDATE_ERROR;
        }
       
        //----------  UpdaterDialog Events ----------------//
       
        private function createDialog(state:String):void
        {
            if(!updaterDialog) {
                updaterDialog = new UpdaterDialog();
                updaterDialog.isFirstRun = this.isFirstRun;
                updaterDialog.applicationName = this.applicationName;
                updaterDialog.installedVersion = this.installedVersion;
                updaterDialog.upateVersion = this.upateVersion;
                updaterDialog.updateState = state;
                updaterDialog.description = this.description;
                updaterDialog.addEventListener(UpdaterDialog.EVENT_CHECK_UPDATE, checkUpdate);
                updaterDialog.addEventListener(UpdaterDialog.EVENT_INSTALL_UPDATE, installUpdate);
                updaterDialog.addEventListener(UpdaterDialog.EVENT_CANCEL_UPDATE, cancelUpdate);
                updaterDialog.addEventListener(UpdaterDialog.EVENT_DOWNLOAD_UPDATE, downloadUpdate);
                updaterDialog.addEventListener(UpdaterDialog.EVENT_INSTALL_LATER, installLater);
                updaterDialog.open();
            }
        }
       
        /**
         * Check for update.
         * */

        private function checkUpdate(event:Event):void
        {
            //trace("checkUpdate");
            appUpdater.checkNow();
        }
       
        /**
         * Install the update.
         * */

        private function installUpdate(event:Event):void
        {
            appUpdater.installUpdate();
        }
       
        /**
         * Install the update.
         * */

        private function installLater(event:Event):void
        {
            isInstallPostponed = true;
            appUpdater.installUpdate();
            destoryUpdater();
        }
       
        /**
         * Download the update.
         * */

        private function downloadUpdate(event:Event):void
        {
            appUpdater.downloadUpdate();
        }
       
        /**
         * Cancel the update.
         * */

        private function cancelUpdate(event:Event):void
        {
            appUpdater.cancelUpdate();
            destoryUpdater();
        }
       
        //----------  Destroy All ----------------//
       
        private function destroy():void
        {
            if (appUpdater) {
                appUpdater.configurationFile = configurationFile;
                appUpdater.removeEventListener(UpdateEvent.INITIALIZED, updaterInitialized);
                appUpdater.removeEventListener(StatusUpdateEvent.UPDATE_STATUS, statusUpdate);
                appUpdater.removeEventListener(StatusUpdateErrorEvent.UPDATE_ERROR, statusUpdateError);
                appUpdater.removeEventListener(UpdateEvent.DOWNLOAD_START, downloadStarted);
                appUpdater.removeEventListener(ProgressEvent.PROGRESS, downloadProgress);
                appUpdater.removeEventListener(UpdateEvent.DOWNLOAD_COMPLETE, downloadComplete);
                appUpdater.removeEventListener(DownloadErrorEvent.DOWNLOAD_ERROR, downloadError);
                appUpdater.removeEventListener(UpdateEvent.BEFORE_INSTALL, beforeInstall);
                appUpdater.removeEventListener(ErrorEvent.ERROR, updaterError);
               
               
                appUpdater = null;
            }
           
            destoryUpdater();
        }
       
        private function destoryUpdater():void
        {
            if(updaterDialog) {
                updaterDialog.destroy();
                updaterDialog.removeEventListener(UpdaterDialog.EVENT_CHECK_UPDATE, checkUpdate);
                updaterDialog.removeEventListener(UpdaterDialog.EVENT_INSTALL_UPDATE, installUpdate);
                updaterDialog.removeEventListener(UpdaterDialog.EVENT_CANCEL_UPDATE, cancelUpdate);
                updaterDialog.removeEventListener(UpdaterDialog.EVENT_DOWNLOAD_UPDATE, downloadUpdate);
                updaterDialog.removeEventListener(UpdaterDialog.EVENT_INSTALL_LATER, installLater);
                updaterDialog.close();
                updaterDialog = null;
            }
            isInstallPostponed = false;
        }
       
        //----------  Utilities ----------------//
       
        /**
         * Getter method to get the version of the application
         * Based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
         *
         * @return String Version of application
         *
         */

        private function getApplicationVersion():String
        {
            var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
            var ns:Namespace = appXML.namespace();
            return appXML.ns::version;
        }

        /**
         * Getter method to get the name of the application file
         * Based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
         *
         * @return String name of application
         *
         */

        private function getApplicationFileName():String
        {
            var appXML:XML = NativeApplication.nativeApplication.applicationDescriptor;
            var ns:Namespace = appXML.namespace();
            return appXML.ns::filename;
        }

        /**
         * Getter method to get the name of the application, this does not support multi-language.
         * Based on a method from Adobes ApplicationUpdaterDialogs.mxml, which is part of Adobes AIR Updater Framework
         * Also based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
         *
         * @return String name of application
         *
         */

        private function getApplicationName():String
        {
            var applicationName:String;
            var xmlNS:Namespace=new Namespace("http://www.w3.org/XML/1998/namespace");
            var appXML:XML=NativeApplication.nativeApplication.applicationDescriptor;
            var ns:Namespace=appXML.namespace();
           
            // filename is mandatory
            var elem:XMLList=appXML.ns::filename;
           
            // use name is if it exists in the application descriptor
            if ((appXML.ns::name).length() != 0)
            {
                elem=appXML.ns::name;
            }
           
            // See if element contains simple content
            if (elem.hasSimpleContent())
            {
                applicationName=elem.toString();
            }
   
            return applicationName;
        }
       
        /**
         * Helper method to get release notes, this does not support multi-language.
         * Based on a method from Adobes ApplicationUpdaterDialogs.mxml, which is part of Adobes AIR Updater Framework
         * Also based on Jens Krause blog post: http://www.websector.de/blog/2009/09/09/custom-applicationupdaterui-for-using-air-updater-framework-in-flex-4/
         *
         * @param detail Array of details
         * @return String Release notes depending on locale chain
         *
         */

        protected function getUpdateDescription(details:Array):String
        {
            var text:String="";

            if (details.length == 1)
            {
                text=details[0][1];
            }
            return text;
        }
    }
}

UpdateDialog.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
<?xml version="1.0" encoding="utf-8"?>
<mx:Window xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml" styleName="updateDialogWindow"
    layout="absolute" maximizable="false" resizable="false" currentState="{_updateState}"
    clipContent="false" showStatusBar="false" width="530" height="180">
   
    <mx:Metadata>
        [Event(name="checkUpdate", type="flash.events.Event")]
        [Event(name="downloadUpdate", type="flash.events.Event")]
        [Event(name="downloadUpdate", type="flash.events.Event")]
        [Event(name="cancelUpdate", type="flash.events.Event")]
    </mx:Metadata>
   
    <mx:Script>
        <![CDATA[
            public static var EVENT_CHECK_UPDATE:String = "checkUpdate";
            public static var EVENT_INSTALL_UPDATE:String = "installUpdate";
            public static var EVENT_DOWNLOAD_UPDATE:String = "downloadUpdate";
            public static var EVENT_CANCEL_UPDATE:String = "cancelUpdate";
            public static var EVENT_INSTALL_LATER:String = "installLater";
           
            [Bindable] public static var UPDATE_DOWNLOADING:String = "updateDownloading";
            [Bindable] public static var INSTALL_UPDATE:String = "installUpdate";
            [Bindable] public static var UPDATE_AVAILABLE:String = "updateAvailable";
            [Bindable] public static var NO_UPDATE:String = "noUpdate";
            [Bindable] public static var CHECK_UPDATE:String = "checkUpdate";
            [Bindable] public static var UPDATE_ERROR:String = "updateError";
           
            [Bindable] private var _isFirstRun:String;
            [Bindable] private var _installedVersion:String;
            [Bindable] private var _updateVersion:String;
            [Bindable] private var _updateDescription:String;
            [Bindable] private var _applicationName:String;
            [Bindable] private var _updateState:String;
            [Bindable] private var _errorText:String = "There was an error checking for updates.";
           
            public function set isFirstRun(value:String):void
            {
                _isFirstRun = value;
            }
           
            public function set installedVersion(value:String):void
            {
                _installedVersion = value;
            }
           
            public function set upateVersion(value:String):void
            {
                _updateVersion = value;
            }
           
            public function set updateState(value:String):void
            {
                _updateState = value;
            }
           
            public function set applicationName(value:String):void
            {
                _applicationName = value;
            }
           
            public function set description(value:String):void
            {
                _updateDescription = value;
            }
           
            public function set errorText(value:String):void
            {
                _errorText = value;
            }
           
            public function downloadProgress(value:Number):void
            {
                if(progressBar) progressBar.setProgress(value, 100);
            }
           
            private function continueUpdate():void
            {
                if (this.currentState == UpdaterDialog.CHECK_UPDATE){
                    this.dispatchEvent(new Event(EVENT_CHECK_UPDATE));
                } else if (this.currentState == UPDATE_AVAILABLE) {
                    this.dispatchEvent(new Event(EVENT_DOWNLOAD_UPDATE));
                }else if (this.currentState == INSTALL_UPDATE) {
                    this.dispatchEvent(new Event(EVENT_INSTALL_UPDATE));
                }
            }
           
            private function cancelUpdate():void
            {
                if (this.currentState == INSTALL_UPDATE) {
                    this.dispatchEvent(new Event(EVENT_INSTALL_LATER));
                    return;
                }
                this.dispatchEvent(new Event(EVENT_CANCEL_UPDATE));
            }
           
            public function destroy():void
            {
                iconImage.unloadAndStop(true);
                iconImage.source = null;
               
                continueButton.removeEventListener(MouseEvent.CLICK, continueUpdate);
                cancelButton.removeEventListener(MouseEvent.CLICK, cancelUpdate);
               
                // becaause we used skins, we have to clear them for garbage collection
                //http://www.firstrowria.com/2009/01/flex-top-5-memory-leaks-in-flex-2-skinning-of-components-eg-button/
                continueButton.styleName = null;
                cancelButton.styleName = null;
               
                while(this.numChildren > 0){
                    this.removeChildAt(0);
                }
            }
        ]]>
    </mx:Script>

    <mx:states>
        <mx:State name="{CHECK_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="86" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="86" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Check for updates" styleName="updateTitle" />
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="Allow the application to check for updates?"  styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_AVAILABLE}">
            <mx:SetProperty name="height" value="360"/>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._installedVersion}" x="230" y="114" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._updateVersion}" x="230" y="134" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="118" y="114" text="Installed Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="127" y="134" text="Update Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="96" text="Application:"  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="96" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Update Available" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="An updated version of the application is available for download." styleName="updateDialogText"/>
            </mx:AddChild>
           
            <mx:AddChild position="lastChild">
                <mx:Label id="releaseLabel" x="10" y="222" text="Release notes" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:TextArea text="{_updateDescription}" x="10" y="248" width="508" height="100" styleName="updateDialogTextArea"/>
            </mx:AddChild>
           
            <mx:AddChild position="lastChild">
                <mx:HRule x="10" y="214" width="508" styleName="updateDialogHRule"/>
            </mx:AddChild>
            <mx:SetProperty target="{cancelButton}" name="y" value="164"/>
            <mx:SetProperty target="{continueButton}" name="y" value="164"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Download later"/>
            <mx:SetProperty target="{continueButton}" name="x" value="269"/>
            <mx:SetProperty target="{continueButton}" name="label" value="Download now"/>
        </mx:State>
        <mx:State name="{NO_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Label x="122" y="86" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="200" y="86" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="No Updates" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="There is no application update available at this time." styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Close"/>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_DOWNLOADING}">
            <mx:AddChild position="lastChild">
                <mx:ProgressBar x="107" y="84" width="411" label=" " id="progressBar" mode="manual" height="15" styleName="updateDiallogProgress"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="20" text="Downloading Update" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="53" text="Download progress..."  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{UPDATE_ERROR}">
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Unexpected error" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="50" text="{_errorText}" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:RemoveChild target="{continueButton}"/>
            <mx:SetProperty target="{cancelButton}" name="label" value="Close"/>
            <mx:SetProperty name="height" value="180"/>
        </mx:State>
        <mx:State name="{INSTALL_UPDATE}">
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._installedVersion}" x="230" y="139" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text  text="{this._updateVersion}" x="230" y="159" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="118" y="139" text="Installed Version:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="127" y="159" text="Update Version:"  styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="152" y="121" text="Application:" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="230" y="121" text="{this._applicationName}" styleName="updateDialogText"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Label x="107" y="19" text="Install update" id="windowTitle4" styleName="updateTitle"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:Text x="107" y="50" text="The update for the application is downloaded and ready to be installed." styleName="updateDialogText"/>
            </mx:AddChild>
       
            <mx:AddChild position="lastChild">
                <mx:Label x="10" y="262" text="Release notes" styleName="updateDialogLabel"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:TextArea id="relaeseNotesTextArea0" text="{_updateDescription}" x="10" y="288" width="508" height="100" styleName="updateDialogTextArea"/>
            </mx:AddChild>
            <mx:SetProperty name="height" value="402"/>
            <mx:AddChild position="lastChild">
                <mx:ProgressBar id="installProgressBar" x="107" y="84" width="411" label=" " mode="manual" height="15" styleName="installDiallogProgress" creationComplete="installProgressBar.setProgress(100,100)"/>
            </mx:AddChild>
            <mx:AddChild position="lastChild">
                <mx:HRule x="10" y="249" width="508" styleName="updateDialogHRule"/>
            </mx:AddChild>
            <mx:SetProperty target="{cancelButton}" name="label" value="Postpone until restart"/>
            <mx:SetProperty target="{cancelButton}" name="y" value="198"/>
            <mx:SetProperty target="{continueButton}" name="y" value="198"/>
            <mx:SetProperty target="{continueButton}" name="x" value="320"/>
            <mx:SetProperty target="{continueButton}" name="label" value="Install update"/>
        </mx:State>
    </mx:states>
   
    <mx:Button x="107" y="129" label="Cancel" id="cancelButton" click="cancelUpdate()" height="34" styleName="updateDialogButton"/>
    <mx:Button x="202" y="129" label="Check for Updates" id="continueButton" click="continueUpdate()" height="34" styleName="updateDialogButton"/>
    <mx:Image source="@Embed('/assets/images/UpdateIcon.png')" x="15" y="25" width="81" height="74" id="iconImage"/>

</mx:Window>

Main.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="vertical" width="428" height="280" creationComplete="init()">

    <mx:Style source="assets/styles.css"/>
   
    <mx:Script>
        <![CDATA[
            import com.thanksmister.UpdateManager;
            import mx.rpc.events.ResultEvent;
           
            private var updater:UpdateManager;
            [Bindable] private var baseURL:String;
            [Bindable] private var updates:String;
           
            private function init():void
            {
                configService.send();
            }
           
            private function handleResult(event:ResultEvent):void
            {
                var xml:XML = event.result as XML;
                baseURL = xml..baseurl.toString();
                updates = xml..updates.toString();
               
                if(updates)
                    updater = new UpdateManager(true, false);
            }
        ]]>
    </mx:Script>
   
    <mx:HTTPService id="configService" method="GET" resultFormat="e4x" url="config/configuration.xml" result="handleResult(event)" />
   
    <mx:Text fontSize="14" color="0xFFFFFFF" text="Update tester.  Please click the button below to begin update checking or wait for the dealy time (3 min). Once the application is successfully updated, the application version will be updated from v1 to v2." x="10" y="10" width="342" height="104"/>
   
    <mx:Button label="Begin update process" color="0xFFFFFFF"  x="125" y="104" click="updater.checkNow()"/>
   
    <mx:Label id="versionText" fontSize="14" color="0xFFFFFFF"/>
    <mx:Label text="{'Base URL: ' + baseURL}" color="0xFFFFFFF"/>
    <mx:Label text="{'Updates: ' + updates}" color="0xFFFFFFF"/>
</mx:WindowedApplication>

I packed up the Flex project. You would use this basically the same as you would use the ApplicationUpdaterUI framework. You need to create an update version of your AIR file and place it and update.xm file on a server. You can test it from the local IDE if you replace the server url with “app:/”. Just remember you can not actually update an AIR application from the IDE, it has to be installed and running on your machine to update, and the update files have to be accessible just like when you use the ApplicationUpdaterUI.

The code is free to use, hose, rip apart, just post back any fixes or enhancements you make.

Source Files

UpdateTester.zip

Update

Don’t forget to add a closing event handler to the dialog box that calls the destory() function. This was not in the original code and there needs to be a way to handle users clicking on the close box of the Window for the update dialog user interface. I also added a change that if you don’t want to show the check for update now option, the “no update available” dialog does not appear either, here is the updated code for statusUpdate function in UpdateManager.as (not in the downloaded zip):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private function statusUpdate(event:StatusUpdateEvent):void
        {
            //trace("statusUpdate");
             event.preventDefault();
             if(event.available){
                this.description = getUpdateDescription(event.details);
                this.upateVersion = event.version;
               
                if(!showCheckState) {
                    createDialog(UpdaterDialog.UPDATE_AVAILABLE);
                } else if (updaterDialog) {
                    updaterDialog.applicationName = this.applicationName;
                    updaterDialog.installedVersion = this.installedVersion;
                    updaterDialog.upateVersion = this.upateVersion;
                    updaterDialog.description = this.description
                    updaterDialog.updateState = UpdaterDialog.UPDATE_AVAILABLE;
                }
             } else {
                if (showCheckState) createDialog(UpdaterDialog.NO_UPDATE);
             }
        }

-Mister

Posted in AIR, Flex | Tagged , , , | 12 Comments

Spark TextFlow LinkElement Rollover in Flex 4

This is a little example of how to create a skinned rollover popup on a LinkElement object in Flash Builder 4 (Flex 4) within a TextFlow object. This is something I have been waiting for since 2001. The old way to create a rollover action on an HTML link was to create an invisible MovieClip that floats over your hyperlink. Flex Builder 4 greatly simplifies this by adding rollover, mouse, and click events to all hyperlinks within the TextFlow object. This is a HUGE plus

I borrowed the click event action from an example by Peter deHaan posted on his blog Flex Examples. You will need the latest build of the Flex Builder 4 SDK and some helpful instructions on how to install the latest Flex Builder 4 SDK.

Example AIR Application Output

LinkElement Rollover Example

Main.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication title="Spark RichEditableText LinkElement Rollover"
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/halo">
 
    <fx:Script>
        <![CDATA[
            import flashx.textLayout.formats.TextDecoration;
            import mx.managers.PopUpManager;
 
            import flashx.textLayout.elements.LinkElement;
            import flashx.textLayout.events.FlowElementMouseEvent;
            import mx.controls.Alert;
           
            private var customToolTip:CustomToolTip;
       
            protected function clickHandler(evt:FlowElementMouseEvent):void {
                var linkEl:LinkElement = evt.flowElement as LinkElement;
                Alert.show("The '" + linkEl.getFirstLeaf().text + "' link would have gone to " + linkEl.href + " in a " + linkEl.target + " window, but it was cancelled.", "Fun with hyperlinks");
                evt.stopImmediatePropagation();
                evt.preventDefault();
            }
           
            protected function rollOverHandler(evt:FlowElementMouseEvent):void
            {
                if(!customToolTip){
                    customToolTip = new CustomToolTip();
                    customToolTip.x = this.mouseX - customToolTip.width/2;
                    customToolTip.y = this.mouseY - 40;
                    PopUpManager.addPopUp(customToolTip, this, false);
                }
                customToolTip.text =  LinkElement(evt.flowElement).href;
            }
           
            protected function rollOutHandler(evt:FlowElementMouseEvent):void
            {
               PopUpManager.removePopUp(customToolTip);
               customToolTip = null;
           
            }
        ]]>
    </fx:Script>
 
    <s:RichEditableText id="richEdTxt"  editable="false" selectable="false" focusEnabled="false" horizontalCenter="0" verticalCenter="0">
        <s:textFlow>
            <s:TextFlow>
                <!--
                <s:linkNormalFormat  textDecoration="{TextDecoration.NONE}"/>
                <s:linkHoverFormat textDecoration="{TextDecoration.UNDERLINE}" />
                <s:linkActiveFormat  textDecoration="{TextDecoration.UNDERLINE}" />
                -->
                <s:p>Text that includes a link to <s:a href="http://adobe.com/" target="_blank"
                    rollOver="rollOverHandler(event);" rollOut="rollOutHandler(event);"
                    click="clickHandler(event);">Adobe</s:a>.</s:p>
            </s:TextFlow>
        </s:textFlow>
    </s:RichEditableText>
 
</s:WindowedApplication>

CustomToolTip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="utf-8"?>
<s:SkinnableContainer xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/halo">
    <fx:Script>
        <![CDATA[
            import spark.skins.spark.BorderSkin;
           
            [Bindable] private var _text:String;
           
            public function set text(value:String):void
            {
                _text = value;
            }
        ]]>
    </fx:Script>
   
    <s:Skin minHeight="25">
   
        <s:Rect id="upFill"
                top="1"
                right="1"
                left="1"
                bottom="1"
                radiusX="10"
                radiusY="10">
            <s:fill>
                <s:LinearGradient rotation="90">
                    <s:GradientEntry color="#222222"  ratio="0" alpha="0.45"/>
                    <s:GradientEntry color="#222222"  ratio="0.45"/>
                    <s:GradientEntry color="#222222"  ratio="0.65"/>
                    <s:GradientEntry color="#222222" ratio=".8"/>
                </s:LinearGradient>
            </s:fill>
            <s:stroke>
                <s:SolidColorStroke color="#222222" weight="0.5"/>
            </s:stroke>
        </s:Rect>
 
        <s:Rect id="highlightFill"
                top="2"
                right="2"
                left="2"
                bottom="2"
                radiusX="10"
                radiusY="10">
            <s:stroke>
                <s:LinearGradientStroke rotation="90" weight="1">
                    <s:GradientEntry color="#FDFDFD" ratio="0" alpha="0.6"/>
                    <s:GradientEntry color="#FDFDFD" ratio="0.2" alpha="0"/>
                </s:LinearGradientStroke>
 
            </s:stroke>
        </s:Rect>
 
        <s:SimpleText id="labelElement" text="{_text}"
                      color="#FFFFFF"
                      right="20"
                      left="20"
                      verticalAlign="middle"
                      horizontalCenter="0"
                      verticalCenter="1"/>
 
        <s:filters>
            <s:DropShadowFilter color="#999999"
                                blurX="5"
                                blurY="5"
                                angle="90"
                                distance="2"
                                alpha="0.8"/>
        </s:filters>
    </s:Skin>

</s:SkinnableContainer>

The only think I couldn’t get working for this example (the commented out section) was changing the format of the hyperlinks within the TextFlow object.

Source File

Additional Resources

http://labs.adobe.com/technologies/flashbuilder4/
http://livedocs.adobe.com/flex/gumbo/langref/spark/primitives/RichEditableText.html
http://blog.flexexamples.com/2009/08/27/creating-a-linkelement-in-a-spark-richeditabletext-control-in-flex-4/
http://blog.flexexamples.com/2009/07/13/downloading-and-installing-flex-4-sdk-builds-from-opensource-adobe-com-flash-builder-4-beta-edition/

-Mister

Posted in AIR, Flash Builder 4, Flex 4, Spark Skins | Tagged , , , , , , , , | 2 Comments

Garbage collection and styles

I read a really great post in my endless search for better garbage collection in AIR and Flex. Bernd Bindreiter of firstrow RIA posted a 5-part series on garbage collection and one of them involves removing skins from Button controls in order to have them garbage collected properly. In his post he removes each individual skin from the button using something like:

1
2
3
myButton.setStyle("overSkin", null);
myButton.setStyle("upSkin", null);
myButton.setStyle("downSkin", null);

What I wondered was if I could do the same thing by just setting the styleName to null:

1
myButton.sytleName = null;

This does work for skins, but as Bernd’s comments point out, other styles such as “color, fontSize, paddingTop” don’t create memory leaks, so this method only works with skins. However, setting the styleName to null is also an effective way for marking the object ready to be garbage collected. I recommend reading the rest of his posts about garbage collection techniques. I am always searching for the holy grail of garbage collection and his posts were very insightful.

-Mister

Posted in Flex | 1 Comment


  • Thanksmister on Twitter



Who's Behind Thanks, Mister!?

I am a professional software developer specializing in creating Rich Internet Applications with Adobe Flex/Flash and desktop applications using Adobe AIR runtime. I provide consulting and contract development work for various companies using Flex, Flash and Adobe AIR runtime.

This blog is my contribution to the community where I share tips, techniques, and development strategies about Adobe Flex and AIR. If you are looking for a Flex or Flash Developer for contract or consulting, please contact me to check my availability: michael.ritchie {a t} gmail

Read more at my about page