Gerardo Contijoch

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

Archive for the ‘ASP.NET’ Category

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 »

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 »

Aprender ASP.NET MVC

Posted by Gerardo Contijoch en abril 25, 2009

Hace poco surgió en la blogósfera, una vez mas, la discusión sobre si ASP.NET MVC es mejor o peor que ASP.NET WebForms y si vale la pena aprender a usarlo. No es la primera vez que hay una discusión de este tipo y no será la última.

Esta vez quisiera aportar mi granito de arena a la discusión y para ello les voy a contar una breve historia personal.

Comencé a trabajar con WebForms hace unos 6 años. Esa fue mi primer experiencia Web, nunca antes había trabajado mas que con HTML puro. Como la mayoría en ese momento (imagino yo), empecé con los tutoriales de Microsoft y con algún Starter Kit. Luego llegó el programa Desarrollador 5 Estrellas de Microsoft, al cual seguí. Y no olvidemos los blogs y sites como Code Project o 4 Guys From Rolla, de los cuales uno puede aprender muchísimo. Sin embargo me resulto muy complicado comenzar a trabajar. Y no era que me faltara dominio del manejo de objetos o falta de habilidades para entender conceptos de programación, ya que en cualquier otro tipo de proyectos no tenía ningún problema. Llegó el punto en que llegué a detestar el desarrollo web. No quería saber nada de la Web, ¡no entendía como había gente que le gustara! Era terriblemente complicado para mi trabajar con conceptos como controles de usuario. Entendía que de alguna manera un objeto ‘TextBox’ terminaba siendo un ‘<input>’ del lado del cliente. Entendía que el ViewState ayudaba a que no se pierdan los valores de los controles. Entendía la diferencia entre procesamiento del lado del cliente y procesamiento del lado del servidor. Sin embargo no entendía como sucedía todo eso y ese era mi problema (y es el de muchos otros). ASP.NET hace un excelente trabajo ocultándole al usuario la verdadera naturaleza de la web y del desarrollo web. ¡No fue hasta un par de años después de comenzar a trabajar con ASP.NET que entendí que era POST y GET! Es increíble, pero sí, hice desarrollo web sin saber lo que era POST y GET (entre muchas otras cosas, por supuesto). Es como conducir un tren sin saber que tiene que ir sobre rieles, el tren va derecho sin que uno haga nada, pero uno no sabe porque es así.

Afortunadamente me topé con el blog de InfinitiesLoop y su famosísimo post Truly Understanding ViewState. Créanme que no estoy exagerando cuando les digo que ese post cambio para siempre mi forma de ver ASP.NET. Fue un click, un instante en que cambió todo. Desde ese momento dejé creer en la magia de ASP.NET y comencé a buscar el porqué y el cómo de todo lo que sucedía en una página. Y fue durante esa búsqueda en la que aprendí que era realmente el desarrollo web. No podía creer todas las ‘mentiras’ (por llamarlas de alguna manera) que había aprendido de ASP.NET. Ojo, ASP.NET no es que sea malo por eso, pero el camino natural de aprendizaje de ASP.NET lo lleva a uno a pensar en terminos de ‘Textboxes’, ‘UpdatePanels’ y propiedades que ‘mágicamente’ persisten en el tiempo en un entorno ‘stateless’ (sin estado) en vez de en posts, forms y requests asincrónicos, que es lo que realmente uno necesita dominar.

¿A qué viene todo esto? Sigan leyendo.

Yo comencé a jugar con ASP.NET MVC desde la Preview 3 (ya hace casi un año) y desde el primer momento me atrapó. No había controles de servidor que me abstraigan de cosas que en realidad no debería abstraerme. No había ViewState, ya no era necesario preocuparme por que guardaba y que no guardaba ahí ni por el tamaño de este. No había más misteriosos scritps que eran inyectados en las páginas sin que lo sepamos. No tenia que preocuparme por decidir si realmente me convenía manejar un evento de un campo de texto en el cliente o del lado del servidor. No existía mas el ciclo de vida de las paginas y sus controles (vean la comparación). Era un enfoque totalmente diferente, en donde el desarrollo web solo se limita a las vistas.

Como podrán notar, tengo preferencia por ASP.NET MVC frente al desarrollo clásico con WebForms, pero eso no significa que no siga trabajando con ellos. Lo sigo haciendo, pero ahora, a diferencia de hace algunos años, se lo que hago, se los pros y los contras de usar o no usar un WebControl, se cuando conviene escribir un HttpHandler en vez de una página y se que consecuencias puede traerme el deshabilitar el ViewState para una página. Ahora tengo más control sobre lo que hago y cómo lo hago porque lo entiendo.

De todos modos prefiero ASP.NET MVC por ser mucho mas sencillo de trabajar. Es más modular (puedo hacer un site entero sin haber escrito una línea de código de lógica de negocios o haber creado las tablas en una base de datos), más fácil de testear (la forma en que esta diseñado lo permiten), es totalmente extensible o personalizable (se puede inyectar código en casi cualquier parte de la cadena de procesamiento de un request), los sites tienden a ser mas livianos (no hay mas ViewState o scripts inyectados por defecto, ni hay que renderizar nada, todo es puro html), etc. Parte de esto se debe a la aplicación del patrón MVC, y parte a la forma en que fue implementado por Microsoft.

