miércoles, 25 de julio de 2012

MailMessage - Envia mail con GMail (1/3)

 

Introducción


Enviar un mail es una tarea bastante recurrente en las características de toda aplicación, pero existen varias formas de configurarla dependiendo de la necesidad

Analizaremos en este artículo como enviar un mail haciendo uso de un servicio de SMTP publico como es GMail.

Para llevar a cabo la tarea se puede configurar directamente las propiedades, pero es aun mejor si se configuran, lo cual permitiría realizar cambios sin tener que recompilar el código.

 

Envió del Mail configurando las propiedades


Enviar un mail definiendo toda la configuración por código no es aconsejable, pero si es útil cuando uno aun esta realizando prueba del funcionamiento del envió del correo para definir la información que después se volcaría al config

 

[TestMethod]
public void SendMailSinConfig()
{

    List<string> destinatarios = new List<string>()
    {
        "xx@gmail.com",
        "xx@hotmail.com"
    };


    //
    // se crea el mensaje
    //
    string body = "";

    using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MailSendTest.MailBody.txt"))
    using (StreamReader reader = new StreamReader(stream))
    {
        body = reader.ReadToEnd();
    }

    MailMessage mail = new MailMessage()
    {
        From = new MailAddress("xx@gmail.com"),
        Body = body,
        Subject = "Mail Test",
        IsBodyHtml = false
    };


    //
    // se asignan los destinatarios
    //
    foreach (string item in destinatarios)
    {
        mail.To.Add(new MailAddress(item));
    }


    //
    // se define el smtp
    //
    SmtpClient smtp = new SmtpClient()
    {
        Host = "smtp.gmail.com",
        Port = 587,
        UseDefaultCredentials = false,
        Credentials = new NetworkCredential("xx@gmail.com", "password"),
        EnableSsl = true
    };
    

    smtp.Send(mail);

}

En este ejemplo además se añadió un adicional que ayuda bastante en la forma de declarar el texto del cuerpo del mensaje, consiste básicamente en embeber un archivo en el assembly del proyecto.

imagen1

En el código se toma el assembly que se esta ejecutando y de este el stream basado en el namespace que define la ruta al archivo dentro del proyecto.

 

Envió del mail usando Archivo de Configuración


Si bien el MailMessage no varia en la forma de programarse, si lo hizo notablemente la definición de datos que suelen modificarse con mas frecuencia durante la implementación, lo cual implicaría recompilar el código continuamente.

[TestMethod]
public void SendMailUsandoConfig()
{
    List<string> destinatarios = new List<string>()
    {
        "xx@gmail.com",
        "xx@hotmail.com"
    };


    //
    // se crea el mensaje
    //
    string body = "";
   
    using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MailSendTest.MailBody.txt"))
    using (StreamReader reader = new StreamReader(stream))
    {
        body = reader.ReadToEnd();
    }

    MailMessage mail = new MailMessage()
    {
        Body = body,
        Subject = "Mail Test",
        IsBodyHtml = false
    };


    //
    // se asignan los destinatarios
    //
    foreach (string item in destinatarios)
    {
        mail.To.Add(new MailAddress(item));
    }


    //
    // se define el smtp
    //
    SmtpClient smtp = new SmtpClient();
    smtp.Send(mail);

}

El cliente de smtp toma la información del .config

<system.net>
  <mailSettings>
    <smtp from="xx@gmail.com">
      <network host="smtp.gmail.com" port="587" password="password" userName="xx@gmail.com" enableSsl="true" defaultCredentials="false" />
    </smtp>
  </mailSettings>
</system.net>

Pero seguramente habrán notado un problema, la información esta disponible a simple vista, lo cual no es bueno si se define el usuario y password de la cuenta de correo. Es por eso que puede aplicarse encriptación a esta sección del config

 

Envió del mail usando config seguro


Para probarlo es que se creo el segundo proyecto de test, en este se han agregado método para poder proteger la sección del archivo de configuración.

Es necesario mencionar que si bien podría haberse usado de forma directa un sistema de protección como es el RSAProtectedConfigurationProvider, se opta por crear una key propia, esto es debido a que será necesario exportar la clave que se genere para poder llevarla a las PCs (o al servidor) donde resida la aplicación cuando se realice el deploy de la misma.

Sera necesario ejecutar algunos comandos, es por eso que se accede a la consola que proporciona el VS, ya que se hará uso del aspnet_regiis, se hace uso de la misma técnica que se aplica en un entorno web para proteger el config.

imagen2

Los pasos para poder realizar esto serian:

1 - Se crea la key

aspnet_regiis -pc "MailSendKeys" –exp

Es muy importante el –exp ya que este permitirá la exportación de las keys a un archivo

2 - Se exporta la key

aspnet_regiis -px "MailSendKeys" "C:\MailSendKeysFile.xml" -pri

El parámetro –pri exporta la ket publica y privada necesarias para poder trabajar en la encriptación

 

3- Se importa en las PCs donde se hará uso la aplicación

aspnet_regiis -pi "MailSendKeys" "C:\MailSendKeysFile.xml"

Sin esta key en la pc donde estará la aplicación no se podrá leer la sección del config

 

Nota: Las key importadas podrán encontrar en la carpeta:

C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys

 

4 – Encriptar la sección del .config

Para ello se definieron dos métodos que solo serán usados una única vez cuando se defina la información que debe configurarse

private void ProtegerMailSettings()
{

    Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    SmtpSection configSection = config.GetSection("system.net/mailSettings/smtp") as SmtpSection;

    if (!configSection.SectionInformation.IsProtected)
    {
        //configSection.SectionInformation.ProtectSection("RSAProtectedConfigurationProvider");
        configSection.SectionInformation.ProtectSection("MailProtectedProvider");
        
        config.Save();
    }

}

private void DesprotegerMailSettings()
{

    Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    SmtpSection configSection = config.GetSection("system.net/mailSettings/smtp") as SmtpSection;

    if (configSection.SectionInformation.IsProtected)
    {
        configSection.SectionInformation.UnprotectSection();
        config.Save();
    }

}

El test de encriptación solo se invoca en una única oportunidad, para que algo como esto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

  <configProtectedData>
    <providers>
      <add name="MailProtectedProvider"
           type="System.Configuration.RsaProtectedConfigurationProvider, System.Configuration, Version=2.0.0.0,
                 Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
           keyContainerName="MailSendKeys"
           useMachineContainer="true" />
    </providers>
  </configProtectedData>
  
  <system.net>
    <mailSettings>
      <smtp from="xx@gmail.com">
        <network host="smtp.gmail.com" port="587" password="password" userName="xx@gmail.com" enableSsl="true" defaultCredentials="false" />
      </smtp>
    </mailSettings>
  </system.net>

</configuration>

 

