New site design

Part of my “blog more” plan has always been to finalize a design for the site which I’m rolling out right now. Textpattern had been giving me some problems with post formatting and performance, so thats been tossed as well and everything’s been moved over to Wordpress. So far I’ve been much happier with the features, although I’m not a big fan of having to litter my root directory with dozens of ‘wp-*’ files.

eBay Desktop released!

After more then a year and a half of development, we’ve finally been able to release eBay Desktop 1.0. After starting as a simple prototype and slowly growing into a viable product, its been a tremendous effort to create. Having lead the development of the app, I’m relieved to see this beast finally released and I’m pretty excited to see how useful it becomes to the eBay user community. I’m hoping to spend the next months worth of posts discussing some of the Flex and AIR techniques used in the app.

Actionscript 3.0 prototyping, viable but costly

Back in the Flash 6 – Flash 8 world, I was a big fan of prototyping, mostly because of the limitations of MovieClip inheritance. I haven’t needed to use it at all in Actionscript 3 but I was still curious about how to accomplish it nowadays. Here’s the example I was able to create in Flex.

Sprite.prototype.spriteHelper = new SpriteHelper();
Sprite.prototype.rotateTo = function(deg:int):void{
	var sh:SpriteHelper = this["spriteHelper"];
	sh.target = this;
	sh.rotateTo(deg);
}
package
{
	import flash.display.Sprite;
	import flash.events.TimerEvent;
	import flash.utils.Timer;

	public class SpriteHelper
	{
		public var _degreeTo:int = 0;
		public var _degCount:int = 0;
		public var _degTimer:Timer = null;
		public var target:Sprite;
		public function rotateTo(deg:int):void{
			_degreeTo = deg;
			_degCount = 0;
			_degTimer = new Timer(10);
			_degTimer.addEventListener(TimerEvent.TIMER, doAnimateRotation);
			_degTimer.start();
		}
		public function doAnimateRotation(event:TimerEvent):void{
			_degCount++;
			if(_degCount <= 40){
				target.rotation = _degreeTo * (_degCount/40);
			}else{
				_degTimer.removeEventListener(TimerEvent.TIMER, doAnimateRotation);
				_degTimer = null;
			}
		}
	}
}
<mx:Box id="myContainer" backgroundColor="#FF0000" width="100" height="100" right="40" y="0" creationComplete="myContainer['rotateTo'](50)"/>

You’ll notice 2 things in the implementation here. Since Sprite is a statically defined class, accessing the prototype chain requires bracket syntax ala myContainer[‘rotateTo’](50), and secondly, most of the functionality of rotateTo has been moved into a separate class. The reason for the latter point is because accessing the prototype chain is costly, as it has to recurse through the inheritance chain until it finds the class with the property. As a result I’ve limited the prototype lookups to just spriteHelper and I move all the functionality into there.

Another thing I found in the docs is that every class creates a prototype chain by default, so merely adding prototype definitions has no effect on the creation of that object.

RegExp pattern for creating file safe names

Different operating systems restrict specific characters from being used in file and folder names. The following code snippet will allow you to trim those special characters when creating File references in AIR.

var fileSafePattern:RegExp = new RegExp('["\\\\ *?<>|:]', 'gi');
var fileSafeName:String = myString.replace(fileSafePattern, "_");
var myFile:File = File.applicationStorageDirectory.resolvePath(fileSafeName);

eBay Desktop demo

I’ve been doing a few presentations on Apollo the past few months centered around accessing eBays web services. While the app shown has remained just a “prototype”, I’m finally able to talk openly about the product and the fact that we’re moving into an official eBay application with a beta program coming soon.

The beta signup is public now so if your interested in getting involved early on and getting your feedback heard, you can sign up over at http://www.sandimasproject.com . I’m planning on watching the results of the beta very closely and trying to interact with a lot of users to get their opinions. We’ve got an amazing opportunity to rebuild eBays interface from the ground up and want to make sure every aspect of using eBay gets improved in the process. So feel free to send me your thoughts through this blog or on the beta site.

If you want to see a demo of the application, I gave a presentation recorded at Adobes ApolloCamp back in March. http://video.onflex.org/2007/03/26/apollo-camp-ebay-and-effectiveui-and-artemis-sean-christmann/

