jump to navigation

Dependency Injection in Flex Applications - Part I - Spring ActionScript and Cairngorm December 18, 2008

Posted by Allen Manning in : Cairngorm, Flex, Refactoring , 9comments

After layering many new features into our applications we see complexity emerge, and as complexity emerges we need to find techniques to keep our objects clean and testable. Complexity, as we looked at earlier, is a major challenge for large applications. Object complexity is increased as object collaboration increases; the more collaboration, the more complexity. This is complexity not only in objects performing their work, doing what they were designed to do, but also in testing.

We looked to some approaches in Unit Testing to help keep Command complexity under control and give our applications more robust testing coverage. Now we are going to upgrade these approach with some discussion and refactoring based on Dependency Injection.

Dependency Injection in a Nutshell
Eric Feminella has a great post on this that is worth reading.  Dependency Injection basically describes separating configuration from implementation. Swapping in and out different object dependencies in different application contexts hasn’t been implemented very cleanly in traditional Object Orientated languages.  In light of this, frameworks like the Spring Framework have risen in popularity over the last few years.

Christophe Herreman and Spring ActionScript

Many thanks to Christophe Herreman for releasing “Spring ActionScript” (formerly known as Prana) a Spring-like IoC container for Actionscript 3.  The project has been out for a while and has already gone through a few iterations.  Christophe has updated the Cairngorm Application Blueprint (the Cairngorm Store) with a Spring ActionScript Enabled version.  We will draw upon these ideas in Spring ActionScript-enabling an example application from a previous post.

Injecting Command Dependencies

Rather than declaring our two Command dependencies (the Model and a Delegate) as internal fields that unit tests in the same package can override (inject) with their Mocks, we will let Spring ActionScript do that work for us.  This results in a cleaner implementation where the Commands are completely de-coupled from our form of injection.

Let’s take a look at the refactoring:

UpdatePreferencesCommand.as (Before)

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
 
    import mx.rpc.IResponder;
 
    /**
     * Command that updates application UserPreferences for a particular User- collaborates with
     * the Account Delegate for persistence, and updates the local Account Model
     *
     * @author amanning
     */
    public class UpdatePreferencesCommand implements Command, IResponder {
 
        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        /** The remote account management proxy */
        internal var _accountDelegate:IAccountDelegate;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor, initialize collaborators
         */
        public function UpdatePreferencesCommand():void {
 
            //once the account delegate has been created we will initialize it it here
            // _accountDelegate = new AccountDelegate(this);
 
        } 
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Execute this command - update the Preferences and wait for response
         *
         * @param cairngormEvent    the CairngormEvent which was dispatched to invoke this command
         */
        public function execute(cairngormEvent:CairngormEvent):void {
 
            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);
 
            // Consider using an Assert?
            if(event.user == null || event.userPreferences == null) {
                throw new Error("Invalid event data");
            }
 
            _accountDelegate.updatePreferences(event.user.id, event.userPreferences);
 
        }
 
        /**
         * Event listner for the successful result from the delegate- update the local model with
         * the results
         *
         * @param data   The data passed back from the server.  While <code>data</code> is typed as
         *               Object, it is often (but not always) an mx.rpc.events.ResultEvent.
         * @see          mx.rpc.IResponder#result
         */
        public function result(data:Object):void {
 
        }
 
        /**
         * Event listener for the error result from the delegate- log the error
         *
         * @param info   The application fault.  While <code>info</code> is typed as Object it is
         * often (but not always) an mx.rpc.events.FaultEvent.
         *
         */
        public function fault(info:Object):void {
 
        }
 
    }
 
}

UpdatePreferencesCommand.as (After, using Spring ActionScript)

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.DelegateLocator;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.IAccountModels;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.Assert;
    import com.allenmanning.exampleflexapp.model.ModelLocator;
 
    import mx.rpc.IResponder;
    import mx.rpc.events.ResultEvent;
 
    /**
     * Command that updates application UserPreferences for a particular User- collaborates with
     * the Account Delegate for persistence, and updates the local Account Model
     *
     * @author amanning
     */
    public class UpdatePreferencesCommand implements Command, IResponder {
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Execute this command - update the Preferences and wait for response
         *
         * @param cairngormEvent    the CairngormEvent which was dispatched to invoke this command
         */
        public function execute(cairngormEvent:CairngormEvent):void {
 
            Assert.isTrue(
                ModelLocator.getInstance().accountModels.userIsLoggedIn,
                "The user must be logged in");
 
            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);
 
            Assert.notNull(event.user, "The user must not be null");
            Assert.notNull(event.userPreferences, "The preferences must not be null");
 
            var delegate:IAccountDelegate = DelegateLocator.getInstance().accountDelegate;
 
            delegate.updatePreferences(event.user.id, event.userPreferences);
 
        }
 
        /**
         * Event listener for the successful result from the delegate- update the local model with
         * the results
         *
         * @param data   The data passed back from the server.  While <code>data</code> is typed as
         *               Object, it is often (but not always) an mx.rpc.events.ResultEvent.
         * @see          mx.rpc.IResponder#result
         */
        public function result(data:Object):void {
 
            var updatedPreferences:UserPreferences =
                ((data as ResultEvent).result as UserPreferences);
 
            Assert.notNull(updatedPreferences, "The preferences must not be null");
 
            ModelLocator.getInstance().accountModels.preferences = updatedPreferences;                    
 
        }
 
        /**
         * Event listener for the error result from the delegate- log the error
         *
         * @param info   The application fault.  While <code>info</code> is typed as Object it is
         * often (but not always) an mx.rpc.events.FaultEvent.
         *
         */
        public function fault(info:Object):void {
            // TODO implement this
        }
 
    }
 
}

There are two major differences in the above refactorings.
_accountDelegate has been removed. We were using an internal variable to inject the delegate into this class before. We would inject a mock for our unit tests after the object has been constructed. We have replaced this with a new DelegateLocator.

In the Prana Store example, the delegates were injected onto the Model Locator. This tight coupling between the business model and the delegates is something we should move away from. Delegates and Models should have now knowledge of each other.

The second major difference that we have added a basic implementation for the onResult function- the results are being set into the model. This is another area that we are using dependency injection. Both the accountDelegate and the accountModel are being injected using Spring ActionScript. The command itself is gaining access to these dependencies through two Singleton Locator Objects.

Injecting The Dependencies
In our application-context.xml we wire up the relationships. In this case, we are using mocks for testing. The DelegateLocator has a mock account delegate injected into it. The same goes for the Account Model in the ModelLocator. In both cases we are injecting Mocks for testing purposes. For a deployment context, the application-context.xml would be different.

application-context.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<objects xmlns="http://www.pranaframework.org/objects"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.pranaframework.org/objects http://www.pranaframework.org/schema/objects/prana-objects-0.6.xsd">
 
	<!-- =================================================================== -->
	<!-- This application context defines a single test configuration, it can be expanded to include
	       a deployment configuration as well, please see comments below.
 
		 1. Testing: Mock Delegates
		 2. Deployment: Remote Object Delegates
 
		 To use a configuration, uncomment it and comment the one you don't
		 want to use. By default, the Mock Delegates configuration is used.
 
	-->
	<!-- =================================================================== -->
 
	<!-- =================================================================== -->
	<!-- 1. Testing: Mock Delegates -->
	<!-- =================================================================== -->
	<object
	       id="DelegateLocator" 
	       class="com.allenmanning.exampleflexapp.account.business.DelegateLocator" 
	       factory-method="getInstance">
 
        <property name="accountDelegate">
            <object class="com.allenmanning.exampleflexapp.account.business.MockAccountDelegate"/>
        </property>
 
 
	</object>
 
    <object
           id="ModelLocator" 
           class="com.allenmanning.exampleflexapp.model.ModelLocator" 
           factory-method="getInstance">
 
        <property name="accountModels">
            <object class="com.allenmanning.exampleflexapp.account.model.MockAccountModels"/>
        </property>
 
    </object>
 
	<!-- =================================================================== -->
	<!-- 2. Deployment: Remote Object Delegates -->
	<!-- =================================================================== -->
    <!--
    <object
           id="DelegateLocator" 
           class="com.allenmanning.exampleflexapp.account.business.DelegateLocator" 
           factory-method="getInstance">
 
            <property name="accountDelegate">
	            <object class="TODO create real account delegate"/>
	        </property>
    </object>
 
 
    <object
           id="ModelLocator" 
           class="com.allenmanning.exampleflexapp.model.ModelLocator" 
           factory-method="getInstance">
 
        <property name="accountModels">
            <object class="com.allenmanning.exampleflexapp.account.model.AccountModels"/>
        </property>
 
    </object>
    -->
</objects>

Testing with Mocks and Spring ActionScript Our unit test for this command, assumes that Mocks have been injected into our Command and take advantage of these to test the object behavior.

?View Code ACTIONSCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.allenmanning.exampleflexapp.account.business.DelegateLocator;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.business.MockAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.IAccountModels;
    import com.allenmanning.exampleflexapp.account.model.MockAccountModels;
    import com.allenmanning.exampleflexapp.account.model.User;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.AssertionError;
    import com.allenmanning.exampleflexapp.model.ModelLocator;
    import com.anywebcam.mock.Mock;
 
    import flexunit.framework.TestCase;
 
    import mx.rpc.events.ResultEvent;
 
    /**
     * Responsible for testing the Command that updates application UserPreferences for a
     * particular User
     *
     * @author   amanning
     */
     public class UpdatePreferencesCommandTest extends TestCase {
 
        //------------------------------------------------------------------------------------------
        // Member Variables
        //------------------------------------------------------------------------------------------
        /** The command that we are testing */
        private var _commandToTest:UpdatePreferencesCommand;
 
        /** A fake user, used in testing */
        private var _fakeUser:User = new User(123456, 'aFakeUser');
 
        /** Fake user preferences, used in testing */
        private var _fakePreferences:UserPreferences = new UserPreferences(false);            
 
        /** A fake event used in testing */
        private var _fakeEvent:UpdatePreferencesEvent =
            new UpdatePreferencesEvent(_fakePreferences, _fakeUser);
 
        /**
         * A mock Account delegate, with configurable expectations - used in testing.  We have
         * injected this mock into the Delegate locator.
         */
        private var _mockAccountDelegate:IAccountDelegate =
            DelegateLocator.getInstance().accountDelegate;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class Constructor
         *
         * @param methodName    the test to add to this test suite
         * @see                 flexunit.framework.TestCase
         */
        public function UpdatePreferencesCommandTest( methodName:String = null ) {
 
            super( methodName );
 
        }
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Called before every test to create testing data, or reset any test-specific properties
         *
         * @see    flexunit.framework.TestCase#setUp
         */
        override public function setUp():void {
 
            _commandToTest = new UpdatePreferencesCommand();
            (_mockAccountDelegate as MockAccountDelegate).mock = new Mock();
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock = new Mock();
 
        }
 
        /**
         * Called before after test to clean up testing data
         *
         * @see    flexunit.framework.TestCase#tearDown
         */
        override public function tearDown():void {
 
            // if this mock was not interacted with, and we expected it to this should throw an
            // exception
            (_mockAccountDelegate as MockAccountDelegate).mock.verify();
            (ModelLocator.getInstance().accountModels as MockAccountModels).mock.verify();
 
        }
 
        public function testUserNotLoggedIn():void {
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property("userIsLoggedIn").returns(false);
 
            try {
 
                _commandToTest.execute(_fakeEvent);
 
            } catch (error:AssertionError) {
 
                // expected
                return;
 
            }
 
            fail('An Assertion Failed Error should have been thrown');
 
        }
 
        /**
         * Testing utility function for executing the command with invalid data, execute should
         * throw an exception
         *
         * @param invalidFakeEvent  a fake event with invalid data
         *
         */
        private function invalidDataTester(invalidFakeEvent:UpdatePreferencesEvent):void {
 
            try {
 
                _commandToTest.execute(invalidFakeEvent);
 
            } catch(e:Error) {
 
                if(e.message.indexOf("No Expectation set for updatePreferences with args") != -1) {
 
                    fail("updatePreferences should not be called with invalid data");
 
                } else {
                    // It was expected to fail
                    return;
                }
 
            }
 
            fail("An exception was expected, the event was not properly populated");
 
        }
 
        public function testExecuteNullDataBoth():void {
 
            invalidDataTester(new UpdatePreferencesEvent(null,null));            
 
        }
 
        public function testExecuteNullDataUser():void {
 
            invalidDataTester(new UpdatePreferencesEvent(_fakePreferences,null));            
 
        }
 
        public function testExecuteNullDataPreferences():void {
 
            invalidDataTester(new UpdatePreferencesEvent(null, _fakeUser));            
 
        }
 
        public function testExecute():void {
 
            //Configure a mock delegate and assign expectations.  We are expecting the Delegate's
            //updatePreferences method to be called only once with the correct arguments
            (_mockAccountDelegate as MockAccountDelegate).mock.method(
                    "updatePreferences").once.withArgs(
                        Number,
                        UserPreferences);
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property("userIsLoggedIn").returns(true);
 
            // The mock will throw an exception, if it's "updatePreferences" method isn't called
            // exactly once with a Number and UserPreferences type arguments.
            _commandToTest.execute(_fakeEvent);
 
        }
 
        public function testOnResult():void {
 
            var accountModel:IAccountModels = ModelLocator.getInstance().accountModels;
            (accountModel as MockAccountModels).mock.property(
                "preferences").withArgs(UserPreferences);
 
            var stubbedResultEvent:ResultEvent = new ResultEvent(
                "result",
                false,
                true,
                _fakePreferences);
            _commandToTest.result(stubbedResultEvent);
 
        }
 
    }
 
}

There is no injection done in the test itself. The test assumes mocks have been injected into Command and we can set up expectations for how the command should interact with these mocks in different contexts.

This Flex Project includes all of the sample code run in a FlexUnit test suite.

Effective View Development October 7, 2008

Posted by Allen Manning in : Flex, Refactoring, Unit Testing , 2comments

In a previous post, we reviewed how Commands are uniquely complex objects because of the number and diversity of collaborators.

Like Commands, Views can grow to be very complex.  We have explored this in the context of delegating to Specialist Classes.  They can be complex and difficult to maintain because it is easy for them to become monolithic.  It is easy for us to not follow good methodologies and make them into unwieldy Autonomous Views.  It is easy for us to ignore unit testing of them because of the intricacies of dealing with their asynchronous nature.

The declarative nature of MXML is a powerful means of describing Views.  Interweaving scripting with the MXML syntax can easily be another area of complexity and confusion.  A common question is- what is running when?  How can I follow the flow of control in this application?

In light of all of these reasons, View objects tend to be the most unmanageable of the entire application.  We need to change that.  We need to re-double our efforts in keeping our Views clean and working well.  We are going to walk through a number of rules of thumb in how to accomplish this goal.

Avoid Monolithic Autonomous Views

Paul Williams has written a great series of articles on View patterns.  In it he does an excellent job warning of the dangers of Autonomous Views.  In particular in how they are difficult to Unit Test.  Paul’s investment in blogging on this topic is a testament to the importance of getting this right.

Big, monolithic Autonomous views are the natural result of a design that hasn’t had much refactoring or thought put into it.  Basically, you just start with a Flex component and keep organically growing it by tacking code here and code there.  Following this approach, we find ourselves trying to maintain a beast of a Flex application.

The good news is, that it is relatively straight-forward to refactor Autonomous views into collaborations of smaller specialist objects:  Extract component refactorings.

Keep Views under 300 lines by Extract Component refactorings

Assuming that there is an MVC framework in place, we shouldn’t be concerned about extract Model and Controller responsibilities out.  Even with Model and Controller objects being extracted, there will still be plenty of complexity in the Views themselves.

As a general rule of thumb, keeping our Views to under 300 lines helps guide us into other important refactorings.  All Views can be broken down to this level.  There are two main extraction refactorings to mention:

Extract to Component

If the View contains any containers or controls, create a custom component out of these elements and extract logic into there.  This should be a very regular practice in View development.  As soon as we start seeing logic forming around a particular element, event listeners and initializes for example, we should be quick to move that logic into the space of its own component.

Below is an example of a simple example application before and after an Extract Component refactoring.  Although the example application is small enough that it doesn’t beg for a refactoring now.  It isn’t difficult to see how this application grown over time can become unwieldy.  All of the event listeners are included in the same script block, and there isn’t a very clear abstracting for this yet-to-be overly complex Autonomous View.

