Sunday, December 02, 2007

Making Flex Menus Easier

The Flex framework makes your life a lot easier when creating an application. However, not everything is necessarily as easy as it should be right out of the box; however, there are extensibility points within the framework to make this job easier. One such particular example is Menus. So let's look at a quick example:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:MenuBar showRoot="false" menuShow="menuShowHandler(event)" itemClick="menuClickHandler(event)">
<mx:XML>
<menuitem id="rootMenu">
<menuitem label="File">
<menuitem id="menuNew" label="New"/>
<menuitem label="Open"/>
</menuitem>
<menuitem label="Edit">
<menuitem id="cut" label="Cut"/>
<menuitem id="copy" label="Copy"/>
<menuitem id="paste" name="paste" label="Paste" enabled="false"/>
<menuitem type="separator" />
<menuitem label="Other menu item"/>
</menuitem>
<menuitem id="windowMenu" label="Window">
<menuitem id="d1" label="Document 1" type="radio" group="docs" toggled="false"/>
<menuitem label="Document 2" type="radio" group="docs" toggled="true"/>
<menuitem id="d3" name="d3" label="Document 3" type="radio" group="docs" toggled="false"/>
<menuitem label="Document 4" type="radio" group="docs" toggled="false"/>
</menuitem>
</menuitem>
</mx:XML>
</mx:MenuBar>

<mx:Script>
<![CDATA[
import mx.events.MenuEvent;

private function menuClickHandler(event:MenuEvent):void {
switch (event.label) {
case "Cut":
cutHandler(event);
break;
case "Copy":
copyHandler(event);
break;
// many more cases if needed....
}
}

private function menuShowHandler(event:MenuEvent):void {
switch (event.label) {
case "Edit":
editHandler(event);
break;
// many more cases if needed....
}
}

private function cutHandler(event:MenuEvent):void {
trace("cut event");
}

private function copyHandler(event:MenuEvent):void {
trace("copy event");
}

private function editHandler(event:MenuEvent):void {
trace("edit event");
}
]]>
</mx:Script>
</mx:Application>

So you look at that, and it's pretty damn easy to create a menu. You create a menu based off of a simple dataProvider that's written in an easy-to-understand XML format. So what's wrong with it? Well, I think there are a few problems:

  • The XML is untyped, so it's easy to make a mistake or forget what the properties are (type, not typed and toggle, not toggled, etc...). It's also easy to forget the values the properties take (for example: type takes check, radio, separator and not checked, checkbox, or something similar)
  • Databinding is not supported, so to disable the cut, copy, and paste menu items, we'd have to access the dataProvider for all three menu items and change it manually. We'd have to do this in a special method and remember to do it everytime, whereas if data binding was supported, it'd be done for us automatically when one property changes (for example, when a textarea has focus or something)
  • Events are global for the whole menu. It's much better to attach events at the most specific point possible because we can avoid large, monolithic functions that essentially just dispatch the event somewhere else based on a large switch-case statement. Let the "framework" handle that for us.

Ok, so those are atleast some of the problems that I have with the current state of menus and specifying the data provider. However, we can fix these problems pretty easily. Since our data provider can be an object (not just XML), we can take advantage of this because MXML really just compiles down into an ActionScript object. Because MXML is essentially a typed object, we solve problem #1 (it can even tell us that type accepts check, radio, or seperator as inputs). Since MXML supports databinding, we solve problem #2, and with some functions of our own, we can define events on our new MXML object and get it routed to these more specific events and solve problem #3.

