a nil value

Blog sobre programación e informática en general (Por Guillermo Zafra)

Browsing Posts in Programación

Supongamos que queremos crear una de esas maravillosas listas dobles de items que nos permita pasar el objeto seleccionado de una lista a otra y viceversa. Esto no tiene ningún secreto, pero si quisieramos que al volver a pasar un item de la segunda lista a la original lo hiciera en la posición en la que estaba originalmente, la cosa se complica un poco.

Esta es una solución. Quizás no la mejor, pero sirve:

Para esto lo primero que tendremos que tener es 3 controles ListBox; dos para las listas de origen y destino, y uno oculto para guardar estáticamente la lista original.

    <asp:ListBox ID="lstOrigen" runat="server" Width="600px" Height ="150px"></asp:ListBox><br /><br />
    <asp:ListBox ID="lstOculto" runat="server" Width="1px" Height ="1px" style="display:none"></asp:ListBox><br />
    <asp:ListBox ID="lstDestino" runat="server" Width="150px" Height ="150px"></asp:ListBox>

Seguidamente, añadiremos nuestro código javascript maravilloso:

       function Add(){
           var lbOrigen = document.getElementById('lstOrigen');
           if(lbOrigen.options.selectedIndex > -1){
                var lbDestino =  document.getElementById('lstDestino'); //obtenemos el contro listbox
                var selectedText = lbOrigen.options[lbOrigen.options.selectedIndex].text;
                var selectedValue = lbOrigen.options[lbOrigen.options.selectedIndex].value;
                for (var i=0; i < lbDestino.options.length; i++) //buscamos coincidencias
                {
                    if (lbDestino.options[i].value.toLowerCase().match(selectedValue.toLowerCase()))
                    {
                                  return false;
                    }
                }
                  var opt = document.createElement("option");
                  opt.text = selectedText;
                  opt.value = selectedValue;
                  lbDestino.options.add(opt);
                  lbOrigen.options.remove(lbOrigen.options.selectedIndex);
           }
        }
        function Remove(){
           var lbDestino = document.getElementById('lstDestino');
           if(lbDestino.options.selectedIndex > -1){
                var lbOculto =  document.getElementById('lstOculto'); //obtenemos el contro listbox
                var lbOrigen =  document.getElementById('lstDestino'); //obtenemos el contro listbox
                var selectedText = lbDestino.options[lbDestino.options.selectedIndex].text;
                var selectedValue = lbDestino.options[lbDestino.options.selectedIndex].value;
                var found = 0; //flag que nos indica si hemos encontrado el item en el listado de destino
                clearListBox(lbOrigen); //limpiamos la lista de origen
                for (var i=0; i < lbOculto.options.length; i++) //buscamos coincidencias del listado original
                {
                    for (var j=0; j < lbDestino.options.length; j++) //buscamos en el listado de añadidos
                    {
                        if (lbOculto.options[i].value.toLowerCase().match(lbDestino.options[j].value.toLowerCase())) //el item fue movido
                        {
                              found = 1; //hemos encontrado el item
                              if (lbOculto.options[i].value.toLowerCase().match(selectedValue.toLowerCase())){
                                 //si el item esta seleccionado lo añadimos al listado de origen
                                 lbOrigen.options.add(crearListItem(selectedText,selectedValue));
                                 break;
                              }
                        }
                    }
                       if (found == 0){ //si no se ha encontrado significa que no está en la lista de destino y debemos añadirlo
                        lbOrigen.options.add(crearListItem(lbOculto.options[i].text,lbOculto.options[i].value));
                       }else{
                         found = 0; //reiniciamos el flag
                       }
 
 
                }
                  lbDestino.options.remove(lbDestino.options.selectedIndex);
           }
        }
 
        function crearListItem(text, value){
                  var opt = document.createElement("option");
                  opt.text = text;
                  opt.value = value;
                  return opt;
        }
        function clearListBox(lb){
            for (var i=lb.options.length-1; i>=0; i--){
                lb.options[i] = null;
            }
            lb.selectedIndex = -1;
        }

Bueno, la función “Add” añadirá el item seleccionado de la lista original a la lista de destino. Para ello comprobamos si hay algun item seleccionado y si este no se encuentra ya en la lista destino, lo añadimos y lo eliminamos del origen.

