Pages

November 10, 2010

Flex 4 Skinning for Layout Reuse

Posted by Jeremy Mitchell
In a prior entry entitled "Sharing MXML Layout Across Components", I demonstrated a technique used to share one MXML layout definition across many components.

However, with the advent of Flex 4 and its skinning architecture, a better way exists.

You'll remember that we had two editors (a report editor and a list editor) similar in layout and structure. Each editor also shared a couple of properties and methods. Fortunately, the Flex 4 skinning architecture makes sharing layout across components a snap. Here's a high-level overview of the chosen architecture:

And here's the application:



View source is enabled.

To build this, I first created a base class called Editor. This base class defines the shared properties and methods of each editor. Here was my first pass at the Editor class:
<!-- Editor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         xmlns:mx="library://ns.adobe.com/flex/mx"
         width="500"
         title="{editorType} Editor"
         preinitialize="onPreinitialize(event)">

    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            public static const REPORT:String="Report";
            public static const LIST:String="List";

            [Bindable]
            public var editorType:String;

            protected function save():void
            {
                throw new Error("Abstract Class - override this method in a subclass.");
            }

            protected function onPreinitialize(event:FlexEvent):void
            {
                // subclass should override this method and set the editorType
                throw new Error("Abstract Class - override this method in a subclass.");
            }
        ]]>
    </fx:Script>

</s:Panel>
Next, I created a skin (New > MXML Skin) based on the Editor class (aka Host Component). Since Editor is a subclass of Panel, I was presented with the option to "Create as copy of spark.skins.spark.PanelSkin". I ticked the checkbox.

With a starting point for my skin (EditorSkin.mxml), I made the proper adjustments to support my editor's custom layout. Specifically, I wrapped the content group (id == contentGroup) with additional layout code. Here was the skin's content group before:
<s:Group id="contentGroup" width="100%" height="100%" 
   minWidth="0" minHeight="0">
</s:Group>
Here is the content group after my adjustments:
<s:Group id="editorLayout"
   width="100%">
 
 <s:layout>
  <s:VerticalLayout paddingTop="10"
        paddingBottom="10"
        paddingLeft="10"
        paddingRight="10"/>
 </s:layout>
 
 <s:BorderContainer width="100%"
        height="30"
        borderWeight="1"
        borderColor="#aab3b3"
        backgroundColor="#ededed">
  
  <s:layout>
   <s:HorizontalLayout verticalAlign="middle"
        paddingLeft="10"
        paddingRight="10"/>
  </s:layout>
  
  <s:HGroup id="toolBarChildrenGroup"/>
  
  <s:Rect width="100%"/>
  
  <s:Label text="{hostComponent.editorType}"
     fontWeight="bold"/>
  
  
 </s:BorderContainer>
 
 <!--- @copy spark.components.SkinnableContainer#contentGroup -->
 <s:Group id="contentGroup"
    width="100%"
    height="100%"
    minWidth="0"
    minHeight="0">
  
  <s:layout>
   <s:VerticalLayout/>
  </s:layout>
  
 </s:Group>
 