Find as you type sorting on large record sets

One of the sorting methods I’ve been working on this week is implementing a fast find as you type method for close to 30,000 records in AS3. The feature works exactly like iTunes library search, just with a lot more data (unless you’re rocking 1800 hours worth of music). The only rule with implementing this kind of feature is that it has to feel instant to the end user, otherwise we’ll have to switch to a Find-after-pressing-search method.

Setup
The records are stored locally as a flat xml structure.

<entry id="20081" name="Antiques" fullName="Antiques" />
<entry id="37903" name="Antiquities (Classical, Amer.)" fullName="Antiques|Antiquities (Classical, Amer.)" />
<entry id="37905" name="Egyptian" fullName="Antiques|Antiquities (Classical, Amer.)|Egyptian" />
<entry id="37906" name="Greek" fullName="Antiques|Antiquities (Classical, Amer.)|Greek" />
<entry id="37907" name="Roman" fullName="Antiques|Antiquities (Classical, Amer.)|Roman" />

The data is then loaded in as an XMLList and bound to a datagrid. From there, searches should match keywords in the fullName attribute and the datagrid will show only the entries matched.

Method #1 - Iteration
There are 2 ways to iterate the search, either use AS3’s built in E4X expressions to generate unique xml lists, or use collection filtering on the existing list. The first method flexes the power of E4X expressions, but takes a hit on swapping out the datagrid with a new dataprovider, the second works on the existing dataprovider but lacks any indexing power xml might provide.
E4X expression filtering

filteredXMLList = rawXMLList.(@fullName.indexOf(searchTerm) > -1);

Filter Time: 200 ms
Collection Filtering

filteredXMLListCollection = new XMLListCollection(rawXMLList);
filteredXMLListCollection.filterFunction = collectionFilter;
private function collectionFilter(item:XML):Boolean{
	return item.@fullName.indexOf(searchTerm) > -1;
}

Filter Time: 300-200 ms
Using E4X expressions is more powerful then filtering, but the total time for both methods still feels sluggish in the UI.

Method #2 - Pattern matching full text.
Again, there are 2 approaches to pattern matching strings in AS3, RegExp and String.indexOf(). The idea is to apply pattern matching on the raw string representation of the xml to create a new xml string containing only the nodes matched. The new string is then cast back into an XMLList to be placed in the datagrid.
RexExp matching

var pattern:RegExp = new RegExp("<[^<]*" + searchTerm + "[^>]*>", "ig");
var result:Array = rawXMLString.match(pattern);
filteredXMLList = new XMLList(result.join(""));

Filter Time: 31000-14000 ms (31-14 seconds)

IndexOf matching

private function searchStringFor(source:String, term:String):String{
	var lowersource:String = source.toLowerCase();
	var lowerterm:String = term.toLowerCase();
	var i:Number = 0;
	var out:String = "";
	var pos:Number = 0;
	while(i > -1){
		pos = lowersource.indexOf(lowerterm, i);
		if(pos > -1){
			out += source.substring(source.lastIndexOf("<", pos), source.indexOf(">", pos)+1);
			i = pos+term.length;
		}else{
			i = pos;
		}
	}
	return out;
}
filteredXMLList = new XMLList(searchStringFor(rawXMLString, searchTerm));

Filter Time: 200-50 ms

While the RegExp and indexOf are performing the same match, RegExp clearly has a long way to go before being viable in AS3. Even using simply pattern matches with String.search() during Method #1 above was 3 times slower then an indexOf during iteration. IndexOf in Method #2 on the other hand has gotten us down to a 200-50 ms filter time. 50 ms is decent but 200 ms still feels slow.

Final Method - Key search + reference copy
At this point, indexOf searches on the full string block appear to be the fastest way of filtering, and it provides the most room for refinement. Two aspects are contributing to the slowdown at this point: 1. unneccessarily searching the full xml structure, 2. constructing a new xml string and casting it to XMLList after each search. We can cover both issues by initially loading the xml string into an XMLList and iterating that list to generate a second string containing just the fullName field and the index that fullName belongs to in the original XMLList

