Flex 3 Style Explorer - The Cairngorm Undoable Version - Explaining the Undoable Event Framework

In the previous post I presented a Cairngorm based approach to a Flex3StyleExplorer type of application.
In this post I will explain how this framework provides support for undo (theoretically it supports redo as well, but I have a bug there and haven’t had time to debug it yet).
Undo is one of the biggest missing pieces in most Rich Internet Applications, so hopefully you will all find this useful. It is worth noting that the approach in this post can be implemented in many applications and what I like the most about it (and am proud the most about as well) is that this solution can be easily implemented in any Cairngorm based application.

First, if you haven’t seen the demo application, check it out here. Click on design this menu and see how every action can be undone, even if you create multiple actions on different components.

Now for the explanation:
First of all I created a new Event that extends a Cairngorm Event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.control.events
{
	import com.adobe.cairngorm.control.CairngormEvent;
 
	import flash.events.Event;
 
	public class UndoableEvent extends CairngormEvent
	{
		public var updateUndoStack:Boolean = true; 
		public var eventType:String = "";
		public function UndoableEvent(type:String, updateUndoStack:Boolean, updateRedoStack:Boolean = false)
		{
			super(type);
			eventType = type;
			this.updateUndoStack = updateUndoStack;
		}
 
		override public function clone():Event
		{
			return new UndoableEvent(this.eventType, this.updateUndoStack);
		}
 
	}
}

Since this event will get dispatched twice (more on this later) we need to override the clone function.

Next we created an Undoable command that extends the Cairngorm command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.control.commands
{
	import com.adobe.cairngorm.commands.ICommand;
	import com.adobe.cairngorm.control.CairngormEvent;
	import com.control.events.UndoableEvent;
	import com.model.ModelLocator;
 
	public class UndoableCommand implements ICommand
	{
		private var __model:ModelLocator = ModelLocator.getInstance();
		public function UndoableCommand()
		{
		}
 
		public function execute(event:CairngormEvent):void
		{
 
			var undoEvent:UndoableEvent;
			if ((event as UndoableEvent).updateUndoStack)
			{
				__model.UndoEventStack.push(event as UndoableEvent);
			}
/* 			if ((event as UndoableEvent).updateRedoStack)
			{
				redoEvent = new UndoableEvent((event as UndoableEvent).eventType,(event as UndoableEvent).valueObject,  (event as UndoableEvent).updateUndoStack);
				//Redo should trigger the same event
				__model.RedoEventStack.push(redoEvent);
			} */
 
		}		
	}
}

What the command does is very simple, every event that gets dispatched is also stored in an array in the model. Most of you probably already know where this is going, every user gesture triggers an event, since all the events get pushed into a stack in the model, they can be dispatched again (in a reverse order) upon undo.

Now lets see what happens upon undo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.control.events
{
	import com.adobe.cairngorm.control.CairngormEvent;
 
	public class EventUndoDesign extends CairngormEvent
	{
		static public var EVENT_ID:String = "designUndoEvent"; 
		public function EventUndoDesign()
		{
			super(EVENT_ID);
		}
 
	}
}

And the corresponding command pulls the last event and dispatches it again (it really dispatches its clone, more on this later):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.control.commands
{
	import com.adobe.cairngorm.commands.ICommand;
	import com.adobe.cairngorm.control.CairngormEvent;
	import com.model.ModelLocator;
 
	import flash.events.EventDispatcher;
 
	public class CommandUndoDesign implements ICommand
	{
		private var __model:ModelLocator = ModelLocator.getInstance();
		public function execute(event:CairngormEvent):void
		{
			__model.undoCount++;
			var e:CairngormEvent = __model.UndoEventStack.pop();
			if (e is CairngormEvent)
				e.dispatch();
		}
 
	}
}

Now there is just one catch. When the user clicks on undo, we want the object to return to its previous state (prior to when the event changed it). So for example, if we changed the font color from yellow to black, the undo event should change it back to yellow. So any event that extends the undoable event should first capture the current state of the object (yellow font for example). The current state is used as an input to the clone function. The next time the event gets dispatched (upon undo) it will hold the previous state of the object, which will trigger the object changing back to its prior state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
package com.control.events
{
	import com.model.valueObjects.IDesignValueObject;
 
	import flash.events.Event;
 
	public class EventDesignObjectSetProperty extends UndoableEvent
	{
		public static var EVENT_ID:String = "eventDesignObjectSetProperty";
		public var whatValue:Object;
		public var whichIndex:Number;		
		public var whichObject:IDesignValueObject;
		public var whichProperty:String;
		public var currentValue:Object;
 
 
		public function EventDesignObjectSetProperty(whatValue:Object,  whichObject:IDesignValueObject, whichProperty:String, updateUndoStack:Boolean,whichIndex:Number = 0, updateRedoStack:Boolean=false)
		{
			super(EVENT_ID, updateUndoStack, updateRedoStack);
			this.whatValue = whatValue;
			this.whichIndex = whichIndex;
			this.whichObject = whichObject;
			this.whichProperty = whichProperty;
			this.currentValue = whichObject.getProperty(whichProperty, whichIndex);
		}
 
		override public function clone():Event
		{
			return new EventDesignObjectSetProperty(currentValue,whichObject, whichProperty, false, whichIndex);
		}
 
	}
}

