Online/Offline Application Take Two (Part 2)
In the first part of this series I explained my overall plan of action to develop an online/offline application using my existing CF application. This part will detail how I built the UI using Flex/Air 2.0. Notice I'm using Air 2.0 which at the time of this writing is still in Beta. The reason for this will become more obvious later (in part 3), but suffice to say I needed to leverage the new NativeProcess introduced in Air 2.0.
At first when planning this part I thought it would be quite difficult to roll my own "browser" to interface with my existing CF application, however this has proven to be a simple task using the HTML component in Flex. Of course there are quirks with the way some html/css/javascript is rendered and interpreted, but for the most part it works out-of-the box.
Ok, so time for some code. Here's a stripped down version of my entire UI applicaiton:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication 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:components="com.airbrwoser.components.*"
width="500" height="400" visible="true"
title="Air Browser"
creationComplete="init()"
backgroundAlpha="0.75" backgroundColor="#383838"
width.Running="900" height.Running="600"
showStatusBar="false"
backgroundAlpha.Running="0">
<s:layout>
<s:BasicLayout/>
</s:layout>
<fx:Script>
<![():void
{
// Align native AIR application window horizontally and vertically
nativeWindow.x = (Capabilities.screenResolutionX - nativeWindow.width) / 2;
nativeWindow.y = (Capabilities.screenResolutionY - nativeWindow.height) / 2;
var launchBrowser:Timer = new Timer(5000);
launchBrowser.addEventListener(TimerEvent.TIMER_COMPLETE,activateBrowser);
launchBrowser.start();
}
private function activateBrowser(e:Event):void{
currentState = 'Running';
// Browse to home page
browser.location = "http://www.myapplication.com";
}
public function reload():void
{
browser.reload();
}
public function forward():void
{
browser.historyForward();
}
public function back():void
{
browser.historyBack();
}
protected function browser_locationChangeHandler(event:Event):void
{
cursorManager.setBusyCursor();
var expireLoading:Timer = new Timer(5000);
expireLoading.addEventListener(TimerEvent.TIMER_COMPLETE,browser_completeHandler);
expireLoading.start();
}
protected function browser_completeHandler(event:Event):void
{
cursorManager.removeBusyCursor();
}
protected function browser_htmlDOMInitializeHandler(event:Event):void
{
cursorManager.removeBusyCursor();
}
]]>
</fx:Script>
<s:states>
<s:State name="Loading"/>
<s:State name="Running"/>
</s:states>
<mx:Canvas includeIn="Running"
top="0" left="0" width="100%" height="100%"
label="Air Browser"
backgroundAlpha=".50"
backgroundColor="#999999"
borderVisible="false"
visible="true"
cornerRadius="25">
<components:TitleBar/>
<mx:HTML name="browser" id="browser"
htmlHost="{new CustomHost()}"
locationChange="browser_locationChangeHandler(event)"
htmlDOMInitialize="browser_htmlDOMInitializeHandler(event)"
complete="browser_completeHandler(event)"
paintsDefaultBackground="false"
includeIn="Running"
left="3" right="3" bottom="24" top="36"
contentBackgroundAlpha="0.0" />
</mx:Canvas>
<mx:VBox horizontalCenter="0" verticalCenter="0" includeIn="Loading" cornerRadius="15">
<s:VGroup top="0" left="0" id="splash">
<mx:Image x="0" y="0" source="@Embed('/assets/images/splash.png')"
width="100%"
height="100%"
creationCompleteEffect="Fade"/>
<mx:Text id="loadStatusMessage"
fontSize="12"
paddingLeft="50"
text="initializing..."
textAlign="center"/>
</s:VGroup>
</mx:VBox>
</s:WindowedApplication>
Just to break it down a little, when the application launches it will show a quick splash screen (used more later when we are starting Railo and setting up the environment), then when loaded it changes to the "Running" state which the HTML component that will be the view port for the web application. It then navigates to the url I provided.
Rather simple stuff, though for a newbie like myself there were some real hurdles to overcome. The main one is with spawning pop-up windows. With the HTML component, if you click a link within the HTML content that pops up a new window (either via anchor target="_blank", or javascript, etc...) it doesn't do anything. It doesn't complain with an error, it just doesn't do anything at all. After some digging in the docs and a few blogs I found that the common solution is to extend the HTMLHost class and override the createWindow function to create a new root window (createRootWindow()) containing an HTMLLoader and change the location to the originally requested URL. That works, but then again it doesn't. The new window does not have any ties to the parent so you'd have to jump through extra hoops to get javascript functions that interact with the parent to work. In order to get that working, I had to in my init() routine create a new root window, then hide the main Flex app. This allowed my javascript functions and pop-ups to work. However this method doesn't allow you to access any Flex, so if you wanted to provide say a Flex dialog it wouldn't let you.
Lots of complications.
Fortunately, I found an easier way. After trying all the methods I could find (using createRootWindow, traversing DOM to override window.open() calls, etc...) and each one coming up short, I found that by simply supplying a custom HTMLHost to the Flex HTML component it changed the behavior to open windows properly and keep a reference to the parent window. That's what the:
htmlHost="{new CustomHost()}"
is all about. Here's the com.airbrowser.utils.CustomHost.as in its entirety:
package com.airbrowser.utils
{
import flash.html.*;
public class CustomHost extends HTMLHost
{
}
}
See, I'm not overriding any functionality, just providing an extended HTMLHost class. Now, there is one quirk I haven't figured out yet (though it's not a big deal to my app so I haven't put much time into it): the newly created windows do not inherit the skin of the parent window. No matter what you try, it always uses the default OS chrome. Again, for me this is no big deal, but just be aware of that.
In wrapping this post up I should probably mention some things to watch for.
First, I found that javascript calls that relocate the browser (i.e. document.location) has to do so with a fully qualified URL (http://webaddress.com/file.htm); relative paths simply will do nothing. That was the only HTML/JS change I had to make to my existing app.
Second thing to watch out for is by default, when you click on a link or other element that changes the location inside the HTML component it does not give you any feedback until the new page is rendered. No progress bar, no busy cursor, etc... This can leave your user thinking nothing is happening and cause repeated clicks (obsessively repeated if your users are like mine ;) ). Notice what that I'm setting and removing the busy cursor in browser_locationChangeHandler() and browser_completeHandler(). This is a quick and dirty way to provide feedback. You could also create a flex dialog with a progress bar.
So that's it. My "client" which will access my already developed CF application. The next post I'll describe how I do that locally with an "embeded" Railo engine. There's a lot of details on that one so it'll be quite lengthy.