¿Porqué aprender ASP.NET MVC?

Rob Connery menciona algunas cuestiones por las que él considera que uno debería aprender ASP.NET MVC y no puedo estar más de acuerdo con él. Sobre todo por la anteúltima: Aprender nuevos conceptos. Usando este framework uno puede aprender nuevos patrones y principios de diseño (Model-View-Controller, Repository, Dependency Injection, Inversion of control, Factory, Template, etc ), mejorar nuestras habilidades con javascript (lo más probable es que querramos darle cierta interactividad a nuestro site, y para eso usamos javascript), conocer nuevas herramientas o librerías (para javascript lo más común es usar jQuery o prototype, para IoC podemos usar un framework como Castle MicroKernel, para el acceso a datos es común ver implementaciones hechas con Linq, NHibernate y Entity Framework), y por sobre todas las cosas, ser mejores desarrolladores y estar mejor preparados para el futuro al conocer las distintas opciones que tenemos para trabajar.

[Update: Aclaro que con WebForms uno también puede aprender patrones, usar javascript, librerías, etc, pero como mencioné anteriormente, el camino natural de aprendizaje con WebForms no hace evidente todas esas cuestiones, mientras que con ASP.NET MVC, sí.]

Con todo esto no quiero decir que ASP.NET MVC les va a solucionar todos los problemas, y que deban abandonar los WebForms, para nada, pero creo que puede llegar a ser un aporte muy interesante en la carrera de un desarrollador web.

Recursos

Hay muchos recursos para aprender ASP.NET MVC, pero la mayoría esta en inglés. Entre los más recomendables se encuentran:

[Update: Se agregaron links a blogs en español sobre ASP.NET MVC.]

De ahora en adelante voy a tratar de postear más contenido relacionado a ASP.NET MVC ya que es un tema que me interesa muchísimo y hay muy poco material en español. Si les interesa y tienen alguna sugerencia o duda sobre el tema, no duden en hacerla.

¡Nos vemos en el próximo post

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

Posted in ASP.NET, ASP.NET MVC, Desarrollo Web | 4 Comments »

Mis problemas con ASP.NET AJAX

Posted by Gerardo Contijoch en abril 5, 2009

Mi relación con ASP.NET AJAX nunca fue buena. Siempre tuve muchos problemas con la implementación de AJAX de Microsoft. Nunca me gustó usarla, me da la impresión de que es muy pesada y hay que saber demasiado para usarla (los problemas de éste y éste post son un ejemplo de ello). Por cuestiones de trabajo, hace unos meses me vi obligado a usarla y todavía me sigo encontrando con problemas de no muy obvia solución.

Recuerdo cuando usé AJAX por primera vez. Fue con Ajax.NET Professional. Tenia que manejar todo con javascript y no había controles ni nada de eso, el HTML se generaba en el servidor y se enviaba como respuesta al request. Por supuesto que esto no tiene porque ser así necesariamente, se puede generar el HTML en el cliente, pero aún así era mas sencillo de usar. Uno tenia que ensuciarse un poco mas las manos con javascript, pero por lo menos uno tenia control sobre lo que hacía. No me pasa lo mismo con ASP.NET AJAX, no se que es lo que pasa por detrás, no se cuando puede fallar ni donde, el framework inyecta scripts por todos lados… no me termina de convencer. Ojo, no quiero decir que Ajax Pro sea mejor que ASP.NET AJAX, simplemente digo que la solución de Microsoft no me da la seguridad que busco.

Por ejemplo, el caso que mencioné en este post. El error que obtenía era básicamente ‘no se puede cargar el archivo xxxx.js’. Resulta que para cargar un archivo de scripts correctamente (dadas ciertas condiciones, no ocurre siempre) tengo que modificarlo de modo que el mismo notifique que su carga ha finalizado. No es algo muy intuitivo que digamos… Uno puede buscar en muchos lados la respuesta hasta encontrarla.

Otro de los problemas que tengo es el uso de los UpdatePanels. Si no se saben usar, pueden resultar muy peligrosos. Éste post muestra un ejemplo de ello. Creo que una de las características más peligrosas de los UpdatePanels es justamente su facilidad de uso. Da la impresión de que al arrastrar el control a una pagina se nos solucionan muchos problemas, pero podemos estar trayendo muchos mas. Para evitarlo, hay que entender como funcionan y saber como hacerlos funcionar como queremos, pero lamentablemente no es mucha la gente que se toma el trabajo (o tiene tiempo) de leer sobre el funcionamiento interno de los controles y por ello muchas veces son más un problema que una solución.

No se… no me termina de convencer todo esto, supongo que será hasta que me acostumbre…

¿A alguien le sucede lo mismo?

¡Nos vemos en el próximo post!

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

Posted in ASP.NET, Desarrollo Web | Etiquetado: , , | 4 Comments »

Como resolver el error ‘It is an error to use a section registered as allowDefinition=’MachineToApplication’ beyond application level.’

Posted by Gerardo Contijoch en febrero 22, 2009