That’s it, now any user gesture that triggers an undoable event can be undone in a click of a button.

Hope that you find this useful, let me know if you have any questions.

Amichai

, , , , , , , , , ,

1 Comment

Flex 3 Style Explorer - The Cairngorm Undoable Version

In one of my recent projects I had to implement a module that lets users design UI components in the application. I immediately thought of basing it on the Flex3StyleExplorer but quickly realized that it wouldn’t work for my needs. So I built something using a different approach and ended up with some cool shit.

First, why couldn’t I use the Good ol’ Flex3StyleExplorer?
A couple of reasons:

1. That sample makes heavy use of the SetStyle function, which on top of being somewhat cryptic, it is also very taxing on the application, which meant that frequent design changes would introduce a big performance hit.
2. There was no easy way to extend that code to implement undo, and the lame “restore to default” option is so Web 1.0.
3. But the biggest reason that made me write something from scratch was the fact that the Flex3StyleExplorer code is as far away from MVC as possible which just gave me a big headache when I tried to think of how to fit it into my application.

So what did I do? Well I wrote a small Framework for user design of Flex user interface components. The sample that I share shows a button and button bar example, but the framework can work for any Flex UI component and can be easily extended to include all the Flex3StyleExplorer examples.

This Framework supports the following:
1. Full support for Undo - any design change to any component can be undone, I also started a redo implementation but had some issues with it, so the redo option is defaulted out for now, until i can fix it.
2. Completely done in Cairngorm with a straight forward decoupling of the Model, View and Controller.
3. Slightly extends Cairngorm to include some dependency injection, this enabled me to avoid having 200 events, one for each UI object property. I call this technique Toolbar anchored to UI Component which listens to design value object.

I will start by describing the Cairngorm part, as it is the most straight forward:
My model is simple, it has value objects that hold the design properties of each UI component. They also implement an interface I call IDesignValueObject which requires them to implement a setProperty and getProperty function.
Here is an example of the button value object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
 
