Pages

October 20, 2010

Sharing MXML Layout Across Components

Posted by Jeremy Mitchell
Frequently, I find myself creating visual components similar in layout and structure. Unfortunately, unlike properties and methods, layout is a tricky thing to share across components via traditional object-oriented inheritance.

Inevitably, layout code (MXML) gets duplicated across your application. The result is a maintenance nightmare and loads of redundant MXML code. Notice the redundant MXML found in the ReportEditor and ListEditor classes of this application.

Inspired by a colleague of mine, Chris Hayen, this entry will demonstrate code reuse through both inheritance (properties and methods) and composition (layout) without resorting to the undesirable use of ActionScript for layout code.

Composition over Inheritance


In this sample application, I have two editors: a report editor and a list editor. Each share a common layout, therefore, I will extract the layout into its own class - EditorLayout.

Written in MXML, EditorLayout will dictate the layout of each editor. Here's my first pass at EditorLayout.
<!--EditorLayout.mxml-->

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox 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="100%" height="100%">
    
    <fx:Script>
        <![CDATA[            
            [Bindable]
            public var title:String;
        ]]>
    </fx:Script>
    
    <mx:HBox width="100%"
             backgroundColor="#ededed"
             height="30"
             verticalAlign="middle"
             paddingRight="10"
             paddingLeft="10"
             borderStyle="solid"
             borderColor="#aab3b3">
        
        <mx:HBox id="toolBarChildrenContainer"/>
        
        <mx:Spacer width="100%"/>
        
        <mx:Label id="titleLabel"
                  color="#333333"
                  fontWeight="bold"
                  text="{title}"/>
    </mx:HBox>

    <mx:VBox id="editorChildrenContainer"
            width="100%"
            height="100%"
            minWidth="0"
            minHeight="0"/>

</mx:VBox>
Note: Whenever possible, I always use MXML to define layout. MXML is for layout. ActionScript is for business logic.
Although each editor will have the same layout, their contents will differ. To support custom contents, I'll add a couple of public properties to allow injection of content into the layout. I also need a couple of methods responsible for rendering this content. Here's my second pass at EditorLayout.
<!--EditorLayout.mxml-->

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox 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="100%" height="100%"
         initialize="onInitialize(event)">
    
    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;
            
            [Bindable]
            public var title:String;
            
            private var _toolBarChildren:Array;
            private var _editorChildren:Array;
            
            public function get toolBarChildren():Array
            {
                return _toolBarChildren;
            }
            
            public function set toolBarChildren(value:Array):void
            {
                _toolBarChildren = value;
            }
            
            public function get editorChildren():Array
            {
                return _editorChildren;
            }
            
            public function set editorChildren(value:Array):void
            {
                _editorChildren = value;
            }
            
            protected function onInitialize(event:FlexEvent):void
            {
                createToolBarChildren();
                createEditorChildren();
            }
            
            protected function createToolBarChildren():void
            {
                for each (var toolBarChild:DisplayObject in _toolBarChildren)
                {
                    if (!toolBarChildrenContainer.contains(toolBarChild))
                    {
                        toolBarChildrenContainer.addChild(toolBarChild);
                    }
                }
            }
            
            protected function createEditorChildren():void
            {
                for each (var editorChild:DisplayObject in _editorChildren)
                {
                    if (!editorChildrenContainer.contains(editorChild))
                    {
                        editorChildrenContainer.addChild(editorChild);
                    }
                }
            }
        ]]>
    </fx:Script>
    
    <mx:HBox width="100%"
             backgroundColor="#ededed"
             height="30"
             verticalAlign="middle"
             paddingRight="10"
             paddingLeft="10"
             borderStyle="solid"
             borderColor="#aab3b3">
        
        <mx:HBox id="toolBarChildrenContainer"/>
        
        <mx:Spacer width="100%"/>
        
        <mx:Label id="titleLabel"
                  color="#333333"
                  fontWeight="bold"
                  text="{title}"/>
    </mx:HBox>

    <mx:VBox id="editorChildrenContainer"
            width="100%"
            height="100%"
            minWidth="0"
            minHeight="0"/>

</mx:VBox>
My editors will also share some properties and methods, so I'll create a base class to leverage traditional object-oriented inheritance.
<!--Editor.mxml-->

<?xml version="1.0" encoding="utf-8"?>
<mx: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" 
          xmlns:comps="comps.*"
          title="{editorType} Editor"
          width="500"
          paddingTop="10"
          paddingBottom="10"
          paddingLeft="10"
          paddingRight="10">
    
    <fx:Script>
        <![CDATA[
            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.");
            }
        ]]>
    </fx:Script>
    
</mx:Panel>
Finally, I'll create my editor classes. An editor class will extend the Editor base class and will be composed of an instance of EditorLayout. Custom contents are injected into the shared layout declaratively using MXML.
<!--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.*" 
              preinitialize="onPreinitialize(event)">
    
    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;
            
            protected function saveButton_clickHandler(event:MouseEvent):void
            {
                save();
            }
            
            protected function publishButton_clickHandler(event:MouseEvent):void
            {
                publish();
            }
            
            override protected function save():void
            {
                //save
            }
            
            protected function publish():void
            {
                //publish
            }

            protected function onPreinitialize(event:FlexEvent):void
            {
                editorType = Editor.REPORT;
            }

        ]]>
    </fx:Script>
    
    
    <comps:EditorLayout title="{editorType}">
        
        <comps:toolBarChildren>
            
            <s:Button id="saveButton" 
                      label="Save"
                      click="saveButton_clickHandler(event)"/>
            
            <s:Button id="publishButton" 
                      label="Publish"
                      click="publishButton_clickHandler(event)"/>
            
        </comps:toolBarChildren>
        
        <comps:editorChildren>
            
            <comps:ReportForm id="reportForm"/>
            
        </comps:editorChildren>
        
    </comps:EditorLayout>
    
</comps:Editor>
<!--ListEditor-->

<?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.*" 
              preinitialize="onPreinitialize(event)">
    
    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;
            
            protected function saveButton_clickHandler(event:MouseEvent):void
            {
                save();
            }
            
            protected function submitButton_clickHandler(event:MouseEvent):void
            {
                submit();
            }
            
            override protected function save():void
            {
                //save
            }
            
            protected function submit():void
            {
                //submit
            }
            
            protected function onPreinitialize(event:FlexEvent):void
            {
                editorType = Editor.LIST;
            }
        ]]>
    </fx:Script>
    
    
    <comps:EditorLayout title="{editorType}">
        
        <comps:toolBarChildren>
            
            <s:Button id="saveButton" 
                      label="Save"
                      click="saveButton_clickHandler(event)"/>
            
            <s:Button id="submitButton" 
                      label="Submit"
                      click="submitButton_clickHandler(event)"/>
            
        </comps:toolBarChildren>
        
        <comps:editorChildren>
            
            <comps:ListForm id="listForm"/>
            
        </comps:editorChildren>
        
    </comps:EditorLayout>
    
</comps:Editor>
The final result (see below) is cleaner code throughout your application (no ActionScript layout code and no redundant MXML code).



View source is enabled

Of course, a better technique involves the use of Flex 4's skinning architecture. Stay tuned.

Update: See blog entry entitled "Flex Skinning to Share Common Layout".