</s:Group>
Then, I mapped the skin class to a CSS style declaration within Styles.css (and don't forget to include the CSS file in your application!).
.editor
{
    skin-class: ClassReference( "skins.EditorSkin" );
}
Finally, the host component (Editor) needed to be altered to accommodate my new skin. First, it needed the proper styleName value to utilize the skin. It also needed to manually inject tool bar contents into the skin. This required three things:

  1. A public property to hold each editor's custom tool bar contents
  2. A declaration of a required skin part
  3. An override of the partAdded() method

Here was my second pass at the Editor class:
<!-- Editor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009"
   xmlns:s="library://ns.adobe.com/flex/spark"
   xmlns:mx="library://ns.adobe.com/flex/mx"
   width="500"
   title="{editorType} Editor"
   styleName="editor"
   preinitialize="onPreinitialize(event)">
 
 <fx:Script>
  <![CDATA[
   import mx.events.FlexEvent;
   
   import spark.components.HGroup;
   
   public static const REPORT:String="Report";
   public static const LIST:String="List";
   
   // the custom tool bar contents will be injected into this HGroup
   // the horizontal layout of the tool bar is enforced by the skin having an HGroup
   [SkinPart(required="true")]
   public var toolBarChildrenGroup:HGroup;
   
   [Bindable]
   public var editorType:String;
   
   // this is where custom tool bar contents will reside
   public var toolBarChildren:Array;
   
   override protected function partAdded(partName:String, instance:Object):void
   {
    super.partAdded(partName, instance);
    
    // when the toolBarChildrenGroup skin part is added, everything inside the 
    // toolBarChildren array will be "injected" into the toolBarChildrenGroup HGroup
    if (instance == toolBarChildrenGroup)
    {
     toolBarChildrenGroup.mxmlContent=toolBarChildren;
    }
   }
   
   protected function save():void
   {
    throw new Error("Abstract Class - override this method in a subclass.");
   }
   
   protected function onPreinitialize(event:FlexEvent):void
   {
    // subclass should override this method and set the editorType
    throw new Error("Abstract Class - override this method in a subclass.");
   }
  ]]>
 </fx:Script>
 
</s:Panel>
Then I simply extended the Editor class and added the content specific to the subclass.
<!-- ReportEditor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<comps:Editor xmlns:fx="http://ns.adobe.com/mxml/2009"
              xmlns:s="library://ns.adobe.com/flex/spark"
              xmlns:mx="library://ns.adobe.com/flex/mx"
              xmlns:comps="comps.*">

    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            protected function saveButton_clickHandler(event:MouseEvent):void
            {
                save();
            }

            protected function publishButton_clickHandler(event:MouseEvent):void
            {
                publish();
            }

            protected function publish():void
            {
                //publish
            }

            override protected function save():void
            {
                //save
            }

            override protected function onPreinitialize(event:FlexEvent):void
            {
                editorType=Editor.REPORT;
            }
        ]]>
    </fx:Script>

    <comps:toolBarChildren>

        <!-- put whatever you like in here. buttons, comboboxes, etc
             but whatever you put in here gets laid out horizontally
             inside a border / background. the subclass defines the tool 
             bar contents, not the super class -->

        <s:Button label="Save"
                  click="saveButton_clickHandler(event)"/>

        <s:Button label="Publish"
                  click="publishButton_clickHandler(event)"/>

    </comps:toolBarChildren>

    <!-- Anything you put here gets "injected" into the skin's contentGroup -->

    <comps:ReportForm/>

</comps:Editor>
<!-- ListEditor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<comps:Editor xmlns:fx="http://ns.adobe.com/mxml/2009"
              xmlns:s="library://ns.adobe.com/flex/spark"
              xmlns:mx="library://ns.adobe.com/flex/mx"
              xmlns:comps="comps.*">

    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            protected function saveButton_clickHandler(event:MouseEvent):void
            {
                save();
            }

            protected function submitButton_clickHandler(event:MouseEvent):void
            {
                submit();
            }

            protected function submit():void
            {
                //submit
            }

            override protected function save():void
            {
                //save
            }

            override protected function onPreinitialize(event:FlexEvent):void
            {
                editorType=Editor.LIST;
            }
        ]]>
    </fx:Script>

    <comps:toolBarChildren>

        <s:Button label="Save"
                  click="saveButton_clickHandler(event)"/>

        <s:Button label="Submit"
                  click="submitButton_clickHandler(event)"/>

    </comps:toolBarChildren>

    <comps:ListForm/>

</comps:Editor>
You can also watch me demonstrate this technique in this short 5 minute video.



Another approach involves "inheriting" the Editor's layout through composition rather than traditional object-oriented inheritance. To do this, the Editor class needs to be a concrete class rather than an abstract class. Here are the necessary changes:
<!-- Editor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         xmlns:mx="library://ns.adobe.com/flex/mx"
         width="500"
         title="{editorType} Editor"
         styleName="editor">

    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            import spark.components.HGroup;

            public static const REPORT:String="Report";
            public static const LIST:String="List";

            [SkinPart(required="true")]
            public var toolBarChildrenGroup:HGroup;

            [Bindable]
            public var editorType:String;

            public var toolBarChildren:Array;

            override protected function partAdded(partName:String, instance:Object):void
            {
                super.partAdded(partName, instance);

                if (instance == toolBarChildrenGroup)
                {
                    toolBarChildrenGroup.mxmlContent=toolBarChildren;
                }
            }
        ]]>
    </fx:Script>

</s:Panel>
Now, I can compose any class with an instance of the Editor class. For example:
<!-- DocumentEditor.mxml -->

<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         xmlns:mx="library://ns.adobe.com/flex/mx"
         xmlns:comps="comps.*">

    <fx:Script>
        <![CDATA[
            protected function verifyButton_clickHandler(event:MouseEvent):void
            {
                // verify
            }

            protected function formatButton_clickHandler(event:MouseEvent):void
            {
                // format
            }
        ]]>
    </fx:Script>

    <comps:Editor editorType="Document">

        <comps:toolBarChildren>

            <s:Button label="Verify"
                      click="verifyButton_clickHandler(event)"/>

            <s:Button label="Format"
                      click="formatButton_clickHandler(event)"/>

        </comps:toolBarChildren>

        <comps:DocumentForm/>

    </comps:Editor>

</s:Group>