This is a kind of follow up to the last year’s “Improving Ext JS MVC Implementation” post. Since then I have joined Sencha and have been working on Ext JS team for several months. Bringing MVC improvements into Ext JS core made all kinds of sense, so here goes.
In any application that has more than one Controller, there is a question: how do these Controllers communicate? Before Ext JS 4.2, the answer was, by calling each other’s methods directly:
Ext.define('My.controller.Foo', { extend: 'Ext.app.Controller', onPanelButtonClick: function(button) { // Suppose that we need to open a window that is governed // by another Controller. To do this, we need to call that // Controller`s method: try { this.getController('Bar').openWindow(button.text); } catch (ex) { Ext.Error.raise('Application error: ' + 'Controller Bar is unavailable'); } }, ... }); Ext.define('My.controller.Bar', { extend: 'Ext.app.Controller', openWindow: function(text) { ... }, ... });
This approach is problematic and error prone: not only we have to do lots of unnecessary stuff (try/catch, error raising, saving stack trace, etc) but, what is more important, this error checking happens at run time, when misspelled Controller or method name will lead to our application throwing an exception that the user cannot do a thing about. Of course we can relieve this problem by unit testing our Controllers; however if one Controller calls methods in several other Controllers we would have to instantiate them all, which then in turn will bring their dependencies. This leads to unmanageable and untestable mess very quickly.
Starting with Ext JS 4.2, there is a solution to this problem: Controller events. The code above can be refactored this way:
Ext.define('My.controller.Foo', { extend: 'Ext.app.Controller', onPanelButtonClick: function(button) { // Instead of calling Bar method directly, we fire an event. // Controller Bar listens to that event and acts accordingly. this.fireEvent('barOpenWindow', button.text); }, ... }); Ext.define('My.controller.Bar', { extend: 'Ext.app.Controller', init: function() { this.listen({ // We are using Controller event domain here controller: { // This selector matches any originating Controller '*': { barOpenWindow: 'openWindow' } } }); }, openWindow: function(text) { ... }, ... });
This way we solve the problems discussed above, while achieving very clean logic separation between Controllers. When Controller Foo wants Controller Bar to open a window, it sends a signal which may or may not be honored, depending on Bar’s availability and other factors Foo does not know or care about. This approach also makes Controller unit testing a lot easier: in fact the only thing left to test besides refs and component selectors are controller event handlers; all other logic can be encapsulated in Controller and tested separately. See more on this in Unit testing MVC Controllers guide.
You can also find more information on Event domains in Ext.app.Controller.listen() method documentation.
Wei wrote:
Nice post.
I am very new to js. I just wonder how to get a return value from openWindow function via fireEvent function. Thanks in advance.
Link | April 22nd, 2014 at 7:38 PM
Razvan wrote:
@Wei,
Here it is: http://nohuhu.org/development/using-synchronous-bidirectional-communication-with-controllers/
Link | October 2nd, 2014 at 9:47 AM
arief wrote:
I’ve been folowing your code, but it didnt work. The error said ‘undefined is not a function ‘. What’s wrong ?
Ext.define(settings.namespace + ‘controller.CommonController’, {
extend: ‘Ext.app.Controller’,
init: function () {
this.listen({
controller: {
‘*’: {
checkMe : ‘checkAccess’
}
}
});
},
checkAccess: function (accessType, callback, panelOwner) {
var url = settings.baseUrl + ‘SecurityUsers/Change’
if (accessType == “viewdetail”) {
url = settings.baseUrl + ‘SecurityUsers/ViewDetail’
} else if (accessType == “viewlist”) {
settings.baseUrl + ‘SecurityUsers/ViewList’
}
var myMask = new Ext.LoadMask(panelOwner, { msg: “Checking Permission…” });
myMask.show();
Ext.Ajax.request({
url: url,
method: ‘get’,
success: function (response) {
var res = Ext.JSON.decode(response.responseText);
if (res.access) {
callback();
} else {
Ext.Msg.alert(“Server”, res.message);
if (res.isGuest) {
var logonWin = Ext.getCmp(settings.namespace + ‘view.LoginWindow’);
if (logonWin && logonWin.isHidden()) {
logonWin.show();
}
}
}
myMask.hide();
},
failure: function (response) {
myMask.hide();
}
});
}
});
//here is in other controller which fired the event
addclick: function (btn) {
var me = this;
function callback() {
me.formMode = “add”;
me.setFormMode(me.formMode);
}
this.fireEvent(‘checkMe’, ‘change’, callback, this.securityUsersPanel());
}
any clue?
Link | October 19th, 2014 at 9:17 PM
arief wrote:
Update!!
It works, thanks. It was just typo in this.securityUsersPanel(), should be this.getSecurityUsersPanel().
Thanks
Link | October 20th, 2014 at 1:26 AM
Deepak wrote:
Hi,
why am i not able to put a debug pointer in the event which is fired,
i event tested it putting “debugger;” keyword but it doesnt work.
Thanks
Link | February 22nd, 2015 at 12:28 AM