package com.model.valueObjects
{
	import mx.collections.ArrayCollection;
 
	[Bindable]
	public class ButtonDVO implements IDesignValueObject
	{
		public var height:Number = 20;
		public var width:Number = 40;
		public var cornerRadius:Number = 0;
		public var textIndent:Number = 0; 
		public var paddingLeft:Number = 0;
		public var paddingRight:Number = 0; 
		public var paddingTop:Number = 0;
		public var paddingBottom:Number = 0;
		public var letterSpacing:Number = 0;
		public var fillColors:ArrayCollection = new ArrayCollection([0xFFFFFF, 0x2a2a2a, 0xFFFFFF, 0x000000]);
		public var fillAlphas:ArrayCollection = new ArrayCollection([1, 1, .75, .65]);
		public var highlightAlphas:ArrayCollection = new ArrayCollection([0.5,0]); 
		public var color:Number = 0x000000;
		public var textRollOverColor:Number = 0x000000;
		public var textSelectedColor:Number = 0x000000; 
		public var borderColor:Number = 0x000000;
		public var themeColor:Number = 0x000000;
		public var fontFamily:String = "arial";
		public var fontSize:Number = 14; 
		public var fontWeight:String = "normal";//bold or none
		public var fontStyle:String = "normal";//italic or normal
		public var textDecoration:String = "none";//underline or none
 
		public function ButtonDVO()
		{
		}
 
		public function getProperty(whichProperty:String, whichIndex:Number = 0):Object
		{
			switch(whichProperty)
			{
				case "fillColors":
				if (whichIndex < this.fillColors.length) 
					return fillColors[whichIndex];
				break;
				case "fillAlphas":
				if (whichIndex < this.fillAlphas.length) 
					return fillAlphas[whichIndex];
				break;
				case "highlightAlphas":
				if (whichIndex < this.highlightAlphas.length) 
					return highlightAlphas[whichIndex];
				break;
				case "height":
				return this.height;
				break;
				case "width":
				return this.width;
				break;
				case "cornerRadius":
				return this.cornerRadius;
				break;
				case "textIndent":
				return this.textIndent;
				break;
				case "paddingLeft":
				return this.paddingLeft;
				break;
				case "paddingRight":
				return this.paddingRight;
				break;
				case "paddingTop":
				return this.paddingTop;
				break;
				case "paddingBottom":
				return this.paddingBottom;
				break;
				case "letterSpacing":
				return this.letterSpacing;
				break;
				case "color":
				return this.color;
				break;
				case "textRollOverColor":
				return this.textRollOverColor;
				break;
				case "textSelectedColor":
				return this.textSelectedColor;
				break;
				case "borderColor":
				return this.borderColor;
				break;
				case "themeColor":
				return this.themeColor;
				break;
				case "fontSize":
				return this.fontSize;
				break;
				case "fontFamily":
				return this.fontFamily;
				break;
				case "fontWeight":
				return this.fontWeight;
				break;
				case "fontStyle":
				return this.fontStyle;
				break;
				case "textDecoration":
				return this.textDecoration;
				break;
			}
			return 0;	
		}
 
 
		public function setProperty(whichProperty:String, whatValue:Object, whichIndex:Number = 0):void
		{
			switch(whichProperty)
			{
				case "fillColors":
				if (whichIndex < this.fillColors.length) 
					this.fillColors[whichIndex] = whatValue as Number;
				break;
				case "fillAlphas":
				if (whichIndex < this.fillAlphas.length) 
					this.fillAlphas[whichIndex] = whatValue as Number;
				break;
				case "highlightAlphas":
				if (whichIndex < this.highlightAlphas.length) 
					this.highlightAlphas[whichIndex] = whatValue as Number;
				break;
				case "height":
				this.height = whatValue as Number;
				break;
				case "width":
				this.width = whatValue as Number;
				break;
				case "cornerRadius":
				this.cornerRadius = whatValue as Number;
				break;
				case "textIndent":
				this.textIndent = whatValue as Number;
				break;
				case "paddingLeft":
				this.paddingLeft = whatValue as Number;
				break;
				case "paddingRight":
				this.paddingRight = whatValue as Number;
				break;
				case "paddingTop":
				this.paddingTop = whatValue as Number;
				break;
				case "paddingBottom":
				this.paddingBottom = whatValue as Number;
				break;
				case "letterSpacing":
				this.letterSpacing = whatValue as Number;
				break;
				case "color":
				this.color = whatValue as Number;
				break;
				case "textRollOverColor":
				this.textRollOverColor = whatValue as Number;
				break;
				case "textSelectedColor":
				this.textSelectedColor = whatValue as Number;
				break;
				case "borderColor":
				this.borderColor = whatValue as Number;
				break;
				case "themeColor":
				this.themeColor = whatValue as Number;
				break;
				case "fontSize":
				this.fontSize = whatValue as Number;
				break;
				case "fontFamily":
				this.fontFamily = whatValue as String;
				break;
				case "fontWeight":
				this.fontWeight = whatValue as String;
				break;
				case "fontStyle":
				this.fontStyle = whatValue as String;
				break;
				case "textDecoration":
				this.textDecoration = whatValue as String;
				break;
			}
 
		}		
 
	}
}

Now, I am sure there are more elegant ways to implement the getProperty and setProperty, maybe something fancy that loops from a reference to this and checks for a property string match, but quick and dirty worked OK for me this time.

Next I created a UI component that listens to this design value object. The listen part is implemented by binding the design properties of the object to this value object.

Here is an example of a Button that listens to a specific design value object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<mx:Button xmlns:mx="http://www.adobe.com/2006/mxml" 
				highlightAlphas="{[__model.menuButton.highlightAlphas.getItemAt(0), __model.menuButton.highlightAlphas.getItemAt(1)]}" 
				fillColors="{[__model.menuButton.fillColors.getItemAt(0), __model.menuButton.fillColors.getItemAt(1), __model.menuButton.fillColors.getItemAt(2), __model.menuButton.fillColors.getItemAt(3)]}" 
				fillAlphas="{[__model.menuButton.fillAlphas.getItemAt(0), __model.menuButton.fillAlphas.getItemAt(1), __model.menuButton.fillAlphas.getItemAt(2), __model.menuButton.fillAlphas.getItemAt(3)]}"
				cornerRadius="{__model.menuButton.cornerRadius}" color="{__model.menuButton.color}" 
				textSelectedColor="{__model.menuButton.textSelectedColor}" textRollOverColor="{__model.menuButton.textRollOverColor}" 
				textDecoration="{__model.menuButton.textDecoration}" textIndent="{__model.menuButton.textIndent}" fontFamily="{__model.menuButton.fontFamily}" fontStyle="{__model.menuButton.fontStyle}" fontSize="{__model.menuButton.fontSize}" fontWeight="{__model.menuButton.fontWeight}"
				height="{__model.menuButton.height}" themeColor="{__model.menuButton.themeColor}" borderColor="{__model.menuButton.borderColor}">
	<mx:Script>
		<![CDATA[
			import com.model.valueObjects.IDesignValueObject;
			import mx.binding.utils.BindingUtils;
			import mx.binding.utils.ChangeWatcher;
			import com.model.ModelLocator;
			import mx.events.PropertyChangeEvent;
			[Bindable] private var __model:ModelLocator = ModelLocator.getInstance();
			[Bindable] public var listensTo:IDesignValueObject = __model.menuButton;
		]]>
	</mx:Script>