Hoy mientras estaba configurando la autenticación de un site me encontré con este error. Es un tanto críptico, pero no tan difícil de entender si pensamos un poco (y conocemos otro poco). El mensaje básicamente nos esta diciendo que hay una sección de la configuración (en mi caso la sección <authentication>, pero puede ocurrir lo mismo con otras secciones) que esta registrada con un atributo allowDefinition=’MachineToApplication’ y por eso utilizarla más allá del nivel de aplicación no esta permitido.

El archivo web.config de mi aplicación se ve algo así (resumido para mayor claridad):

   1: <?xml version="1.0"?>
   2: <configuration>
   3:     ...
   4:     <location path="Admin">
   5:         <system.web>
   6:             <authentication mode="Forms">
   7:                 <forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH" defaultUrl="~/Inicio.aspx" protection="Validation"/>
   8:             </authentication>
   9:
  10:             <authorization>
  11:                 <deny users="?" />
  12:             </authorization>
  13:         </system.web>
  14:     </location>
  15:     <system.web>
  16:         <authorization>
  17:             <allow users="*" />
  18:         </authorization>
  19:         ...
  20:     </system.web>
  21: </configuration>

Aparente no tiene nada de raro, se configura la autenticación y autorización del site, así como una de las secciones del site (la rama Admin). El error me decía que la linea 6 era la del problema.

Ahora veamos de entender el problema para poder resolverlo.

Si buscamos en nuestro web.config veremos que la sección <authorization> no esta definida en ningún lugar y eso se debe a que la misma esta definida en el archivo machine.config. El mismo se encuentra en el path ‘C:\Windows\Microsoft.NET\Framework\<versión del framework>\CONFIG\machine.config’.

La sección <authentication> se encuentra definida de la siguiente manera:

   1: <configuration>
   2:     <configSections>
   3:         ...
   4:         <sectionGroup name="system.web" type="System.Web.Configuration.SystemWebSectionGroup, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
   5:             ...
   6:             <section name="authentication" type="System.Web.Configuration.AuthenticationSection, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" allowDefinition="MachineToApplication"/>
   7:             ...
   8:         </sectionGroup>
   9:     </configSections>
  10: </configuration>

Como se puede ver, efectivamente esta sección esta ‘configurada’ para que solo pueda ser utilizada en el machine.config, en el web.config que se encuentra junto con el machine.config o en el web.config que se encuentra en el root de nuestra aplicación (para ver los posibles valores de este atributo y sus significados pueden ir a esta página). Y justamente ese era el problema. En mi web.config la seccion <authentication> estaba siendo utilizada dentro de la sección <location>. Esto es lo mismo que colocarla dentro de un web.config en el directorio especificado en el atributo path de la sección <location> y por eso ASP.NET se quejaba.

Todo se solucionó cuando moví la sección problemática fuera de <location> quedando el web.config de la siguiente manera:

   1: <?xml version="1.0"?>
   2: <configuration>
   3:     <location path="Admin">
   4:         <system.web>
   5:             <authorization>
   6:                 <deny users="?" />
   7:             </authorization>
   8:         </system.web>
   9:     </location>
  10:
  11:     <system.web>
  12:         <authentication mode="Forms">
  13:             <forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH" defaultUrl="~/Inicio.aspx" protection="Validation"/>
  14:         </authentication>
  15:
  16:         <authorization>
  17:             <allow users="*" />
  18:         </authorization>
  19:     </system.web>
  20: </configuration>

La confusión mía estaba en que quería configurar la autenticación dentro de un subdirectorio, cuando en realidad solo tenia que configurar la autorización para ese subdirectorio. La configuración de autenticación se tiene que aplicar a nivel de site y no de subdirectorio, en cambio, la configuración de autenticación si puede ser diferente para distintos subdirectorios.

En esta página pueden encontrar algo mas de información sobre el tema.

Espero que les sea útil.

¡Nos vemos en el próximo post!

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

Posted in .Net 2.0, ASP.NET, Desarrollo Web | Etiquetado: , , , | 10 Comments »

Ejecutar tareas de forma asincrónica en ASP.NET 2.0 (Parte 2)

Posted by Gerardo Contijoch en enero 30, 2009

En el post anterior hable sobre el método AddOnPreRenderCompleteAsync de la clase Page, el cual nos permite ejecutar una tarea de manera asincrónica en un hilo diferente al que esta procesando la página, pero como vimos entonces, este método tiene la desventaja de que sólo nos permite ejecutar una sola tarea y nada más. Hay muchos casos en los que precisamos realizar diferentes operaciones, no necesariamente relacionadas entre sí y para esos casos hay otra solución. Sugiero la lectura de la primera parte de esta serie ya que parte del código que voy a mostrar acá ya fue explicado allí.

La clase Page tiene un método que nos permite registrar tareas que deseamos que sean ejecutadas independientemente del procesamiento de la página. Estoy hablando de RegisterAsyncTask(). Este método acepta un objeto del tipo PageAsyncTask que contiene toda la información necesaria para poder lanzar las tareas y lograr que seamos notificados en su finalización. Estas tareas se van a ejecutar luego del evento PreRender y antes de que se produzca el evento PreRenderComplete (en la primer parte explico un poco el ciclo de vida de las páginas).

Al igual que con AddOnPreRenderCompleteAsync, es necesario agregar en la directiva Page la siguiente propiedad:

   1: <%@ Page Async="true" ... %>

