Abstract Controllers

July 11, 2013

In a typical real world application there might be quite a number of Views and Controllers similar to each other but doing different things, e.g., operating on different data sets. It is inefficient to repeat the same code over and over in different classes, and some code sharing technique is required. For Views, we can easily abstract most of the code in a base View class and add the required configuration in subclasses:

Ext.define('MyApp.view.AbstractPanel', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.mypanel',
    
    bbar: [{
        xtype: 'button',
        itemId: 'okButton',
        text: 'OK'
    }, {
        xtype: 'button',
        itemId: 'cancelButton',
        text: 'Cancel'
    }],
    
    ...
});

Ext.define('MyApp.view.FooPanel', {
    extend: 'MyApp.view.AbstractPanel',
    alias: 'widget.foopanel',
    
    // We are overriding buttons in this view
    bbar: [{
        xtype: 'button',
        itemId: 'yesButton',
        text: 'Yes'
    }, {
        xtype: 'button',
        itemId: 'noButton',
        text: 'No'
    }],
    
    ...
});

Ext.define('MyApp.view.BarPanel', {
    extend: 'MyApp.view.AbstractPanel',
    alias: 'widget.barpanel',
    
    // This view has the same buttons as base class,
    // and also an informational side bar, so we need to
    // accommodate for it
    dockedItems: [{
        xtype: 'panel',
        itemId: 'sidebar'
    }],
    
    ...
});

What about Controllers then? In fact, nothing prevents us from taking the same approach, albeit we will have to do it in slightly less automated way:

Ext.define('MyApp.controller.AbstractController', {
    extend: 'Ext.app.Controller',
    
    // Matches any MyApp.view.AbstractPanel descendant by xtype
    viewSelector: 'mypanel',
    okSelector: 'button#okButton',
    cancelSelector: 'button#cancelButton',
    
    constructor: function(config) {
        Ext.apply(this, config);
        
        var tplRefs = [{
                ref: 'view',
                selector: this.viewSelector
            }, {
                ref: 'okButton',
                selector: this.viewSelector + ' > ' + this.okSelector
            }, {
                ref: 'cancelButton',
                selector: this.viewSelector + ' > ' + this.cancelSelector
            }];
        
        this.refs = this.refs ? Ext.merge(tplRefs, this.refs) : tplRefs;
        
        this.callParent(arguments);
    },
    
    ...
});

Ext.define('MyApp.controller.Foo', {
    extend: 'MyApp.controller.AbstractController',
    
    // Now we only have to provide selectors, and voila -
    // the refs will be created automagically
    viewSelector: 'foopanel',
    okSelector: 'button#yesButton',
    cancelSelector: 'button#noButton',
    
    ...
});

Ext.define('MyApp.controller.Bar', {
    extend: 'MyApp.controller.AbstractController',
    
    // Buttons are the same so we only provide view selector
    viewSelector: 'barpanel',
    
    // We can add some refs that are unique to this View -
    // in our case, it is the sidebar. The refs should be as
    // precise as it is feasible, to avoid collisions with
    // other Views.
    refs: [
        { ref: 'sidebar', selector: 'barpanel > panel#sidebar' }
    ],
    
    ...
});

In the above example, we have abstracted a lot of duplicating functionality in our base Controller, greatly simplifying children Controller classes. A similar approach can be used with `control` component selectors and `listen` event domain selectors, respectively.

tags: , , ,
posted in Software development by nohuhu

Follow comments via the RSS Feed | Leave a comment | Trackback URL

2 Comments to "Abstract Controllers"

  1. Les wrote:

    The problem with the Abstract/Base controller approach is that you end up with a long list controllers that you need to specify in the Application.

    I take a different approach. Instead of creating an Abstract controller, I have a regular controller and then store all controlling logic into multiple mixins that the controller uses.

    This way I have fewer controllers and the code is still logically divided.

    Here’s an example of such a controller:

    Ext.define(‘Risk.controller.center.drilldown.DrilldownController’, {
    extend: ‘Ext.app.Controller’,
    mixins: {
    drilldownglobal: ‘Risk.controller.center.drilldown.mixin.DrilldownGlobal’,
    drilldownpanel: ‘Risk.controller.center.drilldown.mixin.DrilldownPanel’,
    drilldownstore: ‘Risk.controller.center.drilldown.mixin.DrilldownStore’
    },
    init: function (application) {
    application.initMixins(this);
    this.queryCache = {};
    }
    });

    initMixins is located in the application.js file:

    initMixins: function (controller) {
    Ext.Object.each(controller.mixins, function (key, mixin) {
    if (mixin.init) {
    mixin.init(controller);
    }
    });
    },

  2. Les wrote:

    Here’s an example of a mixin used by the controller:

    /*jslint browser: true, vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, todo: true, sloppy: true, unparam: true */
    /*global Ext, window, Risk */
    Ext.define(‘Risk.controller.center.measure.mixin.MeasureMenu’, {
    uses: [
    ‘Risk.view.center.measure.MeasureMenu’
    ],
    init: function (controller) {
    this.addRefs(controller);
    this.addListeners(controller);
    },
    addRefs: function (controller) {
    controller.addRef([{
    ref: ‘measureMenu’,
    xtype: ‘measuremenu’,
    autoCreate: true
    }, {
    ref: ‘guarantyfundsdropDown’,
    selector: ‘guarantyfundsdropDown’
    }]);
    },
    addListeners: function (controller) {
    var me = this;
    controller.listen({
    component: {
    ‘measurepanel>header’: {
    click: me.onHeaderClick
    },
    ‘measuremenu’: {
    click: me.onMenuClick
    }
    }
    });
    },
    onHeaderClick: function (header, evt) {
    var panels = Ext.ComponentQuery.query(‘measurepanel’),
    items = Risk.view.center.measure.MeasureMenu.prototype.items,
    guarantyFundDropdown = this.getGuarantyfundsdropDown();
    if (panels.length < items.length) {
    this.getMeasureMenu().showMenu(header.ownerCt, guarantyFundDropdown);
    }
    },
    onMenuClick: function (menu, item, evt) {
    var panel = menu.selectedPanel,
    container = panel.ownerCt,
    index = panel.getPanelIndex() + 1,
    text = item.text,
    title = text + ' &#9660', // arrow down
    config = {
    xtype: 'measurepanel',
    titleText: text,
    title: title
    };
    Ext.copyTo(config, item.initialConfig, 'itemId,measureId,toleranceId');
    container.addSplitter(index);
    container.addPanel(index + 1, config);
    container.scrollTargetEl();
    }
    });

Leave Your Comment

 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org