miércoles, 29 de abril de 2009

Empezando con Cairngorm - Desarrollando aplicaciones Flex

Probablemente todos los que hemos estado por un buen rato metidos en el mundo de Flex hemos escuchado algunas ves sobre el framwerok para desarrollar a aplicaciones Cairngorm.

Para los que no lo han escuchado dejenme explicarles un poco cual es su utilidad: Cuando desarrollas utilizando este framework te vez obligado a cumplir con una serie de normas (Patrones de diseño) que brindan una infinidad de ventajas a largo plazo dentro de tu aplicacion. Por ejemplo: El estar dividido en 8 capaz te permite llevar una mejor organizacion del codigo, permite que varias personas trabajen de manera colaborativa al separar eficazmente la capa de datos de la aplicacion (bien sea con objetos remotos, http service, xml, etc) de la capa de presentacion, permite que nuevos integrantes que conozcan el framework se incorporen mucho mas rapidamente al ser un estandar, se reutiliza mucho el codigo. En fin, muchisimas ventajas, dejen que lo empiezen a usar para que vean que al final no van a poder dejar de usarlo.

¿Como esta dividido Cairngorm?

Una aplicacion realizada bajo Cairngorm tiene 8 capaz principales: Vista, Evento, Controlador, Comando, Delegado, Ubicador de servicios, modelo y los servicios de datos:
  1. Vista: Son todos los formularios, la capa de presentacion de la aplicacion, lo que el usuario final ve.
  2. Evento: Recordemos que lex utiliza la programacion orientada a eventos, son el vinculo entre la vista y el comando.
  3. Controlador: Enruta los diferentes eventos posibles de la aplicacion con los diferentes comandos.
  4. Comando: Realiza 3 acciones principales, execute, que ejecuta el metodo del delegado deseado. "result" que se ejecuta cuando se recive la respuesta del servidor correctamente y "fault" que se ejecuta si ocurre alguna exepcion durante la ejecucion del delegado y servicio. En el metodo "result" se debe introducir los datos en el modelo para que las vistas sean actualizadas.
  5. Delegado: Existen muchos metodos para un objeto remoto, el delegado es el encargado de llamarlos, es un vinculo entre el comando y el servicio.
  6. Ubicador de servicios: aqui se realizan todas las definiciones de los objetos, metodos o servicios que se van a utilizar. Se coloca la ruta del servidor donde son expuestos estos servicioes, etc.
  7. Modelo: Contiene todos los datos de la aplicacion, un diagrama de clases, entidades.
  8. Los servicios: Deben ser creados en un servidor, lenguaje java, php, c#, etc. Ese servidor contiene la BD.
Ejemplo de caso de uso con Cairngorm:

Vamos a colocar el siguiente ejemplo basado en la grafica de arriba: El usuario desea guardar un paciente en la base de datos, para que eso suceda debe existir un formulario de introducccion de los datos del paciente (esa seria la "vista"), una vez que el usuario llena los datos del paciente oprime el botón "guardar" (en ese momento el programador debe disparar un "evento" con los datos del paciente), el "controlador" es el encargado de enrutar ese evento hacia el "comando" mas adecuado (El comando debe manejar las ejecuciones o fallos de los servicios) luego el comando llama a un "delegado"donde se hace un llamado al objeto remoto segun las definiciones que se hayan realizado en el "manejador de servicios". Luego de recibir la respuesta del servicio (recuerden que la respuesta la debe manejar el "comando"), el "comando" se encarga de agregar dentro del "modelo" la informacion recibida, y como el "modelo" esta sincronizado (binding) con la vista, entonces la vista se actualiza automaticamente sin requerir nungina accion del programador.

Probablemente no les haya quedado nada claro, es un poco dificil de entender al principio, y puede parecer que son demasiadas cosas para una accion tan simple como "insertar un paciente en la base de datos". Para entenderlo muchisimo mejor vamos a explicar el ejemplo pero esta vez con el codigo y su ejecucion. Pero eso lo haremos en otro tutorial por razones didacticas (para entrar a el tutorial "Ejemplo Cairngorm - Agregar Contacto" haz click aqui), no se preocupen que una vez que lo entiendan van a ver que es fácil y útil. Para bajarse la libreria de Cairngorm hagan click aqui y seleccionen la última version.

Continuar leyendo...

lunes, 27 de abril de 2009

Expandir, Contraer, Cerrar, cambiar tamaño y mover un Panel


Una de las funcionalidades mas buscadas en un panel son CERRAR, EXPANDIR, CONTRAER Y RESIZE. En este tutorial vamos a explicar su implementacion en un Panel clásico del framework de flex. Utilizando un una clase llamada SuperPanel hecha por Wietse Veenstra y publicada en su blog.

Para empezar debemos tener en nuestro proyecto la clase SuperPanel.as y cada vez que queramos utilizarla en vez de crear un panel de tipo mx.Containers.Panel debemos crear un panel tipo SuperPanel.

Es importante resaltar que la clase SuperPanel hereda directamente de la clase panel, por lo tanto es un panel en si mismo y Flex lo reconoce como un panel, con todos sus comportamientos naturales, mas lo que vamos a ver a continuacion.
Antes de empezar les coloco un demo de la aplicación y el link para que puedan descargar el codigo fuente que vamos a estar utilizando en este tutorial.


Lo pimero que debemos hacer es ir al metodo createChildren(), por ser este, el primer método que se invoca en el ciclo de vida del SuperPanel.

override protected function createChildren():void {
super.createChildren();
this.pTitleBar = super.titleBar;
this.setStyle("headerColors", [0xC3D1D9, 0xD2DCE2]);
this.setStyle("borderColor", 0xD2DCE2);
this.doubleClickEnabled = true;

if (enableResize) {
this.resizeHandler.width = 12;
this.resizeHandler.height = 12;
this.resizeHandler.styleName = "resizeHndlr";
this.rawChildren.addChild(resizeHandler);
this.initPos();
}

if (showControls) {
this.normalMaxButton.width = 10;
this.normalMaxButton.height = 10;
this.normalMaxButton.styleName = "increaseBtn";
this.closeButton.width = 10;
this.closeButton.height = 10;
this.closeButton.styleName = "closeBtn";
this.pTitleBar.addChild(this.normalMaxButton);
this.pTitleBar.addChild(this.closeButton);
}

this.positionChildren();
this.addListeners();
}

Lo primero que hacemos en este metodo es definir la apariencia del panel le colocamos un color al header y un color al borde, luego habilitamos que se pueda hacer dobleclick (hay que recordar que el evento doble click solo se dispara si la propiedad doubleClickEnabled es igual a true, por eso la estamos haciendo true) al hacer doble click sobre el titulo del panel, este ultimo se contraerá.

Luego pueden notar los dos condicionales, uno para saber si esta habilido el comportamiento resize y otro para verificar si esta habilitado el comportamiento de cerrar, espandir y contraer. En el caso de que enable resize este "verdadero" entonces se colocara el botón en la esquina inferior derecha del componente (en el metodo initPos se verifica cuales son las cordenadas de esa esquina). Si la propiedad showControls es verdadera entonces se colocan los botones cerrar y expandir sobre dentro de la barra de titulo del panel.

public function positionChildren():void {
if (showControls) {
this.normalMaxButton.buttonMode = true;
this.normalMaxButton.useHandCursor = true;
this.normalMaxButton.x = this.unscaledWidth - this.normalMaxButton.width - 24;
this.normalMaxButton.y = 8;
this.closeButton.buttonMode = true;
this.closeButton.useHandCursor = true;
this.closeButton.x = this.unscaledWidth - this.closeButton.width - 8;
this.closeButton.y = 8;
}

if (enableResize) {
this.resizeHandler.y = this.unscaledHeight - resizeHandler.height - 1;
this.resizeHandler.x = this.unscaledWidth - resizeHandler.width - 1;
}
}

public function addListeners():void {
this.addEventListener(MouseEvent.CLICK, panelClickHandler);
this.pTitleBar.addEventListener(MouseEvent.MOUSE_DOWN, titleBarDownHandler);
this.pTitleBar.addEventListener(MouseEvent.DOUBLE_CLICK, titleBarDoubleClickHandler);

if (showControls) {
this.closeButton.addEventListener(MouseEvent.CLICK, closeClickHandler);
this.normalMaxButton.addEventListener(MouseEvent.CLICK, normalMaxClickHandler);
}

if (enableResize) {
this.resizeHandler.addEventListener(MouseEvent.MOUSE_OVER, resizeOverHandler);
this.resizeHandler.addEventListener(MouseEvent.MOUSE_OUT, resizeOutHandler);
this.resizeHandler.addEventListener(MouseEvent.MOUSE_DOWN, resizeDownHandler);
}
}

