Decouple Controller and View in Sencha MVC

Decouple Controller and View in Sencha MVC

The MVC Architecture for Ext JS and Touch provides a solid foundation for building large and scalable apps; but if you follow the guides and implement in the way described you’ll notice that controllers and views are highly coupled. Controllers associate listeners to view events using component queries, thus making them aware of how the view is composed. As result we have a MVC architecture where the view is very light and does nothing but define components, and controllers will do all the heavy lifting by responding to user gestures, updating the view, dealing with models, business logic and more.

The current MVC

Currently:

  • the view might contain several components;
  • each one of these components fire events;
  • the controller, using component query, will listen to these events;
  • once the event is fired, the controller is notified.

This could be code translated by:

Ext.define('AM.controller.Users', {
    ...
    init: function() {
        this.control({
            'viewport > userlist': {
                itemdblclick: this.editUser
            },
            'useredit button[action=save]': {
                click: this.updateUser
            }
        });
    },
    ...
    updateUser: function(button) {
        console.log('clicked the Save button');
    }
    ...
});

The problem here is that the controller is fully aware of the structure of the view. It knows that inside the useredit view there’s a button with action=save.

This is a silly example, but I’ve seen worst cases where a controller class had 100 view references. Basically the whole controller was very associated with the view. Any changes on the view would result in refactoring the controller too.

DeftJS

Some months ago the guys from DeftJS wrote an awesome guest post at Sencha’s blog entitled Deft JS: Loosely Coupled MVC through Dependency Injection. They notice the same problematic exposed above, and figured it out that using Inversion-of-Control implemented by Dependency Injection would be the way to solve this.

The post intro was very interesting:

That application you just deployed? As experienced software developers, we all know it won’t be long before you’re going to need make to significant UI changes. Regardless of the amount of painstaking forethought, consensus gathering and planning backing it, no software design ever survives first contact with its users unscathed. To deliver truly effective software, we have to be prepared to adapt to an evolving understating of our users’ needs.

So… how do we architect our software, so we can rapidly implement UI changes without breaking the underlying business logic?

You can read more details on the blog post or their website, but basically there’s something called ViewController, which is a controller scoped to a particular view other than the full application. As a result, all your component queries can be simplified.

Ext.define( 'ContactsApp.controller.ContactGridViewController',
    extend: 'Deft.mvc.ViewController',
    mixins: [ 'Deft.mixin.Injectable' ],
    inject: [ 'contactStore' ],
 
    config: {
        contactStore: null
    },
 
    control: {
        contactsGrid: {
            click: 'onContactsGridClick'
        }
        editButton: {
            click: 'onEditButtonClick'
        }
    },
    ...
    destroy: function() {
       if (this.hasUnsavedChanges) {
           // cancel destruction
           return false;
       }
       // allow destruction
       return this.callParent( arguments );
    },
    ...
    onEditButtonClick: function () {
        this.getEditButton.setDisabled( false );
    },
 
    onContactsGridClick: function () {
        // add a ContactEditorView to the TabPanel for the selected item
        ...
    },
);

Control on Containers

Surprisingly, Sencha Touch not only can use control queries inside Ext.app.Controller but also use control for Ext.Container. So we can move all component queries from Controller to View.

Ext.create('Ext.Container', {
    control: {
       'button': {
           tap: 'hideMe'
       }
    },

    hideMe: function() {
        this.hide()
    }
});

Why would you do that? Well, follow the code below:

/*
 * Create order form view
 */
Ext.create('App.view.OrderForm', {
    extend: 'Ext.form.Panel',
    xtype: 'orderform',

    control: {
        'button[action=submit]': {
            tap: 'onBtnSubmitTap'
        }
    },

    //...
    
    /**
     * Get the values from the form, add the id's selected
     * from the categories list, and fire the save event
     */
    onBtnSubmitTap: function(btn) {
        var values = this.getValues(),
            selectedCategories = this.down('#list-categories').getSelection(),
            categories = [];

        Ext.each(selectedCategories, function(category) {
            categories.push(category.getId());
        });

        values.categories = categories;
        this.fireEvent('save', this, values);
        }
    });


/**
 * Order Controller
 */
Ext.create('App.view.OrderController', {
    extend: 'Ext.app.Controller',
    control: {
        'orderform': {
            save: 'onOrderFormSave'
        }
    },

    /**
     * The controller does not need to know how the view is structred.
     * All it needs is the values, nothing else.
     */
    onOrderFormSave: function(formView, values) {
        Ext.Ajax.request({
            url: 'orders/save',
            params: values
            // ...
        });
    }
});

Basically we are inverting the control as well. Instead of having the Controller accessing the internals of the view and pulling out the values, we have the view dealing with its own logic to get the values, and then passing them for the controller to execute the business logic.

This leads to two major improvements:

  • Changes in the view don’t affect controllers. If I change the way users select categories from a list to a radio button group, nothing changes in the controller.
  • Controllers are easily testable. In test specs we just need to mock the values and test if the controller method is responding correctly.

And all of this without using any extensions, just changing the way we approach our architecture. Well, this is not completely true for Ext JS. Unfortunately the event system in Ext JS does not allow delegation in an easy way like Touch. You’d have to inform an array of events on the bubbleEvents config option for each component, something not very feasible. Perhaps in the future releases.

There’s still room for improvements…

This post just exposes some different approaches to the default Sencha MVC. Whether you’re going to use the default MVC or choose an alternative approach like DeftJS, Container Control or something customized, is up to you.

I have some more ideas that I’ll try to exemplify in future posts. Meanwhile let me know your thoughts about the whole subject with your comments.

Further resources