by Adobe
Adobe logo

Created

4 May 2011

 
Storing data offline

 
 

Download the sample code (ZIP, 17.6 MB)
 

Explanation

This sample project is for a Flex desktop application that syncs remote data with a local database on the client. To install and run the desktop application, download the sample files and follow the instructions to install and run the AIR applications. To look at the code, right-click on the dummy SWF in the browser and select View Source or download the sample files and follow the instructions to import the Flash Builder FXP. Multiple application versions are provided: an XML version that does not require a server as well as versions using Flash Remoting with PHP, Java, and ColdFusion servers.
 
 
Creating a desktop application
To create a desktop application with Flash Builder, you set the application type to desktop instead of web when creating the project. Two files are created: the main application MXML file and an application descriptor file. The root tag of the main application file is a WindowedApplication tag instead of the Application tag.
 
<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/mx" initialize="onInit()".../>
The application descriptor file is an XML file that is used by the AIR runtime (the cross-operating system runtime used to install, manage, and run AIR applications) when it launches the application in an operating system window. It has information about things like what the container operating system window should look like, what icon should be used for the application on the client computer, and more.
 
To build the application, you write your code and run and/or debug your application just as you do for a web application. When you run the application though, instead of launching an HTML page that embeds the SWF in a browser window, Flash Builder launches a tool called the Air Debug Launcher (ADL). The ADL allows you to run the application without first having to install it as a desktop application.
 
 
Customizing window chrome
When the user launches a Flex desktop application, the AIR runtime is launched and it creates an operating system window into which it loads the application SWF. By default, it creates a window with the default look and feel for the operating system. If you want to modify the appearance of this main window, you need to set this information in the application descriptor file, not in the SWF (the Flex application).
 
This application is not using a default operating system window. If you open the Flex4_5Trial_desktop_XML-app.xml file, you will see the systemChrome and transparent tags have been uncommented and set to none and true. This file also contains tags for setting how big the window is, where it appears, and if it can be resized, maximized, minimized, and more.
 
<initialWindow> <systemChrome>none</systemChrome> <transparent>true</transparent> (...) </initialWindow>
If you do not use the system chrome, the window gets its appearance from the default skin, WindowedApplicationSkin, for the WindowedApplication container. This skin has a status bar but no title bar, buttons, or gripper. To get these, you can use the SparkChromeWindowedApplicationSkin skin instead. This application uses a custom skin, XYZSkin. Skins and styles (the two ways to customize the look of your application) are introduced and explained in a later sample application.
 
<s:WindowedApplication skinClass="skins.XYZSkin" ...>
 
Manipulating windows
If you do not use the system chrome or the SparkChromeWindowedApplicationSkin skin, your application will not have any buttons to close, minimize, or maximize the application. If you are using your own skin, you will also not be able to resize or move the application window. To provide these functions, you need to add your own buttons and write code to manipulate the window.
 
Two buttons with custom skins have been used in this application. (Instead of creating your own button skins, you can also use skins located in the spark.skins.spark.windowChrome package.)
 
<s:Button id="closeBtn" label="Button" skinClass="skins.CloseButtonSkin" click="this.close()" x="672" y="50"/> <s:Button id="minimizeBtn" x="672" y="72" label="Button" skinClass="skins.MinimizeButtonSkin" click="this.minimize()"/>
When you click the close button, the close() method of the WindowedApplication is called. When you click the minimize button, the minimize() method is called. The WindowedApplication class has additional methods for restoring, maximizing, and setting the window order.
 
To move the window, you need to listen for when the user mouses down on some object in the application, the object you want the user to be able to mouse down on, drag, and drop to move the whole window.
 
protected function onInit():void{ (...) clickAreaForMovingWindow.addEventListener(MouseEvent.MOUSE_DOWN,onMove); }
This application has a Group called clickAreaForMovingWindow, which contains an invisible rectangle the size of the application, beneath all the other objects in the application. If the user clicks on this and not some other object above it like the DataGrid, the window will move.
 
<s:Group id="clickAreaForMovingWindow" left="0" right="0" top="35" bottom="0"> <s:Rect height="100%" width="100%"> <s:fill> <s:SolidColor alpha="0"/> </s:fill> </s:Rect> </s:Group>
If you listen for when the user clicks on the application instead:
 
this.addEventListener(MouseEvent.MOUSE_DOWN,onMove);
... when the user clicks on the scroll bar to scroll the DataGrid, the result would be to move the window instead of scrolling the data.
 
In the event handler, you call the startMove() method for the native operating system window, which starts a system-controlled move of this window.
 
function onMove(event:MouseEvent):void{ this.nativeWindow.startMove(); }
The nativeWindow property is an instance of the NativeWindow class, a reference to the actual operating system window, not the Flex container (WindowedApplication) being displayed in this window. The WindowedApplication class has many of the same methods as the NativeWindow class for manipulating the underlying native operating system window (such as close(), minimize(), and more), and for these actions you can call either method. Some methods such as startMove() though, are only defined for the NativeWindow class.
 
 
Monitoring connectivity
This application displays employee data. If the application is online, it retrieves the data from the server and then updates a local database on the client. That way if the user is offline, the local database can be used to retrieve and display the employee data.
 
To detect network connectivity, you use the URLMonitor class, which monitors the availability of an HTTP or HTTPS endpoint.
 
You define a URL Monitor instance:
 
protected var networkMonitor:URLMonitor;
... and then create an instance of the class, passing to it an instance of the URLRequest class with a url property (which you can set by passing it to the constructor) equal to the url of the endpoint you want to monitor the availability of.
 
protected function onInit():void{ networkMonitor=new URLMonitor(new URLRequest("http://www.adobe.com")); networkMonitor.addEventListener(StatusEvent.STATUS,onNetworkChange); networkMonitor.start(); (...) }
When you run the sample application, it will probably be using a local server that is always available, so for illustration purposes, the availability of the Adobe website is checked instead. That way you can test the application by taking your device running the application offline.
 
After creating the instance, you need to register to listen for its status event and then start the monitor.
 
networkMonitor.addEventListener(StatusEvent.STATUS,onNetworkChange); networkMonitor.start();
Now whenever the availability of that endpoint changes, either becoming available or unavailable, a status event is broadcast. Inside the status event listener, you check the value of the available property of the URLMonitor and execute the appropriate code.
 
protected function onNetworkChange(event:StatusEvent):void{ if (networkMonitor.available){ networkStatusLbl.text="Online"; networkStatusLbl.setStyle("color","green"); } else{ networkStatusLbl.text="Offline"; networkStatusLbl.setStyle("color","red"); } if(employees.length==0)getData(); }
This application has a Label:
 
<s:Label id="networkStatusLbl" text="Offline" color="red" top="104" right="34"/>
... whose text and color are changed appropriately.
 
