Archivo de la categoría 'AJAX'


Autenticación por formularios en ASP.NET

Lunes, 19 Oct, 2009 @ 10:59 | Por Dario Krapp | AJAX, ASP.NET

Prólogo e inicios

En algunas tecnologías web antiguas, como por ejemplo ASP (en el caso de Microsoft) la programación de la autenticación de los usuarios quedaba en manos de los programadores. A pesar de que no es una tarea extremadamente compleja, demandaba tiempo de desarrollo, pruebas y correcciones por cada sitio que se desarrollaba y aunque las técnicas utilizadas para esta funcionalidad se encontraban informalmente estandarizadas, siempre podían existir diferencias en las implementaciones (quien haya caído en la suerte de tener que modificar una aplicación legada, posiblemente sabrá a que nos referimos ), lo que influía en los tiempos y costos del desarrollo y si lo analizamos parecería tener poco sentido el gasto de esfuerzos de desarrollo en una tarea que es tan repetitiva. Quizás esa sea una de las causas por las que ASP.NET provee un sistema de autenticación ya incluido, lo que implica código, desarrollado, testeado, mantenido y listo para usar. En este artículo nos adentraremos en la autenticación por formularios desde sus comienzos en las primeras versiones de ASP.NET hasta la versión 3.5 (y diría que también hasta la versión 4.0, ya que hasta el momento no se han introducido modificaciones sobre estos ítems en la versión 4.0 y según parece, tampoco hay intenciones de hacerlo) y comentaremos algunos otros temas relacionados.

Para empezar diremos que la autenticación es el acto de confirmar que algo o alguien es quien dice ser y autorización es el acto de dar permisos (o denegárselos) a algo o alguien (ya autenticado) sobre un recurso, diremos además que algo o alguien es anónimo si no ha sido autenticado, que la acción de loguearse (login) consta de informar las credenciales a fin de poder autenticarse y la acción de desautenticarse (logout), si me permiten el término aunque no suene de lo mejor, consta de volver a un usuario autenticado al estado de anónimo.

En ASP.NET el modo de autenticación por formularios es aplicable para aplicaciones web que requieren autenticación, siempre que la misma no sea autenticación de Windows, para tales casos ASP.NET ofrecerá la opción de establecer el modo de autenticación Windows, pero ese es otro tema.

ASP.NET a través del modo de autenticación por formularios nos brindará mecanismos y funcionalidades que nos ayudarán a realizar ambas tareas: autenticación y autorización.

Para que el modo de autenticación por formularios funcione, solo debemos activarlo y la forma es simplemente ajustar la propiedad mode del tag authentication (del archivo web.config) en Forms, tal como se muestra a continuación:

<authentication mode="Forms">

El modo de autenticación por formularios creará una cookie con la información básica del usuario (obviamente encriptada), la cual será enviada por el browser en cada solicitud y el Servidor analizará la información de esta cookie para hacer los chequeos de autenticación y autorización necesarios. Este modo de funcionamiento es necesario debido a que en un entorno web las conexiones entre el cliente y el servidor se establecen y se cortan en cada solicitud. Por defecto la cookie tomará el nombre “.ASPXAUTH”.

En el siguiente esquema pueden observarse paso a paso las verificaciones de autenticación y autorización que realizará ASP.NET cuando una solicitud es procesada y se está utilizando autenticación por formularios:


Autenticación por formularios

El proceso graficado será realizado automáticamente por ASP.NET con simple hecho de haber establecido el atributo mode como Forms.
En nuestros ejemplos (que los estaremos realizando sobre Visual Studio 2008 SP1) utilizaremos una aplicación web que inicialmente contendrá la página Default.aspx, que es creada automáticamente junto con el proyecto.

Estructura del proyecto

Si en el ejemplo, luego de haber establecido el modo de autenticación por formularios, intentamos acceder a la página Default.aspx, notaremos que podremos hacerlo sin problemas, pero no debemos preocuparnos, este comportamiento se debe a que aún no hemos definido las reglas de acceso, por ahora cualquier usuario podrá acceder a cualquier recurso del sitio.

Para evitar que esto suceda, deberemos configurar las reglas de acceso. Por ahora, para simplificar lo haremos agregando las siguientes líneas dentro de la sección system.web del archivo web.config:

<authorization>
      <deny users="?"/>
</authorization>

Luego veremos en detalle las opciones disponibles para esta sección de la configuración, pero la sintaxis que utilizamos es bastante intuitiva, estaremos indicando que deberá negársele el acceso a cualquier recurso del sitio a los usuarios anónimos (identificados con el signo “?”, recordemos que un usuario anónimo es aquel que no se ha autenticado).

Si luego de esta modificación intentamos acceder nuevamente a Default.aspx (que es un recurso del sitio) veremos que somos automáticamente redireccionados a la página login.aspx, obteniendo un resultado similar al siguiente en la barra de direcciones del explorador:

http://localhost:6931/login.aspx?ReturnUrl….

Se obtendrá un error dado que la página login.aspx no existe, pero ese detalle mínimo pierde importancia ya que como podemos ver, el mecanismo está funcionando.

Lo que está haciendo ASP.NET es verificar la cookie, y al no encontrarla nos ha enviado a un formulario para que ingresemos nuestras credenciales y podamos autenticarnos.

La pagina login.aspx es la página de redirección que utiliza ASP.NET por defecto, pero es posible modificar este comportamiento definiendo un tag denominado forms dentro del tag authentication en el web.config como se muestra a continuación:

<authentication mode="Forms">
      <forms loginUrl="ingreso1.aspx"></forms>
</authentication>

Y no habrá quien se sorprenda de que luego de esta última modificación, la redirección sea a:

http://localhost:6931/ingreso1.aspx?ReturnUrl…….

Existen varias opciones aplicables al tag forms, algunas de ellas, además de loginUrl son:

  • name: Nombre de la cookie (el valor por defecto es .ASPXAUTH).
  • defaultUrl: pagina de redirección si la autenticación fue satisfactoria y el usuario había ingresado originalmente a la página de logueo (login.aspx por defecto).
  • timeout: tiempo de vida de la cookie.
  • slidingExpiration: si es verdadero, el tiempo de vida de la cookie se reiniciará cada vez que la página es reenviada.
  • cookieless: permite definir si se utilizarán cookies para mantener la información del usuario.
  • requireSSL: En verdadero indicará que el browser enviará la cookie al solo si la conexión es segura (SSL) en caso contrario de no contarse con una conexión segura, no funcionará la autenticación por formulario.
  • domain: Permitirá definir en qué dominio la cookie es válida.
    path: Permitirá definir el path de la cookie.

Para continuar con el ejemplo agregaremos la página ingreso1.aspx que será desde ahora en más nuestra página donde el usuario deberá ingresar sus credenciales para autenticarse (login).

Con estas simples modificaciones en el archivo de configuración hemos conseguido dar un primer paso a incorporarle a nuestro sitio la autenticación por formularios. Ahora que hemos conseguido que nadie pueda ingresar, debemos permitir que algunos usuarios si puedan hacerlo, y en particular aquellos a los que deseemos permitirles el ingreso. En este punto encontraremos dos opciones, una de ellas es encargarnos de autenticar al usuario por nuestra cuenta, lo que implica hacer el chequeo manualmente vía código y la otra será una opción automática con algunas particularidades.

Para el primer caso tomará importancia una clase llamada FormsAuthentication. Esta clase (como su nombre indica) nos ayudará a realizar todas las funcionalidades relacionadas con la autenticación por formularios programáticamente, como por ejemplo autenticar a un usuario y redireccionarlo a la pagina que el mismo había solicitado cuando se le denegó el acceso.

Para realizar la autenticación vía código con la ayuda de la clase FormsAuthentication escribiremos lo siguiente:

protected void Login_Click(object sender, EventArgs e)
        {
            if (Usuario.Text == "Juan" && Clave.Text == "clavedejuan")
                FormsAuthentication.RedirectFromLoginPage(Usuario.Text, false);
            else
            {
                LabelError.Text = "Usuario o clave incorrecto";
            }
        }

Hemos asumido que poseemos los cuadros de texto, Usuario, Clave y el botón Login, donde hemos utilizado el evento Click del mismo para agregar el código que puede verse en las líneas superiores. De más está decir que no hemos mencionado a los validadores con el único objetivo de simplificar el ejemplo.