rawXMLList = XMLList(urlLoader.data);
var count:int = 0;
for each(var node:XML in rawXMLList){
	fullNameIndex += node.@fullName.toLowerCase() + "<" + count + ">";
	count++;
}

This will generate a string looking like

Antiques<0>
Antiques|Antiquities (Classical, Amer.)<1>
Antiques|Antiquities (Classical, Amer.)|Egyptian<2>
Antiques|Antiquities (Classical, Amer.)|Greek<3>
Antiques|Antiquities (Classical, Amer.)|Roman<4>

Now when we do an indexOf search on the above string, we can look ahead to the index inside the < > tags to find which reference in the original rawXMLList to use. Those references are then set into a new XMLList to display.

private function searchStringInAttribute(list:XMLList, attributelist:String, term:String):XMLList{
	var output:XMLList = new XMLList();
	var i:Number = 0;
	var pos:Number = 0;
	var index:int = 0;
	var count:int = 0;
	while(i > -1){
		pos = attributelist.indexOf(term, i);
		if(pos > -1){
			index = int( attributelist.substring(attributelist.indexOf("<", pos)+1, attributelist.indexOf(">", pos)) );
			output[count] = list[index];
			count++;
			i = pos+term.length;
		}else{
			i = pos;
		}
	}
	return output;
}
filteredXMLList = searchStringInAttribute(rawXMLList, fullNameIndex, searchTerm.toLowerCase());

Filter Time: 70-10 ms

These filtering times are excellent and feel extremely smooth. I was a bit surprised to see that performing an indexOf search on a large block of text like this, then looking for the XMLList key in string form, was faster then performing a search inside an array iteration and mapping the array key to the XMLList key. The final source is listed below.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="init()">
	<mx:Script>
		<![CDATA[
			import flash.utils.getTimer;

			[Bindable]
			private var filteredXMLList:XMLList;
			private var rawXMLList:XMLList;
			private var fullNameIndex:String;
			private var urlLoader:URLLoader;

			private function init():void{
				urlLoader = new URLLoader();
				urlLoader.addEventListener(Event.COMPLETE, entriesLoaded);
				urlLoader.load(new URLRequest("entryList.xml"));
			}
			private function entriesLoaded(e:Event):void{
 				rawXMLList = new XMLList(urlLoader.data);
 				var count:int = 0;
 				var node:XML;
 				for each(node in rawXMLList){
 					fullNameIndex += node.@fullName.toLowerCase()+"<"+count+">";
 					count++;
 				}
 				applyFilter();
 				urlLoader.removeEventListener(Event.COMPLETE, entriesLoaded);
 				urlLoader = null;
			}
			private function searchStringInAttribute(list:XMLList, attributelist:String, term:String):XMLList{
				var output:XMLList = new XMLList();
				var i:Number = 0;
				var pos:Number = 0;
				var index:int = 0;
				var count:int = 0;
				while(i > -1){
					pos = attributelist.indexOf(term, i);
					if(pos > -1){
						index = int( attributelist.substring(attributelist.indexOf("<", pos)+1, attributelist.indexOf(">", pos)) );
						output[count] = list[index];
						count++;
						i = pos+term.length;
					}else{
						i = pos;
					}
				}
				return output;
			}
			private function applyFilter():void{
				if(filtertext.text.length < 2){
					filteredXMLList = rawXMLList;
				}else{
					var searchTerm:String = filtertext.text;
					var s:int = getTimer();
					filteredXMLList = searchStringInAttribute(rawXMLList, fullNameIndex, filtertext.text.toLowerCase());
					trace("filter time: "+(getTimer()-s));
				}
			}
		]]>
	</mx:Script>
	<mx:VBox width="100%" height="100%">
		<mx:TextInput id="filtertext" width="200" keyUp="applyFilter()"/>
		<mx:DataGrid id="category_list" width="100%" height="100%" dataProvider="{filteredXMLList}">
			<mx:columns>
				<mx:DataGridColumn id="dateCol" dataField="@id" headerText="Category ID" width="100"/>
				<mx:DataGridColumn id="titleCol" dataField="@name" headerText="Category Name"/>
				<mx:DataGridColumn id="idCol" dataField="@fullName" headerText="Category Path"/>
			</mx:columns>
		</mx:DataGrid>
	</mx:VBox>
</mx:Application>