Pase a ser algo como ser:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

  <configProtectedData>
    <providers>
      <add name="MailProtectedProvider"
           type="System.Configuration.RsaProtectedConfigurationProvider, System.Configuration, Version=2.0.0.0,&#xD;&#xA;                 Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

           keyContainerName="MailSendKeys"
           useMachineContainer="true" />
    </providers>
  </configProtectedData>
  
  <system.net>
    <mailSettings>
      <smtp configProtectionProvider="MailProtectedProvider">
        <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
          xmlns="http://www.w3.org/2001/04/xmlenc#">
          <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
          <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
            <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                <KeyName>Rsa Key</KeyName>
              </KeyInfo>
              <CipherData>
                <CipherValue>OBiEstszUoonJNlFT9FwMePjtX+AdCufd2I8OAzHDYoi7A6GTTuaSqvM6KR9ANLC8fPmcRt/8PMoOK9nnVkRMlc4haKjoL+V1CmEBaqYOD23lBA5bGGIYdtDCZH9N597juNXkx8ISiESNPi9phO999T7cKPbAQ1rkQXmOGOQ9KU=</CipherValue>
              </CipherData>
            </EncryptedKey>
          </KeyInfo>
          <CipherData>
            <CipherValue>zEAsDA84EFb7ta4I0yzOS200HWB9JfPPtioQjn/GEZSGY3rqvmOwKhYGErxIR0E2k+ewPNJKypedeMyB3lAebrHs8kvwKY6R3glrDuvkcJlfN1Ihl3CcFjeBwE5Rkqmx4Jso5d6d8t4P5+GwqxStjJn/evfXBINv3EawkwATc11ch/aSKM1OUMXSz2XUnfAV7mwFehCk+V09YCXL+KjlgqAm3FwSAHCxCV0g1lLnebf7T1d2fELIjw==</CipherValue>
          </CipherData>
        </EncryptedData>
      </smtp>
    </mailSettings>
  </system.net>

</configuration>

 

La sección <configProtectedData> permite definir el proveedor custom que uno creara para hacer uso de la key registrada desde la linea de comando.

imagen4

El keyContainerName corresponde al nombre de la key creada por línea de comando. Mientras que el name del proveedor se asigna a la sección que se esta encriptado.

También es muy importante validar que este definido

imagen3

 

Link de referencia


How To: Encrypt Configuration Sections in ASP.NET 2.0 Using RSA

Importing and Exporting Protected Configuration RSA Key Containers

 

Código


[C#]
 

sábado, 21 de julio de 2012

[Linq] Distinct y GroupBy usando IEquatable<>

 

Introducción


Al hace uso de linq con objetos se presenta un problema ya que no se resuelve de forma correcta la comparaciones entre entidades, esto se debe al hacer uso de un objeto el mismo se compara por medio de la referencia (o sea del puntero en memoria de la instancia de la entidad) y no su contenido

Si se usara tipos simples no se tendría este inconveniente ya que las comparaciones se harían por valor, pero al ser objetos se usa la referencia, es por eso que se será necesario indicar mediante alguna técnica como comparar las entidades.

Para el ejemplo se trabajara con una lista de artículos, declarando una clase del estilo

public class ArticuloBase
{
    public int Id { get; set; }
    public string Nombre { get; set; }

    public int Stock { get; set; }
    public decimal Precio { get; set; }

}

el contenido de las listas tendrá ítems duplicados, aplicando las diferentes técnicas se podría usar linq directamente con los objetos de la lista.

Distinct


Si usamos distinct de linq sobre una lista para evitar duplicados no obtendremos efecto alguno

imagen1

ya que la lista resultante sigue teniendo 4 ítems, cuando debería haber detectado duplicados, para solucionar el problema será necesario definir como es que la entidad debe ser comparada

La primer opción será por medio de un comparador, se define una clase que implemente IEqualityComparer<>

public class ArticuloComparer : IEqualityComparer<ArticuloBase>
{
    public bool Equals(ArticuloBase x, ArticuloBase y)
    {
        return x.Id == y.Id;
    }

    public int GetHashCode(ArticuloBase obj)
    {
        return obj.Id.GetHashCode();
    }
}

esto hace que al usarse como parte del distinct permita conocer que entidades deben ser iguales

imagen2

ahora si se detecto el ítem duplicado y solo se obtiene como resultado 3, es necesario remarcar que en la clase se definio que compara por id, por lo que un diferente stock será descartado.

Existe otra opción valida cuando se usan entidades la cual consiste en implementar la interfaz IEquatable<>, en este caso se definió una clase heredada para separa la implementación y poder testear, pero se podría haber unido todo en una única clase

public class Articulo : IEquatable<Articulo>
{
    public int Id { get; set; }
    public string Nombre { get; set; }

    public int Stock { get; set; }
    public decimal Precio { get; set; }


    #region IEquatable<Articulo> Members

    public bool Equals(Articulo other)
    {
        if (Object.ReferenceEquals(other, null)) return false;

        if (Object.ReferenceEquals(this, other)) return true;

        return this.Id.Equals(other.Id);
    }


    public override int GetHashCode()
    {
        int hashDescription = this.Id == null ? 0 : this.Id.GetHashCode();

        return hashDescription;
    }

    #endregion

}

con esta otra forma se obtiene el mismo resultado

imagen3

 

Group by


Al usar el group by de linq se presentan los mismo problemas que el distinct, la entidad no puede resolver como debe agrupar cada entidad

imagen4

Pero si lo indicamos por medio del IEquatable<>, en ese caso podra reconocer cual, o cuales, propiedades una entidad es igual a otra

imagen5

El articulo 2 ahora si se agrupa y suma su stock

Código


[C#]
 

viernes, 29 de junio de 2012

[ASP.NET] PopUp Edición - Usando Jquery UI Dialog

 

Introducción


En este ejemplo se editaran ordenes de compra pero sin hacer uso alguno de eventos de asp.net, se hará uso de una serie de librerías jquery que ayudaran en el proceso

Solo el botón de filtro y la paginación del gridview conservarán los eventos de asp.net, la edición de la entidad será completamente sin hacer uso de eventos.

Para esto se usara:

  • jquery UI dialog
  • jquery autocomplete
  • jtemplates
  • momentjs

Implementando jQuery Autocomplete


La implementación se encuentra en el archivo autocomplete.js del proyecto, en este simplemente se define

$(function () {

    $("[id*='txtCustomer']").autocomplete({

        source: function (request, response) {

            var params = new Object();
            params.companyName = request.term;

            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "Default.aspx/GetCustomerList",
                data: JSON.stringify(params),
                dataType: "json",
                async: true,
                success: function (data) {

                    response($.map(data.d, function (item) {
                        return {
                            label: item
                        }
                    }))
                },
                error: function (request, status, error) {
                    alert(jQuery.parseJSON(request.responseText).Message);
                }
            });

        },

        open: function (event, ui) {

            $(this).autocomplete("widget").css({
                "width": 200, "font-size": 12
            });

        },

        select: function (event, ui) {

            $("[id*='txtCustomer']").val(ui.item.label);

        }


    });
});

 

Como puede observarse invoca a un WebMethod implementado en el código de la pagina

[WebMethod]
public static string[] GetCustomerList(string companyName)
{
    string[] list = CustomerRepository.GetCustomer(companyName)
                            .Select(x => x.CompanyName)
                            .ToArray();

     return list;
}

Es importante resaltar el uso de una sección de estilo que permite definir el scroll del desplegable

<style type="text/css">
    .ui-autocomplete {
	    max-height: 200px;
	    overflow-y: auto;
	    /* prevent horizontal scrollbar */
	    overflow-x: hidden;
	    /* add padding to account for vertical scrollbar */
	    padding-right: 20px;
    }