Ahora, con todo listo no hay nada mejor que un ejemplo para entender su uso. Imaginemos que tenemos una página con un DropDownList y un GridView, los cuales se cargan independientemente el uno del otro. La carga de ambos podemos lograrla de la siguiente manera:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     private EventHandler ehRecuperarDatosDeGrillaPrincipal = null;
   4:     private EventHandler ehRecuperarDatosDeDropDownList = null;
   5:
   6:     // Datasets usados para guardar los datos a mostrar
   7:     private DataSet dsGrillaPrincipal;
   8:     private DataSet dsDropDownList;
   9:
  10:     protected void Page_Load(object sender, EventArgs e) {
  11:
  12:         // Creamos 2 tareas
  13:         PageAsyncTask pat1 = new PageAsyncTask(new BeginEventHandler(this.RecuperarDatosDeGrillaPrincipal), new EndEventHandler(this.CargarGrillaPrincipal), null, null, true);
  14:         PageAsyncTask pat2 = new PageAsyncTask(new BeginEventHandler(this.RecuperarDatosDeDropDownList), new EndEventHandler(this.CargarDropDownList), null, null, true);
  15:
  16:         // Registramos las 2 tareas para que se ejecuten asincrónicamente
  17:         Page.RegisterAsyncTask(pat1);
  18:         Page.RegisterAsyncTask(pat2);
  19:     }
  20:
  21:     IAsyncResult RecuperarDatosDeGrillaPrincipal(object sender, EventArgs e, AsyncCallback cb, object state) {
  22:         this.ehRecuperarDatosDeGrillaPrincipal = new EventHandler(this.RecuperarSetDeDatos1);
  23:         // Ejecutamos asincrónicamente el método que recupera o procesa los datos que necesitamos
  24:         return this.ehRecuperarDatosDeGrillaPrincipal.BeginInvoke(this, EventArgs.Empty, cb, null);
  25:     }
  26:
  27:     void CargarGrillaPrincipal(IAsyncResult ar) {
  28:         this.ehRecuperarDatosDeGrillaPrincipal.EndInvoke(ar);
  29:
  30:         this.GridView1.DataSource = this.dsGrillaPrincipal;
  31:         this.GridView1.DataBind();
  32:     }
  33:
  34:     IAsyncResult RecuperarDatosDeDropDownList(object sender, EventArgs e, AsyncCallback cb, object state) {
  35:         this.ehRecuperarDatosDeDropDownList = new EventHandler(this.RecuperarSetDeDatos2);
  36:         // Ejecutamos asincrónicamente el método que recupera o procesa los datos que necesitamos
  37:         return this.ehRecuperarDatosDeDropDownList.BeginInvoke(this, EventArgs.Empty, cb, null);
  38:     }
  39:
  40:     void CargarDropDownList(IAsyncResult ar) {
  41:         this.ehRecuperarDatosDeDropDownList.EndInvoke(ar);
  42:         this.DropDownList1.DataSource = this.dsDropDownList;
  43:         this.DropDownList1.DataBind();
  44:     }
  45:
  46:     private void RecuperarSetDeDatos1(object sender, EventArgs e) {
  47:         DataSet ds = new DataSet();
  48:         /*...*/
  49:         this.dsGrillaPrincipal = ds;
  50:     }
  51:
  52:     private void RecuperarSetDeDatos2(object sender, EventArgs e) {
  53:         DataSet ds = new DataSet();
  54:         /*...*/
  55:         this.dsDropDownList = ds;
  56:     }
  57: }

Debo aclarar que en el ejemplo hago uso del delegado EventHandler utilizado en la primera parte de esta serie debido a que estoy mostrando la forma más compleja de recuperar datos (normalmente es la única que usaremos). Las consultas de datos pueden ser realizadas dentro de los métodos RecuperarDatosDeGrillaPrincipal() y RecuperarDatosDeDropDownList(), pero es necesario devolver un objeto que implemente IAsyncResult, y como no tenemos ninguno en nuestro caso, lo creamos mediante el uso del delegado que mencioné. En la primera parte de esta serie se puede ver una de las maneras que hay para recuperar los datos directamente desde estos métodos.

Dos puntos a tener en cuenta. El primero es el tercer parámetro del constructor de PageAsyncTask, un EndEventHandler (igual que el segundo parámetro) que podemos usar en caso de que la tarea que ejecutemos lance un timeout (no se ve en el ejemplo). Este parámetro se configura en la directiva Page con la propiedad AsyncTimeOut (expresada en segundos) de la siguiente manera:

   1: <%@ Page Async="true" AsyncTimeout="20" ... %>

Por defecto este valor viene configurado a 45 segundos.

El otro punto a tener en cuenta es el último parámetro del mismo método, el cual nos permite especificar si deseamos que las tareas se ejecuten de forma secuencial o paralela. En el ejemplo, como las consultas son independientes, decidí ejecutarlas en paralelo.

Ahora veamos otro caso. Imaginemos que agregamos una nueva grilla a nuestra página y que los datos de la misma depende del valor seleccionado en el DropDownList. Esta tarea no podemos cargarla en el método Page_Load ya que necesitamos asegurarnos que la carga del DropDownList ha finalizado. Es por ello que podemos cargar esta tarea justamente en el momento de cargar la lista.