Here's what the example above will look like when done:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
<mx:MenuBar showRoot="false" menuShow="menuItemEventHandler(event)" menuHide="menuItemEventHandler(event)"
itemClick="menuItemEventHandler(event)" itemRollOut="menuItemEventHandler(event)"
itemRollOver="menuItemEventHandler(event)" change="menuItemEventHandler(event)">
<local:MenuItem id="rootMenu">
<local:MenuItem label="File">
<local:MenuItem id="menuNew" label="New"/>
<local:MenuItem label="Open"/>
</local:MenuItem>
<local:MenuItem label="Edit" menuShow="editHandler(event)">
<local:MenuItem id="cut" label="Cut" itemClick="cutHandler(event)"/>
<local:MenuItem id="copy" label="Copy" itemClick="copyHandler(event)"/>
<local:MenuItem id="paste" name="paste" label="Paste" enabled="false"/>
<local:MenuItem type="separator" />
<local:MenuItem label="Other menu item"/>
</local:MenuItem>
<local:MenuItem id="windowMenu" label="Window">
<local:MenuItem id="d1" label="Document 1" type="radio" group="docs" toggled="false"/>
<local:MenuItem label="Document 2" type="radio" group="docs" toggled="true"/>
<local:MenuItem id="d3" name="d3" label="Document 3" type="radio" group="docs" toggled="false"/>
<local:MenuItem label="Document 4" type="radio" group="docs" toggled="false"/>
</local:MenuItem>
</local:MenuItem>
</mx:MenuBar>

<mx:Script>
<![CDATA[
import mx.events.MenuEvent;

private function menuItemEventHandler(event:MenuEvent):void
{
if (event.item is MenuItem)
{
EventDispatcher(event.item).dispatchEvent(event);
}
else if (event.menu && event.menu.dataProvider &&
event.menu.dataProvider[0] is MenuItem &&
event.menu.dataProvider[0].parent is MenuItem)
{
EventDispatcher(event.menu.dataProvider[0].parent).dispatchEvent(event);
}
}
]]>
</mx:Script>

<mx:Script>
<![CDATA[
private function cutHandler(event:MenuEvent):void
{
trace("cut event");
}

private function copyHandler(event:MenuEvent):void
{
trace("copy event");
}

private function editHandler(event:MenuEvent):void
{
trace("edit event");
}
]]>
</mx:Script>
</mx:Application>

You can see the code above looks basically the same, but it does solve all of our problems. We can even reference MenuItems by id's now, treating them as first-class objects. The only tricky part is the menuItemEventHandler defined that you'll also need. This grabs the appropriate MenuItem object and then dispatches the event to the event handler defined for that MenuItem. This is different then the switch-case statement above because it doesn't actually know anything about our menu data -- it just dispatched it through to the event defined on the menu item, so it's the same for all menus.

So how was this so easily accomplished? Well it's with a new MenuItem class. It's basically just an object with some properites, like type, enabled, name, etc... and one important property, called children that's an array of MenuItems. In its simplest form, MenuItem looks like:

package
{
[Event(name="change", type="mx.events.MenuEvent")]
[Event(name="itemClick", type="mx.events.MenuEvent")]
[Event(name="menuHide", type="mx.events.MenuEvent")]
[Event(name="menuShow", type="mx.events.MenuEvent")]
[Event(name="itemRollOut", type="mx.events.MenuEvent")]
[Event(name="itemRollOver", type="mx.events.MenuEvent")]

[DefaultProperty("children")]
public class MenuItem extends EventDispatcher
{
public function MenuItem() {
}

[Bindable]
public var enabled:Boolean = true;

[Bindable]
public var toggled:Boolean;

public var name:String = null;

public var group:String = null;

public var parent:MenuItem = null;

public var label:String = null;

[Inspectable(category="General", enumeration="check,radio,separator")]
public var type:String = null;

public var icon:Object = null;

private var _children:Array = null;

[Inspectable(category="General", arrayType="MenuItem")]
[ArrayElementType("MenuItem")]
public function set children(c:Array):void
{
children = c;
if (c)
for (var i:int = 0; i < c.length; i++)
c[i].parent = this;
}

public function get children():Array
{
return _children;
}

// functions for manipulating children:
}
}

