Gerardo Contijoch

Experiencias del día a día trabajando con .NET – ASP.NET, C#, ASP.NET MVC y demas…

Archive for 27 junio 2009

Investigando los eventos de la clase HttpApplication en ASP.NET

Posted by Gerardo Contijoch en junio 27, 2009

Hace unos días me encontré con este interesantísimo post de Rick Strahl en donde responde a una pregunta que alguien le hizo. La pregunta en cuestión es ¿Cómo es que los handlers de los eventos Application_ en ASP.NET son enlazados para que sean invocados automáticamente?

Es una pregunta interesante ya que normalmente uno enlaza a mano los eventos que desea capturar, pero en el caso de los eventos de la clase HttpApplication, la situación es diferente porque no podemos enlazarlos nosotros mismos (en realidad si podemos, pero no tiene ningún efecto). Veamos como es el asunto.

Cuando agregamos un archivo Global.asax a nuestro proyecto vemos que la clase Global ya posee algunos handlers creados:

   1: public class Global : System.Web.HttpApplication {
   2:
   3:     protected void Application_Start(object sender, EventArgs e) { }
   4:
   5:     protected void Session_Start(object sender, EventArgs e) { }
   6:
   7:     protected void Application_BeginRequest(object sender, EventArgs e) { }
   8:
   9:     protected void Application_AuthenticateRequest(object sender, EventArgs e) { }
  10:
  11:     protected void Application_Error(object sender, EventArgs e) { }
  12:
  13:     protected void Session_End(object sender, EventArgs e) { }
  14:
  15:     protected void Application_End(object sender, EventArgs e) { }
  16: }

Lógicamente, al handler Application_Start nunca vamos a poder enlazarlo nosotros mismos ya que el mismo es ejecutado cuando la aplicación se inicia, pero el resto parecería que si podría ser enlazado debido a que ocurre luego de la inicialización de la aplicación. Bueno, no es tan así.

Para empezar, Application_Start, Application_End y Session_End parecen handlers pero en realidad no existe ningún evento asociado a los mismos, por lo que no es posible enlazarlos de otra manera. La clase HttpApplicationFactory es la encargada de simular estos eventos con los métodos FireApplicationOnStart(), FireApplicationOnEnd() y FireSessionOnEnd().

Segundo, el resto de los handlers no son mas que accesos directos a los eventos de distintos módulos (clases que implementan IHttpModule) que pueden ser cargados. Estos módulos son configurados en el archivo web.config en la seccion <httpModules>. Por ejemplo, uno de los que se carga por defecto es el llamado Session (de tipo System.Web.SessionState.SessionStateModule). El mismo es el encargado de mantener una sesión entre distintos requests y su carga esta definida en el archivo web.config que se encuentra en %WIN_DIR%\Microsoft.NET\Framework\[VERSION_FRAMEWORK]\CONFIG\ (este web.config se carga para todas las aplicaciones que se corran en nuestro equipo). Si en el web.config de nuestra aplicación agregamos la siguiente línea a la sección <httpModules>:

   1: <remove name="Session"/>

veremos que el handler Session_Start no se ejecuta más, ya que no hay ningún módulo llamado Session que provoque un evento Start (y como consecuencia, nuestra aplicación ya no tiene soporte para sesiones). Como no tenemos acceso a esos módulos sino hasta que la aplicación ya haya sido inicializada, no hay manera de enlazar esos eventos con los handlers y por eso necesitamos estos accesos directos que nos provee la clase HttpApplication.

Como se puede ver en el artículo original de Rick Strahl, el enlace ocurre en un método llamado HookupEventHandlersForApplicationAndModules() (con ese nombre, ¿a alguien le queda alguna duda de ello?). Resumiendo la funcionalidad del método, básicamente lo que sucede es que, dado un array de objetos MethodInfo (son cada uno de los handlers que creamos), se itera sobre ellos y se determina si el evento es de la aplicación o pertenece a algún módulo. Esto se logra buscando el ‘_’ en el nombre del mismo y tomando la primer parte. Luego se guarda una referencia a la instancia de HttpApplication o al módulo y se busca via reflection el evento en base al nombre del método (el evento ‘Start’ se asocia al método terminado en ‘Start’, por ejemplo). Un detalle curioso es que esta lógica reconoce a ‘Application_BeginRequest’ y a ‘Application_OnBeginRequest’ como lo mismo, teniendo precedencia el primero si es que se encuentran ambos presentes. Luego se valida que el handler tenga los parámetros correctos y si no es así, se genera un proxy para el mismo (lo que significa que nuestros handlers pueden no aceptar ningún parámetro y van a a ser igual de válidos). Finalmente después de obtener una referencia al evento correcto, se invoca al método Add del mismo para asociar nuestra instancia de MethodInfo (o el proxy que mencioné hace un momento) al evento y de este modo, cuando ocurra, se ejecuta nuestro handler.