La función remove es más complicada. Inicialmente limpiará la lista de origen (donde queremos devolver el item a su posición original) y la volvera a reconstruir item por item.

Para ello recorrera la lista oculta(donde tenemos el listado completo de items originales) y buscara coincidencias en la lista de destino. Si no existe coincidencias significa que dicho item no se movió, por lo que lo recuperamos en la lista de origen. Si lo encuentra significa que dicho item lo hemos movido anteriormente, por lo que tenemos que comprobar si es el item que queremos devolver a su posición inicial (esta seleccionado). De ser así, lo añadiremos igualmente a la lista de origen, recuperando asi el item en la posición original (recordad que todo este proceso se realiza en función a la lista oculta), si no está seleccionado, lo obviamos ya que dicho item permanecerá en la lista de destino.

Finalmente eliminamos el item que hemos movido de la lista destino.

Las dos últimas funciones sirven para borrar todos los items de un ListBox y para crear un ListItem con un value y text determinados.

Un saludo

Cuando creamos un instalador para alguno de nuestros proyectos de Visual Studio podemos necesitar incluir como requisito tener instalada algun paquete o complemento e incluso podermos querer añadirlo al instalador para que, si no está instalado, lo haga.

A esto se le conoce como Bootstrapping y es bastante sencillo.

Para ello lo primero que tenemos que hacer es crear un paquete personalizado para cada complemente que queramos añadir como prerrequisito a nuestro instalador. Por ejemplo, si queremos hacer un instalador para nuestra aplicacion y nos damos cuenta de que requiere el Adobe Reader, podemos hacer un paquete llamado AdobeReader con el redistribuible del Adobe Reader.

Antes de nada saber que estos paquetes se añaden desde Visual Studio a la hora de crear un instalador. Dentr del proyecto de instalación, si vamos a Propiedades del proyecto y ahí a Requisitos Previos obtendremos una lista de los paquetes por defecto que se incluyen en el SDK de Visual Studio, que será similar a la siguiente imagen:

Cada uno de estos requisitos equivale a una carpeta con un ejecutable, unos manifiestos (XML) y otros archivos opcionales de configuración.
Todos estos paquetes se encuentran en una ruta del disco que dependerá de la versión de Visual Studio que estemos utilizando:

Visual Studio 2005: C:\Archivos de programa\Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages
Visual Studio 2008: C:\Archivos de programa\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages

Por lo que lo primero que debemos hacer es ir a la ruta que nos corresponda y crearnos una carpeta con el nombre de nuestro paquete, en nuestro caso “AdobeReader”.

El siguiente paso será meter en dicha carpeta el ejecutable que nos instala el AdobeReader. (Hay que tener en cuenta que existen dos versiones de este instalable, al igual que en muchos otros. Una completa que se instala completamente offline y otra online que descarga contenido desde Internet. La primera hara más pesado el contenido de la instalación pero no requerirá acceso a Internet, y la segunda pues obviamente lo contrario)

El tercer paso es crear el manifiesto “Product.xml”. Vamos, crear un XML con este nombre al que añadiremos lo siguiente:

<?xml version="1.0" encoding="utf-8"?>
<Product ProductCode="AdobeReader" xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper">
  <PackageFiles CopyAllPackageFiles="false">
    <PackageFile Name="AdobeReader-setup.exe" Hash="F774D8891793C46BDDC1F9418D8E427B6CBAE26D" />
  </PackageFiles>
  <InstallChecks>
    <FileCheck Property="" SpecialFolder="ProgramFilesFolder" SearchPath="C:\Archivos de programa\Adobe\Reader 8.0\Reader" FileName="AcroRd32.exe" />
  </InstallChecks>
  <Commands Reboot="Defer">
    <Command PackageFile="AdobeReader-setup.exe">
      <ExitCodes>
        <DefaultExitCode Result="Success" String="Anunexpectedexitcodewasr" FormatMessageFromSystem="true" />
      </ExitCodes>
    </Command>
  </Commands>
</Product>

Este es el manifiesto obligatorio para que el paquete sea reconocido por el Visual Studio. Pero también tenemos el manifiesto “Package.xml” que añadirá información adicional a nuestro paquete tal como un EULA, o el idioma de la instalación:

<?xml version="1.0" encoding="utf-8" ?>
<Package
  xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper"
  Name="AdobeReader"
  Culture="Culture"
  LicenseAgreement="eula.txt">
 
  <PackageFiles>
    <PackageFile Name="eula.txt"/>
  </PackageFiles>
 
  <Strings>
    <String Name="DisplayName">AdobeReader</String>
    <String Name="Culture">es</String>
    <String Name="NotAnAdmin">Debes tener permisos de administrador para poder ejecutar este paquete.</String>
    <String Name="GeneralFailure">A ocurrido un error al intentar instalar el paquete.</String>
  </Strings>
</Package>

En este caso, si añadimos una referencia a “eula.txt” tendremos que añadir el correspondiente fichero con el texto que se deberá mostrar en el EULA.

El cuarto paso es importante, y es que si tenemos el Visual Studio instalado en la lengua de Shakespeare, tendremos que añadir un manifiesto por defecto para dicho idioma. Bastará con crear una carpeta llamada “es” dentro de nuestro paquete y poner una copia del fichero “Product.xml” dentro.

Y ya está! Si volvemos a la ventana del Visual Studio donde se mostraban los prerrequisitos por defecto veremos que aparece nuestro paquete “AdobeReader”. Molto facile e divertente.

Y ahora viene la parte buena, que la he puesto al final para joder un poco, que hay que saber cómo funcionan las cosas y hacerlas a lo Clint Eastwood antes de recurrir a lo fácil. Y es que existe una maravillosa herramienta de Microsoft que nos hace todo el trabajo en unos cuantos clicks. Se llama Bootstrapper Manifest Generator y se puede obtener gratuitamente desde la siguiente URL:

http://code.msdn.microsoft.com/bmg

La aplicación es bastante sencilla y nos da la posibilidad de crear manifiestos mas complejos añadiendo condiciones y otros parámetros de configuración.

Espero que fuera de ayuda para alguien que lo necesite.

Un saludo

En ocasiones nos encontraremos que tenemos que mostrar un listado de elementos excesivamente largo, y aunque volcarlos en un ListBox sea una solución, no lo es de cara al usuario, que tendrá que desplazarse por todo el control en busqueda del item que quiera seleccionar.

Para solucionarlo podemos hacer uso de un script muy sencillo que nos permitirá buscar y seleccionar automaticamente el primer item que incluya el texto introducido, y conforme vayamos introduciendo más carácteres refinará la busqueda.

Para empezar necesitaremos un control ListBox donde tendremos los elementos a buscar y un HTML TEXT donde realizaremos las busquedas. Es recomendable usar un INPUT TEXT en lugar de un TextBox dado que los eventos de cliente del HTML TEXT son reconocidos en tiempo de diseño, mientras que en el TextBox no, aunque a efectos prácticos obtendremos el mismo resultado.

En el control TEXT añadiremos el evento “onkeyup” para llamar a la función “busqueda”.

<asp:ListBox ID="lstItems" runat="server" Width="154px">
        <asp:ListItem>Alda</asp:ListItem>
        <asp:ListItem>Aldana</asp:ListItem>
        <asp:ListItem>Aldair</asp:ListItem>
        <asp:ListItem>Albano</asp:ListItem>
        <asp:ListItem>Brian</asp:ListItem>
        <asp:ListItem>Cinnia</asp:ListItem>
        <asp:ListItem>Elba</asp:ListItem>
        <asp:ListItem>Fiona</asp:ListItem>
        <asp:ListItem>Ginebra</asp:ListItem>
        <asp:ListItem>Iñigo</asp:ListItem>
        <asp:ListItem>Kenneth</asp:ListItem>
        <asp:ListItem>Marvin</asp:ListItem>
        <asp:ListItem>Oscar</asp:ListItem>
        <asp:ListItem>Quinn</asp:ListItem>
        </asp:ListBox><br />
        <input type="text" id="txtBusqueda" runat="server" onkeyup="busqueda(this)" />

Y a continuación añadiremos el siguiente script:

<script language="javascript" type="text/javascript">
    function busqueda(txt){
        var l =  document.getElementById('lstItems'); //obtenemos el contro listbox
        if(txt.value == ""){
            LimpiarListBox(l); //si el campo de busqueda esta vacio quitamos seleccion
        }else{
            for (var i=0; i < l.options.length; i++) //buscamos coincidencias
            {
                if (l.options[i].value.toLowerCase().match(txt.value.toLowerCase()))
                {
                    l.options[i].selected = true;
                    return false;
                }else{
                    LimpiarListBox(l); //si no se encuentran coincidencias quitamos seleccion
                }
            }
        }
    }
 
    function LimpiarListBox(lb){
        lb.selectedIndex = -1;
    }
</script>

Y voilá, ya tenemos nuestro sistema de búsqueda. Hay que decir que este sistema es muy sencillo pero que se puede complicar todo lo que queramos. Por ejemplo para que busque desde el principio o en una parte de la cadena o value del ListItem. También podemos modificar el evento para que solo busque al perder el foco y mil cosas más.

Si se diera el caso de tener que seleccionar varios items convendría añadir un segundo ListBox de elementos seleccionados y añadir uno a uno los elementos que queramos.

Cuando se trabaja de forma asíncrona con AJAX y UpdatePanels resulta imprescindible implementar procesos para informar al usuario de que se están efectuando actualizaciones en el servidor.

Por defecto, ASP.NET AJAX nos proporcionar el complemento UpdateProgress, que unido a un ScriptManager y un UpdatePanel permiten mostrar cualquier indicador mientras el UpdatePanel esta trabajando asíncronamente.

No obstante el UpdateProgress es limitado si queremos realizar tareas o efectos mas complicados. Por ejemplo posicionar el loader dinamicamente en la pantalla segun lo que se actualice, realizar alguna accion de Jscript antes de comenzar la actualización, o permitir al usuario cancelar el proceso.

Para ello existe un maravilloso y muy simple javascript que controla dos eventos importantísimos de la página; el comienzo y el fin del httprequest.

<script language="javascript" type="text/javascript">
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(
  function(sender, e)
  {
        $get('cargador').style.display="block";
   });
 Sys.WebForms.PageRequestManager.getInstance().add_endRequest(
  function(sender, e)
  {
        $get('cargador').style.display="none";
   });
 
</script>

Donde “cargador” podría ser, por ejemplo:

<div id="cargador" runat="Server" style="display:none">
        <table width="200">
            <tr>
                <td height="50" valign="bottom" align="center">Cargando...</td>
            </tr>
            <tr>
            <td height="50" valign="top" align="center"><img alt="cargando..." src="../img/ajax-loader.gif" /></td>
            </tr>
        </table>
    </div>

add_beginRequest se lanzaría antes de comenzar el evento de solicitar información al servidor y add_endRequest justo despues de terminar todo el proceso.

Para cancelar el Request bastaría con mostrar un boton en el beginRequest que lanzara el siguiente script:

function CancelarPostback() {
    var man= Sys.WebForms.PageRequestManager.getInstance();
    if (man.get_isInAsyncPostBack())
        man.abortPostBack();
    }

Aunque ojo! con este “Cancelar” ya que, si bien cancelara el request y forzando el endRequest, el proceso en el servidor es muy probable que se haya ejecutado ya. Mi consejo es que se utilice unicamente para procesos de lectura y no de escritura.

Un saludo

Hace unos días tuvimos un debate entre los compañeros. No voy a explicar la diferencia entre los parámetros pasados por valor o por referencia, así que es aconsejable tener claros los conceptos antes de seguir leyendo.

En este debate, uno de mis compañeros argumentaba que todos los parámetros en .NET se pasan por defecto por valor, a no ser que se indique lo contrario. Mientras que el otro sostenía que esto no era así para los objetos (tipos) que, dado que cuando declaramos un objeto realmente estamos declarando una referencia de dicho objeto, estos se pasaban por referencia en las funciones y no por valor.

Para demostrar este último argumento creó un código simple tal que:

            StringBuilder y = new StringBuilder();
            y.Append ("hello");
            FooByVal (y);
            Console.WriteLine (y);
            FooByRef (ref y);
            Console.WriteLine (y);
 
        void FooByVal (StringBuilder x)
        {
            x.Append (" and goodbye");
        }
 
        void FooByRef (ref StringBuilder x)
        {
            x.Append (" world");
        }