Two cool things to note here are with the meta-data. The first is with [DefaultProperty("children")] defined above the class. This tells the compiler that when children are added in MXML, what property it actually correlates to. Another example of this is in the Flex framework for List and Menu where dataProvider is the default property. Another metadata trick is with [Inspectable(category="General", enumeration="check,radio,separator")] where we tell Flex Builder what the type property accepts so that code-hinting works quite nicely.

So the code above actually work perfectly fine, but I added some extra methods for dealing with children (adding, find, remove, etc...), which I will disclose aren't tested very well. I've uploaded the full MenuItem.as code so you can download it. Mike Schiff, another Flex SDK engineer, was the first one to point out this technique, and his code was the basis for everything here (in fact, it was probably most of it).

This is a really simple technique but it helps with a few pits of creating menus in Flex. Not only that, but it can be applied to all components based on data providers. If you want to define a similar MXML object but want to have non-standard properties, like myType instead of type, you can tell the Flex framework this by using either dataDescriptor or something like labelField/labelFunction.

Wednesday, November 28, 2007

Flex Tutorial Videos

I just found out about a really cool list of videos about Flex/Flex Builder. They are great for learning info, especially about the new features for Flex 3: http://labs.adobe.com/wiki/index.php/Flex_3:Feature_Introductions As per blogging, I'll be posting some more code samples soon.

Saturday, October 20, 2007

Validation/Invalidation Mechanism

It's been quite a while since I blogged last--for that I apologize. When you create a UI component in Flex, there are 4 main methods we ask that you implement: createChildren, commitProperties, validateSize, and updateDisplayList. The Flex framework then calls these methods at the appropriate times, but if you get into component development, you need to understand not only how these mechanisms work, but why we have these mechanisms in the first place. As a new member to the Flex SDK team, this was one of the first questions I had. To help answer this, let's take a look at something I'm currently implementing. Note that whatever I show may or may not end up being the final implementation, but it's a good example to look at. In AIR (Adobe Integrated Runtime), they added NativeMenu's, which are menus native to each operating system and sit at the top of the screen for a window. The API for this is centered around instantiating Menus, MenuItems, and adding them all together. A typical example would look like:

var mainMenu:NativeMenu = new NativeMenu();


var fileMenu:NativeMenu = new NativeMenu();
var printItem:NativeMenuItem = new NativeMenuItem("Print");
printItem.keyEquivalent = "P";
printItem.keyEquivalentModifiers = [Keyboard.CONTROL];
printItem.mnemonicIndex = 0;
...


var saveItem:NativeMenuItem = new NativeMenuItem("Save");
...

   
var newItem:NativeMenuItem = new NativeMenuItem("New");
...


fileMenu.addItem(printItem);
fileMenu.addItem(saveItem);
fileMenu.addItem(newItem);


var fileMenuItem:NativeMenuItem = new NativeMenuItem("File");
fileMenuItem.subMenu = fileMenu;
mainMenu.addItem(fileMenuItem);


// repeat for Edit, View, etc...
               
This can be a really long process. If there's anything Flex has taught us, it's that declarative markup can be a lot easier. If you look at the Flex interface, we use dataProviders to drive our menu. We want to do the same thing here for NativeMenus. So the idea would be:

[Bindable]
private var myMenuData:XMLList =
<>
     <menuitem label="_File">yy
         <menuitem label="_Save" keyEquivalent="F10" />
         <menuitem label="P_rint" mnemonicIndex="4" mnemonicIndexOverride="true" />
         <menuitem type="separator" />
         <menuitem label="E_xit" />
     </menuitem>
     <menuitem label="_Edit">
         <menuitem label="_Copy" toggled="true" type="check" />
         <menuitem label="C_ut"/>
         <menuitem label="_Paste" />
     </menuitem>
 </>;

<mx:Desktopmenu id="desktopMenu" dataProvider="{myMenuData}" labelField="@label" showRoot="false"/>
               