¿Muy complicado? Si, seguro, la clase HttpApplication es una de las más complejas de .NET Framework, pero eso es lo que la hace interesante, ¿no?

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Anuncios

Posted in ASP.NET, Desarrollo Web | Etiquetado: , , , | Leave a Comment »

Inicialización del ModelState en ASP.NET MVC

Posted by Gerardo Contijoch en junio 21, 2009

Hoy mientras trabajaba en un site ASP.NET MVC aprendí una lección que espero no olvidar jamás (su olvido me podría traer muchos dolores de cabeza).

El problema se presentó cuando uno de los campos del form con el que estaba trabajando no se actualizaba luego de un postback. El form no tenia nada de especial, sólo un par de campos, y su posteo provocaba la ejecución de una acción que tampoco hacia nada muy loco. Lo único destacable era que el parámetro con problemas era cargado con un ModelBinder personalizado, pero el mismo funcionaba bien y lo inicializaba correctamente al valor. Lo primero que hice fue verificar que las variables del post lleguen correctamente al server. También verifiqué que no haya ningún código javascript interfiriendo con el llenado de campos. Lo que más me desconcertó fue que dentro de la acción, el parámetro cuyo valor tenia problemas estaba correctamente inicializado, cosa que no esperaba (lo que me llevó a pensar que no había un error en el ModelBinder). Sin embargo había un detalle que pasé de largo: entre los valores del ModelState del request no se encontraba el parámetro con problemas. Fue cuando descubrí eso que supe donde estaba el problema. Un poco más de investigación me confirmó las sospechas. Pero antes de explicarles el problema, veamos el código del ModelBinder:

   1: public class TipoDeOperacionModelBinder : IModelBinder {
   2:     #region IModelBinder Members
   3:
   4:     public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
   5:
   6:         if (bindingContext == null) {
   7:             throw new ArgumentNullException("bindingContext");
   8:         }
   9:
  10:         ValueProviderResult val;
  11:         if (!bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out val)) {
  12:             // Si no encontramos el valor, devolvemos null y ASP.NET MVC se encarga de provocar la excepción necesaria
  13:             return null;
  14:         }
  15:
  16:         int realVal = (int)val.ConvertTo(typeof(int));
  17:         return (TipoDeOperacion)realVal;
  18:     }
  19:
  20:     #endregion
  21: }

Este ModelBinder simplemente toma un valor (indicado por bindingContext.ModelName) y lo transforma en un Enum de tipo TipoDeOperacion. Cabe aclarar que este ModelBinder esta registrado para el tipo TipoDeOperacion, por lo que el propio framework es el encargado de hacer uso del mismo cuando necesite hacer el binding de un parámetro de tipo TipoDeOperacion.

Como dije antes, el parámetro en cuestión se inicializaba correctamente, lo que confirmaba que las variables del post llegaban sin problemas. El problema estaba en que no se encontraba dentro del ModelState y eso se debe sencillamente a que en ningún lugar del código del ModelBinder lo estoy agregando. Resulta que el ModelBinder no solo es responsable de cargar los parámetros de las acciones y actualizar el modelo con el que estamos trabajando, sino que ¡también debe inicializar el ModelState! El resto de los parámetros de la acción no tenían problemas ya que eran procesados por la clase DefaultModelBinder, la cual se encarga de todo. Todo se resolvió cuando agregué el siguiente código en la línea 15:

bindingContext.ModelState.SetModelValue(bindingContext.ModelName, val);