El siguiente ejemplo muestra como hacerlo y además muestra el uso de un EventHander propio para facilitar nuestra tarea:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     private EventHandler ehRecuperarDatosDeGrillaPrincipal = null;
   4:     private EventHandler ehRecuperarDatosDeDropDownList = null;
   5:     private RecuperarDatosDeGrillaSecundariaEventHandler ehRecuperarDatosDeGrillaSecundaria = null;
   6:
   7:     private DataSet dsGrillaPrincipal;
   8:     private DataSet dsDropDownList;
   9:
  10:     protected void Page_Load(object sender, EventArgs e) {
  11:
  12:         // Creamos 2 tareas
  13:         PageAsyncTask pat1 = new PageAsyncTask(new BeginEventHandler(this.RecuperarDatosDeGrillaPrincipal), new EndEventHandler(this.CargarGrillaPrincipal), null, null, true);
  14:         PageAsyncTask pat2 = new PageAsyncTask(new BeginEventHandler(this.RecuperarDatosDeDropDownList), new EndEventHandler(this.CargarDropDownList), null, null, true);
  15:
  16:         // Registramos las 2 tareas para que se ejecuten asincronicamente
  17:         Page.RegisterAsyncTask(pat1);
  18:         Page.RegisterAsyncTask(pat2);
  19:     }
  20:
  21:     IAsyncResult RecuperarDatosDeGrillaPrincipal(object sender, EventArgs e, AsyncCallback cb, object state) {
  22:         this.ehRecuperarDatosDeGrillaPrincipal = new EventHandler(this.RecuperarSetDeDatos1);
  23:         return this.ehRecuperarDatosDeGrillaPrincipal.BeginInvoke(this, EventArgs.Empty, cb, null);
  24:     }
  25:
  26:     void CargarGrillaPrincipal(IAsyncResult ar) {
  27:         this.ehRecuperarDatosDeGrillaPrincipal.EndInvoke(ar);
  28:         this.GridView1.DataSource = this.dsGrillaPrincipal;
  29:         this.GridView1.DataBind();
  30:     }
  31:
  32:     IAsyncResult RecuperarDatosDeDropDownList(object sender, EventArgs e, AsyncCallback cb, object state) {
  33:         this.ehRecuperarDatosDeDropDownList = new EventHandler(this.RecuperarSetDeDatos2);
  34:         return this.ehRecuperarDatosDeDropDownList.BeginInvoke(this, EventArgs.Empty, cb, null);
  35:     }
  36:
  37:     void CargarDropDownList(IAsyncResult ar) {
  38:         this.ehRecuperarDatosDeDropDownList.EndInvoke(ar);
  39:         this.DropDownList1.DataSource = this.dsDropDownList;
  40:         this.DropDownList1.DataBind();
  41:
  42:         this.DropDownList1.SelectedIndex = 0;
  43:
  44:         string id = this.DropDownList1.Items[0].Value;
  45:         // Cargamos la tarea y le pasamos el ID como parámetro
  46:         PageAsyncTask pat3 = new PageAsyncTask(new BeginEventHandler(this.RecuperarDatosDeGrillaSecundaria),
  47:                             new EndEventHandler(this.CargarGrillaSecundaria), null, id, true);
  48:
  49:         Page.RegisterAsyncTask(pat3);
  50:     }
  51:
  52:     IAsyncResult RecuperarDatosDeGrillaSecundaria(object sender, EventArgs e, AsyncCallback cb, object state) {
  53:         this.ehRecuperarDatosDeGrillaSecundaria = new RecuperarDatosDeGrillaSecundariaEventHandler(this.RecuperarSetDeDatos3);
  54:         return this.ehRecuperarDatosDeGrillaSecundaria.BeginInvoke((string)state, cb, null);
  55:     }
  56:
  57:     void CargarGrillaSecundaria(IAsyncResult ar) {
  58:         // El uso de un EventHandler personalizado nos permite devolver el DataSet
  59:         DataSet ds = this.ehRecuperarDatosDeGrillaSecundaria.EndInvoke(ar);
  60:         this.GridView2.DataSource = ds;
  61:         this.GridView2.DataBind();
  62:     }
  63:
  64:     private void RecuperarSetDeDatos1(object sender, EventArgs e) {
  65:         DataSet ds = new DataSet();
  66:         /*...*/
  67:         this.dsGrillaPrincipal = ds;
  68:     }
  69:
  70:     private void RecuperarSetDeDatos2(object sender, EventArgs e) {
  71:         DataSet ds = new DataSet();
  72:         /*...*/
  73:         this.dsDropDownList = ds;
  74:     }
  75:
  76:     private DataSet RecuperarSetDeDatos3(string id) {
  77:         DataSet ds = new DataSet();
  78:         /*...*/
  79:         return ds;
  80:     }
  81: }

La definición del RecuperarDatosDeGrillaSecundariaEventHandler es la siguiente:

   1: public delegate DataSet RecuperarDatosDeGrillaSecundariaEventHandler(string id);

Lo bueno del método RegisterAsyncTask es que hasta no terminar todas las tareas, no se retoma el procesamiento normal de la página por lo que podemos seguir agregando tareas si así lo quisiésemos. De todos modos, hay que tener en cuenta que al igual que con AddOnPreRenderCompleteAsync, estas tareas se ejecutan antes del evento PreRenderComplete, por lo que si las registramos luego de haberse producido ese evento, serán ignoradas por completo.

