Working with Web Services in Flex (Using the Halo: Reach Stats API)

Nov 27, 2010

Tags: , , , , ,

Working with a Web Service in Flex is really easy, especially when using Flex Builder 3 or Flash Builder 4. Flex Builder (and Flash Builder) comes with a built in tool that does a really good job taking a WSDL and building the ActionScript classes needed to interface with a web service. I've seen some issues using this tool at least in Flex Builder 3, for example the ever so annoying "There are no ports for the current service. Check that the WSDL file is correct or select another service". Now it's possible that this is related to the WSDL for the JSON service or Flex Builder 3 might just be getting something it doesn't understand who knows. After the jump I'll walk though building a very simple application that uses the Halo: Reach Stats API to fetch the current Challenges from the service. Due to restrictions on the API you will not be able to just take this sample code and have it work as I have omitted my API key for security reasons, as well view source is off for this swf.

To get started we need to use the "Import Web Service" tool in Flex Builder 3 you will find this under the Data menu. First you need to select your project in the dialog and hit next, now you must enter the location of the service's WSDL in the WSDL URI field. In the case of this demo that is http://www.bungie.net/api/reach/reachapisoap.svc?wsdl.

Web Service Import Dialog (step 2)

Once you click next you will be shown the contents of the service, this seems to be much faster in Flex Builder 3 than in Flash Builder 4, here you can limit what features of the service are available to you. However you will be fine just leaving this screen alone in most cases. Once you hit finish in this dialog Flex Builder will think for awhile (in the case of this service it takes a fair amount of time, as there are allot of classes).

Web Service Import (part 3)

In the case of this api there are a few things we need to remove from the net.bungie package, you need to remove a few classes that duplicate data types (ie Int and Boolean). So open the net.bungie package you will see a massive list of classes, try to ignore that you need to remove Boolean.as,  and Int.as.  Now that the nasty bit is out of the way lets get started with the fun part, to start we need to create a component for laying out each item coming back from our service, in this case it will be the currently active challenges on Halo: Reach.

So in our component we need 6 components, an Image component, a VBox, 3 Label components and a Text component. In this demo we are going to create this component in the components package and call it Challenge and base it off HBox. Since our application will be 400px across we will set this component to be 400x130.  First we start by adding the Image component we need to set its dimensions to be 110x100 the width and height of the challenge images, we will come back to the source in a minute. Next up we need to add the VBox and set its width and height to be 100%, in this VBox we will add our two Label components and our Text component. For the Label and Text components set the width to be 100%. The First Label will be the title of the Challenge so we will set its font to be bold and size to say 14. On the Text component we want to change the selectable property to false, just for consistency with the Label components. Lastly make sure you clear the content of the Labels and Text component, we are going to use Data Binding to populate these fields in the next step. Right now your Challenge component should look similar to bellow.

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="130">
    <mx:Image width="110" height="110"/>
    <mx:VBox width="100%" height="100%">
       <mx:Label text="Label" fontWeight="bold" fontSize="14" width="100%"/>
       <mx:Label text="Label" width="100%"/>
       <mx:Label text="Label" width="100%"/>
       <mx:Text text="Text" width="100%" selectable="false"/>
    </mx:VBox>
</mx:HBox>

Now that that part is done lets move on to setting this component up so we can easily use it, in our application. First lets start with the image, we're going to be using local assets for this instance, you can find these assets at the bottom of this post. I'm going to create one more folder inside of our src folder, and call it assets. We will be referencing this folder in the source of the image. So to get started lets create a script tag in our new component. Next we are going to setup the properties we want accessable from the main component. We need 3 public properties, a String called title, an int called credits and a Date called expires. We also need two private properties one that is called _imageIndex this will be of a data type of int and a String called _description. Next add the bindable meta tag to each of these properties. Now lets start setting our components values, I'm going to skip over the image for now as there's a bit of work involved there. Lets start with the first Label component (our title), we need to set its value to be that of name so we use curly braces around the variable name, this will also invoke data binding so that it updates when the name property is changed. The next Label component we are going to set to have a value of "Credits: {credits}", and the last label we are going to set to use the expires property which will use the default string format of that date instance (ex Wed Apr 12 15:30:17 GMT-0700 2006) which isn't pretty but it works. Lastly for the Text field we want to set this to use the description property.