Parece una pavada de detalle, pero esa sola línea de código me hizo perder varias horas de trabajo por no saber que el ModelState era cargado por los ModelBinders. Esto demuestra la importancia de entender bien las herramientas y frameworks con los que uno trabaja. ASP.NET MVC nos provee de montones de puntos de extensión, pero de nada sirve si no los sabemos aprovechar correctamente.

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Posted in ASP.NET MVC | Etiquetado: , , | Leave a Comment »

Generación personalizada de Ids en ASP.NET 4.0

Posted by Gerardo Contijoch en junio 12, 2009

En mi último post les comentaba sobre un gran problema que hay en ASP.NET (por lo menos hasta que salga la 4.0) con respecto a la generación automática de los ids de controles web. Resumidamente, el problema esta en que ASP.NET puede cambiar el id original que le dimos a nuestros controles (por ejemplo, txtNombre por “ctl00_ContentPlaceHolder1_txtNombre”), lo cual suele provocar que nuestros scripts en javascript deje de funcionar. Casualmente, unas horas después de publicado el post, me encuentro con éste artículo en donde se explica como en ASP.NET 4.0 esto deja de ser un problema ya que se permite la personalización del proceso de generación de ids.

Cabe aclarar que el comportamiento que voy a describir a continuación esta basado en ASP.NET 4.0 CTP y no en la versión definitiva (la cual no salió aún), pero imagino que este es uno de los puntos en donde no va a cambiar el funcionamiento. Otro punto a aclarar es que el id se ve modificado sólo del lado del cliente, es decir, el código del lado del servidor no se ve modificado en nada.

El atributo ClientIDMode

La personalización del Id ahora se puede realizar mediante un nuevo atributo de los controles web llamado ClientIDMode. Dependiendo de su valor, el id generado para el control que lo aplique puede ser fijo (siempre el mismo), dinámico (basado en datos asociados al control) o automático (es decir, igual que en las versiones anteriores de ASP.NET).

Sus valores posibles son:

  • Legacy
  • Static
  • Predictable
  • Inherit

Veamos uno por uno.

Legacy

Este valor indica que el comportamiento de ASP.NET al generar el id va a ser igual al de las versiones anteriores, es decir, el id va a tener el siguiente formato: [IdControlContenedor]_[IdControlHijo].

Static

Éste sea posiblemente el valor más utilizado ya que indica que el id generado sea el que definimos nosotros (es decir, que quede estático o fijo) y no se vea modificado por nada. Hay que tener en mente que esto puede traer conflictos de ids si hay un contenedor con un control con el mismo id dentro de este. Por ejemplo:

   1: <body>
   2:     <form id="form1" runat="server">
   3:     <div>
   4:         <asp:TextBox ID="txtNombre" runat="server" ClientIDMode="Static"></asp:TextBox>
   5:         <uc1:MiControlWeb ID="MiControlWeb1" runat="server" />
   6:     </div>
   7:     </form>
   8: </body>

Donde MiControlWeb esta definido así:

   1: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="MiControlWeb.ascx.cs" Inherits="WebUserControl" %>
   2: <asp:TextBox ID="txtNombre" runat="server" ClientIDMode="Static"></asp:TextBox>

Esto quedaría renderizado así:

   1: <div>
   2:   <input name="txtNombre" type="text" id="txtNombre" />
   3:   <input name="MiControlWeb$txtNombre" type="text" id="txtNombre" />
   4: </div>

Este atributo no debería usarse nunca dentro de un control como Repeater, ya que los ids de los controles contenidos en el serían todos iguales.

Inherit

Este es el modo por defecto, y simplemente indica que el valor de ClientIDMode se hereda del contenedor donde se encuentre el control (como pueden ser un WebControl, un ContentPlaceHolder – en MasterPages- o una página).

Predictable

Este modo de generación produce resultados similares al modo Legacy pero, como lo indica su nombre, los valores son predecibles. Es principalmente útil en los controles enlazados a datos. El id en este caso se genera mediante el uso del dato asociado o enlazado al control. Veamos como funciona.