</style>

Implementando Edición GridView


La edición en el control GridView tiene algunos detalles interesantes para el análisis. la selección se implementa por medio de un simple template conteniendo el tag de html de imagen al cual se le asocia un atributo para identificar el id de la orden que define

Este atributo será luego usado por el evento de la imagen cuando hace $(this).attr(“idorder”)

function RegisterEditOrderEvent() {

    $("[id*='gvOrders'] [id*='imgeditorder']").click(function () {

        var orderId = $(this).attr("idorder");

        $('#popuporderedit').dialog({ title: 'Edicion Orden Nro: ' + orderId });
        $('#popuporderedit').data('orderId', orderId);
        $('#popuporderedit').dialog('open');

    });

}

Nota: hay que remarcar como se asigna el id de la orden al dialogo, usando el .data(), usado luego para saber que id esta mostrando el popup

Se observara también que este evento no esta en el clásico $(function(){… para que se cargue cuando la pagina este disponible, sino que es invocado puntualmente ante determinados eventos.

Esto es necesario porque el gridview sigue conservando los eventos de asp.net, y el postback que estos causando quitan las asociaciones de los eventos de jquery adjunta a los controles, a pesar de usar UpdatePanel estos eventos aun existen, por eso se usa al final de cada acción el ScriptManager.RegisterStartupScript() para que vuelva a lanzar la ejecución y rebindear el evento click

protected void gvOrders_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    gvOrders.PageIndex = e.NewPageIndex;
    LoadGridOrder();

    ScriptManager.RegisterStartupScript(Page, typeof(Page), "registerorderevent", "RegisterEditOrderEvent();", true);

}

protected void btnFiltrar_Click(object sender, EventArgs e)
{
    LoadGridOrder();

    ScriptManager.RegisterStartupScript(Page, typeof(Page), "registerorderevent", "RegisterEditOrderEvent();", true);
}

Edición usando jQuery UI Dialog


La definición es bastante simple, lo que quizás sea complejo es la carga y actualización de los campos de la entidad

$('#popuporderedit').dialog({
    autoOpen: false,
    modal: true,
    resizable: false,
    width: 500,
    heigth: 250,
    title: 'Edicion Orden Nro:',
    open: function (event, ui) {

        initialize();
        loadOrder($(this).data('orderId'));

    },
    close: function (event, ui) {

        //limpia todos los textbox del popup
        $('#popuporderedit :text').val('');

    },
    buttons: {
        Actualizar: function () {

            updateOrder();

        },
        Cancel: function () {
            $(this).dialog("close");
        }
    }
});

Jquery UI Dialog – Carga controles


La carga de los datos en el popup tiene dos momentos, una de inicialización y otra de asignación de los datos de la entidad que se edita

En este caso la inicialización mas que nada implica cargar los combo

function initialize() {

    loadComboBox('ddlCustomer', 'GetAllCustomer');
    loadComboBox('ddlEmployeer', 'GetAllEmploye');
    loadComboBox('ddlShipVia', 'GetAllShipper');
    
}

function loadComboBox(comboname, webmethodname) {

    var template = "{#foreach $T as record}\
                        <option value='{$T.record.id}'>{$T.record.name}</option>\
                    {#/for}";

    var combo = $('#' + comboname);

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "Default.aspx/" + webmethodname,
        data: '{}',
        dataType: "json",
        async: true,
        success: function (data) {

            //combo.setTemplate($("#SelectTemplate").html());
            combo.setTemplate(template);
            combo.processTemplate(JSON.parse(data.d));
           
        },
        error: function (request, status, error) {
            alert(JSON.parse(request.responseText).Message);
        }
    });

}

Es importante notar como se hace uso de jtemplate para definir el <option> de combo. El témplate puede ser definido tanto en una variable, o si este es algo mas complejo podría hacerse en una sección del html

<script type="text/html" id="SelectTemplate">
    {#foreach $T as record}
        <option value="{$T.record.id}">{$T.record.name;}</option>
    {#/for}
</script>

Para que se puede recuperar la info será necesario contar con los webmethod

[WebMethod]
public static string GetAllCustomer()
{
    var list = CustomerRepository.GetAllCustomer()
                            .Select(x => new 
                            {
                                id = x.CustomerID,
                                name = x.CompanyName
                            });

    return JsonConvert.SerializeObject(list);
}

[WebMethod]
public static string GetAllEmploye()
{
    var list = EmployeRepository.GetAllEmploye()
                            .Select(x => new
                            {
                                id = x.EmployeeID,
                                name = x.FullName
                            });

    return JsonConvert.SerializeObject(list);
}

[WebMethod]
public static string GetAllShipper()
{
    var list = ShipperRepository.GetAllShipper()
                            .Select(x => new
                            {
                                id = x.ShipperID,
                                name = x.CompanyName
                            });

    return JsonConvert.SerializeObject(list);
}

Se hace uso de la librería JSON.Net para serializar el objeto anónimo

Para cargar la orden se usa una técnica similar

function loadOrder(orderId) {

    var params = new Object();
    params.orderId = orderId;

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "Default.aspx/GetOrder",
        data: JSON.stringify(params),
        dataType: "json",
        async: true,
        success: function (data) {

            var order = JSON.parse(data.d);

            $('#ddlCustomer').val(order.CustomerID);
            $('#ddlEmployeer').val(order.EmployeeID);

            $('#txtOrderDate').val(order.OrderDate);
            $('#txtRequiredDate').val(order.RequiredDate);
            $('#txtShippedDate').val(order.ShippedDate);

            $('#ddlShipVia').val(order.ShipVia);
            $('#txtFreight').val(order.Freight);
            $('#txtShipName').val(order.ShipName);
            $('#txtShipAddress').val(order.ShipAddress);
            $('#txtShipCity').val(order.ShipCity);
            $('#txtShipRegion').val(order.ShipRegion);
            $('#txtShipPostalCode').val(order.ShipPostalCode);
            $('#txtShipCountry').val(order.ShipCountry);


        },
        error: function (request, status, error) {
            alert(JSON.parse(request.responseText).Message);
        }
    });


}

Definiendo el webmethod que envía los datos de la orden

[WebMethod]
public static string GetOrder(int orderId)
{
    var order = OrderRepository.GetOrderById(orderId);

    return JsonConvert.SerializeObject(new
    {
        OrderID = order.OrderID,
        CustomerID = order.CustomerID,
        EmployeeID = order.EmployeeID,

        OrderDate = order.OrderDate.ToShortDateString(),
        RequiredDate = order.RequiredDate.ToShortDateString(),
        ShippedDate = order.ShippedDate.HasValue ? order.ShippedDate.Value.ToShortDateString() : "",

        ShipVia = order.ShipVia,

        Freight = order.Freight,
        ShipName = order.ShipName,
        ShipAddress = order.ShipAddress,
        ShipCity = order.ShipCity,
        ShipRegion = order.ShipRegion,
        ShipPostalCode = order.ShipPostalCode,
        ShipCountry = order.ShipCountry
    }); 

}

Fue necesario redefinir la entidad que se envía y no directo la entidad Order ya que de no hacerse se obtiene un error de referencia circular, causado por las entidades asociadas en Entity Framework

 

Jquery UI Dialog – Actualización