For the image component we need to create a creation complete method on our component to grab the source path of the swf. First I'm going to create a private bindable property called _sourcePath this will be a String and call my creation complete method setup(). To get the source path we get the url from the Application class and split it on /, remove the last  key from the resulting array then splice it back together with /, this removes the swf name and extension. Lastly I'm going to append assets/ to the resulting string this will give me the assets folder. Now that we have the source path we can setup the image component's source path, we need to set this to be {_sourcePath}{_imageIndex}.png this will bind everything together and come out with the path to the correct image for this component. This bit of trickery I've done here is to ensure we have the full path to the assets folder to prevent the issue of images breaking when they are not located relative to the page rather than the swf.

We're not quite done with the image yet, we need to do some work on calculating the image, since it's not defined in the API as of posting. To do this we need one more property and a setter, our property is a public Boolean we are going to call isWeekly set it to false by default, we'll use this in a bit. Now onto our setter, we're going to call this description() this will look at the IsWeekly property and  set the _imageIndex if the challenge is weekly setting it to be 1 otherwise we are going to try and guess the image index based of the description since this information isn't currently in the api at the time of posting. So now that that is done you component should be a healthy 60 lines plus or minus formatting.

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="130" creationComplete="setup()">
    <mx:Script>
        <![CDATA[
            import mx.core.Application;
            private var _imageIndex:int=0;

            [Bindable]
            private var _sourcePath:String;

            [Bindable]
            private var _title:String;

            [Bindable]
            public var description:String;

            [Bindable]
            public var credits:int;

            [Bindable]
            public var expires:Date;

            public var isWeekly:Boolean=false;

            private function setup():void {
                var temp:Array=Application.application.url.split("/");
                _sourcePath=temp.splice(0, temp.length-1).join("/")+'/assets/';
            }

            public function set title(val:String):void {
                if(isWeekly) {
                    _imageIndex=1;
                }else {
                    if(val.search(/firefight/i)!=-1) {
                        _imageIndex=3;
                    }else if(val.search(/campaign|normal|heroic|legendary|easy/i)!=-1) {
                        _imageIndex=2;
                    }else if(val.search(/matchmaking|multiplayer/i)!=-1) {
                        _imageIndex=4;
                    }else {
                        _imageIndex=0;
                    }
                }

                _title=val;
            }
        ]]>
    </mx:Script>

    <mx:Image width="110" height="110" source="{_sourcePath}{_imageIndex}.png"/>

    <mx:VBox width="100%" height="100%">
        <mx:Label text="{title}" fontWeight="bold" fontSize="14" width="100%"/>
        <mx:Label text="Credits: {credits}" width="100%"/>
        <mx:Label text="{expires}" width="100%"/>
        <mx:Text text="{_description}" width="100%" selectable="false"/>
    </mx:VBox>
</mx:HBox>

[/syntax_highlight]

