Consuming XML Web Services from Javascript : Javier Luna blog

Monday, July 12, 2004

Consuming XML Web Services from Javascript

Si bien es cierto ASP.NET nos brinda un mayor control sobre los elementos que colocamos sobre nuestras paginas, gracias al soporte que se brinda a traves ViewState y el modelo de objetos que propone el Framework para este tipo de aplicaciones.

Sin embargo, algunas veces el constante PostBack, no es muy bienvenido -que digamos- para muchos usuarios y esto trae como consecuencia darles a nuestra aplicaciones un comportamiento diferente, para ciertas interfaces de usuario que ameritan una implementacion hibrida.

La posibilidad de realizar operaciones sobre el servidor web, sin necesidad de postear la pagina, mostrar informacion a la usuario en funcion a las acciones que realize este sobre la interfaz que le otorgamos, tambien es factible de realizar en el mundo de ASP.NET.

Algunos le dice Callbacks, tecnicamente hablando es darle a nuestros scripts por el lado del usuario, una forma de comunicarse con el web server para realizar operaciones y obtener resultados.

Para ello existen muchas formas: Web Services Behaviors, Remote Scripting, Artificios usando Frames ocultos, etc.

En esta oportunidad la intencion es mostrarles, la forma de conseguirlo usando XML Web Services y el XMLHTTP, un componente del Microsoft XML Core Services.


Archivo .ASPX

<%@ Page language="c#" Codebehind="ParentChild.aspx.cs" AutoEventWireup="false" Inherits="DemoWebAppCSharp.Services.ParentChild" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>ParentChild</title>
<script language="javascript" src="../Resource/FactoryDom.js"></script>
<script language="javascript">
var _server = 'http://localhost';
var _app = '/DemoWebAppCSharp';
var _asmx = '/Services/General.asmx'

// Este metodo esta enganchado al evento onload del body.
function Load()
{
ParentRecordSet();
}

// Este metodo esta enganchado al evento onchange del DropDownList
// _child, en su representacion HTML como SELECT.
function Show( object )
{
ChildRecordSet( object.value );
}

// Este metodo esta enganchado al evento onclick del Button _submit
// y permite salvar la informacion de los DropDownList.
function Capture()
{
document.all( '_parent_selected' ).value = document.all( '_parent' ).value;
document.all( '_child_selected' ).value = document.all( '_child' ).value;
}

// Transforma la informacion de los documentos XML a un vocabulario
// comun a traves de una transformacion XSLT
function ToOptions( xmlstring, xsltfile )
{
factory = new FactoryDom();

xmldom = factory.CreateXml( xmlstring );
xsltdom = factory.CreateXslt( xsltfile );

output = factory.Transform( xmldom, xsltdom );
if( output == null )
{
alert( 'Error al ejectuar Transform' );
return null;
}

return output;
}

// Obtiene la informacion de los itemes padres a traves del
// XML Web Services
function ParentRecordSet()
{
url = new UrlTarget( _server + _app + _asmx + '/ParentRecordSet' );

ws = new WebService( url );
xmlstring = ws.Execute();
xsltfile = '../Resource/ParentItem.xslt';

var output = ToOptions( xmlstring, xsltfile );

childNodes = output.documentElement.childNodes;

_parent = document.all( '_parent' );
AddChoose( _parent );

while( ( node = childNodes.nextNode() ) != null )
AddOption( _parent, node );
}

// Obtiene la informacion de los itemes hijos a traves del
// XML Web Services
function ChildRecordSet( Parent )
{
url = new UrlTarget( _server + _app + _asmx + '/ChildRecordSet' );
url.Append( 'Parent', Parent );

ws = new WebService( url );
xmlstring = ws.Execute();
xsltfile = '../Resource/ChildItem.xslt';

var output = ToOptions( xmlstring, xsltfile );

childNodes = output.documentElement.childNodes

_child = document.all( '_child' );
RemoveAll( _child );
AddChoose( _child );

while( ( node = childNodes.nextNode() ) != null )
AddOption( _child, node );
}