Dado este control:

   1: <asp:GridView ID="gvPersonas" runat="server" AutoGenerateColumns="false" ClientIDMode="Predictable">
   2: <Columns>
   3:     <asp:TemplateField HeaderText="Id">
   4:          <ItemTemplate>
   5:               <asp:Label ID="lblPersonaId" runat="server" Text='<%# Eval("Id") %>' />
   6:          </ItemTemplate>
   7:     </asp:TemplateField>
   8:     <asp:TemplateField HeaderText="Nombre">
   9:          <ItemTemplate>
  10:               <asp:Label ID="lblNombre" runat="server" Text='<%# Eval("Nombre") %>' />
  11:          </ItemTemplate>
  12:     </asp:TemplateField>
  13:     <asp:TemplateField HeaderText="Apellido">
  14:          <ItemTemplate>
  15:               <asp:Label ID="lblApellido" runat="server" Text='<%# Eval("Apellido") %>' />
  16:          </ItemTemplate>
  17:     </asp:TemplateField>
  18: </Columns>
  19: </asp:GridView>

El Html renderizado se vería así:

   1: <table id="gvPersonas" style="border-collapse: collapse" cellspacing="0" rules="all" border="1">
   2:   <tbody>
   3:     <tr>
   4:        <th scope="col">Id</th>
   5:        <th scope="col">Nombre</th>
   6:        <th scope="col">Apellido</th>
   7:     </tr>
   8:     <tr>
   9:        <td><span id="gvPersonas_lblPersonaId_0">1</span></td>
  10:        <td><span id="gvPersonas_lblNombre_0">Juan (primer persona)</span></td>
  11:        <td><span id="gvPersonas_lblApellido_0">Lopez</span></td>
  12:     </tr>
  13:     ...
  14:     <tr>
  15:        <td><span id="gvPersonas_lblPersonaId_35">36</span></td>
  16:        <td><span id="gvPersonas_lblNombre_35">Gerardo (36ta persona)</span></td>
  17:        <td><span id="gvPersonas_lblApellido_35">Contijoch</span></td>
  18:     </tr>
  19:   </tbody>
  20: </table>

Noten el sufijo numérico al final de los Ids. Este sufijo puede ser personalizado en los controles enlazados a datos para que sea otro valor y no un simple índice. Esto se logra mediante la aplicación del atributo RowClientIDSuffix. El valor de este atributo debe ser nombre de la propiedad o columna (dependiendo del origen de datos) cuyo valor deseamos incluir en el Id. Veamos un ejemplo para aclarar esto. Dado este código:

   1: <asp:GridView ID="gvPersonas" runat="server" AutoGenerateColumns="false" ClientIDMode="Predictable" RowClientIDSuffix="Apellido">
   2: <Columns>
   3:     <asp:TemplateField HeaderText="Id">
   4:          <ItemTemplate>
   5:               <asp:Label ID="lblPersonaId" runat="server" Text='<%# Eval("Id") %>' />
   6:          </ItemTemplate>
   7:     </asp:TemplateField>
   8:     <asp:TemplateField HeaderText="Nombre">
   9:          <ItemTemplate>
  10:               <asp:Label ID="lblNombre" runat="server" Text='<%# Eval("Nombre") %>' />
  11:          </ItemTemplate>
  12:     </asp:TemplateField>
  13:     <asp:TemplateField HeaderText="Apellido">
  14:          <ItemTemplate>
  15:               <asp:Label ID="lblApellido" runat="server" Text='<%# Eval("Apellido") %>' />
  16:          </ItemTemplate>
  17:     </asp:TemplateField>
  18: </Columns>
  19: </asp:GridView>

Se genera lo siguiente:

   1: <table id="gvPersonas" style="border-collapse: collapse" cellspacing="0" rules="all" border="1">
   2:   <tbody>
   3:     <tr>
   4:        <th scope="col">Id</th>
   5:        <th scope="col">Nombre</th>
   6:        <th scope="col">Apellido</th>
   7:     </tr>
   8:     <tr>
   9:        <td><span id="gvPersonas_lblPersonaId_Lopez">1</span></td>
  10:        <td><span id="gvPersonas_lblNombre_Lopez">Juan (primer persona)</span></td>
  11:        <td><span id="gvPersonas_lblApellido_Lopez">Lopez</span></td>
  12:     </tr>
  13:     ...
  14:     <tr>
  15:        <td><span id="gvPersonas_lblPersonaId_Contijoch">36</span></td>
  16:        <td><span id="gvPersonas_lblNombre_Contijoch">Gerardo (36ta persona)</span></td>
  17:        <td><span id="gvPersonas_lblApellido_Contijoch">Contijoch</span></td>
  18:     </tr>
  19:   </tbody>
  20: </table>

Si prestan atención, verán que el sufijo ahora es el valor del campo Apellido en nuestro origen de datos. Una característica interesante del atributo RowClientIDSuffix es que acepta más de un único valor, por lo que es posible definir nuestro control así:

   1: <asp:GridView ID="gvPersonas" ... ClientIDMode="Predictable" RowClientIDSuffix="Id, Apellido">

Generando un HTML como este:

   1: ...
   2: <tr>
   3:    <td><span id="gvPersonas_lblPersonaId_1_Lopez">1</span></td>
   4:    <td><span id="gvPersonas_lblNombre_1_Lopez">Juan (primer persona)</span></td>
   5:    <td><span id="gvPersonas_lblApellido_1_Lopez">Lopez</span></td>
   6: </tr>
   7: ...

Se puede ver que el sufijo ahora consiste en el campo Id y el campo Apellido.

Referencias:

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Posted in ASP.NET, Desarrollo Web | 1 Comment »

Acceder a variables de servidor desde Javascript

Posted by Gerardo Contijoch en junio 6, 2009

Un problema con el que nos solemos encontrar los desarrolladores web es el de acceder desde un script (en el cliente) a datos que se encuentran sólo disponibles en el servidor. No me refiero a recursos que están en el servidor, sino mas bien a valores o datos que pueden ser resultado (o formar parte) de cierto procesamiento y que no se exponen al cliente.

El caso más común con el que me encuentro yo es conocer el id de ciertos controles desde el lado del cliente. Sabemos bien que ASP.NET tiene la costumbre de renombrar nuestros controles con unos nombres bastante crípticos y esto es un gran problema si queremos acceder a ellos desde javascript. El verdadero nombre de estos controles (lo que se conoce como el ClientId) se encuentra accesible desde el código del lado del servidor, pero no desde el cliente. Y es por esto que Rick Strahl creo la clase wwScriptVariables que nos permite pasarle a nuestros scripts los valores que querramos desde el servidor.

Veamos un ejemplo con el cual vamos a trabajar. Imaginemos que tenemos una página en donde se ingresa un valor y como respuesta, se informa si ese valor es numérico o no. Esta es la parte interesante del código HTML:

   1: <div id="valor">Ingrese un valor: <asp:TextBox ID="txtValor" runat="server"></asp:TextBox>
   2:     <asp:Button ID="btnEnviar" runat="server" Text="Enviar" onclick="btnEnviar_Click" />
   3: </div>
   4: <div id="resultado">
   5:     <asp:Label ID="lblResultado" runat="server" Text=""></asp:Label>
   6: </div>

Y este es el código del lado del servidor:

   1: using System;
   2:
   3: public partial class _Default : System.Web.UI.Page
   4: {
   5:     protected void Page_Load(object sender, EventArgs e){}
   6:
   7:     protected void btnEnviar_Click(object sender, EventArgs e) {
   8:         if (EsNumero(this.txtValor.Text)) {
   9:             this.lblResultado.Text = "Es un número!";
  10:         } else {
  11:             this.lblResultado.Text = "No es un número!";
  12:         }
  13:     }
  14:
  15:     private bool EsNumero(string valor) {
  16:         int val = 0;
  17:         if (int.TryParse(valor, out val)) {
  18:             return true;
  19:         }
  20:         return false;
  21:     }
  22: }

Si ingresamos un valor numérico el resultado se vería así:

RegistrarVariablesServidor-respuesta-original

Como se ve puede ver, todo es muy sencillo, pero imaginemos que queremos aplicarle algún efecto a la página dependiendo del resultado. En mi caso voy a usar jQuery para esto y el efecto va a consistir simplemente en aplicarle un estilo a la respuesta (ya se que lo mismo se puede hacer desde el lado del servidor, pero la idea es modificarlo desde el lado del cliente como un ejemplo).

Normalmente uno agregaría algo similar a esto en la sección head de la página:

   1: <link href="style.css" rel="stylesheet" type="text/css" />
   2:
   3: <script src="jquery-1.2.6.min.js" type="text/javascript"></script>
   4:
   5: <script type="text/javascript" language="javascript">
   6:   $(document).ready(function() {
   7:
   8:     var lbl = $("#lblResultado");
   9:
  10:     lbl.addClass("claseResultado");
  11:
  12:     });
  13: </script >

