Binding the selectedChild of a ViewStack in a Flex MVC structure

Binding the selectedChild of a ViewStack in a Flex MVC structure

Posted 3 May 2009 by Russ in Adobe Flex. Tags: , , .

When coding in Adobe Flex 3 within an MVC framework, at some point you might discover you need to update a ViewStack or TabNavigator as the Model changes. Here’s a simple way to do it.

The scenario

In any Flex application you’re likely to have a number of views (which I’ll call pages for this example), and some form of navigation that enables you to switch from one page to another.

Even if you application is tiny you’re still going to be splitting your application into components and the more you do this, the more valuable bindings become. If you have all your code in the application file for example, you can get away with writing ActionScript that directly references objects in the View and update them. But once you have split your code into components it becomes much more difficult to target component objects in lower down in your package structure (think "child.child.child.child" – not a good idea).

Of course, that’s not how MVC works and it’s exactly what we use data bindings for in Flex. In an MVC pattern, the Model stores the data of the application, the View contains the presentation interface, and the Controller takes care of communication between the two.

For this example, we have a simple application with a navigation bar (a ToggleButtonBar), a stack (ViewStack) of five pages and a set of five tab (TabNavigator)s, each containing a page.

When we click on the navigation bar, our example will update both the stack of pages and the tabs so that the page associated with the current navigation item is displayed. This is shown here and the code is explained in step-by-step detail below. You can download and edit the source code, or right-click on the demo below and choose View Source to see how it all fits together.

You need Flash Player 10 to view this content. You can download this for free from the Adobe website. If you’re using a feed reader you could try loading this post in your web browser.

The problem

The TabNavigator and ViewStack components have selectedChild and selectedIndex properties so at first glance, it would seem reasonable to bind the selectedChild property to the Model so that as you update you click on your app’s navigation, the selectedChild changes.

However the selectedChild expects a Container as its value and you’re more likely to have a string value recorded in the Model (examples might be something like "sectionHome", "sectionProducts", sectionUsers"). The task therefore is to respond to changes to this string value in the Model and select the relevant child Container in the TabNavigator or ViewStack, so we need to be able to match the current value of that string with the relevant view (perhaps matching the string "checkout" to the view that displays the application checkout).

In a more complex application your View elements are going to be fairly complex, with numerous children objects that themselves have children, and so on. Although the structure in this example is very simple, the principle is the same as a very large project in that at the component level, the ViewStack and TabNavigator objects have no knowledge of the ToggleButtonBar and so communicating directly between the two isn’t going to work.

In Flex we use events to communicate throughout the application but these events only travel in one direction: they bubble upwards from the component to the application. But as the TabNavigator and ViewStack components may not be instantiated at the root of the application, we can’t simply work from the top down and communicate changes from the application down to each component. So for this, we’ll need to use data bindings on the ViewStack and TabNavigator.

A note before we start

This article isn’t intended to be an example of MVC as such – I’m simply building a simple MVC structure here as the theory could apply to any MVC framework, such as Cairngorm, Mate or pureMVC. However to plug in one of those frameworks in this example is perhaps a little overkill.

Jump straight to the solution if you want to skip the MVC section.

Example data

To start with we need some data and for this example we have a number of ‘pages’ in the application. So for this we have a simple Page Value Object with name and title properties:

package valueObjects
{
 /**
 * Basic Value Object for an example Page object
 */
 [Bindable]
 public class Page
 {
    /**
    * name and title properties
    */
    public var name:String;
    public var title:String;

    /**
    * Constructor function
    */
    public function Page()
    {
    }
  }
} 

Events

To communicate the fact that a user has clicked on an item in the navigation bar, we need an event. And for this we have a custom event that bubbles up to the application root, taking with it an instance of the item that was clicked on. The object expected is Page Value Objects:

package events
  {
  import flash.events.Event;
  import valueObjects.Page;

  public class NavigationEvent extends Event
  {

    /**
    * A single event type that can be triggered
    */
    public static const DISPLAY_PAGE:String = "navigationEventDisplayPage";

    /**
    * The Page object associated with this event
    */
    public var selectedPage:Page;

    /**
    * Constructor function calls the super class constructor and assigns the ExampleItem to the event
    * NB > sets the bubbles property to true so that this event can be captured at the root
    * of the Application
    */
    public function NavigationEvent(type:String, selectedPage:Page, bubbles:Boolean=true, cancelable:Boolean=false)
    {
    super(type, bubbles, cancelable);
    this.selectedPage = selectedPage;
    }

  }
}

The Model

The place to store our data is the Model and for this example I’ve created a simple class called Model:

In the Model we have a constructor function and then a property which will contain an instance of the Model class, an ArrayCollection property that will hold our example data, and a String property that will contain a string value that represents the page that’s in view at any time:

package components
  {
  import mx.collections.ArrayCollection;
  import valueObjects.Page;

  [Bindable]
  public class Model
  {

    /**
    * Constructor function
    */
    public function Model()
    {
    }

    /**
    * Instance of the Model
    */
    public static var _instance:Model = null;

    /**
    * ArrayCollection to hold the list of pages in this example
    */
    public var data:ArrayCollection = new ArrayCollection();

    /**
    * Property to record the current selected view in the navigation
    */
    public var selectedPageName:String;

The only other code we have in this example is the getInstance() function. When this function is first invoked, a new instance of the Model is created and stored in the _instance property. If getInstance() is then called again, the existing instance is returned, which should mean that we only ever have one instance of our data.

    /**
    * Creates an instance of the Model class or
    * returns the existing instance
    */
    public static function getInstance():Model
    {
      if (_instance == null)
      {
        _instance = new Model();
      }
      return _instance;
    }

The Controller

Now we have our Model, we need a way to communicate with it and this is where the Controller comes in. In this class we have a constructor method, an instance property and a getInstance() function – to ensure that we only invoke one Controller in our application – and a reference to the Model. The Model reference is called in by using the Model’s getInstance function to ensure we use the one Model referenced throughout the application.

package components
  {
  import events.NavigationEvent;
  import components.Model;
  import valueObjects.Page;

  public class Controller
  {
    /**
    * Constructor function
    */
    public function Controller()
    {
    }

    /**
    * Instance of the Controller
    */
    private static var _instance:Controller = null;

    /**
    * A singleton instance of the Model
    */
    private var _model:Model = Model.getInstance();

    /**
    * Creates an instance of the Controller or returns the existing instance
    */
    public static function getInstance():Controller
    {
      if (_instance == null)
      {
        _instance = new Controller();
      }
      return _instance;
    }

We then have a function to add some data to the Model. In this example, this will create a new Page object and add it to the Model’s data property.

    /**
    * Creates a Page object and adds it to the model
    */
    public function createPage(name:String, title:String):void
    {
      var page:Page = new Page();
      page.name = name;
      page.title = title;
      _model.data.addItem(page);
    }

The final function is used to update the selectedPage property of the Model, which it does by receiving a NavigationEvent containing a Page object.

    /**
    * Updates the model when the navigation changes
    */
    public function updateModel(ev:NavigationEvent):void
    {
      var selectedPage:Page = ev.selectedPage;
      _model.selectedPageName = selectedPage.name;
    }

The View and the solution to our problem

The View is made up of a set of components shown in the diagram at the top of this page and in Flex that breaks down like this…

The application (main.mxml)

In our application we begin with a Script block which starts by intantiating singleton instances of our Model and Controller classes:

    /**
    * A singleton instance of the Model
    */
    [Bindable]
    private var _model:Model = Model.getInstance();

    /**
    * A singleton instance of the Controller
    */
    private var _controller:Controller = Controller.getInstance();

Next we have our creationCompleteHandler() function that is called when the application is created. In this example, this function calls the Controller’s createPage function a number of times to create five pages for our example. It then calls the addViews() function (see below) and finally adds an event listener to monitor any NavigationEvent.DISPLAY_PAGE events. This listener passes the event to the updateModel() function in the Controller.

    /**
    * Sets up the Application on load
    * For this example this involves adding some example pages to the model
    * and setting up the required event listeners
    */
    private function creationCompleteHandler():void
    {
    // add some example data to the model
    _controller.createPage("page1", "Page 1");
    _controller.createPage("page2", "Page 2");
    _controller.createPage("page3", "Page 3");
    _controller.createPage("page4", "Page 4");
    _controller.createPage("page5", "Page 5");

    // create some example views for each page in the model
    addViews(); 

    // add event listener for a NavigationEvent
    addEventListener(NavigationEvent.DISPLAY_PAGE, _controller.updateModel);
    }

Next we have two functions that create some views for us. addViews() recursively calls createView() and between them, they create a VBox with some text that is then assigned to our ViewStack and Tabnavigator objects so we have something to see in our example.

    /**
    * Adds a VBox object based on the Page data to the TabNavigator and
    * ViewStack used in this example
    */
    private function addViews():void
    {
      for each (var page:Page in _model.data)
      {
        createView(page, viewStack as ViewStack);
        createView(page, tabNavigator as ViewStack);
      }
    }

    /**
    * Creates a new VBox and child Text object based on a Page object
    * and appends it to the target ViewStack
    */
    private function createView(page:Page, target:ViewStack):void
    {
      var vBox:VBox = new VBox();
      vBox.name = page.name;
      vBox.label = page.title;

      var text:Text = new Text();
      text.text = page.title;
      vBox.addChild(text);

      target.addChild(vBox);
    }

Now we’re onto our MXML that begins with our application’s navigation: a simple ToggleButtonBar that’s been created as a component called NavigationBar. This gets its data from the data property of our Model class and uses the title property of the each Page object stored in the data property.

    <components:NavigationBar
      id="navigationBar"
      dataProvider="{_model.data}"
      labelField="title"
      />

We then have a simple Text object and then our ViewStack, followed by another Text object and then our TabNavigator. However instead of instantiating the basic ViewStack and TabNavigator objects, here we’re using a custom component called BoundViewStack. The only thing we need to do for the second instantiation here is tell it to display as a TabNavigator, which we do by setting the isTabNavigator property to true.

    <mx:Text
      text="ViewStack example:"
      />

    <components:BoundViewStack
      id="viewStack"
      width="100%"
      height="75"

      />

    <mx:Text
      text="TabNavigator example:"
      />

    <components:BoundViewStack
      isTabNavigator="true"
      id="tabNavigator"
      width="100%"
      height="75"
      />

NavigationBar.mxml

Our NavigationBar component simply extends the ToggleButtonBar class by adding an event listener that will monitor clicks on the navigation items, and an event handler to handle those click events.

The dataProvider of this component is set in main.mxml to be the data property of the Model class. That data property is an ArrayCollection of Page objects, so when the user clicks on an item in the navigation bar, they’re effectively clicking on a Page.

We take advantage of that in our event handler by creating a new NavigationEvent and passing it the selected Page object as its selectedPage argument and the event is then dispatched and caught in the event listener we set up in main.mxml.

<?xml version="1.0" encoding="utf-8"?>
  <mx:ToggleButtonBar
    xmlns:mx="http://www.adobe.com/2006/mxml"

    creationComplete="creationCompleteHandler();"
    >

    <mx:Script>
    <![CDATA[
      import events.NavigationEvent;
      import valueObjects.Page;
      import mx.events.ItemClickEvent;

      /**
      * Sets up the component on creation complete
      * For this example it just adds the required event listeners
      */
      private function creationCompleteHandler():void
      {
        addEventListener(ItemClickEvent.ITEM_CLICK, buttonClickHandler);
      }

      /**
      * Passes the clicked ButtonBar item to a new NavigationEvent
      * and dispatches that event
      */
      private function buttonClickHandler(ev:ItemClickEvent):void
      {
        var selectedPage:Page = ev.item as Page;
        var navigationEvent:NavigationEvent = new NavigationEvent(NavigationEvent.DISPLAY_PAGE, selectedPage);
        dispatchEvent(navigationEvent);
      }

    ]]>
    </mx:Script>

</mx:ToggleButtonBar>

So we now have our navigation bar communicating with the Model (via the Controller) each time the user clicks on the navigation bar. Now we need to tie our ViewStack and TabNavigator to the Model.

BoundViewStack.as

The TabNavigator is an extension of the ViewStack class so for our needs, we only need one component for both. We could create distinct TabNavigator and ViewStack components but we’d have to duplicate the code we’re about to write so it makes sense to create a new component that extends TabNavigator called BoundViewStack.

The first few lines of the this simple component takes care of our imports and the constructor function invokes the constructor class of the super class before adding an event listener that will detect when our component has been created. We do this by my monitoring the FlexEvent.CREATION_COMPLETE event:

package components
  {
  import flash.display.DisplayObject;
  import components.Model;
  import mx.binding.utils.ChangeWatcher;
  import mx.containers.TabNavigator;
  import mx.core.Container;
  import mx.events.FlexEvent;
  import mx.events.PropertyChangeEvent;

  public class BoundViewStack extends TabNavigator
  {
    /**
    * Class constructor function invokes the contructor of the TabNavigator class and adds
    * an event listener for the FlexEvent.CREATION_COMPLETE event
    */
    public function BoundViewStack()
    {
      super();
      addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
    }

We then continue with a public property to determine if this component should display as a TabNavigator, and then singleton instances of our Model class:

    /**
    * Property to determine if this should be used as a TabNavigator
    */
    public var isTabNavigator:Boolean = false;

    /**
    * A singleton instance of the model object
    */
    private var _model:Model = Model.getInstance();

Then comes our creationCompleteHandler() function that does a couple of things. If the component is not to be displayed as a TabNavigator it hides the TabNavigator’s tab bar by setting its visibility and includeInLayout properties to false, and its height to 0.

Next is where we solve our problem with the ChangeWatcher class. The ChangeWatcher watches a property of an object and then calls a handler function when the property changes. We know that we have a property in our Model called currentPageName that holds the name of the Page the user clicked on in the navigation bar, so we set up a watch on that property.

    /**
    * Sets up the Class on creation complete
    * A ChangeWatcher is used to monitor the model and if this component is not to display
    * as a TabNavigator, the tabBar is hidden from view
    */
    private function creationCompleteHandler(ev:FlexEvent):void
    {
      if (! isTabNavigator)
      {
        tabBar.visible = false;
        tabBar.includeInLayout = false;
        tabBar.height = 0;
      } 

    // add a ChangeWatcher to monitor the model
    ChangeWatcher.watch(_model, "selectedPageName", modelChangeHandler);
    }

The final piece in the jigsaw is the handler function that the ChangeWatcher calls: modelChangeHandler(). This expects a PropertyChangeEvent in which both the old and new values of the _model.selectedPageName property is available.

As each page in our View has its name property set to match the name property of a Page object (we know this as we created our page views progromatically in the addViews() function of main.mxml), we need to get the child oject that corresponds to the Page the user has selected, which we do with the getChildByName function.

If this returns an object we then want to display it, which we do by updating the TabNavigator’s selectedChild property. This expects a Container so we need cast the DisplayObject we received in the previous step in order for it to work.

    /**
    * Responds to changes in the model by updating the selectedChild
    */
    private function modelChangeHandler(ev:PropertyChangeEvent):void
    {
      var itemToSelect:DisplayObject = getChildByName(ev.newValue as String);

      if (itemToSelect)
      {
        selectedChild = itemToSelect as Container;
      }
    }

And then bang, we’re done. We now have a very basic MVC application structure where everything is tied together with events and bindings, leaving us free to structure our component packages in any way our application demands.

The solution to our problem is very simple so this article is more in-depth than it needs to be, but I hope it explains a little about the advantages of an MVC pattern and that it will lead you on to looking at frameworks like Cairngorm that take care of a lot of the MVC side of things.

One Response to “Binding the selectedChild of a ViewStack in a Flex MVC structure”

  1. hsTed

    01. Sep, 2009

    Since struggling with MVC for a (long) while now, it scares me a little that your article makes a lot of sense to me. Great article/example, thanks!

    Reply to this comment

Leave a Reply