viernes, 20 de agosto de 2010

Uso del patrón Observador para llevar el progreso durante la carga de la página

¿Alguna te ha tocado entrar a una página que corre por detrás un proceso pesado y que por tanto dura bastante en terminar? Si la página no está bien diseñada en términos de experiencia de usuario, terminamos con una sensación de que va tomar toda la eternidad en cargar.

Si queremos evitar este efecto tortuga en nuestros sitios, debemos considerar siempre si podemos agregar un indicador en la página que muestre el progreso avanzado de la tarea que está corriendo el servidor. Para lograr esto podemos tomar ventaja del patrón Observador.

Lo que necesitamos primariamente es correr el proceso de manera asincrónica, en otras palabras, en un hilo diferente. El siguiente diagrama muestra como el proceso pesados en contenido en una clase que extiende de "Thread".




La clase de abajo va a simular un proceso que toma mucho tiempo al tomar varias siestas (Thread.sleep()).

package com.gsolano.longprocess

import java.util.Observable;

/**
* Class with an observable mock long progress.
* @author gsolano
*
*/
public class LongProcess extends Observable {

/**
* Keeps the progress of the process.
*/
protected Float progress;

/**
* Simulates a long process.
*/
public void start() {
int n =10;
for (int i=0;i <= n; i++) {
progress = (float)i/(float)n * 100; // Calculates progress.
try {
Thread.currentThread();
Thread.sleep(2000);
this.setChanged();
this.notifyObservers(progress);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


El progreso de esta clase es calculado en cada iteración notificando al mismo tiempo del cambio que se ha efectuado en el progreso. La siguiente clase es la que observa a "LongProcess".

package com.gsolano.longprocess;

import java.util.Observable;
import java.util.Observer;

/**
* Observer class
*
* @author gsolano
*/
public class LongProcessObserver implements Observer{

protected Float progress;
/**
* Tracks the progress of the long process.
* @return
*/
public Float getProgress() {
return progress;
}

public void update(Observable o, Object arg) {
progress = (Float) arg;
}
}


Para completar el diagrama anterior, necesitamos crear una clase que extienda de Thread para envolver el proceso largo y lanzarlo en un hilo diferente.

/**
*
* Class to run a LongProcess in a separate thread.
*
* @author gsolano
*
*/
public class LongProcessThread extends Thread {

private LongProcess longProcess;

public LongProcess getLongProcess() {
return longProcess;
}

public void setLongProcess(LongProcess longProcess) {
this.longProcess = longProcess;
}

@Override
public void run() {
if(longProcess != null) {
longProcess.start();
}
}
}


Ahora brinquemos a al lado de la parte web. En el siguiente action de struts se maneja dos tipos de eventos:

1. Arranque el proceso pesado:

  • El proceso pesado es creado.
  • Un observador es agregado al proceso.
  • Lance el proceso en un hilo distinto.
  • Se guarda el observador en sesión.

2. Enviar una actualización del progreso del proceso

  • Se saca el observador de la sesión.
  • El valor de progreso se toma del observador y se escribe en la respuesta.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

public class FooProgressAction extends Action{

@Override
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {

String action = request.getParameter("action");

if(action != null) {
if(action.equalsIgnoreCase("progress")) { // If action is ajax request to get progress.
// Get the observer.
LongProcessObserver longProcessObserver = (LongProcessObserver)
request.getSession().getAttribute("observer");
if(longProcessObserver != null) {
// Get the progress from the observer.
Float progress = longProcessObserver.getProgress();
if(progress != null) {
// Send the progress to the page.
response.getWriter().write(progress.toString());
}
return null;
}
} else if(action.equalsIgnoreCase("start")) { // Did someone click the start button?
launchLongProcess(request);
}
}
return mapping.findForward("success");
}

private void launchLongProcess(HttpServletRequest request) {
LongProcess longProcess = new LongProcess();
LongProcessObserver observer = new LongProcessObserver();
// Add the observer to the long process.
longProcess.addObserver(observer);
// Launch long process in a thread.
LongProcessThread longProcessThread = new LongProcessThread();
longProcessThread.setLongProcess(longProcess);
longProcessThread.start();
// Keep the observer in session.
request.getSession().setAttribute("observer", observer);
// Send a flag indicating that party just started!
request.setAttribute("processStarted", true);
}
}
Del lado del cleinte se necesita solamente lógica para comenzar un ciclo de invocaciones Ajax para solicitar el progreso hasta que se alcance el 100%.


<%@ taglib uri="/WEB-INF/tld/c.tld" prefix="c" %>

<html>
<head>
<script language="Javascript">
var seconds = 1;
var run = false;
var ajaxURL;

function checkProgress(url) {
if(typeof url != 'undefined') {
ajaxURL = url;
}

var xmlHttp;
try {
xmlHttp = new XMLHttpRequest(); // Firefox, Opera 8.0+, Safari
} catch (e) {
try {
xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); // Internet Explorer
} catch (e) {
try {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
alert("Ajax not supported");
return false;
}
}
}
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 ) {
var progress = xmlHttp.responseText;
if(progress == 100.0) {
document.getElementById('progress').innerHTML = "Finished!!";
return;
}else {
if(progress) {
document.getElementById('progress').innerHTML = progress + "%";
}
setTimeout('checkProgress()', seconds * 1000);
}
}
};
xmlHttp.open("GET", ajaxURL, true);
xmlHttp.send(null);
}
</script>
</head>
<body>
<div style="position: absolute; left:40%; text-align:center; border: 1px solid; margin: 20px; padding:20px; width: 150px;">
<form action="${pageContext.request.contextPath}/longProcess.do">
<input type="hidden" name="action" value="start" />
<input type="submit" value="Start!" />
</form>

<div id="progress"></div>

<c:if test="${not empty processStarted}">
<script language="Javascript">
setTimeout('checkProgress(\'${pageContext.request.contextPath}/longProcess.do?action=progress\')', 1000);
</script>
</c:if>
</div>
</body>
</html


Resultado:




No hay comentarios:

Publicar un comentario