This tutorial walks you through the sample application which is included in the deepsplink release. After completing the tutorial you will have learned how to leverage deepsplink to build a solid, well structured application with very little code. Besides you get a working sample with full deeplinking support, multilanguage-ready resource loading and authentication. The tutorial is written from a Eclipse with FDT point of view, but doesn't require these tools.
Note that this tutorial is subject to change. It always covers the latest deepsplink release which is currently 0.9.0
Setup
- Download the current deepsplink release.
- Create a new empty as3 flash project and name it deepsplink-sample. Now unzip the downloaded deepsplink release and copy the contents of the sample folder into the new empty deepsplink-sample project. This way you don't need to type any code :)
- Add the src folder as source folder (it contains the sample application source code)
- Add all swc files in the dependencies folder to the project build path (it contains swc code libraries on which the sample application depends, namely splinkresource, splinklibrary deepsplink and swfaddress)
- Duplicate the maxmc.properties file and rename it, then adjust the flexdir path of your copy to point to your Flex-SDK.
flexdir = D:/flexsdk
This file defines properties used in the build file which are likely to be different on your machine. Just duplicate the file and rename the copy so the file is named like your operating system username. After that open your properties file copy and adjust the flexdir path. The other properties defined in the file can remain unchanged. As my windows user name is maxmc, my properties file is named maxmc.properties. Open the ant view and add the build.xml file. The build.xml file contains the ant tasks which compile the sample application source code into sample.swf. The sample.swf files resides inside the deploy folder. So after you copied and adjusted the properties file, you should be able to compile the code inside the src folder into sample.swf. If you use eclipse, open the ant view via window -> show view -> ant, then click the add build file icon and select build.xml. Now run the compile task of the build.xml file, to verify that compilation via ant works. Configuration
index.html
- Open index.html in the deploy folder.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>Deepsplink Sample</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript" src="swfaddress.js"></script>
<style type="text/css">
html,body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="flash">
<p>You need JavaScript and the Adobe Flash Player Version 9.0.115.0+ </p>
<p>You can get Adobe Flash Player
<a href="http://www.adobe.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash&Lang=German">
here</a></p>
</div>
<script type="text/javascript">
swfobject.embedSWF("sample.swf", "flash", "100%", "100%", "9.0.115.0", "expressInstall.swf");
</script>
</body>
</html>Note that the current SWFObject and SWFAddress libraries are included and sample.swf is embedded via SWFObject. <script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript" src="swfaddress.js"></script>
swfobject.embedSWF("sample.swf", "flash", "100%", "100%", "9.0.115.0", "expressInstall.swf");If the user has no sufficient flash player version installed (< Version.9.0.115) or javascript is disabled, a link to the latest flash player download is shown. Otherwise SWFObject replaces the contents of the div with sample.swf. resourcebundles.xml
- Open resourcebundles.xml in the deploy/config folder.
<!DOCTYPE resourcebundles SYSTEM "resourcebundles.dtd">
<resourcebundles defaultLocale="de_DE">
<resourcebundle locale="de_DE">
<fonts path="fonts/">
<font id="Arial" type="default" src="arial.swf" filesize="103574" />
<font id="Arial Bold" type="default" src="arialbold.swf" filesize="103270" />
</fonts>
</resourcebundle>
</resourcebundles>This file configures the splinkresource framework to load fonts, assets and code libraries. Our resourcebundles.xml is quite simple. It only contains one resourcebundle node which defines two fonts. The splinkresource release contains a more extensive sample, which shows more configuration options. Each font node has four madatory attributes. <font id="Arial" type="default" src="arial.swf" filesize="103574" />
an id which is named like the class which extends Font and is compiled into arial.swf a type which is used to access the loaded font via ResourceProvider.getInstance().getFontDataByType("default") in our application a source which references the actual swf which contains the compiled font a filesize which is automagically injected into resourcebundles.xml each time we run the ant task to compile the application. This is done with an ant task named FileSizeInjector. <taskdef name="sizeinjector" classname="org.splink.ant.SizeInjectorTask"
classpath="org.splink.ant.filesizeinjectortask.jar" />
<target name="inject" description="Inject file sizes into the resources xml file">
<sizeinjector file="${resourcefile}" resourcebasedir="deploy" />config.xml
- Now open the config.xml file which contains the deepsplink configuration.
<!DOCTYPE config SYSTEM "config.dtd">
<config>
<pages>
<page id="login" clazz="pages.LoginPage" title="Login Page" />
<page id="404" clazz="pages.TestPage" title="404" />
<page id="home" clazz="pages.TestPage" title="Home Page">
<page id="page1" clazz="pages.TestPage" title="Page 1" />
<page id="page3" clazz="pages.TestPage" title="Page 3" />
</page>
<page id="page2" clazz="pages.TestPage" title="Page 2" request="init-show-hide-finalize">
<page id="page4" clazz="pages.TestPage" title="Page 4" />
<page id="page9" clazz="pages.TestPage" title="Page 9" />
</page>
<page id="page5" clazz="pages.TestPage" title="Page 5">
<page id="page8" clazz="pages.TestPage" title="Page 8">
<page id="page17" clazz="pages.TestPage" title="Page 17" />
</page>
</page>
</pages>
<home id="home" />
<notfound redirect="404" />
<protect redirect="login">
<page id="page8" />
</protect>
<overlay defaultBackingPage="home">
<page id="login" />
</overlay>
</config>The page nodes define deepsplink pages. A page is an encapsulated module which is instantiated by deepsplink. Each page needs to extend the Page class. <page id="page1" clazz="pages.TestPage" title="Page 1" />
Each page node has various mandatory attributes: an unique id through which the page is referenced a clazz attribute which defines the fully qualified classpath of the class which is instantiated for the page node a title which is the title of the page showing up in the browser address bar. an optional request which defines what strategy (IRequestBuilder) to use to transition from one page to another. There are currently 5 different request builders available, hide-initshow-finalize is the default IRequestBuilder which is used when no request is specified. - hide-initshow-finalize - hide-init-show-finalize - initshow-hide-finalize - init-show-hide-finalize - init-hide-show-finalize these request builder names indicate what they do. for instance hide-initshow-finalize first hides all opened pages and then initalizes and shows the pages which are to be opened one by one. Eventually it finalizes the hidden pages. Ah, there is no 'best' IRequestBuilder. It depends on how you want the transition from page to page. In our example app all pages are processed according to the default IRequestBuilder exept the pages beneath page2 which are processed by the init-show-hide-finalize IRequestBuilder. The home node defines the application's home page, which is opened when you launch the sample without a deeplink. <home id="home" />
The notfound node defines via it's redirect attribute which page is opened if a deeplink points to a page which doesn't exist. <notfound redirect="404" />
The protect node defines pages which are only accessible to authenticated users. If a user who is not authenticated tries to access a protected page, the user is redirected to the page defined by the redirect attribute of the protect node. Note that the page to which is redirected needs to implement the IAuthenticationPage interface. (more on that later) <protect redirect="login">
<page id="page8" />The overlay node defines pages which are added on top of other pages. Overlay pages may be nested, but their parent pages need to be overlay pages, too. <overlay defaultBackingPage="home">
<page id="login" />The protect and overlay configuration options are not mandatory as most applications don't need authentication or overlay pages. The overlay node carrys the additional defaultBackingPage attribute which defines the page which is to be opened if an overlay page is the first page and thus has no page in the background. When deepsplink initializes, it reads the configuration file and creates for each page node an instance of the defined clazz. It furthermore provides the instance with the id and title of the node. Protected pages and overlay pages are internally marked and deepsplink handles them witout any further work. Only the authentication page defined via redirect attribute needs andditional care. Probably the best way to get familiar with this, is to experiment and modify the structure and watch how the application changes. Luckily you don't need to recompile the application to see the changes, a reload suffices.
Code
SampleApplication
- Go to the src folder and open the SampleApplication.as file which is the application main class.
package {
public class SampleApplication extends Sprite {
private var _progress : TextField;
public function SampleApplication() {
TestPage;
LoginPage;
scrollRect = new Rectangle(0, 0, 550, 400);
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.stageFocusRect = false;
stage.addEventListener(Event.RESIZE, center);
initialize();
center();
}
private function center(e : Event = null) : void {
x = stage.stageWidth / 2 - width / 2;
y = stage.stageHeight / 2 - height / 2;
}
private function initialize() : void {
addChild(_progress = TextFieldFactory.singleline("...", 12));
var q : IQ = new Q();
q.register(QCompleteEvent.COMPLETE, function(e : QCompleteEvent) : void {
removeChild(_progress);
});
q.register(QErrorEvent.ERROR, function(e : QErrorEvent) : void {
trace(e.errorMessage);
});
var resourceProcessor : ResourcebundleProcessor = new ResourcebundleProcessor("config/resourcebundles.xml");
resourceProcessor.register(ResourcebundleProcessorProgressEvent.PROGRESS, onProgress);
q.add(resourceProcessor);
q.add(new ExternalConfigBootstrapper("config/config.xml", new BootStrategy(this)));
q.start();
}
private function onProgress(e : ResourcebundleProcessorProgressEvent) : void {
var progress : ResourceProgress = e.getResourceProgress();
_progress.htmlText = "Loading: " + progress.getId() + " " + progress.getTotalPercent() + "%";
}In the constructor are all page classes referenced which we defined in our config.xml. This makes sure that the compiler includes them. This is necessary as the classes are otherwise only referenced in the deepsplink config.xml file. public function SampleApplication() {
TestPage;
LoginPage;Then some stage properties are set and scrollrect is applied to the root Sprite to enable early centering. scrollRect = new Rectangle(0, 0, 550, 400);
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.stageFocusRect = false;
stage.addEventListener(Event.RESIZE, center);
The initialize method contains the code to preload the files defined within resourcebundles.xml and to initialize deepsplink. To display the overall resource loading progress percentage we register a listener. After completion of the resource loading the deepsplink initialization starts. Now let's step through the code lines. First a TextField is created from a TextFieldFactory, added to the displaylist and stored in a class field. Note that TextFieldFactory is just a convenience class for the sample. addChild(_progress = TextFieldFactory.singleline("...", 12));Next we create a Q instance. The queue can process asynchronous operations consecutively. These operations can be nested. As the Q solves the problem to simplify and abstract asynchronous operations which are very frequent in flash applications I highly recommend to read more about it... Then two functions are registered. QCompleteEvent.COMPLETE is executed when the queue completes, and QErrorEvent.ERROR is executed if an error occurs somewhere in the enqueued objects. var q : IQ = new Q();
q.register(QCompleteEvent.COMPLETE, function(e : QCompleteEvent) : void {
removeChild(_progress);
});
q.register(QErrorEvent.ERROR, function(e : QErrorEvent) : void {
trace(e.errorMessage);
});Next we create the objects which are actually put into the Q. First we add a ResourcebundleProcessor which expects the path to our resourcebundles.xml file and processes the resources it defines. Additionally the processor informs via ResourcebundleProcessorProgressEvent.PROGRESS event about the progress of the resource loading. var resourceProcessor : ResourcebundleProcessor = new ResourcebundleProcessor("config/resourcebundles.xml");
resourceProcessor.register(ResourcebundleProcessorProgressEvent.PROGRESS, onProgress);
private function onProgress(e : ResourcebundleProcessorProgressEvent) : void {
var progress : ResourceProgress = e.getResourceProgress();
_progress.htmlText = "Loading: " + progress.getId() + " " + progress.getTotalPercent() + "%";The event object even includes information on the overall progress. That is, because of the SizeInjectorTask in our ant build file. The task is executed when you compile the application and it injects the resourcebundles.xml file with the sizes of the resource files defined in it. Now we add ResourcebundleProcessor instance to the Q. This is possible because ResourcebundleProcessor implements the IQable interface. q.add(resourceProcessor);
Then we create an ExternalConfigBootstrapper which of course also implements IQable and also add it to the Q. q.add(new ExternalConfigBootstrapper("config/config.xml", new BootStrategy(this)));ExternalConfigBootstrapper is part of deepsplink. It expects the path to the deepsplink config.xml file and an object which implements the IBootStrategy interface. So after the ResourceProcessor has completed loading and processing the resources, DeepsplinkBootstrapper loads and processes our deepsplink configuration file and builds an IDeepsplinkFacade from it. Then this facade is promoted to our IBootStrategy implementation. At last we invoke start on the Q to get things rolling... q.start();
BootStrategy
- Open the BootStrategy file which is our IBootStrategy implementation.
package {
public class BootStrategy implements IDeepsplinkBootStrategy {
private var _display : DisplayObjectContainer;
public function BootStrategy(display : DisplayObjectContainer) {
_display = display;
}
public function boot(facade : IDeepsplinkFacade) : void {
var navi : IPageNavigationController = PageNavigationFactory.create(null, facade.tree, facade.navigationCommand);
_display.addChild(navi.addView(new NavigationView(["404"])).display);
_display.addChild(facade.display).y = 30;
facade.registerAuthenticator(new MockAuthenticator());
}Look at the boot method. After it is executed deepsplink instantiates the pages and shows the first page (either the home page or the page defined via deeplink). So the boot method is the place to hook into deepsplink before pages are instantiated. First we need to register a main navigation to enable users to access the pages we have configured. A convenient option is to let deepsplink do most of the work. To do that we just need to implement a view which renders the navigation. This can be achieved by creating an IPageNavigationController via the create method of the PageNavigationFactory class. var navi : IPageNavigationController = PageNavigationFactory.create(null, facade.tree, facade.navigationCommand);
The create method expects three parameters: the id of the page which is the common parent page of the pages referenced by the navigation. In our case the id is null because we want to display the root pages which don't have a parent page. an instance of Tree which is a object representation of the page structure we defined in config.xml. an INavigationCommand implementation which provides methods to request page changes. You can obtain the navigation command and the tree from IDeepsplinkFacade. Now we add our view implementation to the displaylist and navigation controller. So our view is now registered with deepsplink and notified if a page change occurs that affects our navigation. _display.addChild(navi.addView(new NavigationView(["404"])).display);
One of the root pages is the notfound page we configured with the id 404. This page is shown, if deepsplink can't resolve a page request. Because we don't want the 404 page to show up in our navigation, we pass the 404 page id to our view to be able to exclude it from rendering. Next we need to add the facade.display to the displaylist, because it contains the page displays. _display.addChild(facade.display).y = 30;
Because we have configured protected pages we need to register an IAuthenticator implementation with the facade. Our implementation is MockAuthenticator, but more on that later. facade.registerAuthenticator(new MockAuthenticator());
IDeepsplinkFacade offers a lot more options, but to keep things simple that's it for now. For an impression what else is possible have a look at IDeepsplinkFacade in the deepsplink API docs NavigationView
- Open NavigationView, our IPageNavigationView implementation which is located in the navigation package.
package navigation {
public class NavigationView implements IPageNavigationView {
private var _display : DisplayObjectContainer;
private var _controller : IPageNavigationController;
private var _btns : Array;
private var _excludeIds : Array = new Array;
public function NavigationView(excludeIds : Array = null) {
if(excludeIds) {
_excludeIds = excludeIds;
}
_display = new Sprite();
}
public function setController(controller : IPageNavigationController) : void {
_controller = controller;
}
public function render(subpageIds : Array) : void {
_btns = new Array;
var nextX : int = 0;
for each (var id : String in subpageIds) {
if(!ArrayUtils.contains(_excludeIds, id)) {
var btn : Btn = new Btn(id, id);
btn.addEventListener(MouseEvent.CLICK, onClick);
_display.addChild(btn.display).x = nextX;
nextX = btn.display.x + btn.display.width + 5;
_btns.push(btn);
}
}
}
private function onClick(event : MouseEvent) : void {
_controller.gotoPage(Btn(event.target).id);
}
public function select(id : String, pageParams : Array = null, changed : Boolean = true) : void {
for each (var btn : Btn in _btns) {
if(btn.id == id) {
btn.mark();
} else btn.unmark();
}
}
public function finalize() : void {
_btns.splice(0, _btns.length);
}
public function get display() : DisplayObject {
return _display;
}Firstly it stores the constructor parameter excludeIds in a class field. Then it creates and stores the DisplayObjectContainer display in which the view is rendered. public function NavigationView(excludeIds : Array = null) {
if(excludeIds) {
_excludeIds = excludeIds;
}
_display = new Sprite();The setController method stores a reference to the IPageNavigationController which we created in our BootStrategy via PageNavigationFactory. public function setController(controller : IPageNavigationController) : void {
_controller = controller;The render method receives subpageIds, an Array of Strings which contains the id_s of the pages the view needs to display. public function render(subpageIds : Array) : void {Now we iterate over the Array and create a Btn instance for each of these id's. Next we add an event listener to the button so a method named onClick is called when the button is clicked. Then we add the button display to the display we created earlier in the constructor and set the x position of the button display, so the buttons are rendered side by side. Finally we store a reference to the Btn instance an the btns Array we created in the first line of the render method. for each (var id : String in subpageIds) {
if(!ArrayUtils.contains(_excludeIds, id)) {
var btn : Btn = new Btn(id, id);
btn.addEventListener(MouseEvent.CLICK, onClick);
_display.addChild(btn.display).x = nextX;
nextX = btn.display.x + btn.display.width + 5;
_btns.push(btn);
}The onClick method typecasts the target of the arriving event object to Btn, retrieves it's id and executes the gotoPage method of the controller with the id of the button (button and page have the same id) as first parameter. private function onClick(event : MouseEvent) : void {
_controller.gotoPage(Btn(event.target).id);This effects in a page change from the current page to the page with the id of the clicked button. When the gotoPage method finished executing and the page request is valid, the select method of our view is called with the id of the button which maps to the current page id. This gives our view the opportunity to reflect the change. public function select(id : String, pageParams : Array = null, changed : Boolean = true) : void {
for each (var btn : Btn in _btns) {
if(btn.id == id) {
btn.mark();
} else btn.unmark();In our case we iterate through the Btn references stored in our btns Array and invoke the mark method on the button which maps to the given id and invoke unmark on all other buttons. Page changes can be requested from several places in our application and also may be requested from outside our application, for instance through the browser back button or a change in the browser address bar. So no matter where the page change request originated, our IPageNavigationView_s select method is informed when it is affected by the request. Affected means that one of it's buttons references the requested page or a child page. Our view also contains a finalize method. It is called by IPageNavigationController if the controllers finalize method is executed. For this view instance this never happens, because we create the controller within BootStrategy and never finalize it. But we are going to reuse the view in other places,.. public function finalize() : void {
_btns.splice(0, _btns.length);So the finalize method is the place to release the resources which were used in the view. Open the Btn class Btn is a class which renders a TextField. If a click happens it dispatches a MouseEvent.CLICK event. It also listens for rollover and rollout events and renders the text depending on the event in bold or normal style. Btn also contains the mark method which rollovers and locks the button and the unmark method which unlocks and rollouts the button. TestPage
- Open the TestPage in the pages package.
package pages {
public class TestPage extends Page {
public function TestPage(supplier : IPageSupplier) {
super(supplier);
setup();
}
private function setup() : void {
var layer : uint = TreeUtils.getNodeById(id, tree.getRootNode()).getLayer();
display.y = 60 * (layer - 1);
var font : FontData = ResourceProvider.instance().getFontDataByType("default");
var bg : DisplayObject =
display.addChild(new Bitmap(new BitmapData(550, 300 - display.y, false, 12000 + layer * 12000)));
var tf : DisplayObject = display.addChild(TextFieldFactory.singleline("<b>" + title + "</b>", 12, font));
bg.alpha = tf.alpha = 0;
var navi : DisplayObject = createNavigation();
setShowStrategy(new ShowStrategy(id, [bg, tf, navi]));
setHideStrategy(new HideStrategy(id, [navi, tf, bg]));
}
private function createNavigation() : DisplayObject {
var navi : IPageNavigationController = PageNavigationFactory.create(id, tree, navigationCommand);
var view : IPageNavigationView = navi.addView(new NavigationView());
display.addChild(view.display).alpha = 0;
view.display.x = 100;
return view.display;
}
override public function initialize() : void {
setInitialized();
}
override public function finalize() : void {
}TestPage is a class which renders a page. If you remember, we defined a structure of pages within the deepsplink config.xml file. For the nodes with their clazz attribute pointing to TestPage an instance of this TestPage is created. In order to be a Page, TestPage extends the Page class which is part of deepsplink. public class TestPage extends Page {A Page is only instantiated once, and that instantiation happens at deepsplink startup. The TestPage receives an IPageSupplier instance as constructor argument and needs to pass it to it's super class Page.
public function TestPage(supplier : IPageSupplier) {
super(supplier);- After that TestPage may perform all operations which only need to take place once. In our TestPage implementation this is done in the setup method. As TestPage is a subclass of Page, it inherits several methods, most of them are declared to be final. All non final methods are designed to be overridden in subclasses. These are the methods onParamsChanged, initialize and finalize. Most of the methods declared as final provide page relevant data or operations.
- In our setup method we first retrieve the layer of our page which defines how many parent pages our page has. Then we move the y position of page display accordingly.
private function setup() : void {
var layer : uint = TreeUtils.getNodeById(id, tree.getRootNode()).getLayer();
display.y = 60 * (layer - 1);Then we retrieve a FontData instance from a singleton ResourceProvider which is part of splinkresource and provides access to the resources we configured in the resourcebundles.xml file. var font : FontData = ResourceProvider.instance().getFontDataByType("default");FontData is the object representation of the font nodes in our resourcebundles.xml. We use the FontData with our TextFieldFactory to conveniently build TextField instances. In our case a single line TextField which renders the title ot the page using the font Arial Bold. TextFieldFactory.singleline("<b>" + title + "</b>", 12, font)Now we add the TextField and a Bitmap background to the page display and set their alpha value to 0 as they need to be invisible at page initialization time. After that we create a navigation view just as we did in our BootStrategy implementation and add it to the page display. var navi : DisplayObject = createNavigation();
private function createNavigation() : DisplayObject {
var navi : IPageNavigationController = PageNavigationFactory.create(id, tree, navigationCommand);
var view : IPageNavigationView = navi.addView(new NavigationView());
display.addChild(view.display).alpha = 0;
view.display.x = 100;
return view.display;The next thing to do is to set two IQable implementations (remember the Q) via the setShowStrategy and setHideStrategy methods which our page inherited from Page. setShowStrategy(new ShowStrategy(id, [bg, tf, navi]));
setHideStrategy(new HideStrategy(id, [navi, tf, bg]));Our show strategy implementation tweens the alpha value of the given DisplayObject instances, in our case these are bg, tf and navi) from 0 to 1. The show strategy is executed after you complete the page initialization by calling the setInitialized method. Our hide strategy does the reverse and tweens their alpha value from 1 to 0. The hide strategy is executed before the finalize method is executed. So just before the initialize method is called by deepsplink, the page display is added to the displaylist. This makes initialize the place to handle preloading, to add components which depend on the presence of the stage, or to initialize objects which are expensive during their runtime. If your initialization process is complete, you must call the setInitialized method which TestPage inherits from page. This enables you to manage asynchronous initialization operations. After your setInitialized call deepsplink proceeds processing. This means if you forget to invoke setInitialized, the application hangs! setInitialized();
After page initialization is done, the show strategy we set earlier is executed and the page fades in. If the user leaves the the page because of a page change request the hide strategy is executed and fades out the page. Finally after the new pages are initialized and shown, the finalize method is invoked by deepsplink. Here we can release resources, stop running operations and prepare the page to be ready for the next initialization. As TestPage is initialized directly after instantiation and nothing is done in the initialized method, there is nothing to finalize. ShowStrategy/HideStrategy
- Open the ShowStrategy located in the page.transitions package.
package pages.transitions {
public class ShowStrategy extends Qable {
private var _displays : Array;
private var _tween : QTween;
public function ShowStrategy(id : String, displays : Array) {
super(id);
_displays = displays;
}
override protected function doStart() : void {
_tween = new QTween();
_tween.register(QCompleteEvent.COMPLETE, onComplete);
var count : uint = 0;
for each (var display : DisplayObject in _displays) {
_tween.add(
new QTweenTarget(display))
.duration(300).property(QTweenConst.ALPHA).to(1).delay(count * 200);
count++;
}
_tween.start();
}
private function onComplete(e : QCompleteEvent) : void {
_tween.finalize();
complete();
}
override protected function doFinalize() : void {
if(_tween) {
_tween.finalize();
_tween = null;
}
}ShowStrategy extends Qable, which is a convenicence base IQable implementation public class ShowStrategy extends Qable {It receives an Array of DisplayObject instances which are to be shown. If the corresponding page is initialized, the doStart method of our ShowStrategy is executed. In doStart we create a QTween instance and add a QCompleteEvent.COMPLETE listener to get informed when the tween completes _tween.register(QCompleteEvent.COMPLETE, onComplete);
The we add a QTweenTarget for each of the DisplayObject instances and defines how our display instances are tweened. for each (var display : DisplayObject in _displays) {
_tween.add(new QTweenTarget(display)).duration(300).property(QTweenConst.ALPHA).to(1).delay(count * 200);Finally we start the tween _tween.start();
After QTween completed we invoke the complete method which is inherited from Qable to notify deepsplink about the show strategy completion private function onComplete(e : QCompleteEvent) : void {
complete();Page lifecycle
- The order in which the pages are initialized, shown, hidden and finalized depends on deepsplink's internal IRequestBuilder implementations. There are several IRequestBuilder implementations (see config.xml, request section) which offer different page request processing orders. The default IRequestBuilder implementation is HideInitializeShowFinalize, it
- hides all pages in succession which need to be finalized for the current request
- initializes all pages in succession which need to be shown for the current request
- shows all pages in succession which need to be shown for the current request
- finalizes all pages in succession which need finalized for the current request
- For simplicity reasons our TestPage doesn't override the onParamsChanged method. But as this is a powerful feature here is a basic description what you can do with page parameters and how you can use them:
- Page parameters allow pages to render themselves different depending on the parameters. If for instance the parameters of the browser address bar are changed, the onParamsChanged method of all currently open pages is executed and provided with the current parameters.
- Conveniently the Page class offers a method named getParamValueForKey which enables us to retrieve the value of a page parameter. The page parameters can also be changed from within a page. The Page class provides an INavigationCommand with a gotoPage method, which takes and Array of PageParameter instances as second argument.
- For more details on this I suggest to read about page parameters in the deepsplink API docs.
LoginPage
- Open the LoginPage in the pages package.
package pages {
public class LoginPage extends Page implements IAuthenticationPage {
private var _authenticator : IAuthenticator;
private var _securityInterceptor : IInterceptor;
private var _btn : Btn;
public function LoginPage(supplier : IPageSupplier) {
super(supplier);
setup();
}
private function setup() : void {
display.x = 150;
display.y = 30;
display.addChild(new Bitmap(new BitmapData(250, 200, false, Math.random() * 1674743789), "auto", true));
setShowStrategy(new ShowStrategy(id, [display]));
setHideStrategy(new HideStrategy(id, [display]));
}
public function set authenticator(authenticator : IAuthenticator) : void {
_authenticator = authenticator;
_authenticator.register(AuthenticatorEvent.SUCCESS, onAuthenticationSuccess);
}
public function set securityInterceptor(securityInterceptor : IInterceptor) : void {
_securityInterceptor = securityInterceptor;
}
override public function initialize() : void {
if(!_authenticator.isAuthenticated()) {
_btn = new Btn("login", "Login");
_btn.addEventListener(MouseEvent.CLICK, onLogin);
} else {
_btn = new Btn("logout", "Logout");
_btn.addEventListener(MouseEvent.CLICK, onLogout);
}
display.addChild(_btn.display).y = 40;
setInitialized();
}
private function onLogout(e : MouseEvent) : void {
_authenticator.authenticate(null);
navigationCommand.gotoPage("home");
}
private function onLogin(e : MouseEvent) : void {
_authenticator.authenticate(null);
}
private function onAuthenticationSuccess(e : AuthenticatorEvent) : void {
navigationCommand.gotoPage(_securityInterceptor.getLastInterceptedId());
}
override public function finalize() : void {
display.removeChild(_btn.display);
}If you remember, we defined protected pages through the protect node in our deepsplink config.xml file. It's redirect attribute points to our LoginPage which is shown in case a page protected page is requested. Deepsplink then queries the IAuthenticator implementation which we registered earlier in our BootStrategy whether the user is authenticated. If the user is not authenticated, deepsplink redirects the request to our LoginPage. As it is defined to be the page which is responsible for authentication, it implements the IAuthenticationPage interface, which is mandatory for pages wishing to get referenced via the redirect attribute. public class LoginPage extends Page implements IAuthenticationPage {LoginPage implements the authenticator method and receives our IAuthenticator implementation. (Remember the MockAuthenticator we registered in BootStrategy) Next we register a listener for it's AuthenticatorEvent.SUCCESS event. public function set authenticator(authenticator : IAuthenticator) : void {
_authenticator = authenticator;
_authenticator.register(AuthenticatorEvent.SUCCESS, onAuthenticationSuccess);LoginPage also implements the securityInterceptor method. The interceptor which is injected here by deepsplink has one particulary interesting method for our scenario. It's named getLastInterceptedId and returns the id of the page which has been requested but redirected here. This enables to change to this page on successful authenthication, which happens in the onAuthenticationSuccess method. private function onAuthenticationSuccess(e : AuthenticatorEvent) : void {
navigationCommand.gotoPage(_securityInterceptor.getLastInterceptedId());In the initialize method we create a button, which is labeled login or logout depending on the user's authentication state. override public function initialize() : void {
if(!_authenticator.isAuthenticated()) {
_btn = new Btn("login", "Login");
_btn.addEventListener(MouseEvent.CLICK, onLogin);
} else {
_btn = new Btn("logout", "Logout");
_btn.addEventListener(MouseEvent.CLICK, onLogout);If the user is not authenticated the login button is created and if is clicked, MockAuthenticator's authenticate method is called. In a real world scenario we would pass in some data obtained from the user instead of null private function onLogin(e : MouseEvent) : void {
_authenticator.authenticate(null);If you take a look at the MockAuthenticator code you see why null suffices in our case. override public function authenticate(data : *) : void {
_isAuthenticated = !_isAuthenticated;
distributeSuccessEvent();As we added the button in the initialize method to the display, we need to remove it in the finalize method. Otherwise each time the LoginPage is initialized another button is added to it's display but never removed. override public function finalize() : void {
display.removeChild(_btn.display);If you remember, the LoginPage is also declared to be an overlay page which means that it lives outside the normal page hierarchy and is always displayed in front of all non overlay pages. Next steps
Now we have covered the sample application you might want to experiment and
- add another page class
- change the page structure
- add some assets
- add a more extensive main navigation
- add a tracking processor
- inject the ResourceProvider singleton instance into pages to get rid of the servicelocator alike pattern
- add logging
- add UI testing capabilities
- use page parameters