By default, the server is polled for its availability immediately after start() is called and then whenever the network status changes (either going online, offline, or switching to another network). This means that when the application starts up, the server availability is checked. Inside the event handler, we can look to see if the employees collection has any records (indicating if any data has been retrieved from a remote or local store yet) and if it does not, have the application retrieve the data. If the server is available, retrieve the remote data. If it is not, check to see if the data has been stored locally (we'll take a look at how this database gets created later) and if it has not, display a message to the user.
 
protected function getData():void{ // if online, use remote data and update local database if(networkMonitor.available){ getRemoteData(); } //if offline, use local data if available else if(xyzdatabase.exists){ getLocalData(); } //if offline and no local data exists else{ Alert.show("You must be online to retrieve initial employee data","Message"); } }
 
Using Flash Remoting to retrieve remote data
If the server is available (which in a real application would be your Java, ColdFusion, or PHP server and not Adobe's server), the data is retrieved using Flash Remoting as shown in the previous sample applications. There is, however, one important difference between making Flash Remoting requests from a desktop instead of a web application. In a desktop application, you need to explicitly specify a full URL for the remoting endpoint, the class on the server that handles and brokers the remoting requests.
 
In the case of PHP, you actually did set the endpoint in the other web application samples; it was set to gateway.php, which assumed it was in the same directory on the server as the SWF. In a desktop application, you need to set the endpoint to the full URL. Of course, you could have used a full URL in the web application as well, but it makes it more flexible to move between development and production servers if you do not.
 
protected const DESTINATION:String="zend"; protected const ENDPOINT:String="http://localhost:10088/TestDrive/Flex4.5Trial_desktop-debug/gateway.php"; protected const SOURCE:String="EmployeeService";
For Java and ColdFusion, an endpoint was not specified; it was actually set dynamically based on the server that the SWF was served from. You can see this by looking at the services-config.xml file for ColdFusion:
 
<channel-definition id="my-cfamf" class="mx.messaging.channels.AMFChannel"> <endpoint uri="http://{server.name}:{server.port}{context.root}/flex2gateway/" class="coldfusion.flash.messaging.CFAMFEndPoint"/> <!—more code--> </channel-definition>
... or for Java:
 
<channel-definition id="my-amf" class="mx.messaging.channels.AMFChannel"> <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint"/> </channel-definition>
To make remoting requests, you need to set the endpoint property for the RemoteObject instance to:
 
  • http://{server.name}:{server.port}{context.root}/flex2gateway/ for ColdFusion or
  • http://{server.name}:{server.port}/{context.root}/messagebroker/amf for Java
...as shown in the example code here:
 
//For ColdFusion protected const DESTINATION:String="ColdFusion"; protected const ENDPOINT:String="http://localhost:8500/flex2gateway/"; protected const SOURCE:String="TestDrive.services.EmployeeService";
//For Java protected const DESTINATION:String="employeeService"; protected const ENDPOINT:String="http://localhost:8400/testdrive/messagebroker/amf"; protected const SOURCE:String="";
The source property is not needed for the Java remoting request, but is added so the same code can be shown in this sample for all three servers.
 
In this sample application, the service object and call responders are defined in ActionScript so they are only instantiated if the data is going to be retrieved remotely. Listeners for the result and fault events are registered and then the remote method, getEmployees(), is called.
 
protected function getRemoteData():void{ var employeeService:RemoteObject=new RemoteObject(); employeeService.destination=DESTINATION; employeeService.endpoint=ENDPOINT; employeeService.source=SOURCE; employeeService.showBusyCursor=true; var callResponder:CallResponder=new CallResponder(); callResponder.addEventListener(ResultEvent.RESULT,onRemoteData); callResponder.addEventListener(FaultEvent.FAULT,onRemoteDataError); callResponder.token = employeeService.getEmployees(); employeeService.getEmployees(); }
When the data is returned, the employees ArrayCollection is populated. If the data is not returned as an ArrayCollection of Employee objects, it is converted to one as shown here and discussed in a previous sample application.
 
protected function onRemoteData(event:ResultEvent):void{ //for Java employees=event.result as ArrayCollection; //for CF, createEmployeeCollection((event.result as ArrayCollection).source); //for PHP, createEmployeeCollection(event.result as Array); syncLocalData(); } protected function createEmployeeCollection(data:Array):void{ for(var i:uint=0; i<data.length; i++) { var emp:Employee=new Employee(); for each(var field:XML in describeType(emp)..accessor){ emp[field.@name]=data[i][field.@name]; } employees.addItem(emp); } }
If an error is returned from the server when attempting to retrieve the remote data, the application checks to see if the data exists locally and if so, retrieves that instead. (We'll get to how the local store is created next). Otherwise, a message is displayed to the user.
 
protected function onRemoteDataError(event:FaultEvent):void{ if(xyzdatabase.exists){ getLocalData(); } else{ Alert.show("Error retrieving employee data","Message"); } }
 
Creating a local database
After the data is retrieved from the server, a local database is created and populated with the employee data so the application can still be used when the user is offline. This is possible because the AIR runtime includes the open source SQLite database engine. (It also includes the Webkit browser engine). You need to create a database file, create the employees table, and then populate the table with the employee data.
 
The first step is to create the database file. You do this by creating an instance of the flash.filesystem.File class, which represents a path to a file or a directory, and specifying the file's name and location. In order to make the file's location operating system independent, you can specify its location relative to a particular folder on the computer: the application's private storage directory, the user's directory, the user's documents directory, or the user's desktop directory. These locations are accessible as static properties of the File class: applicationStorageDirectory, userDirectory, documentsDirectory, and desktopDirectory.
 
This application uses the resolvePath() method to create a file called xyz.db in the application's storage directory.
 
protected var xyzdatabase:File; protected function onInit():void{ (...) xyzdatabase=File.applicationStorageDirectory.resolvePath("xyz.db"); trace(File.applicationStorageDirectory.nativePath); }
The code includes a trace() statement so if you debug the application, you can see the location of this directory on your computer:
 
  • Mac: /Users/{user}/Library/Preferences/com.adobe.samples.Flex4Trial-desktop/Local Store
  • Windows Vista: C:\Users\{user}\AppData\Roaming\com.adobe.samples.Flex4Trial-desktop\Local Store
The unique folder name for the application is set from the value of the id tag in the application descriptor file. By default, this is set to the name of the project but you can set it to different value in the New Project wizard when creating the project or by editing the XML file after. A good practice is to use reverse domain name notation for the id as shown here to guarantee uniqueness.
 
<id>com.adobe.samples.Flex4Trial-desktop</id>
 
Creating a local database
The next step is to use the SQLite database engine to create a database. Classes in the flash.data package are used to work with local SQL databases. The SQLConnection class is used to manage the creation of and connection to local databases.
A SQLConnection object is declared and instantiated.
 
protected var sqlConn:SQLConnection; protected function onInit():void{ (...) sqlConn=new SQLConnection();> }
Then its open() method is used to open a synchronous connection to a database file at a specified location in the file system.
 
protected function syncLocalData():void{ sqlConn.open(xyzdatabase,SQLMode.CREATE); (...) }
The first argument is a reference to the database file and the second argument species to create and then open the database file if it does not already exist. Other possible values are read (SQLMode.READ) or update (SQLMode.UPDATE) which open an existing database for reading or updating. A database created using the open() method is automatically assigned the database name main.
 
Connections to databases can be opened synchronously (using the open() method) or asynchronously (using the openAsync() method). Synchronous operations execute in the main execution thread, so all application functionality (including refreshing the screen and allowing mouse and keyboard interaction) is paused while the database operation or operations are performed. For long-running operations this can cause a noticeable pause in the application. With asynchronous execution, you use event listeners or a responder to determine when an operation completes or fails—as you saw with the remoting requests. The operations run in the background rather than in the main application thread, so the application continues to run and respond to user interaction even while the database operations are being performed.
 
This application uses the synchronous open() method for the initial set up calls for the database because multiple SQL statements need to be executed: the table needs to be created and each of the employee records inserted. The code is shorter to perform all these transactions synchronously instead of daisy chaining together result handlers for asynchronous calls, but more importantly, you get much better performance if you group them all into a single transaction. A grouped transaction requires one write to disk of all the data instead of individual writes for each of the statements; for a grouped transaction, the AIR runtime makes all the changes in memory and then writes them to the database file all at once.
 
 
Creating a local database table
The database has now been created and the next step is create the table. You use the SQLStatement class to execute a SQL statement against a local SQL database that is open through a SQLConnection instance. To execute a statement with the SQLStatement class, you make an instance, set its sqlConnection property to the SQLConnection object managing the connection to the database, set its text property to the SQL text of the statement, and then call its execute() method.
 
Multiple statements are needed; one using CREATE TABLE to create the table and then an INSERT INTO statement for each of the employees.
 
To group all these statements into a single transaction, the SQLConnection begin() method is called before the first statement and the SQLConnection commit() method after the last. At this point, the changes are written to disk and the connection to the database is closed.
 
All of this code is wrapped inside a try/catch block. In synchronous mode, the SQL methods throw an SQLError object if the operation fails. When an error is thrown, a message is displayed.
 
protected function syncLocalData():void{ try{ sqlConn.open(xyzdatabase,SQLMode.CREATE); var sqlStatement:SQLStatement = new SQLStatement(); sqlStatement.sqlConnection = sqlConn; sqlConn.begin(); sqlStatement.text="CREATE TABLE IF NOT EXISTS employees (id INT PRIMARY KEY, firstname TEXT,lastname TEXT,title TEXT,departmentid INT,officephone TEXT,cellphone TEXT,email TEXT,street TEXT,city TEXT,state TEXT,zipcode TEXT,office TEXT,photofile TEXT);" sqlStatement.execute(); sqlStatement.text="DELETE FROM employees" sqlStatement.execute(); sqlStatement.text = "INSERT into employees (id,firstname,lastname,title,departmentid,officephone,cellphone,email,street,city,state,zipcode,office,photofile) VALUES (@id,@firstname,@lastname,@title,@departmentid,@officephone,@cellphone,@email,@street,@city,@state,@zipcode,@office,@photofile)"; for(var i:uint=0; i<employees.length; i++) { sqlStatement.parameters["@id"] = employees[i].id; sqlStatement.parameters["@firstname"] = employees[i].firstname; sqlStatement.parameters["@lastname"] = employees[i].lastname; //more statements for each property sqlStatement.execute(); } sqlConn.commit(); sqlConn.close(); } catch (error:SQLError){ Alert.show("Error saving employee data locally","Message"); sqlConn.close(); } }
Notice there is also a DELETE FROM statement in the middle of the transaction. Every time the application is launched, the remote data is retrieved and inserted in the local database to keep the data fresh. The existing data is deleted and replaced with the new data. If the amount of data is very large, you may want to use a different strategy for data management. Instead of writing this synchronization code yourself, you can use LiveCycle Data Services that takes care of storing your data locally and synchronizing the changes with the server when you go back online.
 
 
Retrieving local data
When the application launches, if the user is offline and the database exists, the data is retrieved from the local database.
 
protected function getData():void{ // if online, use remote data and update local database if(networkMonitor.available){ getRemoteData(); } //if offline, use local data if available else if(xyzdatabase.exists){ getLocalData(); } //if offline and no local data exists else{ Alert.show("You must be online to retrieve initial employee data","Message"); } }
To retrieve data from the local database, you use the same technique and classes used to create and insert data: the SQLConnection and the SQLStatement classes. In this case, only one SQL statement is going to be executed, so the database is opened asynchronously using the openAsync() method in read mode and event listeners are registered for the SQLStatement result and error events.
 
protected function getLocalData():void{ sqlConn.openAsync(xyzdatabase,SQLMode.READ); var sqlStatement:SQLStatement = new SQLStatement(); sqlStatement.sqlConnection = sqlConn; sqlStatement.addEventListener(SQLEvent.RESULT,onGetLocalData); sqlStatement.addEventListener(SQLErrorEvent.ERROR,onSQLError); sqlStatement.text="SELECT * FROM employees;" sqlStatement.execute(); }
If the data is retrieved successfully, employees is populated with the results of the SQLStatement and converted into a collection of Employee objects. To retrieve the results of a SQLStatement, you use the getResult() method of the SQLStatement class, which returns a SQLResult object that has a data property that contains an Array of the data returned as a result of the statement execution.
 
protected function onGetLocalData(event:SQLEvent):void{ sqlConn.close(); var data:Array=((event.currentTarget as SQLStatement).getResult() as SQLResult).data; employees=new ArrayCollection(); createEmployeeCollection(data); } protected function createEmployeeCollection(data:Array):void{ for(var i:uint=0; i<data.length; i++) { var emp:Employee=new Employee(); for each(var field:XML in describeType(emp)..accessor){ emp[field.@name]=data[i][field.@name]; } employees.addItem(emp); } }
If an error occurs, a message is displayed.
 
protected function onSQLError(event:SQLErrorEvent):void { Alert.show("Error retrieving local employee data","Message"); }
 
Customizing the file name and icon
If you packaged and installed the AIR application at this point, the name of the application would appear in the operating system as the name of the MXML file and a default application icon would be used to represent the application.
You specify custom names and icons in the application descriptor file. filename is used by the operating system and name is used by the AIR installer.
 
<id>com.adobe.samples.Flex45Trial-desktop-CF</id> <filename>XYZ Employee Directory</filename> <name>XYZ Employee Directory</name>
You uncomment and set the icon tag to specify a custom file icon. You provide multiple PNG files of various sizes so the operating system can select the one that most closely matches the size it needs. These images must be packaged into the AIR file.
 
<icon> <image16x16>icons/icon_16.png</image16x16> <image32x32>icons/icon_32.png</image32x32> <image48x48>icons/icon_48.png</image48x48> <image128x128>icons/icon_128.png</image128x128> </icon>
 
Using dock and system tray icons
When an installed AIR application is launched on a Mac, the dock icon will be the custom icon specified in the application descriptor file. All dock icons have options to Show, Hide, and Quit the application and if you click the icon, a minimized application is restored. Users can also choose to always keep the application in the dock.
 
To achieve a similar type of functionality on Windows, you create a system tray icon. You check to see if the operating system supports a system tray icon using the static supportsSystemTrayIcon property of the NativeApplication class. The NativeApplication is a reference to the AIR application.
 
protected function onInit():void{ (...) if(NativeApplication.supportsSystemTrayIcon)createWindowsTrayIcon(); }
If a system tray icon is supported, you create one by specifying an image for the icon. You reference the instance of the AIR application as the nativeApplication property of the NativeApplication class. This instance has an icon property that is an instance of the InteractiveIcon class, which has subclasses of DockIcon or SystemTrayIcon. The Interactive class has a property bitmaps that you set equal to an Array of BitmapData objects of different size (so the operating system can use the best size). The setting of this property causes a system tray icon to be displayed.
 
protected function createWindowsTrayIcon():void{ NativeApplication.nativeApplication.icon.bitmaps=[new Icon16().bitmapData,new Icon32().bitmapData,new Icon48().bitmapData,new Icon128().bitmapData]; (...) }
To get BitmapData objects for images, the images are embedded in the application:
 
[Embed("icons/icon128.png")]protected var Icon128:Class; [Embed("icons/icon48.png")]protected var Icon48:Class; [Embed("icons/icon32.png")]protected var Icon32:Class; [Embed("icons/icon16.png")]protected var Icon16:Class;
... and then new instances of these classes are created. The associated BitmapData object is contained in its bitmapData property. PNG files are typically used because they usually provide the best alpha blending.
 
new Icon16().bitmapData
A tool tip for the system tray icon is set using the tooltip property of the SystemTrayIcon class; the parent InteractiveIcon class does not have a tooltip property.
 
protected function createWindowsTrayIcon():void{ NativeApplication.nativeApplication.icon.bitmaps=[new Icon16().bitmapData,new Icon32().bitmapData,new Icon48().bitmapData,new Icon128().bitmapData]; var trayicon:SystemTrayIcon=(NativeApplication.nativeApplication.icon as SystemTrayIcon); trayicon.tooltip="XYZ Employee Directory"; (...) }
By default, the application is closed (exited) when the last window is closed. To get the system tray icon to always appear in the system tray, you change this behavior by setting the WindowedApplication (or NativeApplication.nativeApplication) autoExit property to false. To get the application to reopen, you can listen for when the user clicks on the system tray and reopen the window. (We'll add menu options for the icon next to do this as well.)
 
this.autoExit=false; trayicon.addEventListener(MouseEvent.CLICK,openApp);
In the event handler, the window is activated and made visible in case it's invisible.
 
protected function openApp(event:Event):void{ this.activate(); this.visible=true; }
In order for this to work, the window cannot be completely closed so we need to prevent the default behavior and just make the window invisible instead. To prevent the default behavior, you listen for the window's closing event which is broadcast when the close() method is called but before the window is closed.
 
this.addEventListener(Event.CLOSING,onClosing);
Inside the handler, you use the event object's preventDefault() method to prevent the default behavior and to instead make the window invisible.
 
protected function onClosing(event:Event):void{ event.preventDefault(); this.visible=false; }
Now the user can close the window and then click the system tray icon to reopen it. Putting it together, the createWindowsTrayIcon() method so far appears as shown here.
 
protected function createWindowsTrayIcon():void{ NativeApplication.nativeApplication.icon.bitmaps=[new Icon16().bitmapData,new Icon32().bitmapData,new Icon48().bitmapData,new Icon128().bitmapData]; var trayicon:SystemTrayIcon=(NativeApplication.nativeApplication.icon as SystemTrayIcon); trayicon.tooltip="XYZ Employee Directory"; this.autoExit=false; trayicon.addEventListener(MouseEvent.CLICK,openApp); this.addEventListener(Event.CLOSING,onClosing); (...) }
 
Adding menus
You use the NativeMenu and NativeMenuItem classes in the flash.display package to create a menu for a system tray icon or to create additional menu options for a dock icon. The string passed to the NativeMenuItem constructor is the text that appears in the menu.
 
var menu:NativeMenu=new NativeMenu(); var restoreMenuItem:NativeMenuItem=new NativeMenuItem("Restore"); restoreMenuItem.addEventListener(Event.SELECT,onMenu); menu.addItem(restoreMenuItem); var exitMenuItem:NativeMenuItem=new NativeMenuItem("Exit"); exitMenuItem.addEventListener(Event.SELECT,onMenu); menu.addItem(exitMenuItem);
You listen for the select event of each MenuItem and in the handler, execute the appropriate action. If Restore is selected, the window is activated and made visible. (This will work if the application was minimized or closed.) If Exit is selected, the application is exited by calling the exit() method of the NativeApplication instance.
 
protected function onMenu(event:Event):void{ if(event.currentTarget.label=="Restore"){ this.activate(); this.visible=true; } if(event.currentTarget.label=="Exit") NativeApplication.nativeApplication.exit(); }
The menu is added to the system tray icon by setting the SystemTrayIcon menu property to the Menu instance.
 
trayicon.menu=menu;
The complete createWindowsTrayIcon() method appears as shown here.
 
protected function createWindowsTrayIcon():void{ NativeApplication.nativeApplication.icon.bitmaps=[new Icon16().bitmapData,new Icon32().bitmapData,new Icon48().bitmapData,new Icon128().bitmapData]; var trayicon:SystemTrayIcon=(NativeApplication.nativeApplication.icon as SystemTrayIcon); trayicon.tooltip="XYZ Employee Directory"; this.autoExit=false; trayicon.addEventListener(MouseEvent.CLICK,openApp); this.addEventListener(Event.CLOSING,onClosing); var menu:NativeMenu=new NativeMenu(); var restoreMenuItem:NativeMenuItem=new NativeMenuItem("Restore"); restoreMenuItem.addEventListener(Event.SELECT,onMenu); menu.addItem(restoreMenuItem); var exitMenuItem:NativeMenuItem=new NativeMenuItem("Exit"); exitMenuItem.addEventListener(Event.SELECT,onMenu); menu.addItem(exitMenuItem); trayicon.menu=menu; }
To add additional menu items to a dock icon, you set the menu property of a DockIcon instance to the Menu instance. You can also assign a menu to the context menu of the application that appears when the user right-clicks on the application, to the application menu on a Mac, and to window menus on Windows.
 
 
Deploying an AIR application
When you are ready to deploy the application, you select Project > Export Release Build just as for a web application, but in this case, Flash Builder launches the ADT (the AIR Development Tool) to create a release build consisting of an AIR package file that includes the SWF file, an application descriptor file, assets, and more. The AIR file is the one you must distribute to your users. When you export, you must also associate a digital certificate from a trusted agency to sign the application since it will install on the user's computer with full permissions and have capabilities for interacting with their operating system.
 
In order to install an AIR application, users must have the AIR runtime installed. To provide a more seamless install experience for the user so they can install the application from a web page (instead of having to to download and install the AIR runtime and then download and install the AIR application), Adobe provides a default HTML file and badge.swf file which provides a template for letting users click a badge (a framed, customized image button) that checks and installs the runtime if necessary and then installs the AIR application. If you go the Adobe AIR Marketplace, you will see this is how all the applications are delivered.