I am moving away from MXML and have built a custom component control within ActionScript.
I have the control displaying correctly. The problem comes after I remove it from the display list and add it back in again with the .addElement(control) method.
Here is the code that adds it back in again.
private function displayParameters(parameters:ArrayCollection):void{
for(var index:int = 0; index<parameters.length; index++){
if(parameters[index] is ReportControl){
var control:ReportControl = parameters[index] as ReportControl;
control.percentWidth = 100;
vgParameters.addElement(control);
}
}
}
ReportControl
is the base class for comboBoxMultiSelect
which is shown below. There is nothing graphically special about ReportControl
, it only serves as a programmatic interface for its concrete implementations (polymorphic).
public class comboBoxMultiSelect extends ReportControl{
[Embed("../Assets/Icons/plus-16.png")]
private var plusIcon:Class;
[Embed("../Assets/Icons/minus-16.png")]
private var minusIcon:Class;
private var expanded:Boolean = false;
private var buttonIconChanged:Boolean = false;
private var _drp:ComboBox;
private var _btnMultiple:Button;
private var _horizontalGroup:HGroup;
private var _multiSelector:ReportGridSelector;
private var _multiSelection:Boolean = true;
private var bMultiSelectionChanged:Boolean = false;
public function ToggleExpanded():void{
expanded = !_expanded;
buttonIconChanged = true;
invalidateSize();
invalidateProperties();
invalidateDisplayList();
}
public function comboBoxMultiSelect(){
super();
}
override protected function createChildren():void{
super.createChildren();
if(!_horizontalGroup){
_horizontalGroup = new HGroup();
_horizontalGroup.gap = 0;
_horizontalGroup.percentWidth = 100;
_horizontalGroup.height = ReportControl.SIZE_DEFAULT_HEIGHT;
addChild(_horizontalGroup);
}
if(!_drp){
_drp = new ComboBox();
_drp.text = GuiText;
_drp.percentWidth = 100;
_drp.height = ReportControl.SIZE_DEFAULT_HEIGHT;
_horizontalGroup.addElement(_drp);
}
if(!_btnMultiple && _multiSelection){
_btnMultiple = new Button;
_btnMultiple.setStyle("icon", plusIcon);
_btnMultiple.width = 20;
_btnMultiple.height = ReportControl.SIZE_DEFAULT_HEIGHT;
_btnMultiple.visible = true;
_btnMultiple.addEventListener(MouseEvent.CLICK,
function(event:MouseEvent):void{
ToggleExpanded();
});
_horizontalGroup.addElement(_btnMultiple);
}
}
override protected function commitProperties():void{
super.commitProperties();
if(buttonIconChanged){
if(_expanded==true){
_btnMultiple.setStyle("icon", minusIcon);
}
else{
_btnMultiple.setStyle("icon", plusIcon);
}
buttonIconChanged = false;
}
}
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void{
super.updateDisplayList(unscaledWidth, unscaledHeight);
_horizontalGroup.width = unscaledWidth;
_horizontalGroup.height = unscaledHeight;
}
override protected function measure():void{
super.measure();
measuredMinWidth = measuredWidth = ReportControl.SIZE_DEFAULT_WIDTH;
//minimum size //default size
if(_expanded==true)
measuredMinHeight= measuredHeight = 200;
else
measuredMinHeight= measuredHeight =
ReportControl.SIZE_DEFAULT_HEIGHT;
}
}
When I add the control back in using vgParameters.addElement(control)
, the comboBoxMultiSelect
is not rendering properly. The plusIcon
inside the button _btnMultiple
is not postioned correctly at first, but then quickly corrects itself about 0.5-1 secs later.
I pretty sure the problem lies within comboBoxMultiSelect, just not sure how to force the icon to stay in the same place.
This is highly annoying after all my hard work, anyone have ideas as to what I am doing wrong?
Thanks :)
UPDATE -----> Here is the ReportControl code
[Event (name= "controlChanged", type="Reporting.ReportControls.ReportControlEvent")]
[Event (name= "controlIsNowValid", type="Reporting.ReportControls.ReportControlEvent")]
public class ReportControl extends UIComponent
{
private var _guiText:String;
private var _amfPHPArgumentName:String;
private var _reportResult:ReportResult;
private var _sequence:int;
private var _reportId:int;
private var _controlConfiguration:ReportParameterVO;
private var _isValid:Boolean = false;
internal var _selection:Object;
/**
* SIZE_DEFAULT_HEIGHT = 22
*/
internal static const SIZE_DEFAULT_HEIGHT:int = 22;
/**
* SIZE_DEFAULT_WIDTH = 150
*/
internal static const SIZE_DEFAULT_WIDTH:int = 150;
public function get ControlConfiguration():ReportParameterVO{
return _controlConfiguration;
}
public function set ControlConfiguration(value:ReportParameterVO):void{
_controlConfiguration = value;
_guiText = (value ? value.GuiText:"");
_amfPHPArgumentName = (value ? value.AMFPHP_ArgumentName: "");
_sequence = (value ? value.Sequence : null);
_reportId = (value ? value.ReportId : null);
}
public function get IsValid():Boolean{
return _isValid;
}
public function get ReportID():int{
return _reportId;
}
public function get Sequence():int{
return _sequence;
}
public function get ControlRepResult():ReportResult{
return _reportResult;
}
public function set ControlRepResult(value:ReportResult):void{
_reportResult = value;
}
internal function set Selection(value:Object):void{
_selection = value;
}
internal function get Selection():Object{
return _selection;
}
public function get ParameterSelection():Object{
return _selection;
}
public function get GuiText():String{
return _guiText;
}
public function get AmfPHPArgumentName():String{
return _amfPHPArgumentName;
}
public function ReportControl(){
//TODO: implement function
super();
}
public function dispatchControlChanged():void{
this.dispatchEvent(new ReportControlEvent(ReportControlEvent.CONTROL_CHANGED, this, true));
}
public function dispatchControlIsNowValid():void{
this.dispatchEvent(new ReportControlEvent(ReportControlEvent.CONTROL_IS_NOW_VALID, this, true));
}
public function addSelfToValueObject(valueObject:Object):Object{
valueObject[AmfPHPArgumentName] = _selection;
return valueObject;
}
}
I'll try to give you an example of what I mean with the Spark skinning architecture we've discussed in the comments above. It's not directly an answer to your question, but I thought you might find it interesting. I will have to make it somewhat simpler than your component for brevity's sake and because you seem to have stripped out some of the code for your question so I can't know exactly what it's supposed to do.
This will be a component that will let you toggle between a normal and an expanded state through the click of a Button. First we'll create the skin class. Normally you'd create the host component first, but it'll be easier to explain this way.
Thus we've set up completely how your component will look and how it will lay out. Do you feel your headaches dissipating? I for one find this quite elegant. We have the two states and the height of the component will adjust to the currently selected state as will the icon of the Button. How and when the state is toggled is component behaviour and will be defined in the host component.
Now let's create that host component in plain ActionScript. For this we'll extend SkinnableComponent (note that it could also extend your ReportControl if that would extend SkinnableComponent instead of UIComponent).
Allright, there's a lot more going on here.
SkinState
metadata declarations: when a skin class is assigned to the component, the compiler will check whether that skin has the required states implemented.SkinPart
declarations: the name of the property on the host component must exactly match the id of the tag in the skin class. Asrequired
is set totrue
the compiler will check whether these components do really exist in the skin. If you want optional skin parts, you set it tofalse
.toggleButton
isIEventDispatcher
: from the host component's point of view, alltoggleButton
has to do, is dispatching CLICK events. This means that we could now create a skin with<s:Image id="toggleButton" source="..." />
and the whole thing would keep working the same way. See how powerful this is?partAdded()
method which will be executed whenever a component becomes available. In most cases this is the place where you hook up your event listeners.toggleExpanded()
method, we toggle the boolean just like the component in your question, however we only invalidate the skin state. This will cause the skin to call thegetCurrentSkinState()
method and update its state to whatever value is returned.Et voilà! You have a working component with the behaviour nicely separated into an actionscript class and you didn't have to worry about the layout intricacies. And if you ever wish to create a component with the same behaviour, but it should expand horizontally instead of vertically: just create a new skin that adjusts the
width
instead of theheight
and assign that to the same host component.Oh wait! I nearly forgot to tell you how to assign the skin to the components. You can do it either inline:
or through styling:
Thank you both so much for your answers! The consideration and attention to detail in explaining the concepts is awesome! Très bien!
@RIAstar However due to the amount of code already in place, changing my architecture (separating visual element from behavioural) would force to large a re-factor of the code and would cost to much for a feature that hasn't been explicitly requested. (visual representation of the control being able to change at runtime) It certainly is interesting and I will be adding that into a future version.
That said, I think I've been able to find a solution to my problem. I decided to build off of @SunilD.'s suggestion of validating the control before it's added back in. A hack I know, but humans aren't perfect and thus the code aint either. ;-)
When looking at the control, I noticed it was only the button that was having issues with rendering its image. So to test, I added and removed JUST a button instance with an icon, and I saw the same behaviour! (regardless of how comboBoxMultiSelect was implemented) I ALSO noticed that I didn't see the button do this when it was first created. So why not just reconstruct the button when it gets removed from the display list?
I ended up wiring
comboBoxMultiSelect
to theFlexEvent.REMOVE
event, destroy the button reference, create a new one, and add it back in with AddChild(). Below is an explanation of the event.Sure enough, this fixed the icon from displaying incorrectly and explains what happening. For some reason the button is taking more than one render event to apply its style when it's added back in. Anyone else able to replicate this behaviour?
I guess the real question now is "what is the best way to remove and add-back-in a button to the display list, so that its embedded icon is unaffected?"
One thing that stands out is in your implementation of
updateDisplayList()
. As you know, this is where your component should size and position it's child objects (and/or do any programatic drawing).But rather than set the child object's width/height directly, you should use one of the Flex lifecycle methods:
setActualSize()
orsetLayoutBoundsSize()
. UsesetLayoutBoundsSize()
with spark components.When you set a Flex component's width/height, the component will invalidate itself so that on the next update cycle it can be re-rendered. But since you are trying to render the component in
updateDisplayList()
you should be careful to not invalidate your child objects inside this method.The
setActualSize()
andsetLayoutBoundsSize()
methods set the width/height on a Flex component, but do not invalidate the component.Note, it looks like some child objects are being sized in
createChildren()
as well ... and it's not really clear what the base Flex component is in this case (what class doesReportControl
extend?Doing it this way may get rid of that rendering glitch. It will most certainly execute less code than if you set width/height properties directly.
[Edit]
It may be an interaction with the
HGroup
which is kind of unnecessary in this component. While I think making components this way is fun, it can be more tedious... which is why @RIAStar is wisely pointing out another approach.Some further ideas, if you want to continue down this path:
1) Take a look at the sizing you are doing in
createChildren()
- for example, theHGroup
is given a percentWidth, but inupdateDisplayList()
it is given a fixed width (this may be a red herring, but I would not set the percentWidth).2) You might be able to trick the component into validating itself after you remove it or before you re-add it. A hacky hunch that may be a waste of time.
3) Remove the 'HGroup' from your component. It's kind of unnecessary: the layout requirements are simple enough to do w/a few lines of Actionscript. Your mileage will vary as the layout requirements get more complex!
In
createChildren()
add the combo box and button directly to theUIComponent
. Then size and position them inupdateDisplayList()
, something like this: