Saturday, October 15, 2011

Dynamically building and destroying Sencha Touch components

This item targets Sencha Touch 1.1 - with Sencha Touch 2.0 on the horizon, it will be interesting to see if any of this might be better addressed.

Following some of the advice on optimising a Sencha Touch application, I decided it would be useful to build components on demand and handle their lifecycle in a centralized fashion. Further, encapsulate this behaviour in a controller base type, and retain references to dynamically constructed components statically, so all controller sub types can deal with components consistently. (As should be apparent, this example code is culled from an MVC implementation).

The implementation in its entirety follows, then a brief segmented explanation of the salient points.

1:  m.sencha.controllers.BaseController = Ext.extend(Ext.Controller, {  
2:    /*  
3:     * options.viewportProperty, options.buildFunction, options.name,   
4:     * options.callback, options.maskText  
5:    */  
6:    buildOnDemand: function(options) {  
7:      var requiresBuild = !Ext.isDefined(options.viewportProperty) ||   
8:                options.viewportProperty.isDestroyed;  
9:      if (!requiresBuild) {  
10:        this.activateBuiltComponent(options);  
11:      }  
12:      else {  
13:        var enclosingController = this;  
14:        m.ui.platformFactory.showMask(  
15:           Ext.isDefined(options.maskText) ? options.maskText : 'Loading...',  
16:           function(mask) {  
17:              var compConfig = options.buildFunction();  
18:              options.viewportProperty = compConfig[options.name];  
19:              m.sencha.views.viewport.applyAndAdd(compConfig, options.name);  
20:              mask.hide();  
21:              m.sencha.controllers.BaseController.  
22:                demandBuiltComponents[options.name] = compConfig[options.name];  
23:              enclosingController.activateBuiltComponent(options);  
24:        },  
25:        m.config.shortMaskDelay  
26:       );  
27:      }  
28:    },  
29:    activateBuiltComponent: function(options) {  
30:      m.sencha.views.viewport.setActiveItem(options.viewportProperty);  
31:      if (options.callback)   
32:        options.callback(options.viewportProperty);  
33:    },  
34:    cleanup: function() {  
35:     for(var compName in m.sencha.controllers.BaseController.demandBuiltComponents)   
36:        this.destroyAsNecessary(compName);  
37:    },  
38:    destroyAsNecessary: function(name) {  
39:     var target =   
40:       m.sencha.controllers.BaseController.demandBuiltComponents[name];  
41:     if (Ext.isDefined(target) &&   
42:       (!Ext.isFunction(target.isDestroyed) || !target.isDestroyed))   
43:        m.sencha.views.viewport.remove(target, true);  
44:     m.sencha.controllers.BaseController.demandBuiltComponents[name] = null;    
45:    },  
46:    constructor: function() {  
47:      m.sencha.controllers.BaseController.superclass.constructor.apply(this, arguments);    
48:    }  
49:  }); 
50:  m.sencha.controllers.BaseController.demandBuiltComponents = {};  

To  the important aspects of the implementation:

1:  m.sencha.controllers.BaseController = Ext.extend(Ext.Controller, {  
2:    /*  
3:     * options.viewportProperty, options.buildFunction, options.name,   
4:     * options.callback, options.maskText  
5:    */  
6:    buildOnDemand: function(options) {  
7:      var requiresBuild = !Ext.isDefined(options.viewportProperty) ||   
8:                options.viewportProperty.isDestroyed;  
9:      if (!requiresBuild) {  
10:        this.activateBuiltComponent(options);  
11:      }  
12:      else {  
13:        var enclosingController = this;  
14:        m.ui.platformFactory.showMask(  
15:           Ext.isDefined(options.maskText) ? options.maskText : 'Loading...',  
16:           function(mask) {  
17:              var compConfig = options.buildFunction();  
18:              options.viewportProperty = compConfig[options.name];  
19:              m.sencha.views.viewport.applyAndAdd(compConfig, options.name);  
20:              mask.hide();  
21:              m.sencha.controllers.BaseController.  
22:                demandBuiltComponents[options.name] = compConfig[options.name];  
23:              enclosingController.activateBuiltComponent(options);  
24:        },  
25:        m.config.shortMaskDelay  
26:       );  
27:      }  
28:    },    

  • Line 6 declares the build function, intended to be used by clients to request an object is constructed and managed by the controller statically. An 'options' object is expected, with the options as noted in the comment.
  • Lines 7-9: If the component exists and is not marked as destroyed,  just activate it
  • Lines 13-25: Create a 'closure' style reference to the executing controller, and invoke an Ext deferred task to show a mask while the component is built (this is performed using a local 'platform' object). A deferred task is used as failing to 'yield' for a short period of time may cause the loading mask to not show at all.
  • Lines 17-20: Execute the client supplied build function to create a new instance of the desired component, and have the view port (a panel with a card layout) add the component to itself. Hide the mask.
  • Lines 21-22: Record the component in a static object shared by all controller sub types
  • Line 23: Activate the component 
 
29:    activateBuiltComponent: function(options) {  
30:      m.sencha.views.viewport.setActiveItem(options.viewportProperty);  
31:      if (options.callback)   
32:        options.callback(options.viewportProperty);  
33:    },    
  • Line 30: Have the view port activate the just built or pre-existing component
  • Lines 31-32: If the options supplied include a callback function to be executed after component activation, call it now
Now some general cleanup and management behaviour. Line 34 (following) declares a sweeping cleanup function, line 38 a function to destroy a named component. Note that we examine the object to see if the isDestroyed property is defined by the component instance - this property is dynamic, in that it does not exist for a component unless it has been destroyed.

34:    cleanup: function() {  
35:     for(var compName in m.sencha.controllers.BaseController.demandBuiltComponents)   
36:        this.destroyAsNecessary(compName);  
37:    },  
38:    destroyAsNecessary: function(name) {  
39:     var target =   
40:       m.sencha.controllers.BaseController.demandBuiltComponents[name];  
41:     if (Ext.isDefined(target) &&   
42:       (!Ext.isFunction(target.isDestroyed) || !target.isDestroyed))   
43:        m.sencha.views.viewport.remove(target, true);  
44:     m.sencha.controllers.BaseController.demandBuiltComponents[name] = null;    
45:    },  
46:    constructor: function() {  
47:      m.sencha.controllers.BaseController.superclass.constructor.apply(this, arguments);    
48:    }  
49:  }); 
50:  m.sencha.controllers.BaseController.demandBuiltComponents = {};  

Moderately tidy and (ironically enough) in need of some optimisation itself.

No comments: