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