Don't worry much about the XML format specified above (the underscores, how shortcuts are specified, etc...). That interface is up in the air, but the idea is what's important, and that is it's a lot easier to use XML to declaratively create the menu. One of the odd things about this component is that it's not a UIComponent. What I mean by that is it won't extend UIComponent because we're not actually adding it to the underlying Flash display list. We're just creating NativeMenuItems, where AIR ends up using the underlying operating system menu API (atleast that's what I think happens). Yet, despite not being a UIComponent, we can still discuss why validation/invalidation is an important concept. Let's say someone wants to change the dataProvider for this menu. When they do this, we need to recreate the NativeMenu objects underneath. So let's say we have something like this:
public function set dataProvider(value:Object):void
{
 _dataProvider = value;
 createNativeMenu();
}
Where createNativeMenu loops through our dataProvider and recreates the underlying NativeMenu. Well we also need to do this if someone changes the labelField, iconField, dataDescriptor, or a few other properties (for many Flex classes, this list is even longer). However, when you change the dataProvider, you oftentimes need to change other properties as well. For example, our new dataProvider might use a different field for it's label and icon. If after every setter, we re-created the menu, we'd be doing it too many times. What we really want is something like a commit() function, where after making all the changes one wants, we then recreate the menu. We could do this, and in fact, we thought about doing this, but instead, let's do something really simple so that the programmer doesn't have to remember to do this. Let's just set a flag called invalidatePropertiesFlag and then wait 50 ms to re-create the menu.
public function set dataProvider(value:Object):void
{
 _dataProvider = value;
 invalidateProperties();
}


private invalidatePropertiesFlag:Boolean = false;
           

private function invalidateProperties()
{
 if (!invalidatePropertiesFlag)
 {
     invalidatePropertiesFlag = true;
     var myTimer:Timer = new Timer(100, 1);
     myTimer.addEventListener(TimerEvent.TIMER, validateProperties);
     myTimer.start();
 }
}



private function validateProperties(event:TimerEvent)
{
 if (invalidatePropertiesFlag)
 {
     invalidatePropertiesFlag = false;
     createNativeMenu();
 }
}                
All right, so with this approach, we've successfully solved our problem. Now when multiple properties are set that invalidate our menu, we only end up recreating the menu once (not 3 times). If this approach was applied throughout the framework, though, we'd have a few problems. Individual components would be validated at different times. However, the truth is that components are dependent on each other. For example, if the DateField component changes, it causes the underlying DateChooser component to change as well, so really we need a coordinated effort of validation. This is especially true for UIComponents that are drawn on the screen, where changing the size of one component affects all the other components around it as well as all it's children. This is why we have LayoutManager. It coordinates this whole process I won't get into the complete lifecycle here, but in a gist, LayoutManger calls validateProperties() on all the components in a top-down fashion. Then validateSize() is called bottom-up, essentially asking everyone how big they want to be. Lastly, validateDisplayList() is called which tells everyone how big they are going to be (they don't always get what they want), and this happens top-down. Underneath the hood, UIComponent and Container are base classes that implement these three methods. They do some extra work for you, and then they call commitProperties(), measure(), and updateDisplayList(unscaledWidth, unscaledHeight) respectively. One thing to note is how validateProperties() and validateDisplayList() are top-down, while validateSize() is bottom-up. This is because properties usually propagate top-down (Example is DateField properties changing the underlying DateChooser component). validateSize() is bottom-up because we need to know how big our children are before we can determine our size. Lastly, validateDisplayList() is top-down because ultimately the parent decides how big each children gets to be (it usually tries to respect basic principles, like always listening to the children's minWidth/minHeight). Besides these inter-dependencies between properties, there's another reason why we coordinate this effort. Underneath, the Flash Player essentially uses a timeline with frames. The player doesn't actually update it's display list until the next frame passes. So if I say myComponent.x = 50; the object doesn't actually move until this frame finishes. Here's an example that shows this:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" applicationComplete="init()" layout="absolute" >
<mx:Script>
 <![CDATA[
     private function init():void
     {
         stage.frameRate = 1;
     }

     private function width50():void
     {
         myCanvas.width = 50;

         var myTimer:Timer = new Timer(500, 1);
         myTimer.addEventListener(TimerEvent.TIMER, width200);
         myTimer.start();
     }

     private function width200(event:TimerEvent):void
     {
         myCanvas.width = 200;
     }
 ]]>