Pero eso sólo funciona cuando nuestro control (en este caso el label ‘lblResultado’) no se encuentra dentro de un contenedor, como puede ser una grilla o una master page. Esto se debe a que ASP.NET modifica los Id de los elementos renderizados para evitar la colisión de nombres y por eso, nuestro ‘lblResultado’ se puede transformar en algo como ‘ctl00_ContentPlaceHolder1_lblResultado’ provocando un error en nuestro script. Lo que necesitamos es pasarle a nuestros scripts el verdadero nombre de los controles lo que se puede lograr con algunos code nuggets incrustados en medio de nuestro script:

   1: $(document).ready(function() {
   2:     var lbl = $("#<%= lblResultado.ClientId %>");
   3:     lbl.addClass("claseResultado");
   4: });

Pero esto tiene algunos problemas. Primero y principal, si nuestro script esta en un archivo externo y no embebido en la página, no se pueden usar code nuggets. Otro problema es que el valor que pasemos tiene que estar correctamente escapado para que javascript no lo malinterprete o falle. Sumado a esto, no es muy elegante la solución de incrustar código de servidor dentro de un script y no facilita para nada la reutilización de los scripts.

Ahora veamos como solucionamos este inconveniente con la ayuda de wwScriptVariables. Su uso es sencillísimo, solo hay que instanciarla y mediante el método Add() agregamos los valores que queremos pasarle a nuestros scripts. Luego, desde el lado del cliente, podemos acceder a estos valores desde una nueva variable que tendremos a nuestra disposición, la cual es generada (durante el renderizado) por la clase wwScriptVariables. Veamos como queda nuestro ejemplo con el uso de esta clase.

Del lado del servidor:

   1: using System;
   2:
   3: public partial class NuevaVersion : System.Web.UI.Page
   4: {
   5:     protected void Page_Load(object sender, EventArgs e){}
   6:
   7:     protected void btnEnviar_Click(object sender, EventArgs e) {
   8:
   9:         Westwind.Web.Controls.Controls.wwScriptVariables scriptVar = new Westwind.Web.Controls.Controls.wwScriptVariables(this, "variables");
  10:         scriptVar.Add("lblResultado", this.lblResultado.ClientID);
  11:
  12:         if (EsNumero(this.txtValor.Text)) {
  13:             scriptVar.Add("esNumero", "true");
  14:             this.lblResultado.Text = "Es un número!";
  15:         } else {
  16:             scriptVar.Add("esNumero", "false");
  17:             this.lblResultado.Text = "No es un número!";
  18:         }
  19:     }
  20:
  21:     private bool EsNumero(string valor) {
  22:         int val = 0;
  23:         if (int.TryParse(valor, out val)) {
  24:             return true;
  25:         }
  26:         return false;
  27:     }
  28: }

y del lado del cliente:

   1: $(document).ready(function() {
   2:     var lbl = $("#" + variables["lblResultado"]);
   3:
   4:     // Aplicamos distintas clases dependiendo del resultado
   5:     if (variables["esNumero"] == "true")
   6:     {
   7:         lbl.addClass("resultadoNumero");
   8:     } else {
   9:         lbl.addClass("resultadoTexto");
  10:     }
  11: });

El funcionamiento de la clase es muy sencillo, básicamente lo que hace es registrar los valores que queremos pasar al cliente y dentro del evento PreRender genera el siguiente script:

   1: <script type="text/javascript">
   2: //<![CDATA[
   3: var variables = {
   4:     "lblResultado": 'ctl00_ContentPlaceHolder1_lblResultado',
   5:     "esNumero": 'false'
   6: }
   7: //]]>
   8: </script >

Lo mejor de todo es que Rick pone a nuestra disposición el código completo de la clase para que la modifiquemos a gusto si así lo que deseamos.

La página donde pueden bajar esta clase es ésta, pero aclaro que esa es una versión vieja, hay una versión más nueva acá, la cual agrega nuevas funcionalidades como la posibilidad de actualizar el valor de las variables desde el cliente y pasárselas al servidor. Si no nos interesa esto último, les recomiendo usar la primer versión, ya que tiene menos dependencias y es mucho mas liviana.

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Posted in ASP.NET, Desarrollo Web, Javascript | 1 Comment »