How to inherit states with mxml?

2019-04-06 08:04发布

问题:

I have the following panel component called AdvancedPanel with controlBarContent:

<!-- AdvancedPanel.mxml -->
<s:Panel>
  <s:states>
    <s:State name="normal" />
    <s:State name="edit" />
  </s:states>
  <s:controlBarContent>
    <s:Button 
      includeIn="edit"
      label="Show in edit"
      />
    <s:Button 
      label="Go to edit"
      click="{currentState='edit'}"
      />
  </s:controlBarContent>
</s:Panel>

I created a second panel, called CustomAdvancedPanel based on the AdvancedPanel since I don't want to redeclare the controlBarContent

<!-- CustomAdvancedPanel.mxml -->
<local:AdvancedPanel>
  <s:Button includeIn="edit" label="Extra edit button" />
</local:AdvancedPanel>

This doesn't work, because the 'edit' state in CustomAdvancedPanel isn't declared according to the compiler. I have to redeclare the edit state in CustomAdvancedPanel.mxml as follows:

  <!-- CustomAdvancedPanel.mxml with edit state redeclared -->
    <local:AdvancedPanel>
      <local:states>
        <s:State name="normal" />
        <s:State name="edit" />
      </local:states>
      <s:Button includeIn="edit" label="Extra edit button" />
    </local:AdvancedPanel>

Using the CustomAdvancedPanel inside an application component shows an empty panel with the "Go to edit" button. But when I click it, the "Extra edit button" becomes visible, but the "Show in edit" button inside the controlBar doesn't.

When the CustomAdvancedPanel is empty, without redeclared states and "Extra edit button" the panel works just fine.

I think it is because the State object declared in AdvancedPanel isn't the same as CustomAdvancedPanel, so the state is different, even if they have the same name. However. I can't use the states of AdvancedPanel inside CustomAdvancedPanel without (re)declare them in mxml.

Is there any way to achieve this kind of state-reuse? Or is there a better way to obtain the same result?

回答1:

I suggest you to use Spark's skinning architecture to obtain your goals. Because skin states are inherited in host component you can place all the logic in OOP way. But skins will still contain duplicate code :( Anyway it is better than duplicate code of all the component.

So our AdvancedPanel will look like the following:

package
{
    import flash.events.MouseEvent;

    import spark.components.supportClasses.ButtonBase;
    import spark.components.supportClasses.SkinnableComponent;

    [SkinState("edit")]
    [SkinState("normal")]
    public class AdvancedPanel extends SkinnableComponent
    {
        [SkinPart(required="true")]
        public var goToEditButton:ButtonBase;
        [SkinPart(required="true")]
        public var showInEditButton:ButtonBase;

        private var editMode:Boolean;

        override protected function getCurrentSkinState():String
        {
            return editMode ? "edit" : "normal";
        }

        override protected function partAdded(partName:String, instance:Object):void
        {
            super.partAdded(partName, instance);
            if (instance == goToEditButton)
                goToEditButton.addEventListener(MouseEvent.CLICK, onGoToEditButtonClick);
        }

        override protected function partRemoved(partName:String, instance:Object):void
        {
            super.partRemoved(partName, instance);
            if (instance == goToEditButton)
                goToEditButton.removeEventListener(MouseEvent.CLICK, onGoToEditButtonClick);
        }

        private function onGoToEditButtonClick(event:MouseEvent):void
        {
            editMode = true;
            invalidateSkinState();
        }
    }
}

And for CustomAdvancedPanel:

package
{
    import spark.components.supportClasses.ButtonBase;

    public class CustomAdvancedPanel extends AdvancedPanel
    {
        [SkinPart(required="true")]
        public var extraEditButton:ButtonBase;
    }
}

Of course you can inherit from Panel class but I made sample code more simple.

And the skins:

<?xml version="1.0" encoding="utf-8"?>
<!-- AdvancedPanelSkin.mxml -->
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx">
    <fx:Metadata>
        [HostComponent("AdvancedPanel")]
    </fx:Metadata>
    <s:states>
        <s:State name="normal" />
        <s:State name="edit" />
    </s:states>
    <s:Panel left="0" right="0" top="0" bottom="0">
        <s:controlBarContent>
            <s:Button id="showInEditButton" label="Show in edit" includeIn="edit" />
            <s:Button id="goToEditButton" label="Go to edit" />
        </s:controlBarContent>
    </s:Panel>
</s:Skin>

And:

<?xml version="1.0" encoding="utf-8"?>
<!-- CustomAdvancedPanelSkin.mxml -->
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx">
    <fx:Metadata>[HostComponent("CustomAdvancedPanel")]</fx:Metadata>
    <s:states>
        <s:State name="normal" />
        <s:State name="edit" />
    </s:states>
    <s:Panel left="0" right="0" top="0" bottom="0">
        <s:Button includeIn="edit" label="Extra edit button" id="extraEditButton" />
        <s:controlBarContent>
            <s:Button id="showInEditButton" label="Show in edit" includeIn="edit" />
            <s:Button id="goToEditButton" label="Go to edit" />
        </s:controlBarContent>
    </s:Panel>
