In this lesson, you will:
• Refactor code into an ActionScript component
• Create your own skin
• Manage skin parts and component life cycles
• Learn to use the Scroller
In Lesson 9, “Breaking the Application into Components,” you learned how to build custom components using MXML. There are times when you’ll need even more flexibility than MXML can offer. For these occasions, you can create components directly in ActionScript 3.0.
In this lesson, you’ll create a new component called ShoppingList, which will be an extensive refactoring of the existing List instance that shows your shopping cart items. It will include a new skin and new functionality, and will allow you to make a single component out of several separate pieces.
In an earlier lesson, you learned that any code written in MXML is translated into ActionScript by the Flex compiler before being compiled into a SWF file. In reality, every Flex component is an ActionScript class, regardless of whether it’s a UI control, a container, or some other type of component. Anything you might create in MXML can also be created in ActionScript, and there are things you can do with ActionScript that are not available purely from MXML.
The core Flex components you have used throughout this book—Label, DataGrid, and Button—are written in ActionScript. In general, components that are written for a single project or even quick prototypes of more advanced components are handled in MXML. However, if you want to build a very reusable and skinnable component, you’ll eventually need to embrace ActionScript as your primary method.
The steps you’ll take when creating an ActionScript 3.0 component are similar to the steps for building any ActionScript 3.0 class. First, determine what (if any) superclass your new class will extend. Then, determine what properties you need to declare for your class. Next, determine any new methods you might need to implement. You’ll also need to declare any events your component will dispatch.
If your component is a visual class, as it will be in this lesson, you’ll likely need to consider how your class will interact with a skin to allow you and others to change the visual appearance of your new component.
A word of warning: This lesson is the culmination of much of what you’ve learned in this book. Flex is intended to allow you to build applications quickly by assembling premade components. Flex can look easy, but it does so only because a component developer somewhere embraced the real complexity that lies just beneath the surface and wrestled it into submission.
When you develop Flex components, you become that individual, meaning that it is your job to make it look easy to the outside world by dealing with the complexity inside this little black box we call a component.
To create a well-developed component, you must balance the needs of your component’s end user (sometimes you, sometimes your team or company, or at the most extreme, an unknown audience who will purchase and use it), with an understanding of Flash Player and the Flex framework. While this lesson will not be able to provide all of that understanding, it will touch on several areas of building a component.
There are two types of components in the Flex framework: those that have a skinnable display and those that do not. Classes such as Group, DataGroup, and many others are not skinnable. That means you cannot apply the techniques learned in the previous lesson to change their appearance. These types of components are lighter weight and generally descend from a class named UIComponent.
UIComponent is the base class for components in Flex and defines a lot of the functionality you have already become familiar with, such as automatic sizing and properties such as left, top, right, and bottom. Components that descend from UIComponent directly tend to be more self-contained with regard to their visual experience. In other words, they do not have a separate skin class that controls the way they appear on the screen.
Conversely, components may descend from SkinnableComponent. SkinnableComponent also descends from UIComponent, but it adds a key piece of additional functionality. It allows for the separation of all functionality related to the way the component appears to be defined in a separate class. Put simply, components that in some way descend from SkinnableComponent can have a skin.
Why does this all matter? It changes the definition of the word component. When using nonskinnable components such as Label, the component is mostly self-contained. Any intended visuals of the component must be contained within the class.
However, exactly the opposite is true of skinnable components. When creating a skinnable component, nothing about the way the component appears on the screen is defined inside the component itself; it is all defined in the skin. You can think of a skinnable component as consisting of two halves that must work together to create a whole.
Effectively, this is a separation of form and function. Skins are purely visual; they contain no logic for interactivity. Skinnable components do not have a visual representation without a skin; they are purely the functional aspects of a class.
Examining this image, you’ll see your current shopping cart item view from your Flex Grocer application.
Right now this image does not represent a single component in your code: It is three separate components. This code from ShoppingView shows these declarations:
<s:List id="cartList"
dataProvider="{shoppingCart.items}"
includeIn="State1"
labelFunction="renderProductName"
dragEnter="cartList_dragEnterHandler(event)"
dragDrop="cartList_dragDropHandler(event)"/>
<s:Label text="Your Cart Total { currency.format( shoppingCart.total ) }"/>
<s:Button includeIn="State1" height="17"
label="View Cart"
styleName="cartButton"
click="handleViewCartClick( event )"/>
Ignoring any visual changes for the moment, why might you want to make this into a single component as opposed to leaving it as is? The answer to that question comes down to the interface, encapsulation, and reuse.
To provide this display on the screen correctly right now, you need to remember to do the following:
• Set the includeIn
property correctly on a couple of different instances.
• Pass the shopping cart’s items, not the entire shopping cart, to the List instance.
• Define a CurrencyFormatter on this page.
• Pass the total from the shopping cart into the format()
function before passing it to the Label’s text
property.
• Include functions to handle dragging and dropping.
While all those things are fine if you are the author of this code and only intend to duplicate this block once, consider it from another perspective. If you were going to instruct someone on how to add a listing of their shopping cart items to their page, would you want to explain each of those things? Suppose this component needed to be added in several different places in the application: Would copying all those pieces each time make sense? And lastly, in the abstract, all this code is currently in a class called ShoppingView. ShoppingView’s main job is to present a list of products and a view of the shopping cart to the user. Does code that understands how drag and drop works inside the List class really belong in ShoppingView?
The answer is no. When you create classes in an object-oriented language, you want to have a clear sense of what the resultant object will do. Ideally, it should have as singular a purpose as possible to keep the code maintainable and understandable. Right now ShoppingView does a variety of things and, from an object-oriented perspective, knows too much about the relationship of these objects to allow these pieces to be reusable. To solve that problem, you’re going to take this one function that displays and handles the items in a user’s shopping cart and refactor it into a new object with this purpose. Along the way, you’ll gain the ability to skin this object and simplify its use—all because you have made the commitment to create a component from this functionality.
You cannot create a component without knowing its intended purpose, so let’s examine the current list and related controls alongside a new intended look and feel for this component.
Reexamining the code for the current implementation will give you an initial set of the requirements. You want to be able to replace this code and the associated functions in the ShoppingView with one component. Therefore, it must be able to do the same things.
<s:List id="cartList"
dataProvider="{shoppingCart.items}"
includeIn="State1"
labelFunction="renderProductName"
dragEnter="cartList_dragEnterHandler(event)"
dragDrop="cartList_dragDropHandler(event)"/>
<s:Label text="Your Cart Total { currency.format( shoppingCart.total ) }"/>
<s:Button includeIn="State1" height="17"
label="View Cart"
styleName="cartButton"
click="handleViewCartClick( event )"/>
Looking at this code, you should be able to gather a few important points. The component needs to:
• Display the contents of the shoppingCart’s items collection, which is just a collection of ShoppingCartItem instances.
• Accept drag-and-drop operations as a way of adding items.
• Display the shopping cart’s total.
• Format the shopping cart’s total.
• Facilitate switching to the cartView state.
This code uses generic Flex components: List, Label, and Button. Generic components are fantastic building blocks, but they force your application code, the code contained within files like ShoppingView, to do more work.
For example, you simply can’t tell a generic component to display your ShoppingCart object. Instead, any person using this code has to provide the List with the items collection, the Label with the formatted total, and so on.
Generic Flex components aren’t designed to understand concepts specific to your application. However, when you create your own custom components, you can tailor them to understand the objects that are important to your application and therefore reduce the pain in using them.
If you were to close your eyes and envision the perfect interface for your ShoppingList, what might that be? Perhaps instead of passing in items to a List and totals to a Label, you would just pass the entire shoppingCart and the component would know what to do with it. Perhaps instead of bothering you when someone dragged something onto the component or dropped it, the component would just tell you that there is a new product to add to the cart. This is an important exercise. When creating a new component one of the most critical things to get right is the interface—in other words, how the rest of the world will use your component in code. Here is the proposed interface for the new ShoppingList component:
<components:ShoppingList
shoppingCart="{shoppingCart}"
addProduct="addProductHandler(event)"
viewCart="currentState='cartView'"/>
The new component will accept a ShoppingCart instance as a property. It will let you know when a user attempts to add a product or clicks the view cart button via events that are easily handled. It will hide all the messy details internally, making it much easier to use—and reuse.
The last step before you begin creating your custom component is to choose a base class. That is the class you’ll extend as your starting point. Choosing the base class for your new component is a critical decision and one you cannot make without a thorough understanding of the problem, so let’s start there.
Reexamining the image from earlier, you’ll see your current shopping cart item view on the left and the proposed shopping cart item view on the right. They look quite a bit different, but there are functional differences as well.
When you are deciding on a base class, you’re trying to determine if there is another class that already does most of the needed work on your behalf. For instance, earlier you created ProductList. You did so by extending the DataGroup and changing a few things to make ProductList a viable component for your needs.
In this case, you are making a component that has an area to display a list of items. It also has an area to display the number of items in the cart, an area to display a total, and a View Cart button. Unlike ProductList, this component doesn’t exactly mirror the functionality of any one Flex component. Instead, it’s a composite of many different components interacting in a specific way.
As there isn’t a component in Flex that provides you with the needed functionality, it will be up to you to build it all. While doing so, you are also going to allow for others in the future to change the way your component looks via skinning. Therefore, you’ll use SkinnableComponent as your base class.
You’ll begin building the component to replace the shopping cart items list currently in ShoppingView. Start by creating a new ActionScript class.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer.fxp project from the Lesson18/start folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
Now that you have a class, you need to define the interface explained earlier in code. The steps in this section rely heavily on Flash Builder. Learn to use these tools well, and you’ll save immense amounts of time.
shoppingCart
of type ShoppingCart
.
private var shoppingCart:ShoppingCart;
Be sure to use code completion when typing so that Flash Builder imports cart.ShoppingCart on your behalf.
shoppingCart
in the line of code you just wrote. From the pop-up menu, choose Source > Generate Getter/Setter.This dialog box will generate a new getter and setter function on your behalf, saving you typing and typos.
shoppingCart
property, and your original variable will be renamed with an underscore.
private var _shoppingCart:ShoppingCart;
public function get shoppingCart():ShoppingCart {
return _shoppingCart;
}
public function set shoppingCart(value:ShoppingCart):void {
_shoppingCart = value;
}
This property was the first of three things that made up the ShoppingList interface. The remaining two are both events, which you’ll add next.
addProduct
that will dispatch an event
of type events.ProductEvent
.
[Event(name="addProduct",type="events.ProductEvent")]
viewCart
, which will dispatch an event of type flash.events.Event
.
[Event(name="viewCart",type="flash.events.Event")]
import events.ProductEvent;
import flash.events.Event;
Your public interface is now complete, and you can change your MXML to use your new component.
Although your new component does not yet have any functionality useful to the user, its public interface is complete. This means you can replace your existing code with this new component.
This is a great way to check your design and ensure you met all the requirements before continuing with implementation. If your component can be dropped into the place where it is eventually needed, you likely have the basics covered.
<s:VGroup id="cartGroup" height="100%"
width.cartView="100%">
<components:CartGrid id="dgCart"
includeIn="cartView"
width="100%" height="100%"
dataProvider="{shoppingCart.items}"
removeProduct="removeProductHandler(event)"
/>
<s:Button includeIn="cartView"
label="Continue Shopping"
styleName="cartButton"
click="this.currentState=''"/>
</s:VGroup>
dgCart
but still inside cartGroup
and pass it a reference to the shoppingCart
. Previously, the List was only included in State1, so also add that logic to this tag.
<components:ShoppingList
includeIn="State1"
shoppingCart="{shoppingCart}"/>
addProduct
event by calling the addProductHandler
event listener, which is already defined in this view.
<components:ShoppingList
includeIn="State1"
shoppingCart="{shoppingCart}"
addProduct="addProductHandler(event)" />
Technically this component already has a reference to the shoppingCart
, which means you could manually add a new product anytime you wanted without dispatching and handling this event. However, there are two good reasons not to do so. First, there is already logic on this view to handle the Add Product button click from the ProductList. Reusing this logic means less duplication, but more importantly it means if this logic needs to change, it changes in only one place.
Further, while you are making this component more specific, it is still best to separate the logic that your application performs from the way it is displayed. This component is about displaying items in a specific way and interacting with the user. You really don’t want it to also have the responsibility of understanding how products are added to the cart or you’re back to having components that know too much—part of what we’re correcting by moving some of this code out of ShoppingView.
viewCart
event by setting currentState
to cartView
. The final tag should look like this:
<components:ShoppingList
includeIn="State1"
shoppingCart="{shoppingCart}"
addProduct="addProductHandler(event)"
viewCart="currentState='cartView'"/>
Your new component is now taking the place of the older pieces, but there is now extraneous code in ShoppingView that can be eliminated—the functionality will be moved into the ShoppingList component.
renderProductName()
, cartList_dragEnterHandler()
, and cartList_dragDropHandler()
methods from ShoppingView. You may also delete the following imports, which were used only by these methods:
import mx.core.IUIComponent;
import mx.events.DragEvent;
import mx.managers.DragManager;
import mx.core.DragSource;
The functionality of these methods belongs to the ShoppingList now and will no longer be needed in ShoppingView.
You presently have function with no form. You’ve learned that components based on SkinnableControl are really two halves, one side representing the function and the other the form. Flex can’t figure out what you want displayed on the screen. You’ll deal with that issue next.
You created the stub for your new custom component in the previous section, but now you want to define its visual appearance and then link the two together. Defining the requirements for these two components to talk and establishing the visual display will be the focus of this section.
Components that support skinning in Flex are composed of two pieces. This separation provides enormous capability but also some complexity. The two halves need to communicate and they need to set requirements for each other. The functional side of the component in your case will be responsible for displaying the total. Therefore, it needs to know that there will be a label created by the visual side allowing that to happen.
These requirements are set via three metadata tags that collectively help tame the madness of this dynamic component model. You learned about these tags briefly in Lesson 17, “Customizing a Flex Application with Skins”; however, you’ll now use them to define your component.
The first metadata tag is called SkinPart. The SkinPart metadata is responsible for defining what pieces are required of the skin to be considered legitimate. Using your component as an example, the ShoppingList will need to indicate that it needs some place to put the total, the quantity, and the items. The Flash Builder environment will not allow someone to assign a skin to your component that does not implement all these required parts.
The SkinPart metadata is used inside the class and above a property. In this example:
[SkinPart(required="true")]
public var myLabel:Label;
a component is indicating that the skin must have a Label named myLabel
to be considered a valid skin. If required
is set to false
, it is optional for the skin to implement.
The next piece of metadata is called SkinState. The SkinState metadata tag is responsible for indicating what states are required of the skin. The simplest example of this is the normal and disabled state. In Flex you can set the enabled
property for any UIComponent to false
. Doing so should prevent interaction with the component and often changes the component visually to ensure the user perceives the reason for the lack of interaction.
[SkinState("normal")]
[SkinState("disabled")]
When this metadata is added above the class declaration for a component, it means that any skin for this component must have these two states defined. It does not prescribe what the skin does during a state change. For instance, it is completely your choice if the skin blinks or does nothing in a disabled state, but it must be able to handle this state change in whatever way you see fit.
The final piece of metadata important to skinning resides in the skin itself. This piece of metadata is called HostComponent.
[HostComponent("components.MyList")]
The HostComponent tag is used to associate a skin with its component. In other words, it is used to indicate which halves make the whole. This is extremely important as it allows Flash Builder to do compile-time checking on your behalf. If you create a new skin and specify that it is for a particular component, Flash Builder can check the SkinState and SkinPart metadata of the named component and verify that your skin meets those requirements. That way, you know at compile time, instead of runtime, if there is a problem.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer-PreSkin.fxp project from the Lesson18/intermediate folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
[SkinState("normal")]
[SkinState("disabled")]
You are specifying that anyone making a skin for your component must be able to handle these two states, or it is not to be considered a valid skin.
_shoppingCart
property, add a public variable named totalLabel
of type Label
. Be sure to use code completion, but also be sure that you specify spark.components.Label
.totalLabel
property, add the SkinPart metadata, indicating that this particular part is required. Your code should look like this:
[SkinPart(required="true")]
public var totalLabel:Label;
dataGroup
of type DataGroup
.
[SkinPart(required="true")]
public var dataGroup:DataGroup;
quantityLabel
of type Label
.
[SkinPart(required="true")]
public var quantityLabel:Label;
viewCartBtn
of type Button
.
[SkinPart(required="false")]
public var viewCartBtn:Button;
It should compile successfully without any errors or warnings.
You now have a component waiting to be skinned. It has the required skin parts and the skin states defined. In this section, you’ll create a skin for the new component and apply it so that you can run the application and see some initial results.
<?xml version="1.0" encoding="utf-8"?>
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark">
<!-- host component -->
<fx:Metadata>
[HostComponent("components.ShoppingList")]
</fx:Metadata>
<!-- states -->
<s:states>
<s:State name="normal" />
<s:State name="disabled" />
</s:states>
<!-- SkinParts
name=dataGroup, type=spark.components.DataGroup, required=true
name=totalLabel, type=spark.components.Label, required=true
name=quantityLabel, type=spark.components.Label, required=true
name=viewCartBtn, type=spark.components.Button, required=false
-->
</s:Skin>
Note that the HostComponent metadata was entered on your behalf, the required skin states were created based on the SkinState metadata in your ShoppingList class, and Flash wrote a comment in the code reminding you of the SkinParts you must have to be considered valid.
<s:Image/>
tag with a source embedding the receipt.png @Embed('assets/receipt.png')
.
<s:Image source="@Embed('assets/receipt.png')"/>
This will load the background image for your new component. Here is a quick reminder of the skin you are about to build.
<s:Label/>
tag with an id
of quantityLabel
. Set the left
property to 50
and the top
property to 10
.
<s:Label id="quantityLabel" left="50" top="10"/>
Note that the id
of quantityLabel
is being used. This id
is the same as the property marked with the SkinPart metadata in the ShoppingList. For every required SkinPart in the ShoppingList, you’ll have a matching component here with that id
.
quantityLabel
, add a tag pair for <s:Scroller></s:Scroller>.
Set the left
property to 22
, the top
property to 30
, the width
to 149
, and the height
to 115
. You’ll also set a property called horizontalScrollPolicy
to off
.
<s:Scroller left="22" top="30" width="149" height="115"
horizontalScrollPolicy="off">
</s:Scroller>
In Flex, not every object knows how to scroll its own content. Instead, you wrap these instances inside a Scroller to handle any scrolling needs. In this case, you are setting the size and position of the area you wish to scroll. By default, the Scroller scrolls horizontally and vertically. In this case, you only want vertical scrolling so horizontalScrollPolicy
has been turned off
.
<s:Scroller></s:Scroller>
tag pair, add an <s:DataGroup></s:DataGroup>
tag pair. Set the id
of this DataGroup to dataGroup
, one of your required skin parts. Set the itemRenderer
property to spark.skins.spark.DefaultItemRenderer
.
<s:Scroller left="22" top="30" width="149" height="115"
horizontalScrollPolicy="off">
<s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
</s:DataGroup>
</s:Scroller>
This DataGroup will be responsible for displaying the items in your ShoppingCart. For now, you are using the DefaultItemRenderer, which displays the text from your toString()
method of your ShoppingCartItem. You’ll customize this later.
<s:DataGroup></s:DataGroup>
tag pair, set the layout
property to an instance of the VerticalLayout class, setting the gap
to 0
. Your code for the Scroller should look like this:
<s:Scroller left="22" top="30" width="149" height="115"
horizontalScrollPolicy="off">
<s:DataGroup id="dataGroup"
itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout gap="0"/>
</s:layout>
</s:DataGroup>
</s:Scroller>
<s:Line>
tag pair with an id
of divider
. Set the left
property to 22
, the right
property to 10
, and the top
to 155
. Inside the tag pair, set the stroke
property to an instance of the SolidColorStroke with a color
of #545454
and a weight
of 1
.
<s:Line id="divider" left="22" right="10" top="155">
<s:stroke>
<s:SolidColorStroke color="#545454" weight="1"/>
</s:stroke>
</s:Line>
This code does nothing more than draw a dividing line before the total. You only have two labels and a button left until your skin is complete.
<s:Label/>
below the line with the text
set to Total:
, left
set to 22
, top
to 162
, color
to #0000FF
, and fontSize
to 11
.
<s:Label text="Total:" left="22" top="162" color="#0000FF" fontSize="11"/>
<s:Label/>
with the id
set to totalLabel
, right
set to 12
, top
to 162
, color
to #0000FF
, and fontSize
to 11
.
<s:Label id="totalLabel" right="12" top="162" color="#0000FF" fontSize="11"/>
This label will hold the actual formatted total on the shopping cart.
<s:Button/>
with the id
set to viewCartBtn
, label
set to View Cart
, horizontalCenter
to 12
, and bottom
to 20
.
<s:Button id="viewCartBtn" label="View Cart" horizontalCenter="12" bottom="20"/>
This completes your skin for the moment. The code you added should look like the following snippet:
<s:Image source="@Embed('assets/receipt.png')"/>
<s:Label id="quantityLabel" left="50" top="10"/>
<s:Scroller left="22" top="30" width="149" height=
"115" horizontalScrollPolicy="off">
<s:DataGroup id="dataGroup"
itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<s:VerticalLayout gap="0"/>
</s:layout>
</s:DataGroup>
</s:Scroller>
<s:Line id="divider" left="22" right="10" top="155">
<s:stroke>
<s:SolidColorStroke color="#545454" weight="1"/>
</s:stroke>
</s:Line>
<s:Label text="Total:" left="22" top="162" color="#0000FF" fontSize="11"/>
<s:Label id="totalLabel" right="12" top="162" color="#0000FF" fontSize="11"/>
<s:Button id="viewCartBtn" label="View Cart" horizontalCenter="12" bottom="20"/>
skinClass
and set it equal to skins.ShoppingListSkin
.
<components:ShoppingList
skinClass="skins.ShoppingListSkin"
includeIn="State1"
shoppingCart="{shoppingCart}"
addProduct="addProductHandler(event)"
viewCart="currentState='cartView'"/>
You created the stub for your new custom component and defined its visual appearance. Now it is time to add the final functionality so that both halves of the component work together. This is also the time when you’ll need to understand just a bit about how Flash Player works internally as well as how to manage the internally asynchronous nature of components.
Flash Player is a single-threaded virtual machine. In the simplest sense, that means it does one thing at a time and regardless of how long it might take, it will never, ever interrupt code that is running. It always allows one task to finish before moving on to something else.
The problem with this philosophy is that if something takes a long time to do, it can cause Flash Player to stop updating the screen and mouse movements at a reasonable rate, creating a negative user experience.
To combat that issue, the Flex framework is event based and has an asynchronous component model. This means that certain aspects of what happens inside components happen at predetermined times when Flash Player is most optimally able to deal with changes. It also means that as a developer, you cannot make assumptions about when something is ready, complete, or otherwise available.
The Flex framework has prescribed ways to deal with this complexity. As a developer, if you embrace these concepts, things will go your way. If you try to do your own thing, the framework will find a way to punish you. Things may work seemingly well on your development machine but differently in production. Components may work in one circumstance but not another. Because all these issues have to do with timing that can change from machine to machine, it is imperative that you follow the rules.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer-PreFunction.fxp project from the Lesson18/intermediate folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
_shoppingCart
property, create a new private variable named shoppingCartChanged
typed as a Boolean
. Set it to a default value of false
.
private var shoppingCartChanged:Boolean = false;
This is known as a change flag as its only purpose is to indicate the state of something. Internally this will be used to let your component know when it has a new ShoppingCart that must be displayed to the user.
quantityChanged
and totalChanged
, both typed as Boolean
and with default values of false
.
private var shoppingCartChanged:Boolean = false;
private var quantityChanged:Boolean = false;
private var totalChanged:Boolean = false;
These other change flags will be used for tracking when either the quantity or total need updating.
shoppingCart
property, immediately after _shoppingCart
is set to value
, set the shoppingCartChanged
flag to true
.
public function set shoppingCart(value:ShoppingCart):void {
_shoppingCart = value;
shoppingCartChanged = true;
}
invalidateProperties()
method that your class has due to inheritance.
public function set shoppingCart(value:ShoppingCart):void {
_shoppingCart = value;
shoppingCartChanged = true;
invalidateProperties();
}
Everything that descends from UIComponent in Flex has this method available. This is one of several methods designed to help you deal with the asynchronous way Flex creates components. In Flex, skins can be added to and removed from components at runtime, so you cannot assume that all the parts of the skin are waiting and ready for your commands.
When you call invalidateProperties()
, you are effectively asking the Flex framework to schedule a call to another special method named commitProperties()
at a more opportune time. Flex manages the complexity of all the components that may want to do some work at any given time and calls them in the order most appropriate for performance.
shoppingCart
property setter, override a protected method named commitProperties()
. This method takes no arguments. Immediately inside the method, call super.commitProperties();
.
override protected function commitProperties():void {
super.commitProperties();
}
This method is eventually called whenever you or any other code calls invalidateProperties()
. Flex calls this method at an optimized time for your component to do the work it needs. In addition to the call you made to invalidateProperties()
, other parts of the Flex framework also call this method. It will be called automatically whenever a new skin is added or removed.
super.commitProperties()
, write an if
statement that checks if your shoppingCartChanged
flag is true
and if the dataGroup
has already been created.
override protected function commitProperties():void {
super.commitProperties();
if ( shoppingCartChanged && dataGroup ) {
}
}
The code inside this if
statement will now only execute if your flag is true
and if Flex has already created the dataGroup.
if
statement, set the shoppingCartChanged
flag to false
. Then set the dataProvider
of the dataGroup
to shoppingCart.items
.
override protected function commitProperties():void {
super.commitProperties();
if ( shoppingCartChanged && dataGroup ) {
shoppingCartChanged = false;
dataGroup.dataProvider = shoppingCart.items;
}
}
All this code is mandatory. If you tried to access the dataProvider
property of dataGroup before dataGroup existed, your application would crash. Memorize this pattern. Whenever a Flex component sets a property from outside the component (like your shoppingCart
property) to another visual child component (like something in the skin), the commitProperties()
method is used to ensure that the component will not crash due to timing issues.
This is a great first step, but you have a lot more work to do.
shoppingCart
setter. After the shoppingCartChanged
flag is set to true
but before invalidateProperties()
is called, you need to write an if
statement that checks if the shopping cart just passed to the function exists.
public function set shoppingCart(value:ShoppingCart):void {
_shoppingCart = value;
shoppingCartChanged = true;
if ( _shoppingCart ) {
}
invalidateProperties();
}
It is always possible that a user working with your component passed in a null
value. This check makes sure your code won’t break when it tries to access the data. When developing components for reuse, you must code defensively.
if
statement, add a new event listener to the items
property of the _shoppingCart
. You’ll listen for a CollectionEvent.COLLECTION_CHANGE
event and call a method name handleItemChanged()
if this occurs.
public function set shoppingCart(value:ShoppingCart):void {
_shoppingCart = value;
shoppingCartChanged = true;
if ( _shoppingCart ) {
_shoppingCart.items.addEventListener( CollectionEvent.COLLECTION_CHANGE, handleItemChanged );
}
invalidateProperties();
}
This is the same code you wrote inside the ShoppingCart class so that the ShoppingCart would monitor changes in the ShoppingCartItems. This will serve a similar purpose here.
handleItemChanged()
. It will accept one parameter, an event
of type CollectionEvent
, and return nothing. Inside the method, set the totalChanged
flag to true
and the quantityChanged
flag to true
, and then call the invalidateProperties()
method.
private function handleItemChanged( event:CollectionEvent ):void {
totalChanged = true;
quantityChanged = true;
invalidateProperties();
}
This method will be called anytime you add, remove, or update any of the ShoppingCartItem instances. It sets these two changed flags to true
and asks the Flex framework to call commitProperties()
when it has the opportunity.
currency
of type spark.formatters.CurrencyFormatter
near the top of this class just between the totalChanged
flag and the totalLabel
SkinPart declaration.
private var currency:CurrencyFormatter;
This component is now going to take care of formatting the total before displaying it to the user.
You’ll see there are three possible CurrencyFormatters. The one in the flash.globalization package is the low-level Flash Player version which provides much of the basic functionality. As mentioned in the formatters and validators package, there is also one in the mx
and Spark package. Always defer to the spark package if you are not sure.
super()
assign a new CurrencyFormatter class instance to the currency
property. Then set the useCurrencySymbol
property of the instance to true
. Finally, call the addStyleClient()
method and pass the currency instance.
public function ShoppingList() {
super();
currency = new CurrencyFormatter();
currency.useCurrencySymbol = true;
this.addStyleClient( currency );
}
Previously you created instances of the CurrencyFormatter through MXML. Here you are simply generating the ActionScript code that Flex would normally write on your behalf. The last line, addStyleClient()
ensures that the CurrencyFormatter instance receives updates if you change the locale style in the application. This ensures the CurrencyFormatter here shows the correct currency type as the remainder of the application.
commitProperties()
method. Below your other if
statement, add a new if
statement that checks if the totalChanged
is true
and if totalLabel
exists. If it does, set the totalChanged
flag to false
.
if ( totalChanged && totalLabel ) {
totalChanged = false;
}
if
statement but just below the code that sets totalChanged
to false
, set the text
property of the totalLabel
to the result of calling the currency.format()
method, passing it the shoppingCart.total
as an argument.
if ( totalChanged && totalLabel ) {
totalChanged = false;
totalLabel.text = currency.format( shoppingCart.total );
}
Now each time the items in the ShoppingCart change, the shopping cart’s total will be reformatted and the label in the skin will be updated.
if
block, add one final if
statement. Check if the quantityChanged
flag is true
and if the quantityLabel
exists. If it does, set the quantityChanged
flag to false
.
if ( quantityChanged && quantityLabel ) {
quantityChanged = false;
}
if
statement but just below the line of code that sets quantityChanged
to false
, set the text
property of the quantityLabel
to the result of concatenating the String "Shopping List (" +
with the length of the shopping cart’s items collection and a final ")"
.
if ( quantityChanged && quantityLabel ) {
quantityChanged = false;
quantityLabel.text = "Shopping List (" + shoppingCart.items.length + ")";
}
Now each time the items in the ShoppingCart change, the shopping cart’s quantity will be reformatted and the label in the skin will be updated.
In the next section, you’ll deal with drag and drop as well as the View Cart button.
Your component now updates and reflects data changes in the ShoppingCart instance. However, you still can’t drag an item into the new ShoppingList, and you can’t click the View Cart button. Those are your next tasks.
To perform them, you need to learn about another method available for override in SkinnableComponent descendants. That method is named partAdded()
, and there is a corresponding method named partRemoved()
. The partAdded()
method will be called each time a new part of your skin is created and ready to access. The partRemoved()
method is called when that skin part is removed and no longer part of the component.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer-PreDrag.fxp project from the Lesson18/intermediate folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
commitProperties()
method, override the protected method named partAdded
. This method accepts two parameters: the first is called partName
of type String
and the second is called instance
of type Object
. The method returns void
. Immediately inside the method, call the super.partAdded
, passing along the required arguments.
override protected function partAdded(partName:String, instance:Object):void {
super.partAdded( partName, instance );
}
This method will be called each time a new skin part is built and ready for you to access. The partName
will be the name of the skinPart (dataGroup
, totalLabel
, and so on). The instance will be a reference to the newly created object.
if
statement that checks if the instance
was dataGroup
. Then create an else
block that checks if it was viewCartBtn
.
if ( instance === dataGroup ) {
} else if (instance === viewCartBtn ) {
}
You might have noticed the three equals sign in this code. This means strict equality. Normally in ActionScript the == looks for equality but allows the two items being compared to have different types so long as they evaluate to the same value. Strict equality indicates the value and type must match.
if
statement for the dataGroup, add an event listener to the dataGroup instance for DragEvent.DRAG_ENTER
and specify handleDragEnter
as the listener. Add a second event listener to the dataGroup for DragEvent.DRAG_DROP
and specify handleDragDrop
as the listener for this event.
if (instance === dataGroup ) {
dataGroup.addEventListener( DragEvent.DRAG_ENTER, handleDragEnter );
dataGroup.addEventListener( DragEvent.DRAG_DROP, handleDragDrop );
} else if (instance === viewCartBtn ) {
}
This is just the ActionScript version of add event listeners to dragEnter
and dragDrop
in MXML.
When the partAdded()
method is called by the Flex framework, it passes the partName
, such as dataGroup
, as well as an instance of type Object
. Instead of adding your listener to dataGroup
directly, you could have used (instance as DataGroup).addEventListener()
. Those two statements would yield identical results.
handleDragEnter()
that accepts an event
parameter of type DragEvent
and returns void
.
private function handleDragEnter( event:DragEvent ):void {
}
event.dragSource.hasFormat()
method and pass it the string singleProduct
. If this method returns true
, call DragManager.acceptDragDrop()
, passing it the event.target
typed as an IUIComponent
.
private function handleDragEnter( event:DragEvent ):void {
if(event.dragSource.hasFormat( "singleProduct" )){
DragManager.acceptDragDrop( event.target as IUIComponent );
}
}
This method should look familiar. This is nearly the same method you wrote for the dragEnter
handler previously in ShoppingView. Now you are just handling everything in ActionScript.
handleDragDrop()
that accepts an event
parameter of type DragEvent
and returns void
.
private function handleDragDrop( event:DragEvent ):void {
}
product
of type Product
, and assign its initial value to the result of the event.dragSource.dataForFormat()
method, passing it the string singleProduct
. Cast the result as a Product object.
private function handleDragDrop( event:DragEvent ):void {
var product:Product = event.dragSource.dataForFormat( "singleProduct" ) as Product;
}
This method should also look familiar. It is again nearly the same method you wrote for the dragDrop
handler earlier in ShoppingView.
drag
event, create a new local variable named prodEvent
of type ProductEvent
. Assign its value to a new instance of the ProductEvent, passing the string addProduct
to the first parameter and the Product
object to the second.
var prodEvent:ProductEvent = new ProductEvent( "addProduct", product );
In the very beginning of this exercise, you told the Flex compiler you would dispatch a product event. You are about to fulfill that promise.
prodEvent
event.
private function handleDragDrop( event:DragEvent ):void {
var product:Product = event.dragSource.dataForFormat( "cartFormat" ) as Product;
var prodEvent:ProductEvent = new ProductEvent( "addProduct", product );
dispatchEvent( prodEvent );
}
On a successful drag-and-drop operation, your code now dispatches an event indicating that the product should be added to the cart.
partAdded()
method. In the else
clause for the viewCartBtn part, add an event listener to the viewCartBtn instance for the MouseEvent.CLICK
event, passing handleViewCartClick
as the listener. Here is the final partAdded()
method:
override protected function partAdded(partName:String, instance:Object):void {
super.partAdded( partName, instance );
if ( instance === dataGroup ) {
dataGroup.addEventListener( DragEvent.DRAG_ENTER, handleDragEnter );
dataGroup.addEventListener( DragEvent.DRAG_DROP, handleDragDrop );
} else if ( instance === viewCartBtn ) {
viewCartBtn.addEventListener( MouseEvent.CLICK, handleViewCartClick );
}
}
handleViewCartClick()
that accepts an event parameter of type MouseEvent
and returns void
.
private function handleViewCartClick( event:MouseEvent ):void {
}
viewEvent
of type Event
. Assign it to a new instance of the Event class, passing the string viewCart
. Finally, dispatch the event.
private function handleViewCartClick( event:MouseEvent ):void {
var viewEvent:Event = new Event( "viewCart" );
dispatchEvent( viewEvent );
}
This will dispatch the viewCart
event that you defined long ago at the beginning of this component.
Your component now works quite well, but there is a problem. Skins in Flex can be changed at runtime. You are adding event listeners to a number of components in the skin but not cleaning up after yourself.
The same is true of the data passed to the shoppingCart. Right now you add an event listener; however, if someone provided a new ShoppingCart instance, you would be listening to two collections for changes instead of just the most recent.
partAdded()
method in its entirety. Paste it just below the existing method. Change the name of the function to partRemoved
and change the call to the super class to partRemoved
as well.
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved( partName, instance );
if ( instance === dataGroup ) {
dataGroup.addEventListener( DragEvent.DRAG_ENTER, handleDragEnter );
dataGroup.addEventListener( DragEvent.DRAG_DROP, handleDragDrop );
} else if ( instance === viewCartBtn ) {
viewCartBtn.addEventListener( MouseEvent.CLICK, handleViewCartClick );
}
}
You should have just changed partAdded
to partRemoved
in two places. If you changed it a different number of times, recheck before proceeding.
partRemoved()
method, change all the calls to addEventListener()
to removeEventListener()
. Keep the parameters the same.
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved( partName, instance );
if ( instance === dataGroup ) {
dataGroup.removeEventListener( DragEvent.DRAG_ENTER, handleDragEnter );
dataGroup.removeEventListener( DragEvent.DRAG_DROP, handleDragDrop );
} else if ( instance === viewCartBtn ) {
viewCartBtn.removeEventListener( MouseEvent.CLICK, handleViewCartClick );
}
}
You should have just changed addEventListener
to removeEventListener
in three places. If you changed it a different number of times, recheck before proceeding. Now each time a part is removed, it removes the accompanying event listeners.
shoppingCart
setter function.
Currently this function adds an event listener each time it is called. You’ll now also remove the old event listener.
if
block that checks if the _shoppingCart
property exists and adds an event listener. Paste it as the first line of this method.
public function set shoppingCart(value:ShoppingCart):void {
if ( _shoppingCart ) {
_shoppingCart.items.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleItemChanged );
}
_shoppingCart = value;
shoppingCartChanged = true;
if ( _shoppingCart ) {
_shoppingCart.items.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleItemChanged );
}
invalidateProperties();
}
This method now adds two event listeners, which is worse than before.
_shoppingCart.items.addEventListener()
to removeEventListener()
instead.
public function set shoppingCart(value:ShoppingCart):void {
if ( _shoppingCart ) {
_shoppingCart.items.removeEventListener(CollectionEvent.COLLECTION_CHANGE, handleItemChanged );
}
_shoppingCart = value;
shoppingCartChanged = true;
if ( _shoppingCart ) {
_shoppingCart.items.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleItemChanged );
}
invalidateProperties();
}
This code now checks to see if there was already a shoppingCart
with an event listener. If so, it removes it before adding a listener to a new one.
The ShoppingList is finished. All that is left is to customize the way the DataGroup instance in the skin displays data.
The last step to finish up the presentation of this component is to create a custom renderer and apply it to the DataGroup that the ShoppingListSkin will use to render its data. This will complete the desired look of the component.
As you may remember from Lesson 10, “Using DataGroups and Lists,” extending DataRenderer is a fast and easy way to create a custom renderer for a DataGroup.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer-PreRenderer.fxp project from the Lesson18/intermediate folder. Please refer to the appendix for complete instructions on importing a project should you ever skip a lesson or if you ever have a code issue you cannot resolve.
<fx:Script>
tag pair. Inside the Script block, create a new bindable private variable named item
of type ShoppingCartItem
.
<fx:Script>
<![CDATA[
import cart.ShoppingCartItem;
[Bindable]
private var item:ShoppingCartItem;
]]>
</fx:Script>
data
. Set the item
property you just created to the value typed as a ShoppingCartItem
.
override public function set data(value:Object):void{
this.item = value as ShoppingCartItem;
}
<s:CurrencyFormatter/>
tag with an id
of currency
and set the useCurrencySymbol
property to true
.
<s:CurrencyFormatter id="currency" useCurrencySymbol="true"/>
<s:Image/>
tag. Set its source
equal to assets/{item.product.imageName}
. Then set its width
to 25
and height
to 25
.
<s:Image source="assets/{item.product.imageName}" width="25" height="25"/>
<s:Label/>
tag. Set its left
to 30
, top
to 5
, right
to 30
, and text
to {item.product.prodName}
.
<s:Label left="30" top="5" right="30" text="{item.product.prodName}"/>
<s:Label/>
tag. Set its right
to 1
, top
to 5
, and text
to {currency.format(item.subtotal)}
.
<s:Label right="1" top="5" text="{currency.format(item.subtotal)}"/>
spark.skins.spark.DefaultItemRenderer
, and set it instead to components.ShoppingListRenderer
.You should now have a completely styled custom component.
In this lesson, you have:
• Learned the concepts of custom ActionScript components (page 420)
• Performed an extensive refactor (pages 424–429)
• Created an ActionScript skinnable component (pages 424–429, 439–450)
• Created your own skin (pages 434–438)
• Used the Scroller class (page 436–438)
• Learned to manage skin parts and life cycle (pages 445–452)