</mx:Button>

You will notice that this UI component holds a variable called listensTo, this will enable us later to inject the dependency between the tool bar and the model that it needs to modify, this helps reduce the number of Cairngorm events from N to 1 where N is the number of UI components in the application. Come to think of it, the binding should use the listensTo variable instead of explicitly binding to the model, I will fix that in the next revision.

Finally, there is a tool bar object that allows the user to change the design properties of the UI component. Each tool bar object is anchored to a specific UI component (so a button will have a tool bar for editing the button design and a combo box will have a tool bar for editing the combo box design). This is the 2nd dependency injection which enables us to keep the events that are fired up from each tool bar simple and consistent (each event simply sets a property of the anchor.listensTo value object). Here is an example of the button tool bar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
 
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="80%" xmlns:ns1="com.view.ToolBarComponents.*" xmlns:view="com.view.*">
 
<mx:Script>
	<![CDATA[
		import com.control.events.EventDesignObjectSetProperty;
		import com.model.valueObjects.BUTTON_DESIGN_PROPERTIES;
		import com.model.valueObjects.BUTTON_BAR_DESIGN_PROPERTIES;
		import com.control.events.EventDesignObjectSetProperty ;
		import com.view.components.MenuButton;
		import GlobalUtils.ColorUtils;
		import com.model.ModelLocator;
		import mx.events.ColorPickerEvent;
		import mx.events.SliderEvent;
 
		[Bindable] private var __model:ModelLocator = ModelLocator.getInstance();
		[Bindable] private var anchor:MenuButton = new MenuButton();
 
 
		/***************************************Sliders**********************************/
 
		private function highlightAlphaSlider1ChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.highlightAlphas, true, 0);
			e.dispatch();
		}
		private function highlightAlphaSlider1PressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty  = new EventDesignObjectSetProperty (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.highlightAlphas, true, 0);
			e.dispatch();
		}
		private function highlightAlphaSlider2ChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty  = new EventDesignObjectSetProperty (event.value,anchor.listensTo,BUTTON_DESIGN_PROPERTIES.highlightAlphas, true,1);
			e.dispatch();
		}
		private function highlightAlphaSlider2PressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty  = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.highlightAlphas, true,1);
			e.dispatch();
		}
		private function topFillAlphaSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true, 0);
			e.dispatch();
		}
		private function topFillAlphaSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true,0);
			e.dispatch();
		}
		private function bottomFillAlphaSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true,1);
			e.dispatch();
		}
		private function bottomFillAlphaSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true,1);
			e.dispatch();
		}
		private function topRollOverAlphaSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true,2);
			e.dispatch();
		}
		private function topRollOverAlphaSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value,anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true,2 );
			e.dispatch();
		}
		private function bottomRollOverAlphaSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true, 3);
			e.dispatch();
		}
		private function bottomRollOverAlphaSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillAlphas, true, 3);
			e.dispatch();
		}
 
 
		private function buttonCurveSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.cornerRadius, true);
			e.dispatch();
		}
		private function buttonCurveSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.cornerRadius, true);
			e.dispatch();
 
		}
		private function buttonHeightSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.height, true);
			e.dispatch();
 
		}
		private function buttonHeightSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.height, true);
			e.dispatch();
 
		}
 
		private function buttonBarHorizontalSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.horizontalGap, true);
			e.dispatch();
		}			
 
		private function buttonBarHorizontalSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.horizontalGap, true);
			e.dispatch();
		}			
 
		private function buttonBarBackgroundColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.color,__model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.backgroundColor, true);
			e.dispatch();
		}
 
		private function buttonBarBackgroundAlphaSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.backgroundAlpha, true);
			e.dispatch();
		}								
 
		private function buttonBarBackgroundAlphaSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.backgroundAlpha, true);
			e.dispatch();
		}								
 
		private function buttonGapSliderChangeHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.horizontalGap, true);
			e.dispatch();
		}								
		private function buttonGapSliderPressHandler(event:SliderEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.value, __model.menuButtonBar, BUTTON_BAR_DESIGN_PROPERTIES.horizontalGap, true);
			e.dispatch();
		}								
 
 
 
		/***************************************Color Pickers**********************************/
 
		private function topFillUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.color, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillColors, true, 0);
			e.dispatch();
		}
		private function bottomFillUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.color, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillColors, true, 1);
			e.dispatch();
		}
		private function topRollOverUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.color,anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillColors, true, 2);
			e.dispatch();
		}
		private function bottomRollOverUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty   = new EventDesignObjectSetProperty  (event.color,anchor.listensTo, BUTTON_DESIGN_PROPERTIES.fillColors, true, 3);
			e.dispatch();
		}
		private function themeUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.color, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.themeColor, true);
			e.dispatch();
		}
 
		private function borderUndoableColorPickerChangeHandler(event:ColorPickerEvent):void
		{
			var e:EventDesignObjectSetProperty = new EventDesignObjectSetProperty(event.color, anchor.listensTo, BUTTON_DESIGN_PROPERTIES.borderColor, true);
			e.dispatch();
		}
 
 
	]]>