Actualizar la entidad implica crear un objeto javascript que represente la entidad, lo bueno es que JSON.stringify() lo hace muy fácil, mapeando la entidad directamente

function updateOrder() {

    var order = new Object();
    order.OrderID = $('#popuporderedit').data('orderId');

    order.CustomerID = $('#ddlCustomer').val();
    order.EmployeeID = $('#ddlEmployeer').val();

    order.OrderDate = moment($('#txtOrderDate').val(), "DD/MM/YYYY").toDate();
    order.RequiredDate = moment($('#txtRequiredDate').val(), "DD/MM/YYYY").toDate();

    var shippedDate = moment($('#txtShippedDate').val(), "DD/MM/YYYY");
    order.ShippedDate = shippedDate == null ? null : shippedDate.toDate(); 

    order.ShipVia = $('#ddlShipVia').val();
    order.Freight = $('#txtFreight').val();
    order.ShipName = $('#txtShipName').val();
    order.ShipAddress = $('#txtShipAddress').val();
    order.ShipCity = $('#txtShipCity').val();
    order.ShipRegion = $('#txtShipRegion').val();
    order.ShipPostalCode = $('#txtShipPostalCode').val();
    order.ShipCountry = $('#txtShipCountry').val();

    var params = new Object();
    params.order = order;

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "Default.aspx/ActualizarOrder",
        data: JSON.stringify(params),
        dataType: "json",
        async: true,
        success: function (data) {

            alert('Actualizacion Correcta');
            $('#popuporderedit').dialog("close");

        },
        error: function (request, status, error) {
            alert(JSON.parse(request.responseText).Message);
        }
    });


}

El único inconveniente fue con las fechas, al encontrarse en formato dd/MM/yyyy esta no era reconocida por javascript, es por eso que con ayuda de momentjs se logro cambiar de formato a una fecha correcta

 

Código


Para el ejemplo se hace uso de Visual Studio 21010, la base de datos es Sql Compract 3.5

 

[C#]
 

miércoles, 27 de junio de 2012

[ASP.NET] PopUp Edición - Usando Ajax Toolkit ModalPopupExtender

 

Introducción

Editar información de un registro puede implicar el navegar de una pagina hacia otra, pero también podría aplicarse otras técnicas como ser el desplegar un dialogo que facilite esta tarea al usuario

 

Implementado AutoCompleteExtender

La búsqueda rápida de un cliente por medio de la aproximación mientras se va escribiendo consta de dos parte, una en el html donde se vincula los controles asp.net con el AutoCompleteExtender, definiendo en este tres propiedades importantes: TargetControlID, ServicePath y ServiceMethod

<div id="search" style="width:100%;height:100%">

    <div class="row">
        <div class="cell searchcell">
            <asp:Label ID="Label1" runat="server" Text="Nombre Compañia:"></asp:Label>
            <asp:TextBox ID="txtCustomer" runat="server" Width="200px" />
            <asp:AutoCompleteExtender ID="aceCustomer" runat="server"
                TargetControlID="txtCustomer"
                ServiceMethod="GetCustomerList"
                ServicePath="Default.aspx"
                MinimumPrefixLength="2"
                CompletionInterval="1000"
                EnableCaching="true"
                CompletionSetCount="12" />
        </div>
    </div>
    <div class="row">
        <div class="cell searchcell" style="text-align:right;">
            <asp:Button ID="btnFiltrar" runat="server" Text="Filtrar" CssClass="button"
                onclick="btnFiltrar_Click" />
        </div>
    </div>

</div>

Es por eso que en la pagina se define el código del servicio encargado de retornar la lista de string de respuesta.

[WebMethod]
[ScriptMethod]
public static string[] GetCustomerList(string prefixText, int count)
{
     string[] list = CustomerRepository.GetCustomer(prefixText, count)
                            .Select(x => x.CompanyName)
                            .ToArray();

     return list;
}

Implementando ModalPopupExtender

Como en este caso es necesario realizar la carga de datos en los controles que formaran el dialogo de edición se deberá realizar un postback al servidor en un evento previo a mostrar el popup.

Para poder llevar esto a cabo se requiere adaptar el modalpopup, asignando el target a un control hidden para poder engañarlo y así dejar libre la acción del gridview.

Al realizar esto se podrá controlar desde el servidor cuando se desplego el dialogo, desde el evento del gridview se realizara la carga de la info en el panel para terminar con el show del dialogo.

protected void gvOrders_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
    int OrderId = Convert.ToInt32(gvOrders.DataKeys[e.NewSelectedIndex].Value);

    InitializeEditPopUp();
    LoadEditData(OrderId);

    mpeEditOrder.Show();
}

El resto del ejemplo lo compone el dialogo de edición, en este caso no se hiso uso de tabla html, sino que se opto por usar css tables.

 

Persistencia


Para guardar y recuperar la información de la base se hace uso de Entity Framework Code First, se utiliza este por lo practico de su uso.

No se pone aquí la definición de todas las clases del dominio ya que puede descargarse el código para un mayor detalle, pero si se plantea la definición del mapeo para que pueda observarse lo simple que este queda.