Si probamos el ejemplo intentando ingresar a la página Default.aspx seremos redireccionados a la página Ingreso1.aspx donde deberemos ingresar nuestras credenciales, tal como se muestra a continuación:


Autenticación

Solo podremos acceder a Default.aspx si utilizamos como usuario a “Juan” y como clave a “clavedejuan”.

En el código que terminamos de agregar podemos ver también que nos estamos valiendo del método RedirectFromLoginPage de la clase FormsAuthentication para indicar la autenticación del usuario y la redirección a la página solicitada originalmente por el mismo.

Por otra parte podríamos incluir en la página Default.aspx un botón con la siguiente funcionalidad:

protected void LogOut_Click(object sender, EventArgs e)
        {
            FormsAuthentication.SignOut();
            FormsAuthentication.RedirectToLoginPage();
        }

Al presionarlo se ha utilizado una vez más la clase FormsAuthentication para desautenticar al usuario y reenviarlo a la página de ingreso nuevamente.

Con estas operaciones hemos conseguido que un usuario pueda realizar las operaciones de autenticación, en tal caso será simple reemplazar la sentencia Usuario.Text == “Juan” && Clave.Text == “clavedejuan” que utilizamos en el ejemplo por la llamada a un método que acceda al repositorio que más deseemos para efectuar la validación de las credenciales del usuario.

Si se observa la clase FormsAuthentication se notará que existe un método llamado Authenticate que toma por parámetros un nombre de usuario y una clave y devuelve un valor booleano, podríamos preguntarnos si este método será capaz de realizar la autenticación por nosotros, esta es la segunda opción que habíamos mencionado en algunos párrafos previos y para probar como funciona, podremos modificar el código anterior de la siguiente forma:

protected void Login_Click(object sender, EventArgs e)
        {
            if (FormsAuthentication.Authenticate(Usuario.Text, Clave.Text))
                FormsAuthentication.RedirectFromLoginPage(Usuario.Text, false);
            else
                LabelError.Text = "Usuario o clave incorrecto";
        }

Si ahora intentamos ingresar nuevamente, ya no importa que usuario y clave utilicemos, no podremos hacerlo, lo cual es un resultado bastante esperable, si consideramos le hemos delegado el control de la autenticación ASP.NET pero aún no le hemos indicado quienes serán los usuarios válidos de la aplicación. Si se desea utilizar esta opción automática debe considerarse la particularidad de que los usuarios deberán definirse en el propio archivo de configuración web.config, como se muestra en el siguiente ejemplo:

<authentication mode="Forms">
  <forms loginUrl="ingreso1.aspx" >
    <credentials passwordFormat="Clear">
      <user name ="Pepe" password ="clavedepepe"/>
      <user name ="Pedro" password ="clavedepedro"/>
    </credentials>
  </forms>
</authentication>

Luego de agregar estos valores veremos que es posible ingresar con ambos usuarios. El atributo passwordFormat del tag credentials admitirá además de la opción Clear (claves en formato limpio, o sea en texto legible para quien abra el archivo web.config) las opciones MD5 y SHA1 que permitirán utilizar el hash de la clave del usuario utilizando los algoritmos MD5 o SHA1. Claro está que en tal caso deberemos crear un mecanismo capaz de establecer dichos valores, lo bueno es que para obtener los valores hash en ambos formatos la clase FormsAuthentication ofrecerá el método HashPasswordForStoringInConfigFile que permitirá tomar una clave limpia y obtener el valor de hash correspondiente en el formato deseado. En el ejemplo anterior, si deseamos utilizar por ejemplo MD5 deberemos efectuar las siguientes modificaciones:

<authentication mode="Forms">
  <forms loginUrl="ingreso1.aspx">
    <credentials passwordFormat="MD5">
      <user name="Pepe" password="957995AA67183A4D2A91F7DE3EB9A692"/>
      <user name="Pedro" password="B447CA7CBA96E91D68D43C5867522BF0"/>
    </credentials>
  </forms>
</authentication>

Donde los valores de hash se han obtenido efectuando las siguientes llamadas:

string HashedPasswordPepe = FormsAuthentication.HashPasswordForStoringInConfigFile("clavedepepe " , "MD5");

y