</mx:Script>
 
 
	<mx:HBox width="100%" height="40%">
		<mx:VBox width="100%" height="100%">
			<mx:HRule height="2" width="100%"/>
			<mx:Label text="Fill Colors" width="100%" textAlign="center"/>
			<mx:HRule height="2" width="100%"/>
			<mx:HBox width="100%" height="100">
				<mx:VBox width="100%" height="100%" horizontalAlign="center">
					<mx:Label text="Top" textAlign="center" width="100%"/>
					<mx:ColorPicker name="Fill Colors" id="topFillUndoableColorPicker" enabled="true"
						dataProvider="{ColorUtils.swatches}" change="topFillUndoableColorPickerChangeHandler(event)" selectedColor="{(Number)(__model.menuButton.fillColors.getItemAt(0))}"/>
					<mx:Label text="Transparency" textAlign="center" width="100%"/>
					<mx:HSlider width="50" id="topFillAlphaSlider" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
						change="topFillAlphaSliderChangeHandler(event)" thumbPress="topFillAlphaSliderPressHandler(event)" value="{(Number)(__model.menuButton.fillAlphas.getItemAt(0))}"/>
				</mx:VBox>
				<mx:VRule/>
				<mx:VBox width="100%" height="100%" horizontalAlign="center">
					<mx:Label text="Bottom" textAlign="center" width="100%"/>
					<mx:ColorPicker name="Fill Colors" id="bottomFillUndoableColorPicker" enabled="true"
						dataProvider="{ColorUtils.swatches}" change="bottomFillUndoableColorPickerChangeHandler(event)" selectedColor="{(Number)(__model.menuButton.fillColors.getItemAt(1))}"/>
					<mx:Label text="Transparency" textAlign="center" width="100%"/>
					<mx:HSlider width="50" id="bottomFillAlphaSlider" allowTrackClick="true" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
								change="bottomFillAlphaSliderChangeHandler(event)" thumbPress="bottomFillAlphaSliderPressHandler(event)" value="{(Number)(__model.menuButton.fillAlphas.getItemAt(1))}"/>
				</mx:VBox>
			</mx:HBox>
		</mx:VBox>
		<mx:VRule height="100%" width="2"/>
		<mx:VBox width="100%" height="100%">
			<mx:HRule height="2" width="100%"/>
			<mx:Label text="Roll Over Colors" width="100%" textAlign="center"/>
			<mx:HRule height="2" width="100%"/>
			<mx:HBox width="100%" height="100">
				<mx:VBox width="100%" height="100%" horizontalAlign="center">
					<mx:Label text="Top" textAlign="center" width="100%"/>
					<mx:ColorPicker name="Roll Over" id="topRollOverUndoableColorPicker"
						dataProvider="{ColorUtils.swatches}" change="topRollOverUndoableColorPickerChangeHandler(event)" selectedColor="{(Number)(__model.menuButton.fillColors.getItemAt(2))}"/>
					<mx:Label text="Transparency" textAlign="center" width="100%"/>
						<mx:HSlider width="50" id="topRollOverAlphaSlider" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
						change="topRollOverAlphaSliderChangeHandler(event)" thumbPress="topRollOverAlphaSliderPressHandler(event)" value="{(Number)(__model.menuButton.fillAlphas.getItemAt(2))}"/>
				</mx:VBox>
				<mx:VRule height="100%" width="1"/>
				<mx:VBox width="100%" height="100%" horizontalAlign="center">
					<mx:Label text="Bottom" textAlign="center" width="100%"/>
					<mx:ColorPicker name="Roll Over" id="bottomRollOverUndoableColorPicker"
						dataProvider="{ColorUtils.swatches}" change="bottomRollOverUndoableColorPickerChangeHandler(event)" selectedColor="{(Number)(__model.menuButton.fillColors.getItemAt(3))}"/>
					<mx:Label text="Transparency" textAlign="center" width="100%"/>
					<mx:HSlider width="50" id="bottomRollOverAlphaSlider" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
						change="bottomRollOverAlphaSliderChangeHandler(event)" thumbPress="bottomRollOverAlphaSliderPressHandler(event)" value="{(Number)(__model.menuButton.fillAlphas.getItemAt(3))}"/>
				</mx:VBox>
			</mx:HBox>
		</mx:VBox>
		<mx:VRule height="100%" width="2"/>
	</mx:HBox>
	<mx:HRule height="2" width="100%"/>
	<mx:HBox width="100%" height="50%">
		<mx:VBox width="40%" height="100%">
			<mx:HBox width="40%" height="100%">
				<mx:VBox width="40%" height="100%">
					<mx:HRule height="2" width="100%"/>
					<mx:Label text="Menu Properties" width="100%" textAlign="center"/>
					<mx:HRule height="2" width="100%"/>
					<mx:HBox width="100%" height="100%">
						<mx:VBox width="100%" height="100%" horizontalAlign="center">
							<mx:Label text="Menu Background" textAlign="center" width="100%"/>
							<mx:ColorPicker name="Menu Background" id="menuBackgroundColorPicker" enabled="true"
								dataProvider="{ColorUtils.swatches}" change="buttonBarBackgroundColorPickerChangeHandler(event)" selectedColor="{(Number)(__model.menuButtonBar.backgroundColor)}"/>
							<mx:Label text="Transparency" textAlign="center" width="100%"/>
							<mx:HSlider width="50" id="menuBackgroundAlphaSlider" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
								change="buttonBarBackgroundAlphaSliderChangeHandler(event)" thumbPress="buttonBarBackgroundAlphaSliderPressHandler(event)" value="{__model.menuButtonBar.backgroundAlpha}"/>
						</mx:VBox>
						<mx:VRule height="100%"/>
						<mx:VBox width="100%" height="100%" horizontalAlign="center">
							<mx:Label text="Button Curve" textAlign="center" width="100%"/>
							<mx:HSlider width="100" id="buttonCurveSlider" allowTrackClick="true" minimum="0" maximum="40" snapInterval="0.5" enabled="true" liveDragging="true"
										change="buttonCurveSliderChangeHandler(event)" thumbPress="buttonCurveSliderPressHandler(event)" value="{__model.menuButton.cornerRadius}"/>
							<mx:Label text="Button Height" textAlign="center" width="100%"/>
							<mx:HSlider width="100" id="buttonHeightSlider" allowTrackClick="true" minimum="10" maximum="200" snapInterval="0.5" enabled="true" liveDragging="true"
										change="buttonHeightSliderChangeHandler(event)" thumbPress="buttonHeightSliderPressHandler(event)" value="{__model.menuButton.height}"/>
							<mx:Label text="Space Between Buttons" textAlign="center" width="100%"/>
							<mx:HSlider width="100" id="buttonGapSlider" allowTrackClick="true" minimum="0" maximum="200" snapInterval="0.5" enabled="true" liveDragging="true"
										change="buttonGapSliderChangeHandler(event)" thumbPress="buttonGapSliderPressHandler(event)" value="{__model.menuButtonBar.horizontalGap}"/>
						</mx:VBox>
					</mx:HBox>
				</mx:VBox>
			</mx:HBox>
		</mx:VBox>
		<mx:VRule height="100%" width="2"/>
		<mx:VBox width="20%" height="100%">
			<mx:HRule height="2" width="100%"/>
			<mx:Label text="Theme Colors" width="100%" textAlign="center"/>
			<mx:HRule height="2" width="100%"/>
			<mx:HBox width="100%" height="100">
				<mx:VBox width="100%" height="100%" horizontalAlign="center">
					<mx:Label text="Theme" textAlign="center" width="100%"/>
					<mx:ColorPicker name="Theme Colors" id="themeUndoableColorPicker"
					dataProvider="{ColorUtils.swatches}" change="themeUndoableColorPickerChangeHandler(event)" selectedColor="{__model.menuButton.themeColor}"/>
					<mx:Label text="Border"/>
					<mx:ColorPicker name="Border" id="borderUndoableColorPicker"
								dataProvider="{ColorUtils.swatches}" change="borderUndoableColorPickerChangeHandler(event)" selectedColor="{__model.menuButton.borderColor}"/>
				</mx:VBox>
			</mx:HBox>
		</mx:VBox>
		<mx:VRule height="100%" width="2"/>
		<mx:HBox width="100%" height="100%">
 
			<mx:VBox width="100%" height="100%" horizontalAlign="center">
				<mx:HRule width="100%" height="1"/>
				<mx:Label text="Highlights" width="100%" textAlign="center"/>
				<mx:HRule width="100%" height="1"/>
				<mx:Label text="Top"/>
					<mx:HSlider width="50" id="highlightAlphaSlider1" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
						change="highlightAlphaSlider1ChangeHandler(event)" thumbPress="highlightAlphaSlider1PressHandler(event)" value="{(Number)(__model.menuButton.highlightAlphas.getItemAt(0))}"/>
				<mx:Label text="Bottom"/>
					<mx:HSlider width="50" id="highlightAlphaSlider2" minimum="0" maximum="1" snapInterval="0.05" enabled="true" liveDragging="true"
						change="highlightAlphaSlider2ChangeHandler(event)" thumbPress="highlightAlphaSlider2PressHandler(event)" value="{(Number)(__model.menuButton.highlightAlphas.getItemAt(1))}"/>
 
			</mx:VBox>
		</mx:HBox>
		<mx:VRule height="100%" width="2"/>
	</mx:HBox>
 