// Inserta una opcion por default en los DropDownList
function AddChoose( select )
{
var option = document.createElement( 'option' );
option.value = '-1';
option.text = '-- Elija una opcion --';

select.options.add( option );
}

// Insert una opcion a parti de los datos del node
function AddOption( select, node )
{
var option = document.createElement( 'option' );
option.value = node.selectSingleNode( '@value' ).text;
option.text = node.text;

select.options.add( option );
}

// Elimina todos los options de un DropDownList
// especialmente usado para el _child
function RemoveAll( select )
{
while( select.options.length > 0 )
select.options.remove( 0 );
}

// UrlTarget (Objecto)
// Permite realizar la construccion de una peticion POST hacia un destino
function UrlTarget( target )
{
this._empty = true;
this._target = target;
this._post = '';

this.Append = UrlTarget_Append;
}

// Metodo
function UrlTarget_Append( name, value )
{
if( this._empty )
this._empty = false;
else
this._post += '&';

this._post += name + '=' + value;
}

// WebService (Objeto)
// Encapsula el pedido a un XML Web Service a traves de peticiones POST
function WebService( Url )
{
this._url = Url;
this.Execute = WebService_Execute;
}

function WebService_Execute()
{
factory = new FactoryDom();
http = factory.CreateHttp();
http.Open( 'POST', this._url._target, false );
http.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' )
http.Send( this._url._post );

if( http.status != 200 )
{
message = '';
message += 'Hubo un error al ejecutar el WebService\n';
message += 'url: ' + this._url._target + '\n';
message += 'post: ' + this._url._post + '\n';
message += 'status: ' + http.status;

alert( message );
if( http.status == 500 )
_error.innerHTML = http.responseText;
return null
}

return http.responseText;
}
</script>
</HEAD>
<body onload="Load();">
<form id="ParentChild" method="post" runat="server">
<table>
<tr>
<td><asp:dropdownlist id="_parent" runat="server" Width="168px"></asp:dropdownlist></td>
</tr>
<tr>
<td><asp:dropdownlist id="_child" runat="server" Width="168px"></asp:dropdownlist></td>
</tr>
<tr>
<td id="_error"></td>
</tr>
</table>
<asp:Button id="_submit" runat="server" Text="Submit"></asp:Button>

<!-- Estos elementos no seran vistos en la interfaz del usuario -->
<div style="display: none">
<asp:TextBox id="_parent_selected" runat="server"></asp:TextBox>
<asp:TextBox id="_child_selected" runat="server"></asp:TextBox>
</div>
</form>
</body>
</HTML>

--- End File ---


Lo curioso en este archivo es el uso de dos objetos javascript, importantes para nuestro objetivo, pues ambos nos permiten encapsular cierta funcionalidad y no repetir codigo innecesariamente.

El primero de ellos es UrlTarget, este objeto permite definir cual sera nuestra destino para realizar una peticion y almacena la informacion a enviar a dicho destino, en el formato POST que todos conocemos.

El segundo es WebService, este objeto estable la forma de consumir un XML Web Service que existen en algun lugar de la red. La forma de consumir dicho servicio web es a traves del protocolo POST. Sin embargo, esto no excluye que no podamos hacer peticiones GET o SOAP. Para lo primero, GET, es mucho mas secillo, pues basta con pasar la informacion a traves del url pero, con las limitaciones conocidas de hacerlo por esa forma. Para lo segundo, SOAP, solo habra que construir el documento XML que se ajusta a la forma de solicitar informacion al servicio web y enviarlo como parte de la peticion.

Cuando testean sus XML Web Services a traves del VS.NET, veran los detalles en que esta permitido la forma en que podran consumirlo. Revisan dichas formas y listo.


Archivo .ASPX.CS

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