string HashedPasswordJuan = FormsAuthentication.HashPasswordForStoringInConfigFile("clavedejuan " , "MD5");

Si bien todo esto funciona, creo que es inevitable pensar que se podría dar un próximo paso, ya que al fin de cuentas con este esquema que terminamos de ver, si bien ASP.NET y la clase FormsAuthentication nos han ayudado a controlar la autenticación y la autorización, la funcionalidad de la autenticación debimos codificarla, y cuando utilizamos la autenticación automática con el método Authenticate debimos definir a los usuarios en el archivo web.config. No sería nada malo que ASP.NET pudiese combinar ambas opciones y permitirnos efectuar la autenticación automáticamente sobre cualquier repositorio que deseáramos utilizar.

VN:F [1.7.3_972]
Rating: 8.6/10 (8 votos cast)

Nueva versión del ASP.NET AJAX Control Toolkit

Sábado, 16 May, 2009 @ 00:31 | Por Gustavo Cantero (The Wolf) | AJAX, ASP.NET

Este mes salió a la luz una nueva versión del ASP.NET AJAX Control Toolkit, y este nuevo release viene nuevos tutoriales, la corrección de más de 20 bugs y tres nuevos controles: HTMLEditor, ComboBox y ColorPicker.  Cabe mencionar que esta nueva versión sólo está disponible para .NET Framework 3.5 y Visual Studio 2008.

HTMLEditor

Gracias a la compañía Obout (www.obout.com), que hizo el desarrollo de este control, ahora es posible crear y editar contenido HTML fácilmente en el navegador, utilizando una barra de herramientas con varios botones para cambiar el estilo del contenido.

ajax-htmleditor

ComboBox

En este caso el agradecimiento va dirigido a Dan Ludwig, quien desarrolló el control ComboBox de esta librería, el cual combina una lista desplegable y un cuadro de texto, con la ayuda del conocido “autocomplete”.

ColorPicker

Por último nos queda el control creado por Alexander Turlov, el cual es un Extender que se puede adjuntar a un TextBox para agregarle la funcionalidad de poseer un popup para seleccionar un color.

Nuevos tutoriales

Los nuevos tutoriales, junto con los que ya existían, se pueden encontrar en la siguiente dirección: http://www.asp.net/learn/ajax/

VN:F [1.7.3_972]
Rating: 7.9/10 (8 votos cast)

“Alert” personalizado

Miércoles, 09 Jul, 2008 @ 01:03 | Por Gustavo Cantero (The Wolf) | AJAX, ASP.NET

Desarrollando una aplicación web para un cliente hicimos que todas las ventanas de la misma se mostraran con un ModalPopUp de la librería AJAX Control Toolkit de Microsoft, utilizando dentro de ésta el mismo diseño que para el resto del sitio, logrando así una imagen homogénea en toda la aplicación.  El primer inconveniente que tuvimos fue cuando nuestro cliente nos solicitó que TODOS los popup de la aplicación fueran iguales, incluidos los de los validadores, los cuales se muestran con un SummaryValidator en un “alert” de JavaScript.  Reemplazar esto fue sencillo, ya que simplemente creamos una función llamada “Alert” (nótese que se diferencia de la función original de JavaScript en que la primer letra está en mayúscula) que escribía el mensaje que se le pasara a través de un parámetro dentro del ModalPopUp, lo mostraba, y lo colocaba sobre el resto de los posibles ModalPopUp que hubiesen cambiando el zIndex de los layers, de la misma manera que lo mostramos en el artículo “Deshabilitar controles de la página hasta que finalicen los UpdatePanels”.  Luego simplemente reemplazamos la llamada al alert con el siguiente código:

window.alert = Alert;

El código JavaScript quedaba como se muestra a continuación:

function Alert(msg) {
    $get('divAlertContent').innerText = msg;
    popUpShowed = $find('Alert');
    popUpShowed.show();
    popUpShowed._backgroundElement.style.zIndex += 10;
    popUpShowed._foregroundElement.style.zIndex += 10;
}

y en la página colocamos un código parecido al siguiente (al presentado aquí le quitamos el diseño para que quede más legible):