</mx:VBox>

Some of you may notice that some events update a __model.menuButtonBar instead of using a dependency injected variable, that is just me being lazy and not wanting to seperate the tool bar for the button and the one for the button bar.

OK, this is enough for now, in the next post I will explain the undo implementation.

Example with view source is available here:

Curious to here your feedback,

Amichai Lesser

, , , , , , , , , , , , , , , ,

2 Comments

Monitor Your Application’s Network Calls

As our applications evolve and we add more functionality the chances for making redundant calls for the server grow. User’s activities, timers that work in the background, event handlers etc. can create overlapping operations. A nice trick which helps you monitor the network calls your application makes, is tracing each network call or tracing each response.

If you aggregated all your calls to a single class e.g. CNetworkComm which exposes the method sendHttpRequest(…) then you can add a trace in the beginning of the function specifying the type of the command which is sent to the server.

1
2
3
4
5
6
public function sendHttpRequest(requestType:String, 
         paramsList:ArrayCollection, url:String, 
         listenToReply:Boolean):void
{
      trace ('Sending ' + requestType + ' request to server');
}

This will dramatically minimize the time you spend understanding what your application is trying to do or why various operations take so much time. You may find redundant calls, be able to trace where they are coming from and remove them.

If you didn’t aggregate all network calls into a single class, trace each callback function. For example,