AutonomousView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Extract Component refactoring.  This 
    represents the early days of a yet-to-be unwieldy and monolithic Autonomous View.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
 
        import mx.events.ListEvent;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
 
        /** Widgets to edit - stubbed for now */
        [Bindable]
        private var _widgetsToEdit:ArrayCollection = new ArrayCollection([
            {name:"a"},
            {name:"b"},
            {name:"c"},
            {name:"d"},
            {name:"e"},]);
 
        /** "datagrid change" event listener for this view */
        private function onDataGridChange(event:ListEvent):void {
 
            Alert.show("the datagrid selection has changed");
 
        }
 
        /** "Edit Click" event listener for this view */
        private function onEditClick(event:Event):void {
 
            Alert.show("the edit button has been clicked");
 
        }
 
        /** "Delete Click" event listener for this view */
        private function onDeleteClick(event:Event):void {
 
            Alert.show("the delete button has been clicked");
 
        }
 
    ]]>
    </mx:Script>
 
    <mx:Panel title="Extract Component Refactoring - Autonomous View">
 
        <mx:HDividedBox>
 
            <mx:DataGrid id="dataGrid"
                width="100%" height="100%" 
                change="onDataGridChange(event)" 
                dataProvider="{_widgetsToEdit}"/>
 
            <mx:TextArea 
                height="100%" width="100%" 
                text="{dataGrid.selectedItem.name}"/>
 
        </mx:HDividedBox>
 
        <mx:ControlBar>
            <mx:Button label="Edit" 
                click="onEditClick(event)"/>
            <mx:Button label="Delete" 
                click="onDeleteClick(event)"/>
        </mx:ControlBar>
 
    </mx:Panel>
 
</mx:Application>

The extract to component refactoring will extract the DataGrid and the Control bar into custom components and bring along with them their local models and event listeners.

ExtractComponentView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Extract Component refactoring.  This 
    represents the early days of a yet-to-be unwieldy and monolithic Autonomous View.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
 
    <mx:Panel title="Extract Component Refactoring - Autonomous View">
 
        <mx:HDividedBox>
 
            <local:WidgetDataGrid id="dataGrid"/>
 
            <mx:TextArea 
                height="100%" width="100%" 
                text="{dataGrid.selectedItem.name}"/>
 
        </mx:HDividedBox>
 
        <local:WidgetControlBar/>
 
    </mx:Panel>
 
</mx:Application>

WidgetDataGrid.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    A DataGrid for browsing widgets.
 
    @author amanning
 
-->
<mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"
    width="100%" height="100%"
    dataProvider="{_widgetsToEdit}"
    change="onDataGridChange(event)">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
        import mx.events.ListEvent;
 
        /** Widgets to edit - stubbed for now */
        [Bindable]
        private var _widgetsToEdit:ArrayCollection = new ArrayCollection([
            {name:"a"},
            {name:"b"},
            {name:"c"},
            {name:"d"},
            {name:"e"}]);
 
        /** "datagrid change" event listener for this view */
        private function onDataGridChange(event:ListEvent):void {
 
            Alert.show("the datagrid selection has changed");
 
        }
 
    ]]>
    </mx:Script>
 
</mx:DataGrid>

WidgetControlBar.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="utf-8"?>
<mx:ControlBar xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
 
        import mx.events.ListEvent;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
 
        /** "Edit Click" event listener for this view */
        private function onEditClick(event:Event):void {
 
            Alert.show("the edit button has been clicked");
 
        }
 
        /** "Delete Click" event listener for this view */
        private function onDeleteClick(event:Event):void {
 
            Alert.show("the delete button has been clicked");
 
        }
 
    ]]>
    </mx:Script>
 
       <mx:Button label="Edit" 
                click="onEditClick(event)"/>
 
        <mx:Button label="Delete" 
                click="onDeleteClick(event)"/>
 
</mx:ControlBar>

Delegate by extracting to a “View Specialist”

Create specialized objects that are easily unit testable.  A good example of this is the Shared Date Formatter object which abstracts some complexity that could easily have been spread throughout many views.

Avoid Code Behind and View Helpers

Flex equivalents to ASP Code Behind may seem like an obvious step since it provides a very direct medium for moving scripting code out of MXML files and into ActionScript files.  It does provide a way to separate out the two types of languages, but its usefulness ends there.

Once again, Paul Williams has done a fantastic job in describing the relative merits of Code Behind and View Helpers.