<asp:Button ID="btnAlert" runat="server" Style="display: none" />
<cc1:ModalPopupExtender ID="mpeAlert" runat="server" Enabled="True" PopupControlID="panAlert" DropShadow="true" TargetControlID="btnAlert" OkControlID="btnAcceptAlert" BehaviorID="Alert" />
<asp:Panel ID="panAlert" runat="server" HorizontalAlign="Center" Style="display: none" Width="370px" Height="100px">
    <div id="divAlertContent"></div>
<br />
    <asp:Button ID="btnAcceptAlert" runat="server" Text="OK" />
</asp:Panel>

Esto daba como resultado que todos los “alerts” de la aplicación se mostraran en una ventana del mismo diseño.
El siguiente pedido que nos hizo el cliente fue que, al igual que los alerts “tradicionales”, éstos pudieran cerrarse al pulsar la tecla ESC.   Para lograr esto modificamos la función anterior para que al mostrar el ModalPopUp guardara una referencia al mismo en una variable global llamada popUpShowed.  Luego agregamos una llamada a una función nuestra en el evento “pageLoad” del framework de AJAX, la cual chequea si el usuario pulsó la tecla ESC y, en caso de que popUpShowed no sea nula, cerramos el PopUp al que referencia.
A continuación muestro una página de ejemplo con todo el código para que mostrar la función “Alert” funcionando.

<%@ Page Language="C#" AutoEventWireup="true" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "<a href="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd</a>">
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>">
<head runat="server">
    <title>Página de ejemplo</title>

    <script type="text/javascript">
    var popUpShowed = null;
    function pageLoad() {
        document.body.onkeypress=keyPressHandler;
        window.alert=Alert;
    }
    function showPopUp(popUp){
        popUpShowed = $find(popUp);
        popUpShowed.show();
    }
    function Alert(msg){
        $get('divAlertContent').innerText = msg;
        popUpShowed = $find('Alert');
        popUpShowed.show();
        popUpShowed._backgroundElement.style.zIndex += 10;
        popUpShowed._foregroundElement.style.zIndex += 10;
    }
    function keyPressHandler(e){
        if (popUpShowed != null){
            var kC  = (window.event) ? event.keyCode : e.keyCode;
            var Esc = (window.event) ? 27 : e.DOM_VK_ESCAPE;
            if(kC==Esc){
                popUpShowed.hide();
                popUpShowed = null;
            }
        }
    }
    </script>

    <style>
        .ModalBackground {
            background-color: Gray;
            filter: alpha(opacity=70);
            opacity: 0.7;
        }
        .ModalPopup {
            background-color: #eeeeee;
            border-width: 1px;
            border-style: solid;
            border-color: Gray;
            padding: 3px;
            font-size: small;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server" />
    <input type="button" value="Ejemplo" onclick="alert('Ejemplo desde javascript')" />
    <asp:Button ID="btnAlert" runat="server" Style="display: none" />
    <cc1:ModalPopupExtender ID="mpeAlert" runat="server" Enabled="True" PopupControlID="panAlert"
        DropShadow="true" TargetControlID="btnAlert" OkControlID="btnAcceptAlert" BehaviorID="Alert"
        BackgroundCssClass="ModalBackground" OnOkScript="popUpShowed = null" />
    <asp:Panel ID="panAlert" runat="server" HorizontalAlign="Center" Style="display: none"
        Width="370px" Height="100px" CssClass="ModalPopup">
        <div id="divAlertContent">
        </div>
        <br />
        <asp:Button ID="btnAcceptAlert" runat="server" Text="OK" />
    </asp:Panel>
    </form>
</body>
</html>

Por último les dejo un proyecto hecho con Visual Studio 2008 con un ejemplo del “Alert”

Gustavo Cantero (The Wolf)
MCP – MCSD – MCTS

VN:F [1.7.3_972]
Rating: 8.3/10 (6 votos cast)

Deshabilitar controles de la página hasta que finalicen los UpdatePanels

Jueves, 19 Jun, 2008 @ 00:30 | Por Gustavo Cantero (The Wolf) | AJAX, ASP.NET

Hace unos meses comenzamos a desarrollar para un cliente una aplicación web más o menos compleja, con .NET 3.5 y bastante uso de AJAX. En esta aplicación hay páginas que utilizan varios UpdatePanels, y algunos de estos realizan procesos complejos que pueden llegar a demorar varios segundos, es por esto que nuestro cliente nos pidió que, al iniciar estos procesos, se bloquee la página para que el usuario no pueda ejecutar ninguna otra acción en la misma hasta tanto no finalice el proceso pendiente.  Obviamente no queríamos ejecutar un script “a mano” cada vez que se ejecutaba una acción y otro al finalizar la misma, así que buscamos la manera de automatizarlo un poco.  Comenzamos buscando los eventos propios del motor de AJAX de Microsoft® y encontramos que podemos capturar los del RequestManager cuando inicia una petición al servidor y cuando la misma finaliza.  Esto lo podemos hacer utilizando los métodos add_initializeRequest y add_endRequest de la instancia actual del PageRequestManager, la cual podemos obtener con el siguiente código: Sys.WebForms.PageRequestManager.getInstance().
En el primer evento mostrábamos un ModalPopUp (control que muestra una “ventana” en la página y deshabilita todos los demás controles de la misma) de la librería ASP.NET AJAX Control Toolkit de Microsoft®, el cual contenía un mensaje que decía “Aguarde un momento…”, y en el segundo evento lo cerrábamos.  A continuación se muestra como quedaba el PopUp en la página:

<cc1:ModalPopupExtender ID="mpeLoading" runat="server" BehaviorID="idmpeLoading" PopupControlID="pnlLoading" TargetControlID="btnLoading" EnableViewState="false" DropShadow="true" BackgroundCssClass="ModalBackground" />
<asp:Panel ID="pnlLoading" runat="server" Width="300" Height="50" HorizontalAlign="Center" CssClass="ModalPopup" EnableViewState="false" Style="display: none">
<br />Aguarde un momento...</asp:Panel>
<asp:Button ID="btnLoading" runat="server" Style="display: none" />

y acá está el código JavaScript:

function initializeRequest(sender, args){
    $find('idmpeLoading').show();
}
function endRequest(sender, args){
    $find('idmpeLoading').hide();
}
Sys.WebForms.PageRequestManager.getInstance().add_initializeRequest(initializeRequest);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequest);