namespace DemoWebAppCSharp.Services
{
/// <summary>
/// Descripción breve de Ubigeo.
/// </summary>
public class ParentChild : System.Web.UI.Page
{
protected System.Web.UI.WebControls.DropDownList _parent;
protected System.Web.UI.WebControls.Button _submit;
protected System.Web.UI.WebControls.TextBox _parent_selected;
protected System.Web.UI.WebControls.TextBox _child_selected;
protected System.Web.UI.WebControls.DropDownList _child;

private void Page_Load(object sender, System.EventArgs e)
{
if( ! this.IsPostBack )
{
// Enganchando metodos a los eventos DHTML correpondientes
this._parent.Attributes.Add( "onchange", "Show(this);" );
this._submit.Attributes.Add( "onclick", "Capture();" );
}
}

#region Código generado por el Diseñador de Web Forms
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: llamada requerida por el Diseñador de Web Forms ASP.NET.
//
InitializeComponent();
base.OnInit(e);
}

/// <summary>
/// Método necesario para admitir el Diseñador. No se puede modificar
/// el contenido del método con el editor de código.
/// </summary>
private void InitializeComponent()
{
this._submit.Click += new System.EventHandler(this._submit_Click);
this.Load += new System.EventHandler(this.Page_Load);

}
#endregion

private void _submit_Click(object sender, System.EventArgs e)
{
// Muestra la informacion que ha sido posteada
this.Response.Write( "Parent: " + this._parent_selected.Text + "<br/>Child: " + this._child_selected.Text );
}
}
}

--- End File ---


No hay mucho que explicar de este archivo. Solo, debido a que la informacion para cada DropDownList se ingresa por el lado del cliente a traves de las peticiones a los XML Web Services, en los siguientes postback no habra informacion trascendente en dichos controles. Por lo que se nos apoyamos en los TextBox para salvar la informacion del item seleccionado de los DropDownList por el usuario de manera que podamos enviarsela hacia el servidor por este medio.

La intencion del ejemplo, consite en presentar al usuario dos DropDownList dependiente uno del otro, para efectos practicos les llamaremos _parent y _child. Mas claro el agua.

Los itemes de _parent se solicitan al XML Web Services, al lanzarse el evento onload del documento HTML que presentamos al usuario. Al cual esta enganchado el metodo Load(), en el script por el lado del cliente.

Cada vez que el usuario seleccione un item de _parent, se ejecuta el metodo Show() por el lado del cliente aun, el cual solicitara los itemes de _child a otro WebMethod del XML Web Services, que estamos usando para este ejemplo.


Archivo .ASMX.CS

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;

using System.Xml.Serialization;