El resultado fué que, en ambos casos, el objeto StringBuilder y tenía añadidas las cadenas incluidas en ambas funciones, fuera este pasado por referencia o por valor.

¿Podemos concluir que, en .NET, da lo mismo pasar un objeto por referencia que por valor?

Exactamente no. Para entender la diferencia vamos a representar gráficamente la referencia o puntero de nuestro objeto Y, y el espacio en memoria donde se guarda dicho objeto.

En esta imagen vemos cómo, al declarar el objeto Y, estamos creando una referencia o puntero para dicho objeto. Y al inicializar usando new, reservamos en memoria el espacio que utilizaremos para dicho objeto.

Tal y como vemos en la imagen, la diferencia entre pasar el objeto por referencia o por valor está en que, en el primer caso le pasamos la referencia original a dicho objeto, mientras que en el segundo le pasamos una copia de dicha referencia al mismo objeto. En ambos casos estamos modificando el objeto original desde ambas funciones.

¿Donde está entonces la diferencia?

La diferencia se ve cuando, desde estas funciones modificamos no el objeto en memoria sino la referencia a dicho objeto. Por ejemplo:

            StringBuilder y = new StringBuilder();
            y.Append ("hello");
            FooByVal (y);
            Console.WriteLine (y);
            FooByRef (ref y);
            Console.WriteLine (y);
 
        void FooByVal (StringBuilder x)
        {
            x = new StringBuilder();
            x.Append (" and goodbye");
        }
 
        void FooByRef (ref StringBuilder x)
        {
            x = new StringBuilder();
            x.Append (" world");
        }

En el caso de FooByVal, el objeto Y con valor “hello” permanecerá inalterado al pasar por la función, ya que en esta estamos asignando un nuevo espacio en memoria para la copia de la referencia y modificando otro espacio diferente al original.

En el caso de FooByRef, sin embargo, al pasar la referencia original y volverla a inicializar estaremos asignando un nuevo espacio en memoria para la referencia original, perdiendo el contenido que hemos añadido previamente y asignando uno nuevo ” world”.

Basta con probar ambos casos para entender la diferencia.

En conclusión:

Ambos tenían razón en sus argumentos. Todos los parámetros en .NET son por defecto pasados por valor, pero dado que los objetos son realmente referencias, lo que estamos pasando por valor no es sino la referencia a dicho objeto en memoria.

Un saludo

Bueno, esta es sencilla y fácil de encontrar pero nunca se sabe cuando puede ser útil. Ahora bien, puede confundir al usuario.

<asp:CheckBox runat="server" ID="chkPrueba" onclick="cambiarTextoCheckBox(this);" />

Y la función Javascript:

function cambiarTextoCheckBox(checkbox)
{
  if (checkbox.checked)
    checkbox.nextSibling.innerHTML = 'Activado';
  else
    checkbox.nextSibling.innerHTML = 'Desactivado';
}

Si generamos un UserControl dinámicamente y queremos pasarle parámetros desde la página principal para poder trabajar con ellos tenemos dos métodos:

Metodo fácil:

Utilizando propiedades públicas en nuestro UserControl tal que:

myUserControl ctrl = this.Page.LoadControl("myUserControl.ascx");
ctrl.value1 = "First Parameter";
ctrl.value2 = 200
Panel.Contrlols.Add(ctrl);

Obviamente tendremos que tener dos propiedades publicas en nuestra clase myUserControl para los dos parámetros que queremos pasar.

Y el método ninja:

Añadimos la siguiente función en la página donde hagamos queramos cargar dinámicamente el UserControl:

private UserControl LoadControl(string UserControlPath, params object[] constructorParameters)
{
List constParamTypes = new List();
foreach (object constParam in constructorParameters)
{
constParamTypes.Add(constParam.GetType());
}
 
UserControl ctl = Page.LoadControl(UserControlPath) as UserControl;
 
// Find the relevant constructor
ConstructorInfo constructor = ctl.GetType().BaseType.GetConstructor(constParamTypes.ToArray());
 
//And then call the relevant constructor
if (constructor == null)
{
throw new MemberAccessException("The requested constructor was not found on : " + ctl.GetType().BaseType.ToString());
}
else
{
constructor.Invoke(ctl, constructorParameters);
}
 
// Finally return the fully initialized UC
return ctl;
}