¡Nos vemos en el próximo post!

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

Posted in .Net 2.0, ASP.NET | Etiquetado: , , , , | 1 Comment »

Ejecutar tareas de forma asincrónica en ASP.NET 2.0 (Parte 1)

Posted by Gerardo Contijoch en enero 20, 2009

Navegando por los links de uno de los posts que me encontré hace un tiempo en mi feed reader, encontré un artículo interesantísimo de Jeff Prosise sobre ejecución asincrónica de páginas en ASP.NET 2.0 que no quería dejar de comentar. Ya hace tiempo que trabajo sobre el Framework 2.0, pero por algún motivo nunca llegué a leer algo sobre el tema (¡era hora que lo hiciera!). El artículo del que hablo es éste, y como no encontré mucho material en español sobre el tema voy a comentarles un poco de que trata el artículo y como lograr que las páginas en ASP.NET 2.0 se procesen (al menos en parte) de manera asincrónica.

Cabe aclarar que con las técnicas que voy a presentar nuestros sites no necesariamente se van a cargar más rápido de una manera perceptible para el usuario, sino que más bien se van a aprovechar los recursos para evitar tiempos muertos durante el procesamiento de las páginas.

Es necesario tener conocimiento sobre el ciclo de vida de las paginas en ASP.NET para poder entender como se logra la asincronicidad, por lo que voy a describir esto brevemente.

Ciclo de vida de una página en ASP.NET

Toda página tiene un ciclo de vida que comienza luego de que desde un cliente (como puede ser un browser) se hace una petición o Request por una página. A lo largo de este ciclo de vida se producen distintos eventos que nos van notificando por las etapas por las cuales esta pasando una página. Por ejemplo, el evento Init se produce luego de cargar los controles y es el momento de inicializarlos (setear sus propiedades). Por otro lado, el evento PreRender se da un instante antes de comenzar el renderizado de la página por lo que puede ser un buen lugar para modificar algún detalle de último momento relacionado con la visualización o renderizado de la página (yo tengo la costumbre de hacer visibles/invisibles o habilitar/deshabilitar ciertos controles en este punto y no antes, de modo que la lógica de presentación no se mezcle con la relacionada al binding de grillas, por ejemplo). Pueden encontrar el detalle del ciclo de vida de una página acá (a los más curiosos les recomiendo investigar el método ProcessRequestMain(bool, bool) de la clase Page con .Net Reflector).

El ciclo completo de una página es el siguiente:

  • PreInit
  • Init
  • InitComplete
  • LoadState (evento privado)
  • ProcessPostData (evento privado)
  • PreLoad
  • Load
  • ProcessPostData (una segunda vez)
  • “ChangedEvents” (eventos lanzados por los controles de la página)
  • PostBackEvents (evento privado)
  • LoadComplete
  • PreRender
  • PreRenderComplete
  • SaveState
  • SaveStateComplete
  • Unload

Cuando una página implementa la interface IHttpHandler, el ciclo de vida completo es ejecutado por un único hilo recuperado del ThreadPool, el cual es devuelto una vez finalizada la ejecución de la página. La mayoría de las paginas muestran datos que son procesados o recuperados de algún lado, ya sea una DB, un archivo de configuración, etc, y esa tarea casi siempre es independiente del procesamiento de la página. Si tardamos 5 segundos en recuperar los datos de una tabla de una DB, esos son 5 segundos de tiempo muerto en el que el hilo que esta ejecutando el procesamiento de la página se queda esperando los datos. Como el ciclo de vida de una página es necesariamente secuencial, no podemos aprovechar este tiempo muerto para procesar otras partes de la página, pero lo que si podemos hacer es liberar el hilo (es decir, devolverlo al ThreadPool) de modo que pueda ser reutilizado para el procesamiento de alguna otra tarea.

Ejecución asincrónica

Veamos como hacerlo. Lo primero es habilitar la ejecución asincrónica seteando el atributo Async en la directiva Page a true:

   1: <%@ Page Async="True" ... %>

Lo que esto produce es que la página implemente la interface IHttpAsyncHandler (a diferencia de las páginas ‘comunes’, que implementan IHttpHandler). No entiendo muy bien en dónde ni cómo ocurre esto ya que la clase Page implementa IHttpHandler y no se pueden cambiar sus interfaces así como así. Evidentemente todo este proceso ocurre antes de la propia compilación de la página (ya que luego de la misma, esta no puede ser modificada), pero no se exactamente en que punto (si alguien sabe, que diga).

En fin, me estoy llendo por las ramas. Al implementar esta interface, vamos a poder introducir dos eventos nuevos en el ciclo de vida de la página. Estos se van a ejecutar inmediatamente después del evento PreRender y antes de PreRenderComplete . El primero de ellos lo que va a hacer es ejecutar una tarea de manera asincrónica devolviendo un objeto que implemente IAsyncResult. El segundo, va a ser lanzado cuando el proceso asincrónico finalice, retomando ahí lo que queda del procesamiento de la página.