So lets move back to our application file now that that is over with and start making this a working application. First up we need a private property called service its data type should be ReachApiSoap. If you use auto complete Flex Builder will automatically import this class, if you don't using the default settings the class will be located in the net.bungie package. Now we need a creation complete method so again in keeping with what we've done above we will call this setup() as well. Inside this method we will instanciate the ReachApiSoap class storing the instance on the service property. Next we need to create a listener on the service for the GetCurrentGlobalChallengesResultEvent.GetCurrentGlobalChallenges_RESULT event (long and ugly I know but that's what you get from generated classes). When this event is dispatched we are going to have it call onServiceResponse(), we'll detail that in a minute. We're also going to listen for the fault event on the service and have it call a method called onFault() all this is going to do is show an alert saying the challenges are unavailable. At this point your Application should look similar to bellow.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="setup()">
    <mx:Script>
        <![CDATA[
            import mx.controls.Alert;
            import net.bungie.GetCurrentGlobalChallengesResultEvent;
            import mx.rpc.events.FaultEvent;
            import net.bungie.ReachApiSoap;

            private var service:ReachApiSoap;

            private function setup():void {
                service=new ReachApiSoap();
                service.addEventListener(GetCurrentGlobalChallengesResultEvent.GetCurrentGlobalChallenges_RESULT, onServiceReponse);
                service.addEventListener(FaultEvent.FAULT, onFault);
            }

            private function onServiceReponse(e:GetCurrentGlobalChallengesResultEvent):void {

            }

            private function onFault(e:FaultEvent):void {
                Alert.show('An error has occured fetching the current challenges');
            }
        ]]>
    </mx:Script>
</mx:Application>

Next we are going to make the call to the service and start looking through the data returned from the service, in the case of the Reach Stats API. To do this you need an API key which you can get by going to Bungie.net creating an account and going to the Reach API Key Page of your Game Settings once you have that its a simple matter of calling service.getCurrentGlobalChallenges() in your creation complete method and passing in your API key. Now we move onto the onServiceResponse() where the bulk of the work is going to happen in this application. To get the result from the service we are going to create a variable called result with a data type of ChallengesResponse and set this to be e.result. This will contain all of the challenges currently active, in two arrays. We're going to start with the weekly challenges so they are ordered to the top of our list. So we create a for loop, starting at 0 and ending at the length of result.Weekly its a bit over kill to use a for loop here as there is usually only one weekly challenge but lets call it future proofing. Inside this for loop we are going to create an instance of our Challenge component. Next we start populating the other properties isWeekly with the IsWeeklyChallenge from the Weekly array item, imageIndex is set to the current position in the array, title with the Name property from the Weekly array item, description with the Description property from the Weekly array item, expiry with the ExpirationDate property from the Weekly array item and the credits with the Credits property from the Weekly array item. Once all of that is done we add the challenge instance to the application. Then simply repeat this process for the Daily array in your result. Now the guts of our application are done, its could use some love on the design front. Your application code should look similar to bellow.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="setup()">
    <mx:Script>
        <![CDATA[
            import components.Challenge;
            import net.bungie.ChallengesResponse;
            import mx.controls.Alert;
            import net.bungie.GetCurrentGlobalChallengesResultEvent;
            import mx.rpc.events.FaultEvent;
            import net.bungie.ReachApiSoap;

            private var service:ReachApiSoap;

            private function setup():void {
                service=new ReachApiSoap();
                service.addEventListener(GetCurrentGlobalChallengesResultEvent.GetCurrentGlobalChallenges_RESULT, onServiceReponse);
                service.addEventListener(FaultEvent.FAULT, onFault);
                service.getCurrentGlobalChallenges(API_KEY);
            }

            private function onServiceReponse(e:GetCurrentGlobalChallengesResultEvent):void {
                var result:ChallengesResponse=e.result;

                //Get Weekly Challenges
                for(var w:int=0;w<result.Weekly.length;w++) {
                    var wChal:Challenge=new Challenge();
                    wChal.isWeekly=result.Weekly[w].IsWeeklyChallenge;
                    wChal.title=result.Weekly[w].Name;
                    wChal.description=result.Weekly[w].Description;
                    wChal.credits=result.Weekly[w].Credits;
                    wChal.expires=result.Daily[d].ExpirationDate;
                    this.addChild(wChal);
                }

                //Get Daily Challenges
                for(var d:int=0;d<result.Daily.length;d++) {
                    var dChal:Challenge=new Challenge();
                    dChal.isWeekly=result.Daily[d].IsWeeklyChallenge;
                    dChal.title=result.Daily[d].Name;
                    dChal.description=result.Daily[d].Description;
                    dChal.credits=result.Daily[d].Credits;
                    dChal.expires=result.Daily[d].ExpirationDate;
                    this.addChild(dChal);
                }
            }

            private function onFault(e:FaultEvent):void {
                Alert.show('An error has occured fetching the current challenges');
            }
        ]]>
    </mx:Script>
</mx:Application>

[/syntax_highlight]

The this result of this demo is bellow, if you want the sources you will find these here. One one final note, due to Bungie.net not having a crossdomain file you will be unable to use this code in a "Web Application" without a proxy on your server, I'm using one I found on the Adobe forms here. To do this you need to open the class net.bungie.BaseReachApiSoap and look for where the property endpointURI is being set and change this to point to your proxy instead of Bungie.net.

Note this demo was created using Flex Builder 3 and the Flex 3.5 SDK.

This content requires the Adobe Flash Player Get Flash.

Posted in: Articles |