Añadimos en nuestro UserControl dos constructores; uno vacío y otro con los parámetros que le queramos pasar.

public partial class myUserControl : System.Web.UI.UserControl
{
private string value1;
private int value2;
public myUserControl ()
{
}
 
public myUserControl (string parameter1, int parameter2)
{
this.value1 = parameter1;
this.value2 = parameter2;
}
}

Y por último cuando queramos añadir dinámicamente el control:

Control ctrl = LoadControl("myUserControl.ascx","First Parameter",200) ;
Panel.Controls.Add(ctrl) ;

Fuente: http://blah.winsmarts.com/2006/05/20/loadcontrol-a-usercontrol–and-pass-in-constructor-parameters.aspx

Cualquier control web tanto HTML cómo ASP.NET tiene un ID propio que le identifica en una página web y que no puede ser repetido en dicha página. Sin embargo, cuando utilizamos controles de usuario (.ascx) nos encontramos con un problema, y es que podemos tener controles con identificadores repetidos dentro del control de usuario.

Cuando la página se renderiza en completo HTML estos dos controles con el mismo ID no tendrán el mismo, para solucionarlo se utilizan un identificador único que podemos obtener mediante la propiedad UniqueID.

Este ID único se genera de la siguiente manera:

Si en la página principal tuviéramos un control ID “txtMensaje” y con UserControl “PanelMensaje.ascx”.

Y dentro del UserControl PanelMensaje tuvieramos otro control con ID “txtMensaje”, el resultado sería que este último tendría el ID “PanelMensaje$txtMensaje”. Es decir, que todos los controles hijos de PanelMensaje añadirían el prefijo PanelMensaje$ a su id.

Esto es acumulativo, de manera que si anidamos mas controles se identificarán jerárquicamente por los identificadores de los contenedores y separados por el carácter dolar $.

Algo muy común si generamos los controles dinámicamente y los agrupamos por contenedores.

Siguiendo con la temática del artículo anterior sabemos que conseguir un postback es tarea fácil con la mayoría de los controles web de ASP.NET, pero puede ocurrir que queramos forzar a la página a realizar una acción de postback desde un lugar que no este preparado para ello.

Para conseguir esto utilizaremos la funcion __dopostback:

function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}

Donde eventTarget es el control que realiza el postback y eventArgument el argumento que queremos enviar junto con el postback (si es que queremos).

Llamando a esta función desde cualquier evento de cualquier control HTML conseguiremos enviar la página al servidor. Especialmente útil cuando si estamos generando controles dinámicamente que realicen postbacks al servidor y queremos controlarlas manualmente.

Un saludo

Bueno, cualquiera que haya hecho algún desarrollo web sabrá lo que es un postback, pero en el momento que nos ponemos a trabajar con AJAX nos encontramos con un arsenal de técnicas a nuestra disposición para controlar que las comunicaciones entre el cliente y el servidor sean cómo nosotros queramos y no cómo quiera nuestra aplicación.

No voy a ponerme a explicar todo desde el principio, así que haré un pequeño resumen:

Postback – Acción por la cual, el contenido de una página en el lado del cliente es enviada al servidor para su procesamiento.

Callback – Aunque podemos confundirlo con un postback, los callback surgen con ASP.NET 2.0 y permiten realizar llamadas al servidor desde el código del lado del cliente, es decir, desde javascript. La principal diferencia es que, mientras que un Postback envía toda la página al servidor y la vuelve a renderizar, un Callback unicamente invoca a una función determinada en el código del servidor. Este método se usa generalmente para devolver datos que necesitemos procesar u obtener en el servidor.

Si utilizamos el AjaxControlToolkit o ASP.NET 3.5 y utilizamos los maravillosos UpdatePanels tenemos que saber que aunque generalicemos y hablemos siempre de PostBacks, muchas veces el propio framework nos generará internamente callbacks, y aunque ASP.NET nos haga el trabajo sucio conviene diferenciarlos ya que puede ocurrir que no necesitemos o incluso que no queramos que se haga un postback completo, especialmente si vamos a tener en cuenta el rendimiento de nuestra web.

Powered by WordPress Web Design by SRS Solutions © 2010 a nil value Design by SRS Solutions