</mx:Script>

<mx:Canvas id="myCanvas" backgroundColor="red" width="100" height="100" />
<mx:Button label="Click Me" click="width50()" x="200" y="200" />

</mx:Application>

We change the framerate to 1 per second. Now, when you click the button, sometimes you'll see it shrink to a width of 50 before growing to 200, or sometimes you'll see it just go to 200. It depends on exactly when you click on the button with respect to where in the current frame we are.

There are a few reasons the flash player does this as it helps with performance, but the important point is that Flex ties into this and does validation/re-measuring only when it'll actually affects the screen by tying in to the ENTER_FRAME event. So, in this DesktopMenu component, we should do the same thing and validate ourselves when all other components do (to be honest, I'm not sure if the native menu, since it's using the operating system API underneath can update itself in between frames or not). To do this, we need to implement ILayoutManagerClient, like UIComponent does, and then attach ourselves to the LayoutManager's queue by calling UIComponentGlobals.mx_internal::layoutManager.invalidateProperties(this);. Since we're not a display object, we don't need to do anything in validateSize or validateDisplayList. One thing to note is that despite it's name, LayoutManger should really be though of as a ValidationManager because it doesn't just deal with layouts. One other reason we do this is because sometimes the order of properties matters, and if it's represented as MXML, this order can be lost. So for example, in a List component, we might do something like:

myList.dataProvider = ["a", "b", "c"]; // gets promoted to an ArrayCollection
myList.selectedIndex = 1;

But in MXML, you have:

<mx:List id="myList" dataProvider="{['a', 'b', 'c']}" selectedIndex="1" />
or:
<mx:List id="myList" selectedIndex="1" dataProvider="{['a', 'b', 'c'}" />
You can see that the order actually matters here, but in XML we don't really have an order of operations. commitProperties() sorts all this out for us, and sets dataProvider before setting selectedIndex. All right, that was a lot of code. I'm sure I could be missing some reasons, but to re-cap, why we have LayoutManager and this invalidation/validation scheme:
  • Properties tend to change at the same time, and we don't want to re-do the same work because of this.
  • Property changes often cascade and affect many other properties (changing my position, would affect lots of other elements). This exacerbates the point made above.
  • The Flash Player uses delayed rendering, so changing a UI property doesn't actually display until the next frame
  • MXML representations of objects don't take into account order of operations.
So I hope this helps explain the why behind the Flex scheme and not just the how.

Sunday, September 09, 2007

AutoScrolling for Flex Tree

