mikecar
15 Nov 2009, 6:45 PM
I am posting this solution to a common problem, which I have seen others asking
questions about, in the hope it helps somebody. I also would like some feedback as to
the solution, and your opinion on whether this post should be converted to a FAQ
or something...
USE CASE
You want to execute a server-side task which may take a long time to finish.
During this time, you don't want the user to be able to start another request,
(ie you want the UI to block), and also you would like to provide visual
feedback as to what state the server-side process is in, i.e. some kind of
progress indicator.
POSSIBLE SOLUTIONS
I have seen folks advocating using synchronous AJAX calls (not supported by Ext),
and actually using this use case as a good example of the need for synchronous
AJAX requests. While I don't want to re-open this issue here, IMHO sync AJAX is
something that can be useful, and should be supported. Nevertheless, this
post shows a solution which does not require sync calls, so the good people at
Ext will be happy :)
Current wisdom in Ext forums says that you solve this particular problem by
using a client-side polling task, which asks the server for the progress status,
and updates the UI. This works, but I do not like the approach. I consider polling
a very bad design, and always avoid it if at all possible. Further, this approach
requires that the server-side code has some way of inter-thread communication.
This is because, when you start the actual task on the server, it gets executed
in a particular context (a thread in most topologies). However, the polling
request is executed in a different context (thread), and does not have access to
the data of the original request. So, you need to create some kind of mechanism
to communicate between the two threads. Using a data base table, or a file on
disk has been suggested. Another idea would be to use an application-wide cache,
like memCache in PHP. Still, this complicates the server-side, and it is wasteful
of resources, for something that is very commonplace,and should be easy to
accomplish.
MY SOLUTION
The basic concept behind my approach is that the server starts the task,
and then periodically emits progress information to the client, which displays
a meaningful progress bar or something. The status updates are part of a single
HTTP transaction, so are very efficient.
The problem with implementing this in Ext is that the only "official" mechanism
of executing server-side code is via the Ajax.request method, which cannot be
called synchronously (a requirement in this approach), and also does not provide
events while loading content, only at the end, therefore a progress bar that contains server-side information is impossible.
An alternative approach (which is widely used by non-Ext developers), is to use
a client IFRAME to make the request, and handle the progress updates.
This works because content loaded in the IFRAME is incrementally rendered by the
browser during loading, and not just at the end of the document. It is this feature
of most modern browsers that allows us to use this approach for a progress indicator.
While the concept is simple, the mechanics of putting everything together in a
working solution can be intimidating. The purpose of this post is to show a full
working solution, and provide some reusable code. The server-side is in PHP 5.x, but
I imagine that it can be coded in any environment that supports FLUSHING of the
output to the client under program control.
DESIGN DETAILS
At each status update point,the server-side code emits "scriptlets",
whose purpose is to trigger a client-side function that updates the progress UI.
These scriptlets have the following format:
<script type='text/javascript>
updateProgress(status);
</script>
where "updateProgress" is a function defined by the client, and accessible at that
execution context, and "status" is the server-side status information. Don't worry,
it will become clear later on...
The client browser will "see" these scriptlets at the time they are emitted
by the server, and will execute them immediately, thus updating the UI.
Issues to consider here are :
how and where is this "callback" function defined,so that it can be accessible
by the IFRAME that is doing the script execution ?
How are memory leaks avoided ?
how is the whole process initiated, and shutdown ?
Details follow.
SERVER SIDE CODE
First, you need a reusable class to encapsulate the process. here is the code:
<?php
class AJAX_PROGRESS
{
private $template;
function __construct()
{
$this->template="
<script type='text/javascript'>
window.top.mk_update_progress(%s,'%s','%s');
</script>";
$this->write(str_pad('<HTML><BODY>',4096)); //force browser to render
}
private function write($a)
{
echo $a;
ob_flush();
flush();
}
function advance($perc,$msg1='',$msg2='')
{
$this->write(sprintf($this->template,$perc,$msg1,$msg2));
}
function __destruct()
{
$this->advance(-1);
$this->write('</BODY></HTML>');
}
} // AJAX_PROGRESS
?>
Copy-paste this class to a file, and save it as AJAX_PROGRESS.class.php
or whatever naming conventions you have. It has no external requirements.
What this class does is to handle the incremental generation which will be
the "content" loaded by the client IFRAME.
Next, you need a server-side file that executes a long task,
and uses this class to handle progress indication.
Here is an example, which simulates a long running-task:
<?php
require_once 'AJAX_PROGRESS.class.php';
//pick up any parameters for the task.
//in this example, n=how may times to execute a loop, and delay=the loop delay
$n=$_REQUEST['n'];
$delay=$_REQUEST['delay'];
$pb=new AJAX_PROGRESS();
for ($i=0; $i<$n; $i++)
{
//here you add the code to actually do the work
//call "advance" periodically to update the client
$pb->advance(($i+1)/$n,'Loading...',"Step $i");
usleep($delay*1000);
}
//this destroys the progress object, and ends the output to the client
//it also emits a special call to the client-side function, with a progress value
//of -1, which tells the client that the task is completed
$pb=null;
?>
Copy-paste this to a file called "progress.php" in the same directory as the
class file above, so that it "finds" the class. alternatively, use whatever
server-side mechanism you are accustomed to, as far as placement of code is
concerned, and adjust the "require" statement.
What you need to know at this point is the function "advance", which is called
with three arguments:
$perc=progress percent, in the range 0..1
$msg1=Some text that must be displayed by the client
$msg2=Another piece of text that should be displayed.
In this example, the design assumes that the client side uses an Ext.Msg.Progress
dialog, which takes exactly these arguments. if not using Msg.Progress, adjust the
function arguments and implementation accordingly, the principle is the same.
Notice that the scriptlet emitted by the server assumes that there exists a
function called mk_update_progress, defined at the top window level, so that it
can be "found" by the IFRAME. The client-side code takes care of defining this function.
CLIENT-SIDE CODE
First, you need a reusable,self-contained javascript function, which sets up the
environment, and handles the details. This function does not have dependencies other
than Ext, and can be added to your client-side library, or saved as a single file,
and included in your html via a <script> tag.
function execWithProgress(title,url,params)
{
var iframe,pbw;
window.top.mk_update_progress=function(value,msg1,msg2)
{
if (value<0)
{
pbw.hide();
iframe.remove();
window.top.mk_update_progress=null;
return;
}
pbw.updateProgress(value,msg1,msg2);
}
pbw=Ext.Msg.progress(title,'','');
if (params) url+='?'+Ext.urlEncode(params);
iframe=Ext.getBody().createChild({tag:'IFRAME',style:'display:none',src:url});
}
You call this function with three parameters:
title: the title for the progress dialog
url: the url of the server file that initiates the long task, and supplies status information
params: An object hash that contains key/value pairs that are passed as parameters of the GET request.
Notice that, because of the use of javascript closures, this function does not
pollute the global namespace, and cleans up after itself, so you can place it
virtually anywhere. it will create the hidden IFRAME, the progress dialog,
and clean up when the server-side task is done. You only need to call it
to execute the whole process, it is that simple!
Example code that uses this function,and uses the file "progress.php" we
defined earlier :
var content=new Ext.Panel(
{
border:false,
tbar:[
{text:'Call server',handler:function(){
execWithProgress('Calling Server','progress.php',{n:10,delay:200});
}}
],
items:[
]
}
);
This is it. I hope this post helps somebody, and will provide a "clean"
solution to this very common use case, as a fully worked-out example.
Enjoy!
Mike Cariotoglou
Ext Newbie
P.S Excuse my English, I am Greek :)
questions about, in the hope it helps somebody. I also would like some feedback as to
the solution, and your opinion on whether this post should be converted to a FAQ
or something...
USE CASE
You want to execute a server-side task which may take a long time to finish.
During this time, you don't want the user to be able to start another request,
(ie you want the UI to block), and also you would like to provide visual
feedback as to what state the server-side process is in, i.e. some kind of
progress indicator.
POSSIBLE SOLUTIONS
I have seen folks advocating using synchronous AJAX calls (not supported by Ext),
and actually using this use case as a good example of the need for synchronous
AJAX requests. While I don't want to re-open this issue here, IMHO sync AJAX is
something that can be useful, and should be supported. Nevertheless, this
post shows a solution which does not require sync calls, so the good people at
Ext will be happy :)
Current wisdom in Ext forums says that you solve this particular problem by
using a client-side polling task, which asks the server for the progress status,
and updates the UI. This works, but I do not like the approach. I consider polling
a very bad design, and always avoid it if at all possible. Further, this approach
requires that the server-side code has some way of inter-thread communication.
This is because, when you start the actual task on the server, it gets executed
in a particular context (a thread in most topologies). However, the polling
request is executed in a different context (thread), and does not have access to
the data of the original request. So, you need to create some kind of mechanism
to communicate between the two threads. Using a data base table, or a file on
disk has been suggested. Another idea would be to use an application-wide cache,
like memCache in PHP. Still, this complicates the server-side, and it is wasteful
of resources, for something that is very commonplace,and should be easy to
accomplish.
MY SOLUTION
The basic concept behind my approach is that the server starts the task,
and then periodically emits progress information to the client, which displays
a meaningful progress bar or something. The status updates are part of a single
HTTP transaction, so are very efficient.
The problem with implementing this in Ext is that the only "official" mechanism
of executing server-side code is via the Ajax.request method, which cannot be
called synchronously (a requirement in this approach), and also does not provide
events while loading content, only at the end, therefore a progress bar that contains server-side information is impossible.
An alternative approach (which is widely used by non-Ext developers), is to use
a client IFRAME to make the request, and handle the progress updates.
This works because content loaded in the IFRAME is incrementally rendered by the
browser during loading, and not just at the end of the document. It is this feature
of most modern browsers that allows us to use this approach for a progress indicator.
While the concept is simple, the mechanics of putting everything together in a
working solution can be intimidating. The purpose of this post is to show a full
working solution, and provide some reusable code. The server-side is in PHP 5.x, but
I imagine that it can be coded in any environment that supports FLUSHING of the
output to the client under program control.
DESIGN DETAILS
At each status update point,the server-side code emits "scriptlets",
whose purpose is to trigger a client-side function that updates the progress UI.
These scriptlets have the following format:
<script type='text/javascript>
updateProgress(status);
</script>
where "updateProgress" is a function defined by the client, and accessible at that
execution context, and "status" is the server-side status information. Don't worry,
it will become clear later on...
The client browser will "see" these scriptlets at the time they are emitted
by the server, and will execute them immediately, thus updating the UI.
Issues to consider here are :
how and where is this "callback" function defined,so that it can be accessible
by the IFRAME that is doing the script execution ?
How are memory leaks avoided ?
how is the whole process initiated, and shutdown ?
Details follow.
SERVER SIDE CODE
First, you need a reusable class to encapsulate the process. here is the code:
<?php
class AJAX_PROGRESS
{
private $template;
function __construct()
{
$this->template="
<script type='text/javascript'>
window.top.mk_update_progress(%s,'%s','%s');
</script>";
$this->write(str_pad('<HTML><BODY>',4096)); //force browser to render
}
private function write($a)
{
echo $a;
ob_flush();
flush();
}
function advance($perc,$msg1='',$msg2='')
{
$this->write(sprintf($this->template,$perc,$msg1,$msg2));
}
function __destruct()
{
$this->advance(-1);
$this->write('</BODY></HTML>');
}
} // AJAX_PROGRESS
?>
Copy-paste this class to a file, and save it as AJAX_PROGRESS.class.php
or whatever naming conventions you have. It has no external requirements.
What this class does is to handle the incremental generation which will be
the "content" loaded by the client IFRAME.
Next, you need a server-side file that executes a long task,
and uses this class to handle progress indication.
Here is an example, which simulates a long running-task:
<?php
require_once 'AJAX_PROGRESS.class.php';
//pick up any parameters for the task.
//in this example, n=how may times to execute a loop, and delay=the loop delay
$n=$_REQUEST['n'];
$delay=$_REQUEST['delay'];
$pb=new AJAX_PROGRESS();
for ($i=0; $i<$n; $i++)
{
//here you add the code to actually do the work
//call "advance" periodically to update the client
$pb->advance(($i+1)/$n,'Loading...',"Step $i");
usleep($delay*1000);
}
//this destroys the progress object, and ends the output to the client
//it also emits a special call to the client-side function, with a progress value
//of -1, which tells the client that the task is completed
$pb=null;
?>
Copy-paste this to a file called "progress.php" in the same directory as the
class file above, so that it "finds" the class. alternatively, use whatever
server-side mechanism you are accustomed to, as far as placement of code is
concerned, and adjust the "require" statement.
What you need to know at this point is the function "advance", which is called
with three arguments:
$perc=progress percent, in the range 0..1
$msg1=Some text that must be displayed by the client
$msg2=Another piece of text that should be displayed.
In this example, the design assumes that the client side uses an Ext.Msg.Progress
dialog, which takes exactly these arguments. if not using Msg.Progress, adjust the
function arguments and implementation accordingly, the principle is the same.
Notice that the scriptlet emitted by the server assumes that there exists a
function called mk_update_progress, defined at the top window level, so that it
can be "found" by the IFRAME. The client-side code takes care of defining this function.
CLIENT-SIDE CODE
First, you need a reusable,self-contained javascript function, which sets up the
environment, and handles the details. This function does not have dependencies other
than Ext, and can be added to your client-side library, or saved as a single file,
and included in your html via a <script> tag.
function execWithProgress(title,url,params)
{
var iframe,pbw;
window.top.mk_update_progress=function(value,msg1,msg2)
{
if (value<0)
{
pbw.hide();
iframe.remove();
window.top.mk_update_progress=null;
return;
}
pbw.updateProgress(value,msg1,msg2);
}
pbw=Ext.Msg.progress(title,'','');
if (params) url+='?'+Ext.urlEncode(params);
iframe=Ext.getBody().createChild({tag:'IFRAME',style:'display:none',src:url});
}
You call this function with three parameters:
title: the title for the progress dialog
url: the url of the server file that initiates the long task, and supplies status information
params: An object hash that contains key/value pairs that are passed as parameters of the GET request.
Notice that, because of the use of javascript closures, this function does not
pollute the global namespace, and cleans up after itself, so you can place it
virtually anywhere. it will create the hidden IFRAME, the progress dialog,
and clean up when the server-side task is done. You only need to call it
to execute the whole process, it is that simple!
Example code that uses this function,and uses the file "progress.php" we
defined earlier :
var content=new Ext.Panel(
{
border:false,
tbar:[
{text:'Call server',handler:function(){
execWithProgress('Calling Server','progress.php',{n:10,delay:200});
}}
],
items:[
]
}
);
This is it. I hope this post helps somebody, and will provide a "clean"
solution to this very common use case, as a fully worked-out example.
Enjoy!
Mike Cariotoglou
Ext Newbie
P.S Excuse my English, I am Greek :)