namespace DemoWebAppCSharp.Services
{
/// <summary>
/// Descripción breve de General.
/// </summary>
[WebService(Namespace="http://guydotnetxmlwebservices.com")]
public class General : System.Web.Services.WebService
{
public General()
{
//CODEGEN: llamada necesaria para el Diseñador de servicios Web ASP .NET
InitializeComponent();
}

#region Código generado por el Diseñador de componentes

//Requerido por el Diseñador de servicios Web
private IContainer components = null;

/// <summary>
/// Método necesario para admitir el Diseñador. No se puede modificar
/// el contenido del método con el editor de código.
/// </summary>
private void InitializeComponent()
{
}

/// <summary>
/// Limpiar los recursos que se estén utilizando.
/// </summary>
protected override void Dispose( bool disposing )
{
if(disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}

#endregion

[WebMethod]
public ParentContainer ParentRecordSet()
{
ParentContainer container = new ParentContainer();
container.Fill();
return container;
}

[WebMethod]
public ChildContainer ChildRecordSet( Int32 Parent )
{
ChildContainer container = new ChildContainer();
container.Fill();
return container.FilterBy( Parent );
}
}

/// <summary>
/// Business Entity para salvar informacion del tipo Parent
/// </summary>
public class ParentItem
{
private Int32 _id;

public Int32 Id
{
get { return this._id; }
set { this._id = value; }
}

private String _name;

public String Name
{
get { return this._name; }
set { this._name = value; }
}

public ParentItem()
{}

public ParentItem( Int32 Id, String Name )
{
this._id = Id;
this._name = Name;
}
}

/// <summary>
/// Una colleccion del tipo ParentItem fuertemente tipeado
/// </summary>
public class ParentCollection : CollectionBase
{
public void Add( ParentItem Item )
{
this.List.Add(Item);
}

public ParentItem this[ Int32 index ]
{
get { return (ParentItem)List[ index ]; }
set { List[ index ] = value; }
}
}

/// <summary>
/// Un container para la coleccion ParentCollection
/// </summary>
[XmlRoot("ParentRoot")]
public class ParentContainer
{
private ParentCollection _collection;

[XmlArray("ParentCollection")]
public ParentCollection Collection
{
get { return this._collection; }
set { this._collection = value; }
}

public ParentContainer()
{
this._collection = new ParentCollection();
}

/// <summary>
/// Llena informacion inicial en la coleccion
/// </summary>
public void Fill()
{
this._collection.Add( new ParentItem( 1, "simplegeek" ) );
this._collection.Add( new ParentItem( 2, "spoutlet" ) );
this._collection.Add( new ParentItem( 3, "fabrik" ) );
}
}

/// <summary>
/// Business Entity para salvar informacion del tipo Child
/// </summary>
public class ChildItem
{
private Int32 _id;

public Int32 Id
{
get { return this._id; }
set { this._id = value; }
}

private Int32 _parent;

public Int32 Parent
{
get { return this._parent; }
set { this._parent = value; }
}

private String _name;

public String Name
{
get { return this._name; }
set { this._name = value; }
}

public ChildItem()
{
}

public ChildItem( Int32 Id, Int32 Parent, String Name )
{
this._id = Id;
this._parent = Parent;
this._name = Name;
}
}

/// <summary>
/// Una colleccion del tipo ChildItem fuertemente tipeado
/// </summary>
public class ChildCollection : CollectionBase
{
public void Add( ChildItem Item )
{
this.List.Add(Item);
}

public ChildItem this[ Int32 index ]
{
get { return (ChildItem)List[ index ]; }
set { List[ index ] = value; }
}
}

/// <summary>
/// Un container para la coleccion ChildCollection
/// </summary>
[XmlRoot("ChildRoot")]
public class ChildContainer
{
private ChildCollection _collection;

[XmlArray("ChildCollection")]
public ChildCollection Collection
{
get { return this._collection; }
set { this._collection = value; }
}

public ChildContainer()
{
this._collection = new ChildCollection();
}

/// <summary>
/// Llena informacion inicial a la coleccion
/// </summary>
public void Fill()
{
this._collection.Add( new ChildItem( 1, 1, "longhorn" ) );
this._collection.Add( new ChildItem( 2, 1, "avalon" ) );
this._collection.Add( new ChildItem( 3, 2, "soa" ) );
this._collection.Add( new ChildItem( 4, 2, "ws" ) );
this._collection.Add( new ChildItem( 5, 2, "indigo" ) );
this._collection.Add( new ChildItem( 6, 3, "nodes" ) );
this._collection.Add( new ChildItem( 7, 3, "network" ) );
this._collection.Add( new ChildItem( 8, 3, "agents" ) );
}

/// <summary>
/// Filtra la collection inicial a traves de un criterio
/// </summary>
/// <param name="Parent">El codigo del padre</param>
/// <returns></returns>
public ChildContainer FilterBy( Int32 Parent )
{
ChildContainer child = new ChildContainer();

foreach( ChildItem Item in this._collection )
{
if( Item.Parent == Parent )
child._collection.Add( Item );
}

return child;
}
}
}

--- End File ---


Para este archivo tenemos que explicar algunas cosas.

Primero, podran percatarse el uso del System.Xml.Serialization, este namespace permite -obviamente- serializar la informacion que tengamos de las clases que implementemos. De por si, cuando intentamos devolver informacion a traves de un XML Web Services, cometemos el un error involutario al hacerlo como un DataSet, esta estructura es de uso generico y para efectos de implemtacion de nuestra aplicaciones en general dentro del mundo .NET, su uso solo deberia estar sesgado -en lo posible- a la capa de acceso a datos.

En las demas capas, necesitamos de estructuras de datos, fuertemente tipeadas -AKA Strongly Typed- para brindar una mayor escalabilidad en el producto que entregamos.

En nuestro ejemplo, usamos el CollectionBase, para dar soporte a las coleciones que necesitmos implementar. Su uso es recomendado para encapsular conjuntos de datos para cada uno de los Business Entities que requerimos. Su comportamiento es relativamente similar en cada diferente coleccion, es decir: Add, Remove, this, etc.

Cuando se libere la proxima version de VS.NET (Codename: Whidbey), esta soportara la version 2.0 de C#, la que traera entre algunas cosas interesantes, el uso de templates <T>, dicha estructura nos permitira reducir muchas lineas de codigo, y seran usadas muy puntualmente para este tipo de requerimiento, las colecciones.

Volviendo a nuestro ejemplo en el codigo, se observa tambien, el uso de ciertos atributos como: XmlArray, XmlRoot, etc.

Estos atributos, se encuentran dentro del System.Xml.Serialization, y permiten customizar la forma en que se ha de serializar la estructura que vayamos a devolver a traves del XML Web Services.

Si intentasen, devolver un ArrayList a traves de un WebMethod, veran que el documento XML entregado a quien consuma dicho servicio web, tendra un vocabulario, similar a este.


<ArrayOfAnyType>
<anyType />
<anyType />
...
</ArrayListOfAnyType>


Obviamente, dicho vocabulario, no nos da mucha informacion -que digamos- debido a que se encuentra en terminos genericos.

El uso de Business Entities, Collections (a traves de CollectionBase) y Containers, permitiran entregar un documento XML mucho mas explicito en la informacion que contienen.


Archivo PARENT.XSLT

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ws="http://guydotnetxmlwebservices.com">

<xsl:template match="/">
<select>
<xsl:apply-templates select="/ws:ParentRoot/ws:ParentCollection/ws:ParentItem" />
</select>
</xsl:template>

<xsl:template match="ws:ParentItem">
<option>
<xsl:attribute name="value">
<xsl:value-of select="ws:Id" />
</xsl:attribute>

<xsl:value-of select="ws:Name" />
</option>
</xsl:template>
</xsl:stylesheet>

--- End File ---



Archivo Child.XSLT

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ws="http://guydotnetxmlwebservices.com">

<xsl:template match="/">
<select>
<xsl:apply-templates select="/ws:ChildRoot/ws:ChildCollection/ws:ChildItem" />
</select>
</xsl:template>

<xsl:template match="ws:ChildItem">
<option>
<xsl:attribute name="value">
<xsl:value-of select="ws:Id" />
</xsl:attribute>

<xsl:value-of select="ws:Name" />
</option>
</xsl:template>
</xsl:stylesheet>

--- End File ---


Estos dos ultimos archivos que aqui les alcanzo, permite tranformar la el documento XML que se obtiene a traves del XML Web Services, para consefuir un vocabulario que nos permita una mayor facilidad para construir los OPTIONS de cada DropDownList, por el lado del cliente.

Es importante recalcar, el uso de este namespaces dentro de los documentos XSLT:

xmlns:ws="http://guydotnetxmlwebservices.com"

Esta definicion, simplemente es necesaria, si la obviasemos, no podremos conseguir los resultados deseados al realizar las transformaciones.

Hay que entender que el documento XML viene firmado por un namespace, desde su origen, en la definicion misma del XML Web Services. Dicho namespace debe ser tomado encuenta, cada vez que usemos dicho documento, dentro de nuestra aplicacion.

Call a .NET Webservice using XMLHTTP, XMLDOM and VBScript
http://www.eggheadcafe.com/articles/20001006.asp

Web Methods Make it Easy to Publish Your App's Interface over the Internet
http://msdn.microsoft.com/msdnmag/issues/02/03/WebMethods/default.aspx

No comments: