PureMVC Tutorial – Flex, PureMVC, Jabber and XIFF 3: Part 8 – The Roster View & Mediator

Introduction
Part 1 – Frameworks
Part 2 – Directory structure
Part 3 – Application and ApplicationFacade
Part 4 – Notifications, Commands & Use Cases
Part 5 – Model & Proxy
Part 6 – The Application View & Mediator
Part 7 – The Login View & Mediator
Part 8 – The Roster View & Mediator
Part 9 – The Chat View & Mediator
Conclusion, Demo & Downloads

By now you should be becoming a bit of an old hand at this PureMVC lark, so I’m going to be explaining stuff in a bit less detail.  If you get stuck re-read the previous sections until you have everything clear in your head.

The Roster is very simple.  We want a Flex DataGrid displaying usernames and login status and we need a ‘Start chat…’ button that opens a chat window.

In order to implement this view we’ll need a new event called ChatEvent.as.  The JID class is part of the XIFF framework and is used to identify a Jabber user.  The RosterView will only use START_CHAT, but we’ll need SEND_MESSAGE in the next and final view.

   1: package org.davekeen.xiffer.events {
   2:     import flash.events.Event;
   3:     import org.jivesoftware.xiff.core.JID;
   4:     
   5:     /**
   6:     * This event is related to the ChatViews
   7:     * 
   8:     * @author Dave Keen
   9:     */
  10:     public class ChatEvent extends Event {
  11:         
  12:         public static const START_CHAT:String = "start_chat";
  13:         public static const SEND_MESSAGE:String = "send_message";
  14:         
  15:         private var jid:JID;
  16:         private var message:String;
  17:         
  18:         public function ChatEvent(type:String, jid:JID, message:String = null, bubbles:Boolean=false, cancelable:Boolean=false) { 
  19:             super(type, bubbles, cancelable);
  20:             
  21:             this.jid = jid;
  22:             this.message = message;
  23:         } 
  24:         
  25:         public function getJID():JID {
  26:             return jid;
  27:         }
  28:         
  29:         public function getMessage():String {
  30:             return message;
  31:         }
  32:         
  33:         public override function clone():Event { 
  34:             return new ChatEvent(type, jid, message, bubbles, cancelable);
  35:         } 
  36:         
  37:     }
  38:     
  39: }

We’ll also need to define one new notification in ApplicationFacade.as called OPEN_CHAT_WINDOW:

   1: public static const OPEN_CHAT_WINDOW:String = "open_chat_window";

Next we create our RosterView.mxml file in components:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" width="100%" height="100%">
   3:     <mx:Script>
   4:         <![CDATA[
   5:         import flash.events.Event;
   6:         import mx.events.ListEvent;
   7:         import org.davekeen.xiffer.events.ChatEvent;
   8:         import org.jivesoftware.xiff.core.JID;
   9:         
  10:         private function onChatClick():void {
  11:             if (rosterGrid.selectedItem) {
  12:                 // Get the selected item and convert it to a JID
  13:                 var jid:JID = new JID(rosterGrid.selectedItem.jid);
  14:                 
  15:                 // Dispatch an event for the mediator
  16:                 dispatchEvent(new ChatEvent(ChatEvent.START_CHAT, jid));
  17:             }
  18:         }
  19:         
  20:         ]]>
  21:     </mx:Script>
  22:     <mx:TitleWindow id="titleWindow" title="Buddy list" enabled="false">
  23:         <mx:DataGrid id="rosterGrid" editable="false" width="160" height="300" showHeaders="true">
  24:             <mx:columns>
  25:                 <mx:DataGridColumn dataField="displayName" headerText="Username" />
  26:                 <mx:DataGridColumn dataField="online" width="55" headerText="Online?" />
  27:             </mx:columns>
  28:         </mx:DataGrid>
  29:         <mx:Button width="160" label="Chat..." click="onChatClick()" />
  30:     </mx:TitleWindow>
  31: </mx:Canvas>

Add this component to the display list of our top-level Application.mxml component so that the <mx:Canvas> component now reads:

   1: <mx:Canvas left="0" top="0" right="0" bottom="0">
   2:     <view:LoginView id="loginView" />
   3:     <view:RosterView y="30" id="rosterView" />
   4: </mx:Canvas>