public class NorthwindContext : DbContext
    {

        public NorthwindContext()
            : base("Northwind")
        {
            Database.SetInitializer<NorthwindContext>(null);
        }


        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {

            modelBuilder.Entity<Order>()
                            .HasKey(x => x.OrderID);
            
            modelBuilder.Entity<Order>()
                            .HasOptional(x => x.Customers)
                            .WithMany(x=>x.Orders)
                            .HasForeignKey(x=>x.CustomerID);

            modelBuilder.Entity<Order>()
                             .HasOptional(x => x.Shipper)
                             .WithMany(x=>x.Orders)
                             .HasForeignKey(x=>x.ShipVia);

            modelBuilder.Entity<Order>()
                             .HasOptional(x => x.Employees)
                             .WithMany(x => x.Orders)
                             .HasForeignKey(x => x.EmployeeID);
            

            modelBuilder.Entity<Customer>()
                            .HasKey(x => x.CustomerID);

            modelBuilder.Entity<Product>()
                            .HasKey(x => x.ProductID);

            modelBuilder.Entity<Shipper>()
                            .HasKey(x => x.ShipperID);


            modelBuilder.Entity<Employe>().ToTable("Employees");
            
            modelBuilder.Entity<Employe>()
                             .HasKey(x => x.EmployeeID)
                             .Ignore(x=>x.FullName);

        }

        public DbSet<Customer> Customers { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<Shipper> Shippers { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Employe> Employees { get; set; }

    }

Problemas encontrados

Durante la realización del articulo encontré algunos problemas que podrían considerarse como tips a tener en cuenta.

ReadOnly en TextBox con CalendarExtender

Para permitir al usuario seleccionar una fecha se hace uso de CalendarExtender, algo que se quería evitar es el por escribir, por eso se asigno la propiedad ReadOnly="true", el resultado de esto fue que el control no tomaba la selección de la fecha cuando se lo consultaba en el evento del server.

Se debió quitar el ReadOnly para que el control volviera a funcionar

Controles de Validación

Para la validación de algunos controles se hizo uso de controles de asp.net, los cuales se encontraban dentro del Panel que se despliega en el popup. El uso de estos controles hacia que eventos no se lanzaran.

El problema se debía que los controles de validación dentro del modalpopup no tener el mismo nombre en la propiedad ValidationGroup, solo fue cuestión de asignar un mismo nombre de grupo y se resolvió.

 

Código

Para el ejemplo se hace uso de Visual Studio 21010, la base de datos es Sql Compract 3.5

   

martes, 29 de mayo de 2012

[ASP.NET] - GridView sumar columnas con jquery

 

Introducción

Cuando se trabaja integrando código cliente (javascript) con asp.net seguramente en algún punto se necesite realizar operaciones con tablas, siendo el control GridView  usado en la mayoría de estos casos, es por eso se intentara mostrar las diferentes formas en que se puede realizar operaciones con código cliente usando jquery para trabajar con tablas

Para el ejemplo se listara productos con sus respectivos precios, el usuario completara la cantidad que se solicita de ese producto, realizando el calculo del precio final que deberá abonarse.

 

Calculo del precio por producto

La primer operación que debe programarse corresponde al calculo a nivel de fila, o sea de cada producto. La implementación completa podrá verse en el código dentro de al carpeta “Ej1”.

Como se observa en la imagen la columna del precio unitario es fija, el usuario ingresa una cantidad y al quitar el foco del textbox automáticamente se calcula el precio.

Para lograrlo será necesario adjuntar un evento de cambio a cada textbox del gridview, recordemos que el control termina renderizado como una tabla html, por lo tanto, se puede trabajar en el cliente con el html resultante.

<script language="javascript" type="text/javascript">

    $(document).ready(function() {

        $("#<%=GridView1.ClientID%> [id*='txtCantidad']").change(function() {
        
            var tr = $(this).parent().parent();
            var precio = $("td:eq(1)", tr).html();

            $("td:eq(3) span", tr).html($(this).val() * precio);

        });

    });
    
</script>

Ante un evento se recupera el textbox que lanza la acción, es por eso que se utiliza $(this), el this en este caso es el textbox, se sube dos niveles en la estructura del html, o sea a nivel del <tr> que contiene a ese textbox, para luego aplicar la selección a otra columna, en este caso la del precio, por eso se utiliza td:eq(1) seria la segunda columna (recordar que los índices comienzan en cero)

El ultimo paso será asignar el resultado, se aplica la misma técnica, se selecciona la columna y por tratarse de un label de asp.net el cual renderiza a <span> se usa este selector en jquery.

Si bien en la implementación puede observarse solo una única forma para asignar el evento a los textbox contenido en la columna de gridview pueden ser usados otro selectores de jquery dependiendo la necesitad:

 

Usando el atributo class

En este caso el selector hace uso de un atributo del control para poder localizarlo, todos los textbox que el gridview genere tendrán el mismo atributo “class”

$('#<%=GridView1.ClientID%> .cantidad').change(function() { ….

Es necesario definir este atributo en el control del témplate del grid

Usando un doble selector

En este caso se aplicara una doble selección, primero se selecciona lo definido a la derecha y sobre esto aplica lo definido en al izquierda.

$('td:eq(2) :text', '#<%=GridView1.ClientID%> tr').change(function() {

La parte derecha selecciona todas las filas del grid, mientras que la izquierda toma solo la tercer columna (Cantidad Pedido), el uso de “:text” define que solo los textbox de la celda serán incluidos

 

Usando el operador like *=

Por medio del operador de selección mediante búsqueda aproximada, se puede tomar todos aquellos controles que tengan un misma patrón en su id

$("#<%=GridView1.ClientID%> [id*='txtCantidad']").change(function() {

En el caso del gridview creara un control por cada dato que se le asigne, repitiendo el textbox N veces, pero todos tendrán la palabra txtCantidad en alguna parte de su id

 

Calculo del precio Total

Calcular el precio total, implica sumar una columna completa para tener el precio total que se sumara en el footer del gridview. La implementación completa podrá verse en el código dentro de al carpeta “Ej2”.

El añadido que se realiza a la suma de la fila del grid solo implica la llamada a una funciona para recalcular el total.

Un punto conflictivo cuando se suma una columna es el hecho de paginar la grid, vamos a ver en este caso como se lograría una implementación con y sin paginado

 

Calculo total sin paginado

Al no paginar se puede reconocer el footer como ultima fila de la tabla pudiendo aplicar selectores que tengan en cuenta esta situación.

function CalcularTotal() {

    var total = 0;
    $('#<%=GridView1.ClientID%> tr:not(:last)').each(function() {
    
        var coltotal = parseFloat($("td:eq(3) span", this).html());
        
        if (!isNaN(coltotal)) {
            total += coltotal;
        }

    });

    $('#<%=GridView1.ClientID%> tr:last td:eq(3) span').html(total);
}

Se puede hacer uso de “:last” para agregar o descartar esta ultima fila del grid donde se presentara el total.

Se recorre todas las filas del gridview, dejando fuera el footer, por eso es “:not(:last)”, se esta descartando el ultimo <tr>. Pero si se toma exactamente la ultima fila cuando se quiere asignar el total resultante, por eso se usa el “tr:last”

Nota: por supuesto hay que quitar la propiedad AllowPaging="True" del gridview para probarlo sin paginado

 

Calculo total con paginado

Al tener paginado la ultima final ya no será el footer, por lo tanto usar el :last no funciona, se requiere otras técnicas de selección

function CalcularTotal() {

    var total = 0;
    $("#<%=GridView1.ClientID%> [id*='lblPrecio']").each(function() {

        var coltotal = parseFloat($(this).html());

        if (!isNaN(coltotal)) {
            total += coltotal;
        }

    });

    $("#<%=GridView1.ClientID%> [id*='lblTotal']").html(total);

}

 

Se decide ir directamente a los controles que contienen la información.

Al tomar el valor del <span> (que representa el lblPrecio), puede que este no sea un valor numérico valido, es por eso que se parsea a float, si devuelve NaN es porque no es un numero que sumar.

 

Código

Se ha usado Visual Studio 2008, es necesario SP1 del VS, como base de datos Sql Compact 3.5

 

jueves, 17 de mayo de 2012

[Winforms] TreeView– Cambiar Estado Node con CheckBox

 

Introducción

Muchas veces es necesario cambiar el estado de ítems predecesores o hijo según ciertos criterios, el control TreeView por posee una estructura jerárquica en sus datos es un caso de esta situación.

 

Validar estado de los nodos


En la implementación de código que se puede observar aplica conceptos simples como de validación de estados de los nodos del treeview.

Básicamente consiste en dos parte

  •    validar si el nodo tiene un nodo padre, para lo cual será necesario verificar si todos los hijos están marcados
  •    cambiar el estado a todos los hijos según la marca de este nodo

Estas acciones se realizan sobre el mismo evento ya que implica de forma contextual al nodo que interviene en la acción

 

private void tvCategoryproducts_AfterCheck(object sender, TreeViewEventArgs e)
{
    //
    // Se remueve el evento para evitar que se ejecute nuevamente por accion de cambio de estado 
    // en esta operacion
    //
    tvCategoryproducts.AfterCheck -= tvCategoryproducts_AfterCheck;

    //
    // Se valida si el nodo marcado tiene presedente
    // en caso de tenerlo se debe evaluar los nodos al mismo nivel para determinar si todos estan marcados, 
    // si lo estan se marca tambien el nodo padre
    //
    if (e.Node.Parent != null)
    {
        bool result = true;
        foreach (TreeNode node in e.Node.Parent.Nodes)
        {
            if (!node.Checked)
            {
                result = false;
                break;
            }
        }

        e.Node.Parent.Checked = result;

    }

    //
    // Se valida si el nodo tiene hijos
    // si los tiene se recorren y asignan el estado del nodo que se esta evaluando
    //
    if (e.Node.Nodes.Count > 0)
    {
        foreach (TreeNode node in e.Node.Nodes)
        {
            node.Checked = e.Node.Checked;
        }
    }


    tvCategoryproducts.AfterCheck += tvCategoryproducts_AfterCheck;

}

 

Optimización usando Linq

Linq nos puede evitar tener que escribir un montón de código, sino lo creen solo basta con dar una mirada a la implementación alternativa que evalúa los nodos marcados

En este caso hice uso de los método de extensión ayudado con lambda, pero se podría haber usado la notación linq sin problemas

 

private void tvCategoryproducts_AfterCheck_UsingLinq(object sender, TreeViewEventArgs e)
{
    //
    // Se remueve el evento para evitar que se ejecute nuevamente por accion de cambio de estado 
    // en esta operacion
    //
    tvCategoryproducts.AfterCheck -= tvCategoryproducts_AfterCheck_UsingLinq;

    //
    // Se valida si el nodo marcado tiene presedente
    // en caso de tenerlo se debe evaluar los nodos al mismo nivel para determinar si todos estan marcados, 
    // si lo estan se marca tambien el nodo padre
    //
    if (e.Node.Parent != null)
    {
        e.Node.Parent.Checked = !e.Node.Parent.Nodes.Cast<TreeNode>().Any(x => !x.Checked);
    }

    //
    // Se valida si el nodo tiene hijos
    // si los tiene se recorren y asignan el estado del nodo que se esta evaluando
    //
    if (e.Node.Nodes.Count > 0)
    {
        e.Node.Nodes.Cast<TreeNode>().ToList().ForEach(x => x.Checked = e.Node.Checked);
    }


    tvCategoryproducts.AfterCheck += tvCategoryproducts_AfterCheck_UsingLinq;

}

Código


Se utilizo Visual Studio 2010 y Sql Compect 3.5

  [C# SkyDrive]

lunes, 30 de abril de 2012

[ASP.NET] PopUp Filtro – usando window.open()

 

Introducción

El objetivo del artículo apunta a demostrar como implementar la búsqueda de un determinado ítem haciendo uso de ventanas que se abran en popup.

Para la implementación se utilizara la funcionalidad de window.open() provista por javascript.

El ejemplo consiste en un listado de producto, pudiendo ser filtrados mediante una ventana popup que lista los proveedores.

El código fue implementado de dos formas distintas, si bien son muy parecidas tienen sutiles cambios, estas dos formas las denomine:

  • Selección con evento
  • Selección sin evento

Imagino se preguntaran del porque de esta diferencia del uso de un evento o no, básicamente difieren en el uso de controles asp.net (con evento) o tag de html (sin evento), cada alternativa cambia la forma en que se implementa la solución, en concreto la forma en que se toma el ítem elegido.

Empezaremos analizando la parte de la implementación que es común en ambos casos para luego ir a los detalles que afectan el usar un control de asp.net o uno de html

Abrir ventana de búsqueda

La definición del botón de búsqueda se logra adjuntando el evento click de jquery al tag <input>, como se refleja en la imagen

Esta acción lanzara la ventana de búsqueda que se ha implementado mediante el uso de window.open()

 

Filtro de productos

Una vez seleccionado el proveedor en la ventana de búsqueda esta acciona, o ejecuta, el botón de filtro, el cual se ha implementado, en en la pagina original, es decir la ventana hija invoca un boton en la ventana padre.

 

protected void btnFiltrar_Click(object sender, EventArgs e)
{
    LoadGridProducts();

    //
    // muestro la descripcion del proveedor
    //
    Suppliers supplier = NorthwindData.GetSupplierById(txtId.Text);

    string CompanyName = "";
    if (supplier != null)
    {
        CompanyName = supplier.CompanyName.Encode();
    }

    //
    // asigna el nombre al label que esta fuera del updatepanel
    //
    string script = @"$('#{0}').text('{1}');";
    script = string.Format(script, lblSupplierName.ClientID, CompanyName);

    ScriptManager.RegisterStartupScript(this, typeof(Page), "filterinfo", script, true);

}
private void LoadGridProducts()
{
    int? SupplierId = null;

    int id = 0;
    if (Int32.TryParse(txtId.Text, out id))
    {
        SupplierId = id;
    }
    

    gvProducts.DataSource = NorthwindData.GetProductsBySupplier(SupplierId);
    gvProducts.DataBind();
}

Algo que quizás no parezca tener sentido al principio es porque el nombre del proveedor seleccionado es asignado mediante código javascript (o mejor dicho jquery), en lugar de hacerlo directamente, bueno esto se deba al uso del control UpdatePanel, el label donde se asigna el nombre del proveedor seleccionado esta por fuera del panel lo cual requiere que al asignar un valor sea realice de forma indirecta.

 

Selección sin evento (usando html <img>)

Empezaremos analizando como realizar la selección usando un control simple de html, en este caso en el grid de proveedores se usa el tag <img> dentro del item template

Pro medio de la ayuda de jquery se asocia el evento click, usando el atributo class del <img> como selector

<script type="text/javascript">

    $(function() {

        $('#<%=gvSuppliers.ClientID %> .imgSelection').click(function() {

            var supplierID = $(this).attr('SupplierID');
            
            var tr = $(this).parent().parent();
            var companyName = $('td:eq(2)', tr).text();

            window.opener.$("[id*='txtId']").val(supplierID);
            window.opener.$("[id*='lblSupplierName']").text(companyName);

            window.opener.$("[id*='btnFiltrar']").click();

            window.close();
        })

    });

</script>

Como el evento lo lanza el propio tag <img> se puede acceder a este mediante $(this) tomando el atributo, pero para recuperar información adicional debe subirse hasta el tag <tr> de la tabla que genera el gridview para así poder acceder a los datos de las celdas, el td:eq(2), hace referencia a la segunda columna del grid

La integración de jquery con javascript nos permite realizar una búsqueda en la página padre usando window.opener, y mediante el *= sobre el id, una búsqueda aproximada, similar a usar el like.

en las ultimas dos líneas se lanza el evento click del boton de filtro y se cierra la ventana.

 

Selección con evento (uso control asp.net ImageButton)

A diferencia del caso sin evento, en este se hace uso de un control de asp.net para seleccionar el proveedor del grid, como se puede observar en la imagen

Este simple cambio impacta en la forma en que se implementa el resto

<script type="text/javascript">

    function SupplierSelected(supplierID, companyName) {

        window.opener.$("[id*='txtId']").val(supplierID);
        window.opener.$("[id*='lblSupplierName']").text(companyName);

        window.opener.$("[id*='btnFiltrar']").click();

        window.close();
    }

</script>

El javascript usado se reduce, ya no se implementa la búsqueda en al tabla del html que genera el gridview, pues se dispone acceso directo al control desde código .net

protected void gvSuppliers_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
    
    int supplierId = Convert.ToInt32(gvSuppliers.DataKeys[e.NewSelectedIndex].Value);

    string companyName = gvSuppliers.Rows[e.NewSelectedIndex].Cells[2].Text;


    string script = @"$(function(){{
                        SupplierSelected('{0}','{1}');
                    }});"; //se define {{ para poder usar el string.Format()

    script = string.Format(script, supplierId, companyName.Encode());

    ScriptManager.RegisterStartupScript(Page, typeof(Page), "popupclose", script, true);

}

La asignación del valor de retorno no es tan directa, sino que debe ser escrita en el retorno del postback del evento, esto es necesario ya que el window.opener solo puede usarse desde código cliente.

 

Código
[C#]
 

[ASP.NET] PopUp Filtro – Usando jquery UI Dialog

 

Introducción

El objetivo del artículo apunta a demostrar como implementar la búsqueda de un determinado ítem haciendo uso de ventanas que se abran en popup.

Para implementarlo se hará uso de la librería jquery UI, la cual incluye funcionalidad para crear cuadros de dialog modales.

El código fue implementado de dos formas distintas, si bien son muy parecidas tienen sutiles cambios, estas dos formas las denomine:

  • Selección con evento (usando un ImageButton)
  • Selección sin evento  (usando el tag html <img>)

Imagino se preguntaran del porque de esta diferencia del uso de un evento o no, básicamente difieren en el uso de controles asp.net (con evento) o tag de html (sin evento), cada alternativa cambia la forma en que se implementa la solución, en concreto la forma en que se toma el ítem elegido.

Empezaremos analizando la parte de la implementación que es común en ambos casos para luego ir a los detalles que afectan el usar un control de asp.net o uno de html

 

Abrir ventana de filtro

La definición de la ventana por medio de jquery UI solo implica asociar al <div> que representara el dialog

 

$(function() {

    $('#btnOpenSupplierSearch').click(function() {

        registerGridEvent();
        
        $('#dialog').dialog('open');

    });

    $('#dialog').dialog({
        autoOpen: false,
        modal: true,
        width: 700,
        heigth: 250,
        title: 'Buscar Proveedor',
        buttons: {
            "Cancel": function() {
                $(this).dialog("close");
            }
        }
    });

});

En este caso será por medio de una acción del botón de búsqueda que se abrirá la ventana.

 

Selección sin evento (usando html <img>)

Es muy importante en este punto notar como se asocia el <img> que representa la selección de cada row del grid con su correspondiente acción en javascript.

En esta acción desde javascript no solo se toma la información del html, sino que además se cierra el dialog y se lanza el evento de búsqueda accionado el botón que tiene esta funcionalidad.

 

function registerGridEvent() {

    $('#<%=gvSuppliers.ClientID %> .imgSelection').click(function() {

        var supplierID = $(this).attr('SupplierID');

        var tr = $(this).parent().parent();
        var companyName = $('td:eq(2)', tr).text();

        //asigno datos a los controles
        $("[id*='txtId']").val(supplierID);
        $("[id*='lblSupplierName']").text(companyName);

        //cierro el popup de seleccion
        $('#dialog').dialog("close");
        
        //aplico el filtro
        $("[id*='btnFiltrar']").click();
    })

}

Re-asignación de evento jquery

Existe un punto de conflicto entre los eventos de asp.net y jquery, espacialmente cuando se usa el UpdatePanel para conservar los eventos y se mezclan acción de jquery.

Estos dos mundos no se llevan muy bien, un claro ejemplo se podría apreciar con el paginado del gridview, si bien al ir cambiando de pagina no se nota el refresco ya que el UpdatePanel lo hace simple, por debajo se genera una invocación al servidor para ejecutar del evento asociado al grid.

 

protected void gvSuppliers_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    gvSuppliers.PageIndex = e.NewPageIndex;

    LoadGridSuppliers();

    //
    // registro el evento click del boton, ya que al pagina el postback 
    // quita la asociacion del evento que jquery
    //
    string script = "registerGridEvent();";

    ScriptManager.RegisterClientScriptBlock(this, typeof(Page), "registerevent", script, true);

}

Jquery se ve afectado si es que en ese bloque se tenia eventos asociados en algún tag, en este caso concreto el tag <img> esta asociado al evento click() de jquery, por lo tanto hay que volver a re-asociar para que se revalide la acción.

Es por eso que en el evento de paginado en el server se lanza la acción en el cliente por medio del

ScriptManager.RegisterClientScriptBlock()

Esto es necesario para que a la vuelta del postback se ejecute la function javascript y asigne nuevamente el evento click() de jquery al selector definido.

 

Selección con evento (uso control asp.net ImageButton)

En este caso el camino tiene algunas vueltas entres server y cliente:

De la acción del ImageButton se ingresa al evento SelectedIndexChanged en el código .net, quien luego de tomar la información del gridview armara el script que asignara la info, cerrando el dialogo y por ultimo lanzando la acción de botón para filtrar los productos.

 

protected void gvSuppliers_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{

    int supplierId = Convert.ToInt32(gvSuppliers.DataKeys[e.NewSelectedIndex].Value);

    string companyName = gvSuppliers.Rows[e.NewSelectedIndex].Cells[2].Text;


    string script = @"$(function(){{
                        SupplierSelected('{0}','{1}');
                    }});"; //se define {{ para poder usar el string.Format()

    script = string.Format(script, supplierId, companyName.Encode());

    ScriptManager.RegisterStartupScript(Page, typeof(Page), "popupclose", script, true);

}

 

function SupplierSelected(supplierID, companyName) {

    $("[id*='txtId']").val(supplierID);
    $("[id*='lblSupplierName']").text(companyName);

    //cierro el popup de seleccion
    $('#dialog').dialog("close");

    //aplico el filtro
    $("[id*='btnFiltrar']").click();
    
}

Un punto que no hay que pasar por alto que afecta al evento del control dentro del dialog, jquery reasigna las acciones del dialogo a nivel del body y no del form, por eso línea cambia esa acción de esta forma los eventos de asp.net se habilitan nuevamente.

 

Código
[C#]
 

domingo, 29 de abril de 2012

[ASP.NET] PopUp Filtro – Usando Ajax Toolkit ModalPopupExtender

 

Introducción

En esta oportunidad analizaremos como implementar un filtro usando los controles de ajax tookit, mas concretamente el control ModalPopupExtender

Se hará uso de una lista de productos la cual será filtrada por proveedores, desplegados en el popup.

Algo que notaran en el ejemplo será dos formas de implementar la selección del proveedor:

- en una se contara con al ayuda de ToolkitScriptManager para registrar la información seleccionada, será necesario un .js (ListProducts.js), encargado de tomar la info y asignarla a los controles

- la otra será una asignación directa usando jquery (esta ultima seria la recomendable, porque requiere menos objetos involucrados en su implementación)

 

Abrir ventana de búsqueda

La apertura de la ventana de filtro será realizada por medio de un evento asp.net, pero para poder hacerlo es necesario deshabilitar el javascript que asocia el botón con el ModalPopupExtender.

Para lograrlo es que se asigna la propiedad TargetControlID a un objeto hidden, como muestra la imagen

Esto permite que desde el botón se pueda cargar los ítems en el gridview que formara parte del popup de filtro, si se hubiera asignado directo el botón al ModalPopupExtender, este deshabilita los evento de asp.net, por lo que la carga del grid no se hubiera podido llevar a cabo.

 protected void btnOpenSupplierSearch_Click(object sender, EventArgs e)
 {
     LoadGridSuppliers();

     ModalPopupExtender1.Show();

 }

private void LoadGridSuppliers()
 {
     gvSuppliers.DataSource = NorthwindData.GetAllSuppliers();
     gvSuppliers.DataBind();
 }

El uso del Show() sobre el ModalPopupExtender abrirá la ventana, luego de cargar el grid que contiene.

 

Selección ítem en el popup

La selección del proveedor será implementada por medio de un botón de imagen de asp.net, el cual lanzara el evento del gridview, es por eso que se asocia el CommandName=”Select”

La acción de este evento podría ser implementada de dos formas:

Usando al funcionalidad de ToolkitScriptManager

En la implementación de esta opción puede implicar varios puntos a tener en cuenta, el primero involucra hacer uso del ToolkitScriptManager para asignar la información que se quiere devolver como resultado de la selección

protected void gvSuppliers_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
    string id = Convert.ToString(gvSuppliers.DataKeys[e.NewSelectedIndex].Value);
    string name = gvSuppliers.Rows[e.NewSelectedIndex].Cells[2].Text;

    //
    // asigno la informacion a los controles que estan fuera de updatepanel
    //
    ToolkitScriptManager1.RegisterDataItem(txtId, id);
    ToolkitScriptManager1.RegisterDataItem(lblSupplierName, name);


    //
    // Cargo grid de productos con el dato seleccionado
    //
    LoadGridProducts(id);

    //
    // cierro el popup
    //
    ModalPopupExtender1.Hide();
}

 

Pero además implica hacer uso de JavaScript para poder tomar la información enviada en el RegisterDataItem(), es por eso que se asocia un .js que cumplirá esta función

<asp:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Path="~/ListProducts.js" />
    </Scripts>
</asp:ToolkitScriptManager>

El contenido del .js implementa el siguiente código

Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(SupplierFilterHandler);

function SupplierFilterHandler(sender, args) {
    var datos = args.get_dataItems();

    //obtenermos el id geenrado por asp.net, o sea el ClientID
    var txtId = $("[id*='txtId']").attr('id');
    var lblSupplierName = $("[id*='lblSupplierName']").attr('id');
    

    if (datos[txtId] == undefined)
        return;

    $("[id*='txtId']").val(datos[txtId]);
    $("[id*='lblSupplierName']").text(datos[lblSupplierName]);

}

La idea es poder tomar la información enviada desde el código .net desde el cliente y asignarla a los controles todo ello desde código cliente, sin mezclar parte de .net con parte javascript, separando cada responsabilidad

Se usa el *= ya que el id del control es generado por asp.net (el cual puede cambiar del asignado desde el diseñador), por eso se ubica el nombre real generado, ya que no se disponer del ClientID para tomarlo desde el .js

 

Usando directamente jquery

Este seria el camino directo para asignar la información del filtro en la pagina principal

protected void gvSuppliers_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
    string id = Convert.ToString(gvSuppliers.DataKeys[e.NewSelectedIndex].Value);
    string name = gvSuppliers.Rows[e.NewSelectedIndex].Cells[2].Text;

    //
    // asigno la informacion a los controles que estan fuera de updatepanel
    //
    string script = @" $(""[id*='txtId']"").val('{0}');
                       $(""[id*='lblSupplierName']"").text('{1}');";

    script = string.Format(script, id, name.Encode());

    ScriptManager.RegisterStartupScript(this, typeof(Page), "filterinfo", script, true);

    //
    // Cargo grid de productos con el dato seleccionado
    //
    LoadGridProducts(id);

    //
    // cierro el popup
    //
    ModalPopupExtender1.Hide();
}

 

En esta implementación solo es necesario definir el script que se encargara de asignar los datos a los controles y enviarlo al cliente por medio del ScriptManager.RegisterStartupScript().

Esto es mucho mas directo y sin vueltas que el paso anterior, aunque requiere generar código javascript (ayudado con jquery) inserto en medio del con código .net en el servidor, a veces puede gustar, a veces no, mezclar este tipo de código, pero si hay que reconocer que es el camino directo en su implementación.

 

Código
[C#]
 

lunes, 2 de abril de 2012

ASP.NET–GridView anidados (maestro-detalle) (GridView nested)

 

Introducción

El objetivo en el articulo será el demostrar como armar un grid con jerarquía, en este caso mostrando las ordenes de compra de un cliente

Además se pretende demostrar como se puede armar una entidad compleja que retorne los datos en una estructura anidada de listas

 

Detectar despliegue detalle

Uno de los puntos mas importantes en la implementación esta relacionado con la forma en que se detecta el expandir\colapsar del detalle

En la imagen se remarcan dos atributos creados con este propósito, la idea es poder relacionar la imagen con el tag <tr> que permite ocultar o mostrar el grid del detalle

La idea detrás de esto es ayudar luego a jquery para poder determinar que elementos intervienen en la acción

con jquery se extrae el id de la orden, para luego usarlo de selector del <tr> que posee ese mismo id

el toggle() solo permite switchear entre visible\invisible, y detectando el estado mostrar una imagen u otra

 

Estructura de datos jerárquica

La estructura usada para el modelo relaciona tres clases

La idea es mostrar como al cargar los datos, es analizar como los métodos se invocan unos a otros para recuperar la info enlazada en al jerarquía

 

Código

El codigo fue confeccionado con Visual Studio 2008 y base de datos Sql Compact 3.5

[C#]
[C# SkyDrive]

martes, 20 de marzo de 2012

jqGrid–Crear jerarquía de registros con Tree Grid

 

Introducción

La finalidad buscada con el articulo articulo será demostrar como por medio del componente jqGrid se pueden representar estructuras de árbol en diferentes niveles de agrupación

El ejmeplo esta basado en la ordenes de compra de la db Northwind, tomando el campo “OrderData” y agrupando por año y mes

Es por eso que la opciones de expandir primero mostrarán los años, luego los mese y al final la info de la orden.

Se puede dejar el campo de Customer sin ingresar un valor, lo cual listara todas las ordenes registradas

Es muy importante notar en el código como se crea el grid que se enviara por json al grid

var grid = new
{
    page = 1,
    records = orderList.Count,
    total = orderList.Count,

    rows = from item in orderList
            select new
            {
                id = item.IdUnique,
                cell = new string[]
                {
                    item.Id,
                    item.Desc,
                    item.Level.ToString(),
                         item.Parent,
                         item.IsLeaf.ToString().ToLower(),
                        "false"
                }
            }

 

};

luego de los campos de datos se debe definir en orden los siguiente ítems:

  • nivel
  • parent
  • si es un nodo hoja
  • si debe mostrarse expandido el nodo

 

Código


El ejemplo esta confeccionado con VS 2010 y Sql Compact (la db se encuentra en la carpeta App_Data)

[C#]
[C# Skydrive]