There are a slew of issues with these approaches, but two main ones come to mind:

If the problem we are trying to solve is to reduce very large Script blocks, then the two Extract Component refactorings mentioned above are cleaner, more maintainable and easier to Unit Test.

Use Templates

Thanks to Peter Ent for discussing this approach- the concept is quite simple, but the emergent design built upon its common usage is extremely powerful.  Templates let us create an MXML View component with configurable containers and controls as children.  This allows us to abstract away the common styling and layout and only expose the changeable regions.

Let’s use the Extract Component refactoring above, and refactor it into a Template.  We will change our new, more manageable, Autonomous View into a Template View.  We perform an Extract Template refactoring of the Panel, and create a more generic ListBrowserTemplate.

ExtractTemplateView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Template refactoring.  We have abstracted
    the layout into a Template view.  This application only populates two configurable regions, the
    rest of the work is handled by the template.  The configurable regions are populated with 
    extracted components.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
 
    <local:ListBrowserTemplate>
 
        <local:browseRegion>
 
            <local:WidgetDataGrid id="dataGrid"/>
 
        </local:browseRegion>
 
        <local:detailRegion>
 
            <mx:TextArea 
                    height="100%" width="100%" 
                    text="{dataGrid.selectedItem.name}"/>
 
        </local:detailRegion>
 
    </local:ListBrowserTemplate>
 
</mx:Application>

ListBrowserTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    A Panel Template for managing items in a list and viewing details of items selected.
 
    @author amanning
 
-->
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:local="*" 
        width="100%" height="100%"
        creationComplete="onCreationComplete()">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import mx.core.UIComponent;
        import mx.core.Container;
        //------------------------------------------------------------------------------------------
        // Properties
        //------------------------------------------------------------------------------------------
        /** One or more child elements for the browsable region- dynamically added to the view */
        private var _browseRegionComponents:Array = new Array();
 
        /** One or more child elements for the details region- dynamically added to the view */
        private var _detailRegionComponents:Array = new Array();
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /** The "creation complete" event listener for this view- populate regions*/
        private function onCreationComplete():void {
 
            addComponentsToTemplate(browseRegionCanvas,_browseRegionComponents);
            addComponentsToTemplate(detailRegionCanvas,_detailRegionComponents);
 
        }  
 
        /** 
         * Setter for the the write-only <code>_browseRegionComponents</code> property - set the 
         * list of children components to a local property so that during creation complete they can 
         * be added to the view.
         * 
         * @param componentList   the list of one or more children to add to this view, these
         *                        children represent the configurable region of this template
         */
        public function set browseRegion(componentList:Array):void {
 
            _browseRegionComponents = componentList;
 
        }
 
        /** 
         * Setter for the the write-only <code>_detailRegionComponents</code> property - set the 
         * list of children components to a local property so that during creation complete they can 
         * be added to the view.
         * 
         * @param componentList   the list of one or more children to add to this view, these
         *                        children represent the configurable region of this template
         */
        public function set detailRegion(componentList:Array):void {
 
            _detailRegionComponents = componentList;
 
        }
 
        /**
         * Given a list of UIObjects (the configurable region of this view) manually add them to 
         * this view.
         * 
         * Note:  This should be called during the creation complete phase of this components 
         * lifecycle
         * 
         * @param container     the container to dynamically add the children to
         * @param components    the components to add to the container
         */
        public function addComponentsToTemplate(container:Container, components:Array):void {
 
            container.removeAllChildren();
            for each(var subcomponent:UIComponent in components) {
 
                container.addChild(subcomponent);
 
            }
 
        }
 
    ]]>
    </mx:Script>
 
 
 
    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <mx:DividedBox width="100%" height="100%" direction="horizontal">
        <mx:Canvas id="browseRegionCanvas" width="100%" height="100%"/>
        <mx:Canvas id="detailRegionCanvas" width="100%" height="100%"/>
    </mx:DividedBox>
 
    <local:WidgetControlBar/>
 
</mx:Panel>

These are simple examples, so the benefit may not be directly apparent. As the complexity grows, we will see that these refactorings give us modular and manageable Views. We now have room to breathe- Monolithic Autonomous Views, problematic View Helpers, and buggy Code Behind Base classes are a thing of the past.