Cabe aclarar que la variable idmpeLoading del código anterior contiene el identificador del ModalPopUp a mostrar.
Hasta este punto todo funcionaba bien, el problema surgió cuando utilizamos paneles modales con UpdatePanels dentro, en ese momento la ventana con el mensaje “Aguarde un momento…” se “dibujaba” debajo del panel que estaba utilizando el usuario, no cumpliendo con su finalidad, ya que de esta manera no se deshabilitaban todos los controles de la página.
Paso siguiente revisamos el código del ModalPopUp (el mismo se puede bajar desde el mismo link que mostré antes) y encontramos que a los paneles se les estable un valor fijo en el estilo zIndex para mostrarlos sobre los demás controles.  Luego revisamos las propiedades del objeto de JavaScript que representa el panel y encontramos que posee dos objetos interesantes: _backgroundElement y _foregroundElement.  Estos representan los DIVs del fondo y del contenido de PopUp respectivamente, entonces para hacer que estos objetos de HTML se posicionen sobre todos los demás simplemente tenemos que incrementarles la propiedad zIndex de su estilo.  Entonces, nuestro código JavaScript queda como se muestra a continuación:

function initializeRequest(sender, args){
    var mpeLoading = $find(idmpeLoading);
    mpeLoading.show();
    mpeLoading._backgroundElement.style.zIndex += 10;
    mpeLoading._foregroundElement.style.zIndex += 10;
}
function endRequest(sender, args){
    $find(idmpeLoading).hide();
}
Sys.WebForms.PageRequestManager.getInstance().add_initializeRequest(initializeRequest);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequest);

Por último, para hacer que esta funcionalidad esté presente en todas las páginas de nuestra aplicación, lo agregamos en la página “master” del sitio, logrando que en cualquier página (que utilice esta master) posea la funcionalidad aquí descripta.

En la siguiente imagen muestro como queda una aplicación de ejemplo que cree con Visual Studio 2008 para este artículo:

A continuación dejo el proyecto con el código fuente para quien quiera utilizar esta utilidad:

Gustavo Cantero (The Wolf)
MCP – MCSD – MCTS

VN:F [1.7.3_972]
Rating: 8.0/10 (4 votos cast)