We've been mostly fixing bugs on the SDK as we prepare for the release for MAX, and one of the bugs that was assigned to me was SDK-9645, https://bugs.adobe.com/jira/browse/SDK-9645. The problem was that in a Tree (and other List-based components), when you set the horizontalScrollPolicy to auto, the scrollbars actually don't come out when they should. This seems like a bug at first, but we did this by design for performance reasons. In order to display the scrollbar properly, we need to measure the width of all the items (on-screen or not) and this would just take too much time to do by default. So instead, to get a scrollbar to show up, you need to set maxHorizontalScrollPosition, which is how much the user can scroll. However, this can be annoying, especially as you might not know how big your data is. So I've created a simple component that extends Tree that measures the width of all the renderers so the scrollbars are calculated correctly. In order to do this, in updateDisplayList, which is called whenever we need to redraw, I call measureWidthOfItems, which calculates the max width for all the item renderers. This means that redrawing takes some more time and is a little slower. Anyways, here's the code:
package{
    import flash.events.Event;
   
    import mx.controls.Tree;
    import mx.core.mx_internal;
    import mx.core.ScrollPolicy;
    import mx.events.TreeEvent;
   
    public class AutoSizeTree extends Tree
    {
         public function AutoSizeTree(){
              super();
              horizontalScrollPolicy = ScrollPolicy.AUTO;
         }
        
         // we need to override maxHorizontalScrollPosition because setting
         // Tree's maxHorizontalScrollPosition adds an indent value to it,
         // which we don't need as measureWidthOfItems seems to return exactly
         // what we need.  Not only that, but getIndent() seems to be broken
         // anyways (SDK-12578).
        
         // I hate using mx_internal stuff, but we can't do
         // super.super.maxHorizontalScrollPosition in AS 3, so we have to
         // emulate it.
         override public function get maxHorizontalScrollPosition():Number
         {
              if (isNaN(mx_internal::_maxHorizontalScrollPosition))
                  return 0;
              
              return mx_internal::_maxHorizontalScrollPosition;
         }
   
         override public function set maxHorizontalScrollPosition(value:Number):void
         {
              mx_internal::_maxHorizontalScrollPosition = value;
              dispatchEvent(new Event("maxHorizontalScrollPositionChanged"));
           
              scrollAreaChanged = true;
              invalidateDisplayList();
         }
        
         override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
         {
              // we call measureWidthOfItems to get the max width of the item renderers.
              // then we see how much space we need to scroll, setting maxHorizontalScrollPosition appropriately
              var diffWidth:Number = measureWidthOfItems(0,0) - (unscaledWidth - viewMetrics.left - viewMetrics.right);
             
              if (diffWidth <= 0)                     maxHorizontalScrollPosition = NaN;                else                     maxHorizontalScrollPosition = diffWidth;                                     super.updateDisplayList(unscaledWidth, unscaledHeight);           }      } }
[Code updated 3/20/09] Because we're doing this extra calculation, it means for a Tree with a lot of items, we may become slower. There are better ways of doing the same thing besides measuring every single renderer in updateDisplayList(), but this is definitely the easiest way. If performance is really a problem with this, let me know, and I'll try to come up with something a little smarter. Below is an example, with the top one being the new AutoSizeTree component and the bottom one being the old one. Notice how the top is a little slower when resizing with an effect (this shows it's a little slower when laying out in general) when lots of the items are open. [Post updated on 3/20/09 to fix an issue: SDK-18499]

Re-Introduction

For some technical reasons, I had to repost this introduction, but I'm also going to start blogging for real now. I have Internet at home, and we're getting a little more free time at work as we're almost done bug fixing for the next release. Anyways, on to the introduction.

Who are you? I am Ryan Frishberg, and I just graduated from Carnegie Mellon University. In July, I moved out to the Bay Area and started work at Adobe on the Flex SDK team. I’ve always been interested in software development and web technology, and Flex is absolutely awesome.

Why are you blogging now? Since I’ve tried to stay on top of the Web 2.0 and Rich Internet Application world, I read way too many blogs each day. I found out that through all the jargon propaganda they give you (don’t worry, I’ll be dosing out similar content soon), the blogs are actually useful. In fact, about half the time I Google for a technical question, I end up going to a blog. So I hope that by starting this blog, I can contribute to some of the technical knowledge (as well as the proper jargon and propaganda) out there. Also, I just started working on Flex, which means that I’ll have some really cool issues to talk about

What will I talk about in the blog? I would like to talk about web technologies, especially Flex. I’ll give examples on using Flex for beginners to show you how easy it is to create something useful in the framework. I also hope to cover more advanced topics that aren't covered as much by other blogs, like bugs I'm currently working on or limitations to the framework.

Content is on its way soon, and if you want to reach me with any questions, feel free to contact me via email.