Search code examples
apache-flexactionscriptadobecustom-controls

Custom Composite Control not rendering correctly for only 0.5-1 sec after being added back into a VGROUP


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;
    }

}

Solution

  • 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.

    <!-- my.skins.ComboBoxMultiSelectSkin -->
    <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
            xmlns:s="library://ns.adobe.com/flex/spark"
            height.normal="25" height.expanded="200">
    
        <fx:Metadata>
            [HostComponent("my.components.ComboBoxMultiSelect")]
        </fx:Metadata>
    
        <s:states>
            <s:State name="normal" />
            <s:State name="expanded" />
        </s:states>
    
        <s:layout>
            <s:HorizontalLayout gap="0" />
        </s:layout>
    
        <s:ComboBox id="comboBox" width="100%" />
        <s:Button id="toggleButton" width="20"
                  icon.normal="@Embed('../Assets/Icons/plus-16.png')"
                  icon.expanded="@Embed('../Assets/Icons/minus-16.png')"/>
    
    </s:Skin>
    

    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).

    [SkinState("normal")]
    [SkinState("expanded")]
    public class ComboBoxMultiSelect extends SkinnableComponent {
    
        [SkinPart(required="true")]
        public var toggleButton:IEventDispatcher;
    
        [SkinPart(required="true")]
        public var comboBox:ComboBox;
    
        private var expanded:Boolean;
    
        override protected function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
    
            switch (instance) {
                case toggleButton:  
                    toggleButton.addEventListener(MouseEvent.CLICK, handleToggleButtonClick); 
                    break;
                case comboBox:
                    comboBox.addEventListener(IndexChangeEvent.CHANGE, handleComboSelection);
                    break;
            }
        }
    
        private function handleToggleButtonClick(event:MouseEvent):void {
            toggleExpanded();
        }
    
        private function handleComboSelection(event:IndexChangeEvent):void {
            //handle comboBox selection
        }
    
        protected function toggleExpanded():void {
            expanded = !expanded;
            invalidateSkinState();
        }
    
        override protected function getCurrentSkinState():String {
            return expanded ? "expanded" : "normal";
        }
    }
    

    Allright, there's a lot more going on here.

    • First look at the SkinState metadata declarations: when a skin class is assigned to the component, the compiler will check whether that skin has the required states implemented.
    • Then the SkinPart declarations: the name of the property on the host component must exactly match the id of the tag in the skin class. As required is set to true the compiler will check whether these components do really exist in the skin. If you want optional skin parts, you set it to false.
    • Note that the type of toggleButton is IEventDispatcher: from the host component's point of view, all toggleButton 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?
    • Because the skinpart properties are not assigned immediately, we override the 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.
    • In the 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 the getCurrentSkinState() 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 the height 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:

    <c:ComboBoxMultiSelect skinClass="my.skins.ComboBoxMultiSelectSkin" />
    

    or through styling:

    @namespace c "my.components.*";
    
    c|ComboBoxMultiSelect {
        skinClass: ClassReference("my.skins.ComboBoxMultiSelectSkin")
    }