PDA

View Full Version : Ext.ux.MsgBus Plugin by Saki



jsakalos
20 Sep 2009, 10:44 AM
Hi all,

I wrote (yet another) Message Bus plugin. See http://blog.extjs.eu/plugins/ext-ux-msgbus-plugin/ for details and let me know what do you think.

Cheers,
Saki

jsakalos
20 Sep 2009, 1:52 PM
I've just uploaded the live demo of the plugin at http://examples.extjs.eu/?ex=msgbus

Enjoy!

jmariani
15 Oct 2009, 5:33 PM
Hi. Have this code (a form button). The form has plugins: ['msgbus']. Once I click the button, the data is saved and then... nothing happens. Even the form.reset() doesn't executes. Any ideas?


buttons:[{
text: 'Grabar',
formBind: true,
iconCls: 'database_save',
scope: this,
// Function that fires when user clicks the button
handler:function(){
var thisForm = Ext.getCmp('frmAppMenu');
thisForm.getForm().submit({
method:'POST',
success:function(form, action){
form.publish('bdApp.db.recordInsert', 'appMenu');
form.reset();
},

Scorpie
15 Oct 2009, 10:27 PM
Hi. Have this code (a form button). The form has plugins: ['msgbus']. Once I click the button, the data is saved and then... nothing happens. Even the form.reset() doesn't executes. Any ideas?


buttons:[{
text: 'Grabar',
formBind: true,
iconCls: 'database_save',
scope: this,
// Function that fires when user clicks the button
handler:function(){
var thisForm = Ext.getCmp('frmAppMenu');
thisForm.getForm().submit({
method:'POST',
success:function(form, action){
form.publish('bdApp.db.recordInsert', 'appMenu');
form.reset();
},


Try to debug form. I think you need to have thisForm instead of form.

jsakalos
16 Oct 2009, 2:00 AM
Does the success callback execute at all? Set a breakpoint there.

jmariani
16 Oct 2009, 5:45 AM
Yes, it does execute.
Adding an explicit form variable (var thisForm...) does the trick, but I don't think is an elegant way to do it.


handler:function(){
var thisForm = Ext.getCmp('frmAppMenu');
thisForm.getForm().submit({
method:'POST',
success:function(form, action){
thisForm.publish('bdApp.db.recordInsert', 'appMenu');
form.reset();
},

jmariani
16 Oct 2009, 5:47 AM
I mean, how do I get the PanelForm containing the button without explicity getCmping it by name?

tonedeaf
16 Oct 2009, 7:59 AM
I mean, how do I get the PanelForm containing the button without explicity getCmping it by name?
Go the other route: Create a ref: for the button, run the button handler in the scope of the PanelForm and access the button from the PanelForm using a dot.
More details here: http://www.vinylfox.com/the-hottest-extjs-30-feature-youve-never-heard-about/

jmariani
16 Oct 2009, 9:36 AM
I'll try. Also, I'll try adding the plugin to the button directly instead of the form.

jsakalos
17 Oct 2009, 12:20 AM
Go the other route: Create a ref: for the button, run the button handler in the scope of the PanelForm and access the button from the PanelForm using a dot.
More details here: http://www.vinylfox.com/the-hottest-extjs-30-feature-youve-never-heard-about/


Yes, this is the way.

RonaldBrinkerink
13 Mar 2010, 4:49 AM
Hi,

@joe

I'm trying to get the msgbus added to all components and add initial subscriptions to the bus for each component.
This way a component can publish a message without having knowledge of the existence of other components. Other components might have subscriptions to these messages and execute them.



For a widgets base class with msgbus included would be:


initComponent:function()
this.plugins = this.plugins || []
this.plugins.push('msgbus');
...


this way you won't interfere with another userspace defined plugins


I'm thinking of overriding and not extending.


Ext.override(Ext.Container, {
constructor: function() {
this.plugins = this.plugins || []
this.plugins.push('msgbus');
Ext.Container.superclass.constructor.call(this);
},
listeners: {'render' : function() {
this.initSubscriptions();
}
/**
* initSubscriptions:demessagebusvoorzienvandeabonnementenopberichten
*/
initSubscriptions:
function() {
if (!this.subscriptions) {this.subscriptions=[]}
this.subscriptions.push({subject: this.getXType() + '.**', method: 'message'})
for (i = 0; i < this.subscriptions.length; i++) {
this.subscribe(this.subscriptions[i].subject, {fn:eval('this.' + this.subscriptions[i].method)})
}
}
});


I'm not sure if override will only add functions or will override funtions and plugins as well.
If not, should i create a base widget class extending Ext.Container and add the messagebus to it and also the initSubscriptions function?

Thanks, Ronald

jsakalos
13 Mar 2010, 5:36 AM
You call parent constructor w/o arguments. Take a look at this (http://blog.extjs.eu/patterns/file-patters/javascript-extension-file-pattern/) to see how the constructor override should look like.

Then, you add listeners object to prototype what means that all instances would share the same listeners object. The above link will show you the correct way too.

Note: I didn't analyze it further; these two have to be fixed first.

Scorpie
13 Mar 2010, 5:50 AM
@jsakalos:

Is it possible to add the plugin to an extended Observable class?

RonaldBrinkerink
13 Mar 2010, 7:45 AM
@jsakalos

This is what i've got so far but i'm getting an error in the renderall function of ext-all-debug :

items is undefined at:

var items = ct.items.items, i, c, len = items.length;

My code so far:



Ext.override(Ext.Container, {
// constructor

constructor:

function(config){

// parent constructor call pre-processing - configure listeners here
config = config || {};
config.listeners = config.listeners || {};
Ext.applyIf(config.listeners, {
//add listeners config here
'afterrender' : this.initSubscriptions
});
// call parent constructor
Ext.Container.superclass.constructor.call(


this, config);

},


// initComponent : de messagebus voorzien van de abonnementen op berichten
initComponent:


function() {

this.plugins = this.plugins || []
this.plugins.push('msgbus');
var config = {
plugins: this.plugins
}; // eo config object
// apply config
Ext.apply(


this, Ext.apply(this.initialConfig, config));

// call parent initComponent
Ext.Container.superclass.initComponent.apply(


this);

},
/**
*initSubscriptions:demessagebusvoorzienvandeabonnementenopberichten
*/
initSubscriptions:


function() {

if (!this.subscriptions) {this.subscriptions=[]}
this.subscriptions.push({subject: this.getXType() + '.**', method: 'message'})
for (i = 0; i < this.subscriptions.length; i++) {
this.subscribe(this.subscriptions[i].subject, {fn:eval('this.' + this.subscriptions[i].method)})
}
}
});



It looks like its caused by the initComponent, even when i have an empty function with just the superclass.iniComponent.apply(this).

What's wrong here?

The main question is still what the best approach would be. Overriding Ext.Container or Ext.Component, or Extending one of these classes and create one Widget base class containing the msgbus.

Thanks again.
Ronald

Animal
13 Mar 2010, 8:34 AM
Hi Ronald.

You are overriding Container's actual methods, but in them, you are calling the Container's superclass versions of the method.

So Container's constructor and initComponent do not get called.

What you need to do is use the createSequence method to inject your new method in before the Container method.

As a note, the line



this.subscribe(this.subscriptions[i].subject, {fn:eval('this.' + this.subscriptions[i].method)})


should, I think be



this.subscribe(this.subscriptions[i].subject, { fn: this[this.subscriptions[i].method] })


Access the property of "this" as named by the String referenced by this.subscriptions[i].method

jsakalos
13 Mar 2010, 9:29 AM
An option to createSequence would be to call Ext.Container.prototype.constructor instead of parent (haven't tested - off the top of my head).

Animal
13 Mar 2010, 9:36 AM
That won't work. At the time that statement will be called, Container.prototype.initComponent will be the function you are in so that would cause recursion until stack overflow.

jsakalos
13 Mar 2010, 9:41 AM
Right, wrote before thinking... :s

RonaldBrinkerink
13 Mar 2010, 10:48 AM
Hi Nigel,

My goal is to have a msgbus on all my components / widgets.

example:

listview widget throws messages like this.publish(subject : 'bob.list.selectionchange', message: {data: {city: 'haaksbergen', street: 'hengelosstraat', number: 10}})
content widget listens to messages based on a subject like '*.*.selectionchange' and reloads data using the message
google widget also listens to '*.*.selectionchange' and changes the center of the map
The messagebus hub should therefore be inserted into any component and the component should by default listen to all events targeted to its own namespace. Next it should listen to the mesages passed by the config.subscriptions array.

Overriding the constructor to insert the plugin and the default subscriptions sounds like the way to go but so far I'm having trouble getting it to work.

Could you help me out here and give an example on how to get the plugin and the default subscriptions in every container?

Thanks a lot.
Ronald

jsakalos
13 Mar 2010, 10:51 AM
Ronald, post your latest code. Overriding the constructor is one of the valid ways, we just need to find the correct code.

RonaldBrinkerink
13 Mar 2010, 11:00 AM
Ext.override(Ext.Container, {
// constructor
constructor: function(config){
// parent constructor call pre-processing - configure listeners here
config = config || {};
config.listeners = config.listeners || {};
Ext.applyIf(config.listeners, {
//add listeners config here
'afterrender' : function() {this.initSubscriptions}
});
// call parent constructor
Ext.Container.superclass.constructor.call(this, config);
},

// initComponent : de messagebus voorzien van de abonnementen op berichten
initComponent: function() {
this.plugins = this.plugins || []
this.plugins.push('msgbus');
var config = {
plugins: this.plugins
}; // eo config object
// apply config
Ext.apply(this, Ext.apply(this.initialConfig, config));
// call parent initComponent
Ext.Container.superclass.initComponent.call(this);
},
/**
* initSubscriptions : de messagebus voorzien van de abonnementen op berichten
*/
initSubscriptions: function() {
if (!this.subscriptions) {this.subscriptions=[]}
this.subscriptions.push({subject: this.getXType() + '.**', method: 'message'})
for (i = 0; i < this.subscriptions.length; i++) {
this.subscribe(this.subscriptions[i].subject, { fn: this[this.subscriptions[i].method] })
}
}
});

jsakalos
13 Mar 2010, 5:46 PM
That cannot work - see Animal's post. You're overriding Container constructor but you do not call the original constructor but superclass constructor. The same is true for initComponent.

Superclass call is good for extensions but not for overrides.

jsakalos
13 Mar 2010, 6:10 PM
Something like this could work (haven't tested though):

Ext.override(Ext.Container, {
constructor:Ext.Container.prototype.constructor.createInterceptor(function(config) {
// your code here
})
})

RonaldBrinkerink
14 Mar 2010, 3:33 AM
@jsakalos

Thank you. Getting closer now. :). this works like a charm for initComponent, the msgbus plugin is added.

The constructor however does not get the listener 'afterrender' added. It doesn't seem to be called at all.



Ext.override(Ext.Container, {
// constructor
constructor:Ext.Container.prototype.constructor.createInterceptor(function(config) {
// by default subscribe to the messagebus.
// Afterrender to make sure that the msgbus is available.
config = config || {};
config.listeners = config.listeners || {};
Ext.applyIf(config.listeners, {
'afterrender' : function(){this.initSubscriptions}
});
return true
}),
// initComponent.
initComponent:Ext.Container.prototype.initComponent.createInterceptor(function(){
// By default add the msgbus to any container
this.plugins = this.plugins || [];
this.plugins.push('msgbus');
return true
}),
/**
* initSubscriptions : de messagebus voorzien van de abonnementen op berichten
*/

initSubscriptions: function() {
if (!this.subscriptions) {this.subscriptions=[]}
this.subscriptions.push({subject: this.getXType() + '.**', method: 'message'})
for (i = 0; i < this.subscriptions.length; i++) {
this.subscribe(this.subscriptions[i].subject, {fn:eval('this.' + this.subscriptions[i].method)})
}
}
});


I've also tried to use createSequence


initComponent:Ext.Container.prototype.constructor.createSequence(function(config){
// Test createSequence
config = config || {}
config.test='test'
return config
}

The config doesn't get the test value so I presume somehow the function is not called before nor after the original constructor function. Any ideas here?

Thanks,
Ronald

jsakalos
14 Mar 2010, 4:07 AM
You can add it in initComponent but a bit different way:


this.on('afterrender', ...);


Anyway, overriding afterRender menthod the same way as initComponent seems more appropriate to me.

Animal
15 Mar 2010, 1:10 AM
If you are getting in this deep, avoid events (they might be suspended, or another listener my return false to them).

IMNSHO, events should only be used by application code. You are writing Component code.

Override the afterRender too if you need to inject some logic at that point.

jsakalos
15 Mar 2010, 6:49 AM
@Animal, +1 to overriding afterRender, I also think that events should be only used in user space.

RonaldBrinkerink
15 Mar 2010, 11:43 AM
Well so far so good. The code is working and the only question remaining is how to overcome the problem that Nigel and Josef are referring to on overriding the event. Is there a better way to make sure that initSubscriptions is called at every creation of a containar. I use the afterrender because at that time I'm sure that the messagebus plugin is there. So if it is better not to override the events, how to reach my goal her?

The working code:


/*!
* Bob Library 2.0
* Copyright(c) 2009-2010 Brein BV
* http://www.brein.nl/licentie
*/
/**
* @override Ext.container
* Toevoegen van de messagebus plugin
* Toevoegen van afterrender
* Toevoegen van de eigen standaard subscriptions op de messagebus
* Uitvoeren van de in de configuratie meegegeven messages
*/
Ext.override(Ext.Container, {
// initComponent
initComponent:Ext.Container.prototype.initComponent.createInterceptor(function(){
// By default add the msgbus to any container
this.plugins = this.plugins || [];
this.plugins.push('msgbus');
this.on('afterrender', function(){
// De subscriptions toevoegen aan de messagebus
this.initSubscriptions()
// De meegegeven messages van de configuratie uitvoeren
if(this.messages) {this.initMessages()}
})
return true
}),
/**
* @method: initSubscriptions
* De messagebus voorzien van de abonnementen op berichten
* Voorbeeld van toevoegen van subscriptions
* <code>
* ... todo: voorbeeld code uit de voorbeeld applicatie ...
* </code>
*/
initSubscriptions: function() {
if (!this.subscriptions) {this.subscriptions=[]}
this.subscriptions.push({subject: this.getXType() + '.**', method: 'message'})
for (i = 0; i < this.subscriptions.length; i++) {
this.subscribe(this.subscriptions[i].subject, { fn: this[this.subscriptions[i].method] })
}
},
/**
* @method: initMessages
* In de configuratie kunnen messages zijn meegegeven welke na de initialisatie dienen te worden utigevoerd.
* <code>
* ... todo: voorbeeld code uit de voorbeeld applicatie ...
* </code>
*/
initMessages: function() {
for (i=0;i<this.messages.length;i++) {
this.publish(this.messages[i].subject, this.messages[i].message)
}
},
/**
* @method : message
* Algemene methode welke wordt uitgevoerd als een bericht binnenkomt voor dit widget.
* Het subject bevat de namespace van de widget en de uit te voeren functie
* De message bevat inhoudelijke gegevens van het bericht.
*/
message: function(subject, message) {
/**
* Er is een bericht ontvangen gestuurd door een andere widget.
* Als de uit te voeren functie bestaat in dit widet dan ook uitvoeren met de message als bericht.
* Subject bevat: [klant].[widget].[methode].[classificatieschema].[docunid]
*/
channel = subject.split('.')
method = channel[2]
if(typeof(this[method])=="function") {this[method](subject, message)}
}
});

Animal
15 Mar 2010, 2:11 PM
Create your own afterRender method as a sequece (or interceptor if yu want to execute it after) of the native Container's afterRender.

See http://www.extjs.com/learn/Tutorial:Creating_new_UI_controls

RonaldBrinkerink
16 Mar 2010, 2:28 AM
Thank you Nigel,

it's all working with intercepting the render function and not in initComponent:




render: Ext.Container.prototype.render.createInterceptor(function(){


// De subscriptions toevoegen aan de messagebus
this.initSubscriptions();
// De meegegeven messages van d configuratie uitvoeren
if(this.messages) {this.initMessages()};
// retourneer true t.b.v. originele render
return true;
})



Create your own afterRender method as a sequece (or interceptor if yu want to execute it after) of the native Container's afterRender.

See http://www.extjs.com/learn/Tutorial:Creating_new_UI_controls

Thanks all for the help.

Ronald

jsakalos
16 Mar 2010, 3:50 AM
afterRender is called later than render, however, if it works with render it's fine. Very well done!

RonaldBrinkerink
18 Jun 2010, 4:50 AM
@saki,

I need a little diffferent logic in the subject filtering of the bus.
The subject contains this logic: 'tld.organization.application.portal.widget.event.database.schema.functionalkey.technicalkey'


i.e. nl.brein.website.home.news.archivedb.news message.$2010/4500.023._01234568901234567890
(functionalkey = $2010/4500.023)
The current getFilterRE will throw an error because the schema and the functionalkey values can contain any character. Is there an easy way to create a filter function getFilterRe() that will leave the wildcard method in tact? I was thinking of replacing the periods in the subject with a predefined string, build the re and the replace the predefined string back to the period bu I'm not sure if that would be the right approach.

Thanks you for your reply.

sumit.madan
14 Jul 2010, 2:00 AM
Saki,

I noticed that the messages are still fired when the component (subscribed to them) has been destroyed. I have to manually unsubscribe each message in the "beforedestroy" event of the component.

Can we centralize it and remove the firing of messages from MsgBus for components which have been destroyed?

Inside the subscribe() function, maybe change:

this.bus.on('message', this.subs[subject].fn, config.scope || this, config);to

this.mon(this.bus, 'message', this.subs[subject].fn, config.scope || this, config);

Not sure if this will work, will test and let you know.

jsakalos
14 Jul 2010, 2:27 AM
No, that won't work - this and this.bus are different variables. The way would be too hook at beforeDestroy method of the parent and unsubscribe the to-be-destroyed component therein.

sumit.madan
14 Jul 2010, 6:35 AM
Actually, it works! I verified this with the no. of registered events inside the this.bus object before and after the component is destroyed.

So, this line:

this.mon(this.bus, 'message', this.subs[subject].fn, config.scope || this, config);actually removes the 'message' event for the component from the global bus instance when the component gets destroyed.

The only thing that is not removed is the this.subs object inside the component (which you're destroying in the unsubscribe() function.) Since this.subs not an event, it has to be removed manually.

Component.mon() is indeed a very useful function!

sumit.madan
14 Jul 2010, 9:04 AM
Another implementation (without mon):


,init:function(cmp) {
this.cmp = cmp;
cmp.bus = this.getBus();
cmp.bus.addEvents('message');
cmp.subs = {};
this.applyConfig();
this.cmp.on({'beforedestroy': {scope:this.cmp, single:true, fn:function() {
this.unsubscribeAll();
}}});
} // eo function init

,unsubscribeAll:function() {
Ext.iterate(this.subs, function(key, value) {
this.unsubscribe(key);
}, this);
}