El metodo "positionChildren" se encarga de ubicar a los botones de cerrar, expandir, contraer y cambiar tamaño dentro del panel.

El el segundo metodo addListeners() se encarga de iniciar los "Listeners" o "Escuchadores" que monitorearán cuando algun click, dobleclick o drag ocurra sobre los botones que recien acabamos de agregar en el metodo createChildren().

public function panelClickHandler(event:MouseEvent):void {
this.pTitleBar.removeEventListener(MouseEvent.MOUSE_MOVE, titleBarMoveHandler);
this.parent.setChildIndex(this, this.parent.numChildren - 1);
this.panelFocusCheckHandler();
}

public function titleBarDownHandler(event:MouseEvent):void {
this.pTitleBar.addEventListener(MouseEvent.MOUSE_MOVE, titleBarMoveHandler);
}

public function titleBarMoveHandler(event:MouseEvent):void {
if (this.width < screen.width) {
Application.application.parent.addEventListener(MouseEvent.MOUSE_UP, titleBarDragDropHandler);
this.pTitleBar.addEventListener(DragEvent.DRAG_DROP,titleBarDragDropHandler);
this.parent.setChildIndex(this, this.parent.numChildren - 1);
this.panelFocusCheckHandler();
this.alpha = 0.5;
this.startDrag(false, new Rectangle(0, 0, screen.width - this.width, screen.height - this.height));
}
}

public function titleBarDragDropHandler(event:MouseEvent):void {
this.pTitleBar.removeEventListener(MouseEvent.MOUSE_MOVE, titleBarMoveHandler);
this.alpha = 1.0;
this.stopDrag();
}

public function panelFocusCheckHandler():void {
for (var i:int = 0; i < this.parent.numChildren; i++) {
var child:UIComponent = UIComponent(this.parent.getChildAt(i));
if (this.parent.getChildIndex(child) < this.parent.numChildren - 1) {
child.setStyle("headerColors", [0xC3D1D9, 0xD2DCE2]);
child.setStyle("borderColor", 0xD2DCE2);
} else if (this.parent.getChildIndex(child) == this.parent.numChildren - 1) {
child.setStyle("headerColors", [0xC3D1D9, 0x5A788A]);
child.setStyle("borderColor", 0x5A788A);
}
}
}

Luego de iniciar todos los listeners y variables del panel tenemos los handlers (métodos) que se ejecutarán cuando ocurran los eventos que empezamos a escuchar. El primero de estos "panelClickHandler" se encarga de darle el foco al panel que el usuario le acaba de dar "click", lo trae al frente de los otros panels.

El segundo método "titleBarDownHandler" se ejecuta cuando el usuario hace un MOUSE_DOWN sobre el titlebar y se encarga de empezar a escuchar el MOUSE_MOVE para mover el panel junto con el mouse mientras se mantenga presionado el boton de este ultimo (una implementacion de drag and drop).

El tercer metodo "TitleBarMoveHandler" es el encargado de iniciar el drag para mover el panel, se ejecuta cuando el listener recien agregado en el "titleBarDownHandler" detecta un movimiento del mouse, este método tambien escucha un MOUSE_UP para terminar con el drag and drop y si usuario deja de presionar el botón del mouse.

El cuarto metódo "titleBarDragDropHandler" se encarga de terminer el drag and drop del panel, y se ejecuta cuando ocurre un MOUSE_UP. Por último el metodo "panelFocusCheckHandler" le camiba la apariencia al panel segun si tiene o no tiene el foco del mouse.