Now we need to create RosterMediator.as in the view folder, and register it with PureMVC.  Using the rule we applied in the previous section we can see that we need to do this registration in ApplicationMediator.as:

   1: public function ApplicationMediator(viewComponent:Object) {
   2:     // pass the viewComponent to the superclass where 
   3:     // it will be stored in the inherited viewComponent property
   4:     super(NAME, viewComponent);
   5:     
   6:     facade.registerMediator(new LoginMediator(application.loginView));
   7:     facade.registerMediator(new RosterMediator(application.rosterView));
   8: }

All that is left now is to configure our mediator using the same steps as in the previous section.

1. Add listeners

RosterView.mxml dispatches only ChatEvent.START_CHAT so let’s add a listener in the constructor:

   1: public function RosterMediator(viewComponent:Object) {
   2:     // pass the viewComponent to the superclass where 
   3:     // it will be stored in the inherited viewComponent property
   4:     super(NAME, viewComponent);
   5:     
   6:     rosterView.addEventListener(ChatEvent.START_CHAT, onStartChat);
   7: }
   8:  
   9: private function onStartChat(chatEvent:ChatEvent):void { }

2. Add notifications

The Roster window need to enable and disable depending on whether or not the user is connected to a jabber server, and also needs to fill in the DataGrid with the list of buddies.  To achieve these goals the mediator will need to register an interest in ApplicationFacade.VALID_LOGIN and ApplicationFacade.DISCONNECT.

   1: override public function listNotificationInterests():Array {
   2:     return [
   3:             ApplicationFacade.VALID_LOGIN,
   4:             ApplicationFacade.DISCONNECT
   5:             ];
   6: }

And here are the clauses for the switch statement:

   1: override public function handleNotification(note:INotification):void {
   2:     switch (note.getName()) {
   3:         case ApplicationFacade.VALID_LOGIN:
   4:              break;
   5:         case ApplicationFacade.DISCONNECT:
   6:             break;
   7:         default:
   8:             break;        
   9:     }
  10: }

3. Fill in the event listeners and switch clauses

Firstly we’ll fill in the onStartChat event listener.  This will very simply send an ApplicationFacade.OPEN_CHAT_WINDOW with the JID as the argument.  Although we could have passed the ChatEvent itself as a parameter in this case there is no need to use a value object as to open a chat window we only require a single item of information – the JID.

   1: private function onStartChat(chatEvent:ChatEvent):void {
   2:     sendNotification(ApplicationFacade.OPEN_CHAT_WINDOW, chatEvent.getJID());
   3: }

You’ll notice that the ApplicationFacade.OPEN_CHAT_WINDOW notification is not mapped to a command – this is because the only responder to the notification will be the ChatMediator that we are about to create in the next section so there is no need to have a command.  However, one of the beautiful things about PureMVC is that if it turned out later in the development that this notification needed to affect the model in some way we could implement this merely by adding a new command and registering it, and there will be no need to make any changes to the rest of the application.

Compile and run the application and enjoy the working buddy list!  Only one view left to go…

PureMVC Tutorial – Flex, PureMVC, Jabber and XIFF 3: Part 7 – The Login View & Mediator

Introduction
Part 1 – Frameworks
Part 2 – Directory structure
Part 3 – Application and ApplicationFacade
Part 4 – Notifications, Commands & Use Cases
Part 5 – Model & Proxy
Part 6 – The Application View & Mediator
Part 7 – The Login View & Mediator
Part 8 – The Roster View & Mediator
Part 9 – The Chat View & Mediator
Conclusion, Demo & Downloads

In this part of the tutorial we’ll create the login bar, and by the end of this section our application will be able to connect to any XMPP server with a username & password!

In the components package create a new MXML file name LoginView.mxml and in the view package create a new mediator called LoginMediator.as using Add->New Mediator… (if you don’t see this menu item be sure you’ve installed the PureMVC FlashDevelop templates from PureMVC: First thoughts & FlashDevelop templates correctly).

As we explained in the previous section add a helper method to the LoginMediator to cast the viewComponent:

   1: private function get loginView():LoginView {
   2:     return viewComponent as LoginView;
   3: }

Note that in order to get the application to compile you’ll need to explicitly import the LoginView using:

   1: import org.davekeen.xiffer.view.components.LoginView;