</s:Skin>


回答2:

AFAIK the component's state does not cross over to inherited components. Think about it - if that were the case (if you could inherit states) then it would make life really complicated whenever you want to extend a component; you would have to be aware of all inherited states and not step on their toes.



回答3:

I reckon it's a limitation of OO programming, but not sure what exactly. I'm no Flex expert but I thought about it from an object-oriented programming point of view and here's what I think happens:

First consider that when you create an object, Flex (or any OO language) automatically creates a copy of that object AND a private copy of its parent object, which in turn creates a private copy of its parent object and so on up the entire object tree. That might sound weird but as an example of this, when you write super() in a constructor you are calling the constructor of the parent class.

Flex has what it calls "properties". This is the equivalent of what in Java would be a private member field (variable) with a public getter and setter method. When you declare

<local:states>xyz</local:states>

you are effectively saying

states = xyz

which in turn is the AS equivalent of saying

setStates(xyz)

The important part, and this is a general rule about properties, is that setStates is a public method, anyone can call this. However the states array itself is private. If you don't declare one, CustomAdvancedPanel has no states property. Neither does it have a setStates or getStates method. However as setStates/getStates are public, it inherits them from AdvancedPanel so it funcions as if it has these methods. When you call one of these methods (get or set the states array), it actually calls the method where it exists, which is in its parent object, AdvancedPanel. When AdvancedPanel executes the method the value of the states array in AdvancedPanel itself is read or set. This is why when you don't redeclare any states in CustomAdvancedPanel everything works perfectly - you think you are setting and getting the states array in CustomAdvancedPanel but in fact behind the scenes you are operating on the states array in the AdvancedPanel parent object, which is perfectly fine and good.

Now you redefine the states array in CustomAdvancedPanel - what is happening? Remember that declaring a property in Flex is like declaring a private class-level variable and public getters and setters. So you are giving CustomAdvancedPanel a private array called states and public getters and setters to get/set that array. These getters and setters will override the ones from AdvancedPanel. So now your application will interact with CustomAdvancedPanel the same way but behind the scenes you are no longer operating on the methods/variables of AdvancedPanel but rather on the ones you have declared in CustomAdvancedPanel itself. This explains why when you change the state of CustomAdvancedPanel, the part that is inherited from AdvancedPanel does not react, since its display is linked to the states array in AdvancedPanel, which still exists independently.

So why isn't the includeIn allowed in the basic example where you don't redeclare the states? I don't know. Either it's a bug, or perhaps more likely, there's a legitimate language/OO reason why it could never work.

It's possible that my explanation is not totally accurate. That's as far as I understand things. I myself don't know why that would really happen considering the Button in question is part of the superclass. A couple of interesting tests would be:

  1. move the click handler into an actual public method instead of inline.
  2. add super.currentState='edit' to the click handler.

If you want to learn more about all this inheritance stuff, write some simple classes in ActionScript or Flex with one class inheriting another, and run various function calls to see what happens.



回答4:

"Or is there a better way to obtain the same result?"

Since you asked, and because you didn't make a clear case as to the need for the extra CustomAdvancedPanel component, putting the "Extra edit button" in the AdvancedPanel component is the simplest solution.

<!-- AdvancedPanel.mxml -->
<s:Panel>
  <s:states>
    <s:State name="normal"/>
    <s:State name="edit"/>
  </s:states>
  <s:Button includeIn="edit" label="Extra edit button"/>
  <s:controlBarContent>
    <s:Button 
      includeIn="edit"
      label="Show in edit"/>
    <s:Button 
      label="Go to edit"
      click="{currentState='edit'}"/>
  </s:controlBarContent>
</s:Panel>


回答5:

Assaf Lavie is right, it would be very confusing, if a custom component had its parent's states. i'd say consider using skins:

  • Flex 4 Component States vs. Skin States
  • Advanced Flex 4 Skinning Techniques


回答6:

Of course the politically correct way is to use skins. However, for those who really just want to brute force state inheritance for MXML classes here is a work around that I have found.

For this method to work, the extending MXML class should declare exactly the same states of the base MXML class, no more and no fewer, all with identical names.

Then in the extending class insert the following method:

        override public function set states(value:Array):void
        {
            if(super.states == null || super.states.length == 0)
            {
                super.states  = value;

                for each (var state:State in value)
                {
                    state.name = "_"+state.name;
                }
            }
            else
            {
                for each (var state:State in value)
                {
                    state.basedOn = "_"+state.name;
                    super.states.push(state);
                }
            }
        }

This works because as the component is created the states variable is set twice, once by the base class, and once by the extending class. This workaround just combines them together.