public function titleBarDoubleClickHandler(event:MouseEvent):void {
this.pTitleBar.removeEventListener(MouseEvent.MOUSE_MOVE, titleBarMoveHandler);
Application.application.parent.removeEventListener(MouseEvent.MOUSE_UP, resizeUpHandler);

this.upMotion.target = this;
this.upMotion.duration = 300;
this.upMotion.heightFrom = oH;
this.upMotion.heightTo = 28;
this.upMotion.end();

this.downMotion.target = this;
this.downMotion.duration = 300;
this.downMotion.heightFrom = 28;
this.downMotion.heightTo = oH;
this.downMotion.end();

if (this.width < height ="=" visible =" false;" visible =" true;">

En el código de arriba encontramos la implementación del efecto de minimizar cuando hacemos dobleclick sobre el título del panel. El método titleBarDoubleClickHandler se encarga de definir los valores iniciales para los efectos Resize que se definieron al inicio de la clase, estos efectos cambian la altura del panel hasta contraerlo completamente, quedando solo visible la barra del titulo, o visceversa.

public function normalMaxClickHandler(event:MouseEvent):void {
if (this.normalMaxButton.styleName == "increaseBtn") {
if (this.height > 28) {
this.initPos();
this.x = 0;
this.y = 0;
this.width = screen.width;
this.height = screen.height;
this.normalMaxButton.styleName = "decreaseBtn";
this.positionChildren();
}
} else {
this.x = this.oX;
this.y = this.oY;
this.width = this.oW;
this.height = this.oH;
this.normalMaxButton.styleName = "increaseBtn";
this.positionChildren();
}
}

public function closeClickHandler(event:MouseEvent):void {
this.removeEventListener(MouseEvent.CLICK, panelClickHandler);
this.parent.removeChild(this);
}

Estos dos metodos son los encarcados de las funciones de expandir, contraer y cerrar el panel, el primero de ellos se ejecuta cuando se hace click sobre el botón "normalMaxButton" y lo primero que hace es detectar si el panel se encuentra expandido o contraido y luego ejecuta el codigo para expandir o contraer el codigo segun el caso. El segundo metodo "closeClickHandler" se llama cuando el usuario hace click sobre el botón cerrar, este metodo elimina el listener sobre el evento click que habiamos definido durante la funcion create children y hace que el panel se auto elimine.

public function resizeOverHandler(event:MouseEvent):void {
this.resizeCur = CursorManager.setCursor(resizeCursor);
}

public function resizeOutHandler(event:MouseEvent):void {
CursorManager.removeCursor(CursorManager.currentCursorID);
}

public function resizeDownHandler(event:MouseEvent):void {
Application.application.parent.addEventListener(MouseEvent.MOUSE_MOVE, resizeMoveHandler);
Application.application.parent.addEventListener(MouseEvent.MOUSE_UP, resizeUpHandler);
this.resizeHandler.addEventListener(MouseEvent.MOUSE_OVER, resizeOverHandler);
this.panelClickHandler(event);
this.resizeCur = CursorManager.setCursor(resizeCursor);
this.oPoint.x = mouseX;
this.oPoint.y = mouseY;
this.oPoint = this.localToGlobal(oPoint);
}

public function resizeMoveHandler(event:MouseEvent):void {
this.stopDragging();

var xPlus:Number = Application.application.parent.mouseX - this.oPoint.x;
var yPlus:Number = Application.application.parent.mouseY - this.oPoint.y;

if (this.oW + xPlus > 140) {
this.width = this.oW + xPlus;
}

if (this.oH + yPlus > 80) {
this.height = this.oH + yPlus;
}
this.positionChildren();
}

public function resizeUpHandler(event:MouseEvent):void {
Application.application.parent.removeEventListener(MouseEvent.MOUSE_MOVE, resizeMoveHandler);
Application.application.parent.removeEventListener(MouseEvent.MOUSE_UP, resizeUpHandler);
CursorManager.removeCursor(CursorManager.currentCursorID);
this.resizeHandler.addEventListener(MouseEvent.MOUSE_OVER, resizeOverHandler);
this.initPos();
}

Estos últimos cuatro metodos se dedican a implementar la funcionalidad de resize para el panel: El primero de ellos se encarga de cambiar el icono del mouse cuando tengamos el cursor sobre la esquina inferior derecha (para indicarnos que podemos hacer un resize del panel). El segundo metodo es muy similiar pero lo que hace es devolver el icono del mouse a su estado natural (la flechita) cuando quitemos el curso de la esquina inferior derecha del panel.

El tercer metodo "resizeDownHandler" se ejecuta cuando presionamos el botón izquierdo del mouse sobre el boton "resizeHandler" que habiamos insertado en el createChildren(), este metodo se encarga de inicializar los valores X y Y de la esquina y de iniciar los listeners para el evento MOUSE_MOVE.

El metodo "resizeMoveHandler" se encarga de cambiar el tamaño del panel en funcion del movimiento del mouse. Solo se ejecuta mientras tengamos el boton derecho del mouse preisonado, ya que una de las primeras cosas que hace es agregar un listener para el evento MOUSE_UP para detectar cuando dejamos de apretar el botón.

El método "resizeUpHandler" el ultimo metodo de todos se encarga de eliminar el listener de MOUSE_MOVE para no continuar con el resize (cambio de tamaño) del panel.

¡Y eso es todo! Como pueden apreciar es una clase bastante sencilla y facil de usar, por eso me gusta tanto, aunque tiene algunos bugs que iran descubriendo y puliendo sobre la marcha. En los próximos tutoriales publicare modificaciones bastante interesantes que he realizado sobre este u otros panels. Espero que haya sido lo suficientemente claro, cualquier pregunta no duden en preguntarme.


Continuar leyendo...

sábado, 25 de abril de 2009

Tutorial HTTP Service con Flex y PHP

Existen dos grandes maneras de comunicarse remotamente a través de aplicaciones web, son dos enfoques que tienen diferencias muy marcadas, y que si aprendemos a utilizarlos bien, podemos explotar los mejores aspectos de cada método y lograr aplicaciones con una capa de datos robusta y veloz.

Uno de los enfoques (SOAP) es a través de exposiciones de metodos remotos para que puedan ser llamadas desde otras computadoras, si quieres aprender un poco mas sobre eso puedes entrar a mi tutorial Video tutorial WebService basico con php donde lo explico as detalladamente.

El otro enfoque, que utilizaremos en este tutorial, tiene que ver con los metodos mas primitivos que existen en el protocolo http, que seguramente los que han trabajado con formularios web han escuchado algunos de estos: POST, GET, PUT, DELETE, etc. Un ejemplo de arquitectura web que utiliza este enfoque es llamado REST.

Vamos a emprezar, la aplicación que vamos a desarrollar va a tener dos lados, cliente y servidor. Del lado del cliente se creará una aplicacion flex llamada myHTTPService.mxml que se utilizará para visualizar todos los usuarios insertados en una base de datos y existirá un formulario para la insercion de un nuevo usuario. Del lado del servidor se creará una archivo PHP llamado request.php que permitirá acceder a la base de datos para realizar las consultas e inserciones pedidas por el cliente flex.



El lado del Cliente
Código myHTTPService.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="send_data()" height="503">
<mx:Script>
<![CDATA[
import mx.utils.ObjectProxy;
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.rpc.http.HTTPService;
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;

private var service:HTTPService


[Bindable]
private var usuarios : ArrayCollection = new ArrayCollection();

[Bindable]
public var cards: Array = ["PUT","POST","GET","DELETE"];

[Bindable]
public var selectedItem:Object = "GET";


public function send_data() : void
{
var obj : Object = new Object();
useHttpService(obj);
}

public function procesar() : void
{
if(nombre.text.length>0)
{
var obj : Object = new Object();
obj.nombre = nombre.text;
obj.id = codigo.text;

useHttpService(obj);
}
else
{
Alert.show("El nombre es obligatorio");
}
}

public function traspasar() : void
{
var a : Object = dg.selectedItem;
nombre.text = a.nombre;
codigo.text = a.id;
}

public function useHttpService(parameters:Object):void {
service = new HTTPService();
//service.destination = "http://localhost/tutoriales/httpservice/request.php";
service.url = "http://ejemplos.net78.net/httpservice/request.php";
service.method = "POST";
service.addEventListener("result", httpResult);
service.addEventListener("fault", httpFault);
service.useProxy = false;
service.send(parameters);
}


public function httpResult(event:ResultEvent):void {
var result:Object = event.result;
//Do something with the result.
if(result.users.user is ObjectProxy)
usuarios = new ArrayCollection([{nombre:result.users.user.nombre , id:result.users.user.id}]);
else
usuarios = result.users.user;
}


public function httpFault(event:FaultEvent):void {
var faultstring:String = event.fault.faultString;
Alert.show(faultstring);
}
]]>
</mx:Script>
<mx:DataGrid id="dg" x="10" y="10" dataProvider="{usuarios}" click="traspasar()"/>
<mx:Canvas x="10" y="182" width="260" height="98" backgroundColor="#FFFFFF">
<mx:Label x="10" y="10" text="Nombre:"/>
<mx:Label x="10" y="42" text="Id:"/>
<mx:TextInput x="90" y="8" id="nombre"/>
<mx:TextInput x="90" y="40" id="codigo" enabled="false"/>
<mx:Button x="173" y="66" label="Insertar" click="procesar()"/>
</mx:Canvas>


</mx:Application>




Ahora, que debemos destacar de esta aplicacion, la esencia de todo esta en la funcion useHttpService: Esta funcion se encarga de crear una peticion a un servicio http, para eso creamos un objeto de tipo HTTPService que va a simular un comportamiento parecido al que sucede cuando le damos a submit en un formulario web estandar, pero en este caso, en lugar de colocar varios input text, comboboxes, etc. Vamos a armar un objeto (en esta funcion lo llamamos "parameters") donde cada atributo de este objeto se comportara como una variable independiente en el lado de php, es decir, si al objeto le colocamos la propiedad nombre, entonces en php haremos $_POST['nombre'] para recibir el valor de esa propiedad.

Recuerden que en Flex un objeto de la clase Object puede tener cualquier atributo que nosotros queramos, solo debemos asignarle un valor a ese atributo inventado y a partir de ese momento el objeto contiene ese atributo con ese valor que le dimos.


Ahora vamos a analizar esa funcion paso por paso:

public function useHttpService(parameters:Object):void {
service = new HTTPService();
service.url = "http://ejemplos.net78.net/httpservice/request.php";
service.method = "POST";
service.addEventListener("result", httpResult);
service.addEventListener("fault", httpFault);
service.send(parameters);
}

El objeto service que acabamos de crear debemos pasarle varios parametros:
  1. URL: la direccion de la pagina que va a recibir nuestro formulario.
  2. Method: puede ser POST, GET, PUT, DELETE, ETC.
  3. send(parameters): Recuerden que el objeto parameters contiene todas las variables que queremos pasar al lado de PHP.
  4. Por ultimo escuchamos dos eventos "result" y "fault", el primero de ellos se disparará si el servicio se invoca correctamente (si encontro el URL que pusimos arriba y si en el lado del servidor PHP nos devolvio un resultado coherente) y nos llevará a la funcion httpresult donde procesaremos los resultados, el segundo de los eventos se disparará si la llamada al servicio no puede ser procesada y nos llevará al método httpfault.
El lado del servidor

En el lado del servidor vamos a crear un archivo php llamado reques.php, este archivo deberá procesar correctamente las peticios POST que haga el lado del cliente y deberá devolver la lista de usuarios que han sido insertados dentro de la base de datos:

Código request.php


//connect to the database
$mysql = mysql_connect("localhost", "root", "");
mysql_select_db( "phprestsql" );


//if the username and email address are filled out
if( $_POST["nombre"])
{
//add the user
$Query = "INSERT INTO user VALUES ('".$_POST['nombre']."','')";
$Result = mysql_query( $Query );
}


//return a list of all the users
$Query = "SELECT * from user";
$Result = mysql_query( $Query );


$Return = "<users>";


while ( $User = mysql_fetch_object( $Result ) )
{
$Return .= "<user><id>".$User->id."</id><nombre>".$User->nombre."</nombre></user>";
}
$Return .= "</users>";
mysql_free_result( $Result );
print ($Return);



Como pueden ver el lado de php es muy basico y es similar al que se utilizaria si estubieramos trabajando con formularios comunes y corrientes. Debemos recibir la variable "nombre" que pasamos desde flex y luego realizar una insercion en la base de datos. Luego de realizar la insercion consultamos toda la tabla de usuarios para implimir un xml en la pantalla que sera lo que constituirá el objeto de respuesta de vuelta hacia flex, ese se recibira como parametro del metodo httpresult en caso de que la llamada al servicio se haya ejecutado correctamente.

Espero que hayan entendido todo prefectamente, trate de ser lo mas claro posible, cualquier pregunta no duden en preguntarme a través de los comentarios de este blog. Les dejo un ejemplo funcionando de la aplicacion y los codigos fuentes de ambos lados (cliente y servidor).





Continuar leyendo...