Now we want to add the LoginView to the display list by including it in Application.mxml (giving it an id of loginView so we can reference it from the mediator).  Add this MXML within the <mx:Application> tag (stick it at the end just before </mx:Application>):

   1: <mx:Canvas left="0" top="0" right="0" bottom="0">
   2:     <view:LoginView id="loginView" />
   3: </mx:Canvas>

Now we need to register our mediator with PureMVC.  Now this point is important to understand as it seems to confuse  – since LoginView is a sub-component of Application we need to register the view within the Application’s mediator.  This can be made into a general rule:

  • If the mediator you are registering heralds the top level component (i.e. Application.mxml) register it in StartupCommand.
  • If the mediator you are registering heralds a child of another component register it in the constructor of that component’s mediator.

Just to confuse things I’d better point out that this rule doesn’t apply in quite the same way if you are dynamically creating and removing mediators, but we’ll save that for another tutorial.

Anyway, the upshot of all that is that we call registerMediator in the constructor of ApplicationMediator:

   1: public function ApplicationMediator(viewComponent:Object) {
   2:     // pass the viewComponent to the superclass where 
   3:     // it will be stored in the inherited viewComponent property
   4:     super(NAME, viewComponent);
   5:     
   6:     facade.registerMediator(new LoginMediator(application.loginView));
   7: }

Here is the code for LoginView.mxml.  As this is a PureMVC tutorial, not a Flex one, I’m not going to go in any detail about how it works, but these are the only things you need to care about:

  • When the user clicks ‘Connect’ it dispatches a LoginViewEvent.LOGIN event containing the username, password and server.
  • When the user clicks ‘DIsconnect’ it dispatches a LoginViewEvent.LOGOUT event.
  • It exposes a showInvalidLoginAlert() method that pops up a ‘Invalid username/password’ window.
   1: <?xml version="1.0" encoding="utf-8"?>
   2: <mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" width="100%" height="100%">
   3:     <mx:Script>
   4:         <![CDATA[
   5:         import mx.controls.Alert;
   6:         import org.davekeen.xiffer.events.LoginViewEvent;
   7:         
   8:         /**
   9:          * Dispatch a connect event to the mediator
  10:          */
  11:         private function onConnectClick():void {
  12:             dispatchEvent(new LoginViewEvent(LoginViewEvent.LOGIN, usernameTextInput.text, passwordTextInput.text, serverComboBox.text));
  13:         }
  14:         
  15:         /**
  16:          * Dispatch a disconnect event to the mediator
  17:          */
  18:         private function onDisconnectClick():void {
  19:             dispatchEvent(new LoginViewEvent(LoginViewEvent.LOGOUT));
  20:         }
  21:         
  22:         /**
  23:          * Enable or disable the connect button depending on whether or not the user has entered something into both username and password fields
  24:          */
  25:         private function usernamePasswordChange():void {
  26:             connectButton.enabled = (usernameTextInput.text.length > 0 && passwordTextInput.length > 0);
  27:         }
  28:         
  29:         /**
  30:          * Show an invalid login alert and clear the username and password fields
  31:          */
  32:         public function showInvalidLoginAlert():void {
  33:             // Popup an alert
  34:             Alert.show("Invalid username/password", "Error", Alert.OK, this);
  35:             
  36:             // Clear the input fields and notify the change handler (this will disable the connect button)
  37:             usernameTextInput.text = "";
  38:             passwordTextInput.text = "";
  39:             usernamePasswordChange();
  40:         }
  41:         
  42:         ]]>
  43:     </mx:Script>
  44:     
  45:     <mx:Form defaultButton="{connectButton}" paddingTop="0" paddingBottom="0" paddingLeft="0" paddingRight="0">
  46:         
  47:         <mx:HBox>
  48:             
  49:             <mx:Label text="Username:" selectable="false" fontSize="14" />
  50:             <mx:TextInput id="usernameTextInput" width="100" change="usernamePasswordChange()" />
  51:             
  52:             <mx:Label text="Password:" selectable="false" fontSize="14" />
  53:             <mx:TextInput id="passwordTextInput" change="usernamePasswordChange()" displayAsPassword="true" width="100" />
  54:             
  55:             <mx:Label text="Server:" fontSize="14" />
  56:             <mx:ComboBox id="serverComboBox" editable="true">
  57:                 <mx:ArrayCollection>
  58:                     <mx:String>jabber.se</mx:String>
  59:                     <mx:String>jabber.org</mx:String>
  60:                 </mx:ArrayCollection>
  61:             </mx:ComboBox>
  62:             
  63:             <mx:Button id="connectButton" label="Connect" enabled="false" click="onConnectClick()" />
  64:             <mx:Button id="disconnectButton" label="Disconnect" enabled="false" click="onDisconnectClick()" />
  65:             
  66:         </mx:HBox>
  67:         
  68:     </mx:Form>
  69:     
  70: </mx:Canvas>

