New Example – Ext.ux.MsgBus Demo
20. September 2009 – 23:48Hi there,
I’ve just uploaded demo of Ext.ux.MsgBus Plugin at http://examples.extjs.eu/?ex=msgbus.
Enjoy!
For good of all productive developers
Hi there,
I’ve just uploaded demo of Ext.ux.MsgBus Plugin at http://examples.extjs.eu/?ex=msgbus.
Enjoy!
Recently I’ve been looking for a better way of inter-component communication in Ext so I’ve read a couple of posts with Message Bus implementations, I’ve skimmed over OpenAjax Hub page but I haven’t found anything suitable for my needs.
The first approach was Simple Message Bus Example but that is more concept proof than a really usable way in production environment.
I like the concept of message subjects (topics) as dot separated sequence of tokens with the ability to subscribe to messages with specific subjects and with wildcard support.
For example, if a component would subscribe to the subject eu.extjs.desktop.** it would receive message with subject eu.extjs.desktop.wallpaper.set but it wouldn’t receive message eu.extjs.taskbar.hide.
So this plugin was born.
Ext.ux.MsgBus fits in any component that is descendant of Ext.util.Observable and it does not need any other changes/overrides. You would stick it only into the components that must participate in bus messaging. It adds subscribe and publish methods to the component.
Usage example:
var p = new Ext.Panel({ plugins:['msgbus'] ,onWallpaper:function(subject, message) { // do something } // the rest of config }); p.subscribe('eu.extjs.desktop.wallpaper.**', {fn:p.onWallpaper, single:true});
The above would call p.onWallpaper callback once upon the receipt of a “wallpaper” message.
Other example:
p.publish('eu.extjs.this.panel.move', {oldx:100, oldy:200, x:300, y:400});
I haven’t tested it fully yet so take it more as an initial idea than as a bullet-proof, worldwide-tested code. Also, I didn’t try in any means to implement OpenAjax standards and use this plugin for an inter library communication. Consider it as a one possibility of intra-Ext, inter-component communication.
Any comments and/or bug reports are welcome.
// vim: sw=4:ts=4:nu:nospell:fdc=4 /** * Message Bus Plugin * * @author Ing. Jozef Sakáloš * @copyright (c) 2009, by Ing. Jozef Sakáloš * @date 19. September 2009 * @version $Id: Ext.ux.MsgBus.js 29 2009-09-23 09:51:55Z jozo $ * * @license Ext.ux.MsgBus.js is licensed under the terms of the Open Source * LGPL 3.0 license. Commercial use is permitted to the extent that the * code/component(s) do NOT become part of another Open Source or Commercially * licensed development library or toolkit without explicit permission. * * License details: http://www.gnu.org/licenses/lgpl.html */ /*global Ext,window */ /** * @class Ext.ux.MsgBus * * Creates new Ext.ux.MsgBus object * @constructor * @param {Object} config The config object */ Ext.ux.MsgBus = function(config) { Ext.apply(this, config, { }); }; // eo constructor Ext.override(Ext.ux.MsgBus, { /** * @cfg {String} busName Name of the global Observable instance */ busName:'Ext.ux.Bus' /** * @private */ ,bus:false /** * Initializes the plugin and component * @private */ ,init:function(cmp) { this.cmp = cmp; cmp.bus = this.getBus(); cmp.bus.addEvents('message'); cmp.subs = {}; this.applyConfig(); } // eo function init // {{{ /** * Returns or creates the global Observable instance * @private */ ,getBus:function() { var bus = window; var a = this.busName.split('.'); var last = a.pop(); Ext.each(a, function(n) { if(!Ext.isObject(bus[n])) { bus = false; return false; } else { bus = bus[n]; } }, this); if(false === bus) { Ext.ns(this.busName); return this.getBus(); } if(!(bus[last] instanceof Ext.util.Observable)) { bus[last] = new Ext.util.Observable(); } return bus[last]; } // eo function getBus // }}} // {{{ /** * Creates RegExp for message filtering. * Override it if you need another logic. * @param {String} subject The message subject * @return {RegExp} RegExp used for message filtering */ ,getFilterRe:function(subject) { var a = subject.split('.'); var last = a.length - 1; a[last] = '**' === a[last] ? '.*' : a[last]; var re = /^\w+$/; Ext.each(a, function(token, i) { if(!re.test(token) && '*' !== token && '.*' !== token) { throw 'Invalid subject: ' + subject; } if('*' === token) { a[i] = '\\w+'; } }); return new RegExp('^' + a.join('\\.') + '$'); } // eo function getFilter // }}} // {{{ /** * Applies new methods to the component * @private */ ,applyConfig:function() { Ext.applyIf(this.cmp, { /** * Subscribes to messages (parent component method) * @param {String} subject Dotted notation subject with wildcards. * See http://www.openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Topic_Names * @param {Object} config Same as addListener config object * @return {Boolean} success true on success, false on failure (subscription exists) */ subscribe:function(subject, config) { var sub = this.subs[subject]; if(sub) { return false; } config = config || {}; config.filter = this.getFilterRe(subject); this.subs[subject] = {config:config, fn:this.filterMessage.createDelegate(this, [config], true)}; this.bus.on('message', this.subs[subject].fn, config.scope || this, config); return true; } /** * Unsubscribes from messages (parent component method) * @param {String} subject Dotted notation subject with wildcards. * @return {Boolean} success true on success, false on failure (nonexistent subscription) */ ,unsubscribe:function(subject) { var sub = this.subs[subject]; if(!sub) { return false; } this.bus.un('message', sub.fn, sub.scope || this, sub.config); delete this.subs[subject]; sub = null; return true; } // eo function unsubscribe /** * Publishes the message (parent component method) * @param {String} subject Message subject * @param {Mixed} message Message body, most likely an object */ ,publish:function(subject, message) { this.getFilterRe(subject); this.bus.fireEvent('message', subject, message); } // eo function publish /** * Returns current subscriptions * @return {Object} subscriptions */ ,getSubscriptions:function() { return this.subs; } // eo function /** * @private */ ,getFilterRe:this.getFilterRe /** * Filters incoming messages * @private */ ,filterMessage:function(subject, message, config) { if(config.filter.test(subject)) { (config.fn || this.onMessage).call(config.scope || this, subject, message); } } // eo function filterMessage /** * Default message processing function * @param {String} subject The message subject * @param {Mixed} message The message body */ ,onMessage:Ext.emptyFn }); } // eo function applyConfig // }}} }); // eo override // register ptype Ext.preg('msgbus', Ext.ux.MsgBus); // eof
Enjoy!
The accompanying example: Ext.ux.MsgBus
Hi all,
I’ve just uploaded very simple, extremely simple, example of how to implement Message Bus in Ext using the global Ext.util.Observable instance.
Enjoy!
See also: Ext.ux.MsgBus Plugin
I’ve run across the following code in some of Ext 3.x examples:
this.mon(toolsUl, 'click', this.onClick, this);
Looks like event handler installation but what it really does anyway?
So I delved into the code and remembered ExtJS Conference and now it is clear to me.
Imagine you create a component and then you need to install an event handler, for example, on its body. Something like:
var handler = function() { alert('You clicked my body'); }; var p = new Ext.Panel({ renderTo:Ext.getBody() ,title:'Panel with a listener on the body' }); p.body.on('click', handler);
Easy enough, we’ve done something like this many times. But, if the panel is ever to be destroyed we need to remove this listener ourselves as Ext knows nothing about it.
Something like:
var p = new Ext.Panel({ renderTo:Ext.getBody() ,title:'Panel with a listener on the body' ,beforeDestroy:function() { this.body.un('click', handler); } });
If we install our listener as inline function, such as:
p.on('click', function() {alert('You clicked my body')});
then it is impossible to remove this listener selectively, you would need to use
p.body.removeAllListeners();
to remove all body listeners.
Now, if we use mon (m stands for “managed”) this way
p.mon(p.body, 'click', handler);
then the listener is automatically removed by Ext on panel destroy. Nice, isn’t it? Saves a lot of our work and also handles our laziness or forgetfulness about destroying and cleanups of components.
Mind the first argument, that is the element to install the listener to. It can, but not necessarily have to be within the component. Also remember that when you destroy p Ext only removes listener from the element so if the element is outside of p you need to remove it yourself if it is not necessary anymore.
Should you ever need to remove the listener installed by mon you use mun with same arguments. You cannot selectively remove inline listeners.
mon and mun are defined in Component so you can use them from Component down the descendant classes chain.
Warning! mon and mun are not documented (yet?) so there is still (a little) risk that they will change or will be removed.
As I’ve already said, you cannot use the return value of an API function call as result is not know at the moment of the call return. It is never stressed enough that
Ext.Direct calls are ASYNCHRONOUS.
Therefore, all Ext.Direct created API functions, for example Example.Car.go() take one extra argument in addition to arguments you have specified in server-side class definition. That argument is callback function.
In our example, Example.Car.go() takes one argument, speed per server side method definition, however, it takes two in fact: Example.Car.go(speed, callback)
Let’s call it with a callback to test it. Type in the Firebug console:
Example.Car.go(80, console.info);
console.info is a function that prints its arguments in a human readable form. We use it as the callback so we see:
As you can see, the callback is called with two arguments:
response – it is that what is returned by the server-side Car.go() method. It can be string (as in our example), boolean, object, array or void (nothing).e – it is Ext.Direct.Event object or, being more specific, Ext.Direct.RemotingEvent.
The event e has some useful properties and methods, most important of them are:
status is true if the call was successful, false if the call failedresult contains same value as the first argument – it is server response datatype is rpc in our example but it also can be exception or others
action is the class namemethod is name of the method calledgetTransaction() is method to retrieve the transaction used in this server round trip. You would use it if you want more data on the transaction.
Let’s call go method again:
Example.Car.go(300, console.info);
The callback is called as before but the arguments differ:
response is undefined – no data from servere has type exception and has property message that contains error message textThat is almost all we need to know to start using Ext.Direct in an application. I’ve written a simple one so you have something to play with.
First, create file example.js with the following content:
/** * Mastering Ext.Direct example script * * @author Ing. Jozef Sakalos * @copyright (c) 2009, by Ing. Jozef Sakalos * @date 16. September 2009 * @version 1.0 * @revision $Id$ */ Ext.BLANK_IMAGE_URL='ext/resources/images/default/s.gif'; Ext.onReady(function() { /** * This function is called when Ext.Direct request * response arrives from the server. * * @param {String/Object/Array/Null} response * That what is returned by server method * @param {Ext.Direct.RemotingEvent/Ext.Direct.ExceptionEvent} e */ var callback = function(response, e) { // uncomment if you want to inspect arguments in Firebug Console // console.log(response, e); var status = '<b>Success</b>' var text = ''; // success handling - e.status is success flag: // true is success, false is failure if(true === e.status) { // response argument is same as e.result text = response; } // failure handling else { status = '<b><i>Failure</i></b>' // in the case of an exception, we don't have response but message text = e.message; } // grab the center body var body = win.items.itemAt(1).body; // display the response body.createChild({ tag:'div' ,cls:'response' ,html:status + ': ' + text }); // scroll down body.scrollTo('top', 100000, true); }; // create Ext.Direct test window var win = new Ext.Window({ title:'Mastering Ext.Direct by Saki' ,width:600 ,height:400 ,closable:false ,layout:'border' ,border:false ,items:[{ // west region with buttons region:'west' ,width:160 ,minSize:160 ,split:true ,defaults:{minWidth:120} ,layout:'table' ,bodyStyle:'padding:20px' ,layoutConfig:{columns:1, tableAttrs:{style:{width:'100%'}}} ,items:[{ xtype:'button' ,text:'Car.start()' // a delegate is needed if we want to pass arguments ,handler:Example.Car.start.createDelegate(null, [callback]) },{ xtype:'button' ,text:'Car.go(80)' // a delegate is needed if we want to pass arguments ,handler:Example.Car.go.createDelegate(null, [80, callback]) },{ xtype:'button' ,text:'Car.go(250)' // a delegate is needed if we want to pass arguments ,handler:Example.Car.go.createDelegate(null, [250, callback]) },{ xtype:'button' ,text:'Car.stop()' // a delegate is needed if we want to pass arguments ,handler:Example.Car.stop.createDelegate(null, [callback]) },{ xtype:'button' ,text:'Send All' // another option is inline function that executes calls ,handler:function() { // the following calls will be combined in one request Example.Car.start(callback); Example.Car.go(80, callback); Example.Car.go(250, callback); Example.Car.stop(callback); } }] },{ // responses are displayed here region:'center' ,autoScroll:true ,tbar:['->', { text:'Clear' ,handler:function(){win.items.itemAt(1).body.update('')} }] }] }); win.show(); }); // eof
Then tune your index.php so that it reads:
<?require_once("config.php");?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" type="text/css" href="ext/resources/css/ext-all.css"> <script type="text/javascript" src="ext/adapter/ext/ext-base.js"></script> <script type="text/javascript" src="ext/ext-all-debug.js"></script> <title id="page-title">Mastering Ext.Direct by Saki</title> <style type="text/css"> .x-table-layout-cell { height:40px; text-align:center; vertical-align:middle; } .x-table-layout-cell table { margin:auto } .response { padding:4px 0 4px 20px; font-size:13px; } </style> <script type="text/javascript" src="api.php"></script> <script type="text/javascript"> Ext.Direct.addProvider(Example.API); </script> <script type="text/javascript" src="example.js"></script> </head> <body> </body> </html>
Good! Play with Ext.Direct and let me know what you would like to have in next parts.
After completing part 1, we have the basic setup running. Although Ext.Direct PHP stack example classes are fine for demonstrating the functionality we will need our own PHP classes in the real life. I’m not going to explain basics of PHP object oriented programming, anyway, there is a couple of things to point out.
Let’s create (useless) class Car from part1. For that, create file classes/Car.php with the following content:
<?php // vim: sw=4:ts=4:fdc=4:nospell class Car { /** * @remotable */ public function start() { return "Started"; } // eo function start /** * @remotable */ public function go($speed) { return "The speed is $speed"; } // eo function go /** * @remotable */ public function stop() { return "Stopped"; } // eo function stop public function repair() { return "Done"; } // eo function repair } // eo class car // eof
Then edit api.php and add Car to $api->add() call so that it reads:
// ... $api->add( array( 'Echo' => array('prefix' => 'Class_'), 'Exception' => array('prefix' => 'Class_'), 'Time', 'File', 'Car' ) ); // ...
Reload index.php and type in Firebug console:
Example.Car.start();
You should get the following response:
{"type":"rpc","tid":2,"action":"Car","method":"start","result":"Started"}
Now type:
Example.Car.repair();
Error, yes? Why? Take a deeper look at Car source code. Some methods have comments
/**
* @remotable
*/
but repair does not have it. And that is the single reason why it is not exported to API so it is not known to the client application. You can verify it by typing:
Example.API.actions.Car;
in Firebug console.
If you want to know how is it done study ExtDirect_API PHP class in classes/API.php file and ReflectionClass documentation. In any case, it makes our life very easy as we only need to:
Can it be simpler?
Type in Firebug console:
console.log(Example.Car.start());
Now you see that the return value of the call is undefined despite of the fact that server side method returns string "Started". This leads us to a very important note:
Ext.Direct calls are ASYNCHRONOUS.
That means that the code execution does NOT wait until the call is finished but it immediately continues. So never try to use the return value, neither assume that you have data available at the line that follows the call.
The value returned from the server side method call is wrapped in the request response and it arrives when it arrives. It can take milliseconds or seconds depending on the connection speed, server execution time and other factors.
I will discuss response processing in the next part
Edit go method of our Car class so that it reads:
/** * @remotable */ public function go($speed) { if(0 >= $speed || 200 < $speed) { throw new Exception("Speed must be between 0 and 200"); } return "The speed is $speed $unit"; } // eo function go
and execute:
Example.Car.go(300);
You get the following response:
{"type":"exception","tid":3, "message":"Speed must be between than 0 and 200", "where": "#0 [internal function]: Car->go(300)\n #1 \/ddata1\/devel\/extjs.eu\/direct\/ExtDirect\/Router.php(176): call_user_func_array(Array, Array)\n #2 \/ddata1\/devel\/extjs.eu\/direct\/ExtDirect\/Router.php(62): ExtDirect_Router->rpc(Object(stdClass))\n #3 \/ddata1\/devel\/extjs.eu\/direct\/router.php(23): ExtDirect_Router->dispatch()\n #4 {main}"}
You see that the response type changed from rpc (remote procedure call) to exception and the response contains member message that is the text of the Exception we’ve thrown at server side. So far so good, however, where member contains debugging data that, while useful for debugging, shouldn’t be disclosed in a production environment.
You can freely throw exceptions while debugging your Ext.Direct application but you should consider another method(s) of handling errors for production systems. I will tackle it again later in this series.
Type the following:
Example.Car.start();Example.Car.go(80);Example.Car.stop();
The request is:
[ {"action":"Car","method":"start","data":null,"type":"rpc","tid":4}, {"action":"Car","method":"go","data":[80],"type":"rpc","tid":5}, {"action":"Car","method":"stop","data":null,"type":"rpc","tid":6} ]
and the response is:
[ {"type":"rpc","tid":4,"action":"Car","method":"start","result":"Started"}, {"type":"rpc","tid":5,"action":"Car","method":"go","result":"The speed is 80"}, {"type":"rpc","tid":6,"action":"Car","method":"stop","result":"Stopped"} ]
Ext.Direct combines requests that come within a configurable time frame into one to minimize the server round trips count.
Now we know a couple of basics:
My first idea how to the name this article was “Ext.Direct for Dummies” just because I feel as a one as long as Ext.Direct is concerned. I’ve first heard about it during Ext Conference in April and I think that it is one of the brightest ideas of Ext 3.x release. Nevertheless, I had no time do dig into into it and to understand the concepts fully.
Now I’ve decided to take a journey of discovering what is under hood, how to setup client and server side and how to use Ext.Direct effectively in applications. If you want, I invite you to travel with me.
It is for developers who are familiar (at least) with basic javascript and Ext object oriented programming, who are able to setup a web page and have it running from a http server and who can code in a server-side programming language. (I will use PHP in this article so PHP developers will have a slight advantage.)
Rich Internet Applications (RIA) consist of two parts: client side and server side. Client cannot call server functions directly but sends requests, server processes them calling the appropriate functions and returns results back to client.
Let’s say we have a server side class Car that has methods start, go and stop. From the client viewpoint, we need to ask server: Please, start the Car, then go with it and then stop it.
Now, imagine that we could directly call
Car.start(); Car.go(); Car.stop();
and these would call server side methods of server side class Car. Nice, isn’t it? You need to remember only one set of class names and their methods, code is neat and less bug prone.
And that is what Ext.Direct does. You export list of server side classes and their methods that should be made available for client to call and Ext.Direct takes care of the rest so that you can really use Car.start() in your code.
direct under your http server document root. Name does not matter in fact but I will use direct as the root for testing in this article.ext subdirectory under directdirectfirephp subdirectory under directAt this point, your direct directory listing should read the following:
+ cache/ + classes/ + data/ + ext/ + ExtDirect/ + firephp/ api.php router.php
We still need to do some work before we can see it running. First, create config.php file with the following content:
<? // vim: sw=4:ts=4:fdc=4:nospell // authentication would come here in the real world // switch it to false if you do not want to use FirePHP // $useFirePHP would be false also for a production system $useFirePHP = true; // require FirePHP files if($useFirePHP) { require_once("firephp/lib/FirePHPCore/FirePHP.class.php"); require_once("firephp/lib/FirePHPCore/fb.php"); } // define empty fb() function so code does not break // on any forgotten fb() calls later else { function fb() {}; } // eof ?>
We also need index.php so we have something to run:
<?require_once("config.php");?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" type="text/css" href="ext/resources/css/ext-all.css"> <script type="text/javascript" src="ext/adapter/ext/ext-base.js"></script> <script type="text/javascript" src="ext/ext-all-debug.js"></script> <title id="page-title">Mastering Ext.Direct by Saki</title> <script type="text/javascript" src="api.php"></script> <script type="text/javascript"> Ext.Direct.addProvider(Example.API); </script> </head> <body> </body> </html>
Ext.Direct Pack comes with example PHP classes (Echo, Exception, File and Time) and with api.php file. You can leave classes untouched for the moment but edit api.php to read the following:
<?php require_once("config.php"); session_start(); // Include ExtDirect PHP Helpers require_once('ExtDirect/API.php'); require_once('ExtDirect/CacheProvider.php'); // disable caching for development, enable for production //$cache = new ExtDirect_CacheProvider('cache/api_cache.txt'); $api = new ExtDirect_API(); $api->setRouterUrl('router.php'); // default // disable caching for development, enable for production //$api->setCacheProvider($cache); $api->setNamespace('Example'); $api->setDescriptor('Example.API'); $api->setDefaults(array( 'autoInclude' => true, 'basePath' => 'classes' )); $api->add( // these are example classes from Ext.Direct PHP stack array( // real class name is Class_Echo, therefore prefix 'Echo' => array('prefix' => 'Class_'), // real class name is Class_Exception, therefore prefix 'Exception' => array('prefix' => 'Class_'), 'Time', 'File' ) ); $api->output(); $_SESSION['ext-direct-state'] = $api->getState(); // eof ?>
So far, so good… Now you can navigate to http://yourserver/direct/index.php and if everything went right you will see the blank page and no errors in Firebug.
Open Firebug console and type the following:
Example.Time.get();
You can see that request is sent to server:
{"action":"Time","method":"get","data":null,"type":"rpc","tid":2}
and that response comes back:
{"type":"rpc","tid":2,"action":"Time","method":"get","result":"09-05-2009 19:46:38"}
You can try other classes and methods:
Example.Echo.send("Test to be echoed"); Example.File.list("."); Example.Time.get();
and you can also put fb($someVariable) statement in various places of php code if you want to know what’s going on here and there.
We have set up very minimum of Ext.Direct server and client side. The main purpose of this part is to get acquainted with basic components of this technology.
Hi all,
I’ve just posted new example on how to drag rows from grid to tree.
Enjoy!
Hi all,
I’ve just uploaded new example of how to invoke “Save As..” dialog when downloading files from the server.
Enjoy!
Hi,
I’ve uploaded example of keeping state of asynchronously loaded tree at http://examples.extjs.eu.
The core state-keeping code is written in the form of plugin so it can be sticked in any existing tree.
Enjoy!