<mx:HTTPService url="http://myserver/getUsersInfo.php" 
      resultFormat="e4x" result="getUsersInfoResultHandler(event)"    
      fault="getUsersInfoFaultHandler(event)"/>
 
<mx:HTTPService url="http://myserver/getExtraInfo.php"  
      resultFormat="e4x" result="getExtraInfoResultHandler(event)" 
      fault="getExtraInfoFaultHandler(event)"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private function getUsersInfoInResultHandler(event:ResultEvent):void
{
    trace("getUsersInfoInResultHandler was called");
    …
}
private function getExtraInfoResultHandler(event:ResultEvent):void
{
    trace("getExtraInfoResultHandler was called");
    …
}
private function getUsersInfoFaultHandler(event:FaultEvent):void
{
    trace("getUsersInfoFaultHandler was called");
    …
}
private function getExtraInfoFaultHandler(event:FaultEvent):void
{
    trace("getExtraInfoFaultHandler was called");
    …
}

In any event, it is best practice to trace both outgoing calls and incoming responses so you would be able to better understand what your application does.

, ,

No Comments

Optimizing FLEX Application Performance

Is your FLEX application slow? Does it take a long time to load? Does every operation you do make the application sweat?

Let’s talk a bit about FLEX based application performance. Let’s take a simple user directory application in which I have an entry for each registered user which contains the user’s personal details such as name, address, DOB, his picture etc. The application is pretty basic and enables the application administrator to view all the registered users, add / delete / modify users etc.

UsersApp

Choose the right container

The first thing to take into consideration is which container to use. Different FLEX containers behave differently. In general, there are 2 types of containers – a child based container and an item renderer based container. The former includes the Canvas, HBox, VBox etc. These containers are basic in the sense that every additional child added to them, is allocated the required resources in the view and memory. Therefore, if you have 10,000 records of users and you load them into a VBox container, the VBox container will hold each one of them in memory. Assuming that every record is not tiny, then your screen will be able to display only a fraction of these 10,000 records – let’s say 50. In this case, there is no reason to keep the additional 9950 records in memory. Keeping these records in memory may consume a lot of system resources and make your application very slow. It can affect it both when loading (if you load the entire 10,000 records) and every time you try to scroll.

The second type of containers is more sophisticated – the item renderer based containers such as List, TileList, Grid, etc. These containers implement deferred loading and deferred instansiation so they don’t load all the items into memory. They measure how many items fit in the stage (the main drawing area) and allocate this number for items to be displayed. So even if the application needs to load 10,000 items to a List, it will only load and instantiate the number of items that fit into the screen. These containers also reuse renderers. Let’s say that the view has 50 items in the stage. When a user scrolls down, new items enter the stage. In the same time, old items leave the stage. The container will reuse the same item renderers for the new items that entered the stage so the application doesn’t instanitate or load any unnecessary items.

Take a look at the following code samples. While both produce the same visual result, the first way uses a VBox and loads all items to the stage, while the second way uses a List control which loads items on demand.

Using VBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private function addItemToView(name:String, address:String,    image:String):void
{
    var vbox:VBox = new VBox();
 
    var nameLabel:Label = new Label();
    nameLabel.text = name;
 
    var addressLabel:Label = new Label();
    addressLabel.text = address;
 
    var image:Image = new Image();
    image.source = url;
 
    vbox.addChild(nameLabel);
    vbox.addChild(addressLabel);
    vbox.addChild(image);
 
    itemsContainer.addChild(vbox);
}
 
<mx:VBox id="itemsContainer"
 
      width="50%" height="100%"
 
      backgroundColor="0xffffff"
 
/>

Using List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<mx:List
 
   dataProvider="{itemsList}"
 
   rowHeight="110">
 
     <mx:itemRenderer>
 
       <mx:Component>
 
         <mx:VBox>
 
           <mx:Label text="{data.name}" />
 
           <mx:Label text="{data.address}" />
 
           ...
 
           <mx:Image source="{data.url}" />
 
         </mx:VBox>
 
       </mx:Component>
 
     </mx:itemRenderer>
 
</mx:List>

Fetch meta data from the Server

So after we understand how to display our registered users we can simply load the information when the application starts. A potential bottle neck is loading the data provider in case the number of users is high. If the number of users is relatively large, then passing the information means passing an XML record for each user. Each XML record may look something like the following:

1
2
3
4
5
<user id="1" first_name="John" last_name="Doe" 
 
   address="1st Elm Street" phone_no="123456" 
 
   image="thumbnail.jpg"/>

Passing a large number of these records means a large amount of data traversing the network. Instead, when the application loads you can request the server to send a summary of the meta data, e.g. how many registered users are in the system. Then, start loading a fragment of those users, e.g. 100 at at time which will take a short time to complete and then fetch the next 100 registered users only when the user asks for it, i.e. when they scroll down or go to the next page. This design pattern is called lazy loading or deferred loading. In this constellation, there is a good chance that the average user may never trigger the loading of the entire data set  from the server.

Aggregate server commands

Another common mistake is not aggregating client / server commands. For example, while the list of registered users is displayed, the administrator can delete a specific user. The first thing that comes into mind is simply send the server a “delete user” command along with the ID of the user to be deleted. If I translate it into XML command it will look like the following:

1
2
3
4
5
<delete_user>
 
   <user id="1"/>
 
</delete_user>

While this will work, imagine that the administrator wants to delete 10 users (or 100 users or maybe even delete all users). Since you already

have a “deleteUser” method, you might be tempted to reuse that function and the code will probably look something like the following:

1
2
3
4
5
6
7
8
9
10
private function deleteUserList(usersList:ArrayCollection)
{
 
      for each (var user:Object in usersList)
 
      {
           // this is a call to the server
           deleteUser(user.id);
      }
}

What we have here is N discrete requests to the server and you want to avoid that. The reason you want to avoid that issuing a new request to the server for each user means that your application will become chatty and produce more traffic as every request has its request header not to mention that lower levels (TCP SYN / ACK etc.). The chattier your application, it means that it is more exposed to fluctuations in the network conditions and at the end of the day it could mean a longer time for each transaction to complete. On top of it, if you provide the ability to delete several users in a batch it would be much easier to the user too, so instead of hitting the delete button 10 times and confirming the deletion, they will check the user to be deleted and delete them in a single action. The delete XML would look something like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
<delete_users>
 
   <users>
 
     <user id="1"/>
 
     <user id="2"/>
 
     <user id="3"/>
 
   </users>
 
</delete_users>

, , , ,

No Comments