We’ll also need to create the custom LoginViewEvent (in the events folder):

   1: package org.davekeen.xiffer.events {
   2:     import flash.events.Event;
   3:     
   4:     /**
   5:     * Events passed between the login view component and its mediator
   6:     * 
   7:     * @author Dave Keen
   8:     */
   9:     public class LoginViewEvent extends Event {
  10:         
  11:         public static const REGISTER:String = "login_view_register";
  12:         public static const LOGIN:String = "login_view_login";
  13:         public static const LOGOUT:String = "login_view_logout";
  14:         
  15:         private var username:String;
  16:         private var password:String;
  17:         private var server:String;
  18:         
  19:         public function LoginViewEvent(type:String, username:String = null, password:String = null, server:String = null, bubbles:Boolean = false, cancelable:Boolean = false) { 
  20:             super(type, bubbles, cancelable);
  21:             
  22:             this.username = username;
  23:             this.password = password;
  24:             this.server = server;
  25:         }
  26:         
  27:         public function getUsername():String {
  28:             return username;
  29:         }
  30:         
  31:         public function getPassword():String {
  32:             return password;
  33:         }
  34:         
  35:         public function getServer():String {
  36:             return server;
  37:         }
  38:         
  39:         public override function clone():Event { 
  40:             return new LoginViewEvent(type, username, password, server, bubbles, cancelable);
  41:         }
  42:         
  43:     }
  44:     
  45: }

Now we’re ready to start the interesting bit – implementing our LoginMediator.  These are the steps I take when implementing a mediator:

  1. Add listeners for all the events dispatched from the view component to the constructor and create event listener methods for them.
  2. Identify which notifications this mediator is interested in and add them to the listNotificationInterests method’s array and the handleNotification method’s switch statement.
  3. Fill in the event listener methods and switch statement clauses.

Now we’ll do each of these steps in turn for our LoginMediator.

1. Add listeners

Our view component dispatches LoginViewEvent.LOGIN and LoginViewEvent.LOGOUT event, so we’ll add our listeners in the constructor and add two empty event listener methods:

   1: public function LoginMediator(viewComponent:Object) {
   2:     // pass the viewComponent to the superclass where 
   3:     // it will be stored in the inherited viewComponent property
   4:     super(NAME, viewComponent);
   5:     
   6:     loginView.addEventListener(LoginViewEvent.LOGIN, onConnectClick);
   7:     loginView.addEventListener(LoginViewEvent.LOGOUT, onDisconnectClick);
   8: }
   9:  
  10: /**
  11:  * The connect button was clicked in the view
  12:  * 
  13:  * @param    loginViewEvent
  14:  */
  15: private function onConnectClick(loginViewEvent:LoginViewEvent):void { }
  16:  
  17: /**
  18:  * The disconnect button was clicked in the view
  19:  * 
  20:  * @param    loginViewEvent
  21:  */
  22: private function onDisconnectClick(loginViewEvent:LoginViewEvent):void { }

2. Add notifications