Para atrapar estos eventos vamos a hacer uso del método AddOnPreRenderCompleteAsync de la clase Page:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:     protected void Page_Load(object sender, EventArgs e) {
   3:         Page.AddOnPreRenderCompleteAsync(
   4:             new BeginEventHandler(this.BeginAsync),
   5:             new EndEventHandler(this.EndAsync)
   6:         );
   7:     }
   8:
   9:     IAsyncResult BeginAsync(object sender, EventArgs e, AsyncCallback cb, object state) {
  10:         /*...*/
  11:     }
  12:
  13:     void EndAsync(IAsyncResult ar) {
  14:         /*...*/
  15:     }
  16: }

Debido a que los eventos estos son lanzados antes del evento PreRenderComplete, es necesario que este método sea ejecutado antes de llegar a esta fase del procesamiento de la página.

Veamos ahora como recuperar datos para cargar una grilla de manera asincrónica:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     SqlCommand cmd;
   4:
   5:     protected void Page_Load(object sender, EventArgs e) {
   6:        Page.AddOnPreRenderCompleteAsync(
   7:            new BeginEventHandler(this.RecuperarDatos),
   8:            new EndEventHandler(this.CargarGrilla)
   9:        );
  10:     }
  11:
  12:     IAsyncResult RecuperarDatos(object sender, EventArgs e, AsyncCallback cb, object state) {
  13:        SqlConnection cnn = new SqlConnection("...");
  14:        cnn.Open();
  15:
  16:        this.cmd = new SqlCommand("SELECT * FROM ...", cnn);
  17:
  18:        // Atencion con el CommandBehavior, si no lo usamos de esta forma, necesitamos mantener una
  19:        // referencia a la conexión a nivel de clase para poder cerrarla despues!
  20:        return cmd.BeginExecuteReader(cb, state, CommandBehavior.CloseConnection);
  21:     }
  22:
  23:     void CargarGrilla(IAsyncResult ar) {
  24:        SqlDataReader reader = cmd.EndExecuteReader(ar);
  25:        this.GridView1.DataSource = reader
  26:        this.GridView1.DataBind();
  27:        reader.Close();
  28:     }
  29: }

Esta técnica puede aplicarse también a la ejecución de WebServices ya que los mismos ofrecen una forma Begin[WebMethod] y End[WebMethod] para cada uno de los métodos que se expongan.

Sin embargo, hay ocasiones en lo que deseamos hacer es algo más complejo que una simple consulta o simplemente el método ExecuteReader no nos sirve. En esos casos podemos hacer algo como lo siguiente:

   1: public partial class AsyncForm : System.Web.UI.Page {
   2:
   3:     // Referencia al EventHandler que vamos a usar para llamar al método que recupera los datos
   4:     private EventHandler ehRecuperarDatos = null;
   5:     // Aca vamos a guardar los datos que recuperemos
   6:     private DataSet dsDatos = null;
   7:
   8:     protected void Page_Load(object sender, EventArgs e) {
   9:
  10:         Page.AddOnPreRenderCompleteAsync(
  11:             new BeginEventHandler(this.RecuperarDatos),
  12:             new EndEventHandler(this.CargarGrilla)
  13:             );
  14:     }
  15:
  16:     IAsyncResult RecuperarDatos(object sender, EventArgs e, AsyncCallback cb, object state) {
  17:
  18:         // Esto es necesario porque tenemos que devolver un IAsyncResult
  19:         this.ehRecuperarDatos = new EventHandler(this.RecuperarDataSet);
  20:         return this.ehRecuperarDatos.BeginInvoke(this, EventArgs.Empty, cb, null);
  21:     }
  22:
  23:     private void RecuperarDataSet(object sender, EventArgs e) {
  24:         DataSet ds = new DataSet();
  25:         /*...*/
  26:         this.dsDatos = ds;
  27:     }
  28:
  29:     void CargarGrilla(IAsyncResult ar) {
  30:         this.ehRecuperarDatos.EndInvoke(ar);
  31:         this.GridView1.DataSource = this.dsDatos;
  32:         this.GridView1.DataBind();
  33:     }
  34: }

En el ejemplo estoy usando un EventHandler genérico, pero tranquilamente podríamos crear nuestro propio EventHandler con parámetros propios y usarlo para facilitarnos la tarea de recuperación y/o procesamiento de los datos.

Como se ve, no es muy difícil ejecutar tareas en segundo plano, pero éste método nos limita a sólo una única tarea. En un próximo post voy a mostrar otro método para ejecutar más de una tarea asincrónicamente.

[Update: Pueden encontrar la segunda parte de este artículo acá.]

¡Nos vemos en el próximo post!

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

Posted in .Net 2.0, ASP.NET | Etiquetado: , , , , | 1 Comment »

Error de compilación CS0016 en site corriendo en IIS

Posted by Gerardo Contijoch en diciembre 30, 2008

Hoy por primera vez necesite levantar un site en II7 sobre Windows Vista y me encontré con un viejo problema: el error de compilación CS0016, que se presenta con el siguiente mensaje:

Compiler Error Message: CS0016: Could not write to output file ‘c:\Windows\Microsoft.NET\Framework\[version de framework]\Temporary ASP.NET Files\root\fc8191ba\bb891bec\App_global.asax.pwk2xkjx.dll’ — ‘Access is denied. ‘

El nombre del archivo puede ser diferente ya que es generado dinámicamente al azar.

Este problema no es nuevo, ya que con otras versiones de Windows también sucede lo mismo y se debe a que el usuario con el cual estamos ejecutando el proceso de compilación no tiene permisos de escritura sobre el path indicado ni sobre el directorio temporal de Windows (esto último no lo dice en ningún lado, pero es así). Este usuario es el usuario configurado en el Application Pool asociado a nuestro site. Por defecto es el usuario NETWORK SERVICE, por lo que el problema se soluciona simplemente dándole permisos de modificación y escritura (no es necesario Full Control como figura en muchos lugares) a ese usuario sobre los paths arriba indicados.

Actualización 10/06/2009:

Luego de investigar un poco más sobre este tema, encontré aquí que el problema también puede ser causado el antivirus McAfee. Resulta que este antivirus tiene una característica que bloquea la ejecución de scripts dentro de carpetas temporales y esto puede estar interfiriendo. Para desactivar esta característica sólo tienen que seguir las instrucciones aquí expuestas.

Otra posibilidad es que el directorio de archivos temporales de Windows no este registrado entre las variables de entorno de sistema (algo raro, pero puede pasar si andamos tocando lo que no debemos). Para verificar esto, tenemos que ir a System Properties (o propiedades de Mi PC) y ahi seleccionar el tab Advanced, al final de todo hay un boton llamado Environment variables…, el cual clickeamos y en la sección System variables de la ventana que se abre tendremos que encontrar a TEMP y a TMP. Si no se encuentra alguno de los dos, entonces hay que agregarlos y asociarles como valor el path de los archivos temporales (tipicamente %Systemroot%\Temp). Luego de hacer esto, reinician IIS y listo. Tengan en cuenta que si no estan seguros de lo que hacen es mejor no tocar estos settings porque pueden dejar de funcionar muchas cosas si hacen algo mal.

Referencias:

¡Nos vemos en el próximo post!

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

Posted in ASP.NET, Desarrollo Web | Etiquetado: , | 16 Comments »

Sys.WebForms.PageRequestManagerParserErrorException al hacer un postback parcial

Posted by Gerardo Contijoch en diciembre 26, 2008

Hace unos días me encontré con otro problema bastante molesto en ASP.NET AJAX al cual no le encontré solución. Se presenta cuando se hace un postback parcial y se muestra un error como el siguiente:

Sys.WebForms.PageRequestManagerParserErrorException: The message received from the server could not be parsed. Common causes for this error are when the response is modified by calls to Response.Write(), response filters, HttpModules, or server trace is enabled.

Details: Error parsing near ‘<!DOCTYPE html P’.

(Noten que el nodo <!DOCTYPE> parece estar truncado en la respuesta)

Luego de buscar un rato, me encontré con este post de Eilon Lipton (¡el creador de este error!) donde explica porque sucede y como se puede evitar.

Cuando uno hace un postback parcial la respuesta viene en un formato especial el cual es interpretado por los scripts del lado del cliente de ASP.NET AJAX. Si nosotros durante el procesamiento del postback modificamos la respuesta, entonces los scripts van a ser incapaces de procesarla, produciendo este mensaje de error.

Para evitarlo hay que tener presente los siguientes puntos:

Nunca ejecutar Response.Write(“…”); durante un postback asincrónico.

Esto se debe a que al escribir directamente sobre la respuesta, estamos modificándola sin que los objetos encargados de procesarla lo sepan y por lo tanto, no pueden prever esos cambios.

Uso de filtros de las respuestas y HttpModules.

Al igual que con el punto anterior, si modificamos la respuesta de alguna manera, vamos a estar provocando este error seguro.

No habilitar el Trace.

Cuando uno habilita el trace en las paginas, básicamente lo que se esta haciendo es una serie de Response.Write(“…”); con la información de tracing.

Llamadas a Server.Transfer();

Cuando uno ejecuta Server.Transfer(), la respuesta deja de ser la respuesta que espera el objeto que provoca el postback asincrónico y pasa a ser el HTML generado por la página a la cual hicimos el transfer. Obviamente, esto va a interferir con el procesamiento de la respuesta (¡ya que la esta sería una pagina completa totalmente diferente a la respuesta esperada!).

En mi caso particular, no estaba haciendo nada de lo mencionado arriba, sino que estaba agregando una cookie a la respuesta, lo cual evidentemente la modifica, así que podemos agregar a la lista:

No agregar cookies a la respuesta de un postback asincrónico.

Al agregar las cookies a la respuesta, la estamos modificando (ya que enviamos la información de la cookie), por lo que hay que evitar hacerlo.

Entonces, ¿cómo se soluciona el problema cuando no podemos dejar de usar cookies o HttpModules? La solución que encontré yo fue muy simple: reescribí la página en cuestión y saque todo lo referente a ASP.NET AJAX. Todo funcionó de maravillas con un postback clásico.

¡Nos vemos en el próximo post!

PD: Si realmente necesitan modificar la respuesta en una llamada parcial, este post puede serles utiles, yo no lo probe, pero parece que funciona…

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

Posted in ASP.NET, Desarrollo Web | Etiquetado: , , | 3 Comments »