The login view is interest in knowing if a client is connected or not (so that it can enable/disable the ‘Connect’ & ‘Disconnect’ buttons accordingly.  It is also interested in knowing if a login attempt was invalid so that it can popup the ‘Invalid username/password’ alert.  This translates into listening for ApplicationFacade.VALID_LOGIN, ApplicationFacade.INVALID_LOGIN and ApplicationFacade.DISCONNECT.

Firstly we’ll add these to the listNotificationInterests method:

   1: override public function listNotificationInterests():Array {
   2:     return [
   3:             ApplicationFacade.VALID_LOGIN,
   4:             ApplicationFacade.INVALID_LOGIN,
   5:             ApplicationFacade.DISCONNECT
   6:             ];
   7: }

Now we’ll add empty clauses for each notification in the handleNotification method’s switch statement:

   1: override public function handleNotification(note:INotification):void {
   2:     switch (note.getName()) {
   3:         case ApplicationFacade.VALID_LOGIN:
   4:             break;
   5:         case ApplicationFacade.INVALID_LOGIN:
   6:             break;
   7:         case ApplicationFacade.DISCONNECT:
   8:             break;
   9:         default:
  10:             break;        
  11:     }
  12: }

3. Fill in the event listeners and switch clauses

Let do the switch clauses first.  Its all simple stuff – in the event of a valid login we want to enable the ‘Disconnect’ button and disable the ‘Connect’ button, in the event of a disconnect we want to enable the ‘Connect’ button and disable the ‘Disconnect’ button, and in the event of an invalid login we want to popup the ‘Invalid username/password’ alert box:

   1: override public function handleNotification(note:INotification):void {
   2:     switch (note.getName()) {
   3:         case ApplicationFacade.VALID_LOGIN:
   4:             loginView.connectButton.enabled = false;
   5:             loginView.disconnectButton.enabled = true;
   6:             break;
   7:         case ApplicationFacade.INVALID_LOGIN:
   8:             loginView.showInvalidLoginAlert();
   9:             break;
  10:         case ApplicationFacade.DISCONNECT:
  11:             loginView.connectButton.enabled = true;
  12:             loginView.disconnectButton.enabled = false;
  13:             break;
  14:         default:
  15:             break;        
  16:     }
  17: }

And now lets fill in our event listeners.  Again, these are very simple – all they do is send the appropriate notification which will then get auto-mapped to the appropriate command – in this case either LoginCommand or LogoutCommand.

   1: private function onConnectClick(loginViewEvent:LoginViewEvent):void {
   2:     sendNotification(ApplicationFacade.LOGIN, loginViewEvent);
   3: }
   4:  
   5: private function onDisconnectClick(loginViewEvent:LoginViewEvent):void {
   6:     sendNotification(ApplicationFacade.LOGOUT, loginViewEvent);
   7: }

Notice that for the parameter of the notifications I am just passing the same LoginViewEvent we received from the view component.  It could be argued that this breaks encapsulation as the commands shouldn’t really know anything about events.  However, after coding a few PureMVC projects I’ve noticed that its very common for the event you receive from the view to contain the same bits of information needed by the relevant command – in this case LoginCommand needs to know the username, password and server which is exactly the information contained in a LoginViewEvent.  Because of this I don’t really see the need to create an extra object, and when passing an event as notification parameter I just think of it as a value object instead of an event, but you are certainly justified in taking another view on this.

Now that we have the mediator calling the LoginCommand and LogoutCommand we’d better fill these in.  Commands can do various different things, but a very common pattern for commands, and what we’ll be using here, is:

  1. Retrieve the proxy we want to do something with using retrieveProxy.
  2. Call a method on that proxy, possibly with parameters ripped out of the notification parameter.

With that in mind we can very simply implement LoginCommand.as:

   1: override public function execute(note:INotification):void {
   2:     var loginViewEvent:LoginViewEvent = note.getBody() as LoginViewEvent;
   3:     var xmppProxy:XMPPProxy = facade.retrieveProxy(XMPPProxy.NAME) as XMPPProxy;
   4:     
   5:     xmppProxy.connect(loginViewEvent.getUsername(), loginViewEvent.getPassword(), loginViewEvent.getServer());
   6: }

… and LogoutCommand.as:

   1: override public function execute(note:INotification):void {
   2:     var xmppProxy:XMPPProxy = facade.retrieveProxy(XMPPProxy.NAME) as XMPPProxy;
   3:     
   4:     xmppProxy.disconnect();
   5: }

Guess what?  We have a working application!  Compile the application and play about with it – you’ll be able to log in and out of Jabber servers to your heart’s content 🙂  If you download a proper jabber client (http://www.jabber.org/clients has a big list of clients for various platforms) you’ll be able to see your user coming on and offline as you click ‘Connect’ and ‘Disconnect’.

Be proud!  All that’s left for us to do now is to create a buddy list (called the Roster in Jabber parlance) and the chat windows themselves.  If you’ve come this far maybe you’d like to have a go yourself without reading further.  If not, read on – Roster view here we come.