viernes, 20 de febrero de 2015

El Demo del Día: Graficar Datos en el Cliente en ASP.NET MVC con JSON, Canvas y JavaScript

Graficar Datos en el Cliente en ASP.NET MVC con JSON, Canvas y JavaScript

Después de un par de semanas sin publicar un demo, les presento un post muy interesante de como trabajar en forma desconectada en el cliente para realizar consultas y gráficos de datos en ASP.NET MVC para lo cual usaremos jQuery, JSON, Canvas y JavaScript.

Requerimiento

Se desea crear una aplicación web que permita consultar los productos por categoría y mostrar los datos mas gráficos de diferentes tipos: Barras, Columnas, Lineas y Pie, pero el requisito es que sea desconectada tanto del Servidor de Datos como del Servidor Web, es decir, el filtro para la consulta y el gráfico se hará en el cliente.

Solución

- Crear una aplicación en ASP.NET MVC con un método de acción que se conecte a la BD una sola vez y devuelva un objeto con 2 listas: categorías y productos.
- Devolver una vista al cliente con la lista de categorías y la lista de tipos de gráficos.
- Ni bien carga la vista en el cliente hacer una llamada asíncrona usando $.ajax de jQuery para llamar a un método de acción que devuelva la lista de productos en formato JSON.
- Cuando el usuario seleccione una Categoría o seleccione un Tipo de Gráfico se llamará a una función JavaScript que filtrará los datos por la categoría seleccionada y los presentará en una tabla además lo dibujará en un Canvas.

Crear el Procedimiento Almacenado en la Base de Datos de SQL Server

Para el ejemplo usaremos la Base de Datos Northwind de SQL Server, en la cual crearemos el siguiente Procedimiento almacenado:

Create Procedure [dbo].[uspCategoriesProductsListar]
As
Select CategoryID,CategoryName From Categories
Select ProductID,ProductName,SupplierID,CategoryID,UnitPrice,UnitsInStock From Products

Crear la Librería de Clases Entidades del Negocio

Crear la librería de clases llamada: "Northwind.Librerias.EntidadesNegocio" y agregar la clase "beCategoria.cs":

namespace Northwind.Librerias.EntidadesNegocio
{
    public class beCategoria
    {
        public int IdCategoria { get; set; }
        public string Nombre { get; set; }
    }
}

Agregar otra clase a la librería llamada "beProducto.cs":

namespace Northwind.Librerias.EntidadesNegocio
{
    public class beProducto
    {
        public int IdProducto { get; set; }
        public string Nombre { get; set; }
        public int IdProveedor { get; set; }
        public int IdCategoria { get; set; }
        public decimal PrecioUnitario { get; set; }
        public short Stock { get; set; }
    }
}

Finalmente, agregar una clase llamada "beCategoriaProducto.cs" que agrupe las 2 listas:

using System;
using System.Collections.Generic;

namespace Northwind.Librerias.EntidadesNegocio
{
    public class beCategoriaProducto
    {
        public List<beCategoria> ListaCategoria { get; set; }
        public List<beProducto> ListaProducto { get; set; }
    }
}

Grabar y compilar la Librería de Entidades del Negocio.

Crear la Librería de Acceso a Datos

Crear la librería de clases llamada: "Northwind.Librerias.AccesoDatos", referenciar a la librería de clases de entidades creada anteriormente y agregar la clase "daCategoriaProducto.cs" y escribir el siguiente código:

using System;
using System.Collections.Generic; //List
using System.Data; //CommandType, CommandBehavior
using System.Data.SqlClient; //SqlConnection, SqlCommand, SqlDataReader
using Northwind.Librerias.EntidadesNegocio; //beCategoriaProducto, beCategoria, beProducto

namespace Northwind.Librerias.AccesoDatos
{
    public class daCategoriaProducto
    {
        public beCategoriaProducto obtenerListas(SqlConnection con)
        {
            beCategoriaProducto obeCategoriaProducto = new beCategoriaProducto();
            List<beCategoria> lbeCategoria = null;
            List<beProducto> lbeProducto = null;

            SqlCommand cmd = new SqlCommand("uspCategoriesProductsListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader();
            if (drd != null)
            {
                lbeCategoria = new List<beCategoria>();
                beCategoria obeCategoria;
                int posIdCat = drd.GetOrdinal("CategoryID");
                int posNomCat = drd.GetOrdinal("CategoryName");
                while (drd.Read())
                {
                    obeCategoria = new beCategoria();
                    obeCategoria.IdCategoria = drd.GetInt32(posIdCat);
                    obeCategoria.Nombre = drd.GetString(posNomCat);
                    lbeCategoria.Add(obeCategoria);
                }
                obeCategoriaProducto.ListaCategoria = lbeCategoria;
                if (drd.NextResult())
                {
                    lbeProducto = new List<beProducto>();
                    int posIdProducto = drd.GetOrdinal("ProductID");
                    int posNombre = drd.GetOrdinal("ProductName");
                    int posIdProveedor = drd.GetOrdinal("SupplierID");
                    int posIdCategoria = drd.GetOrdinal("CategoryID");
                    int posPrecioUnitario = drd.GetOrdinal("UnitPrice");
                    int posStock = drd.GetOrdinal("UnitsInStock");
                    beProducto obeProducto;
                    while (drd.Read())
                    {
                        obeProducto = new beProducto();
                        obeProducto.IdProducto = drd.GetInt32(posIdProducto);
                        obeProducto.Nombre = drd.GetString(posNombre);
                        obeProducto.IdProveedor = drd.GetInt32(posIdProveedor);
                        obeProducto.IdCategoria = drd.GetInt32(posIdCategoria);
                        obeProducto.PrecioUnitario = drd.GetDecimal(posPrecioUnitario);
                        obeProducto.Stock = drd.GetInt16(posStock);
                        lbeProducto.Add(obeProducto);
                    }
                    obeCategoriaProducto.ListaProducto = lbeProducto;
                }
                drd.Close();
            }
            return (obeCategoriaProducto);
        }
    }
}

Grabar y compilar la librería de acceso a datos creada.

Crear la Librería de Reglas del Negocio

Crear la librería de clases llamada: "Northwind.Librerias.ReglasNegocio", referenciar a la librería de clases de entidades y también a la de acceso a datos, luego agregar la clase "brCategoriaProducto.cs" y escribir el siguiente código:

using System;
using System.Configuration; //ConfigurationManager
using System.Data.SqlClient; //SqlConnection
using System.Collections.Generic; //List
using Northwind.Librerias.EntidadesNegocio; //beCategoriaProducto
using Northwind.Librerias.AccesoDatos; //daCategoriaProducto

namespace Northwind.Librerias.ReglasNegocio
{
    public class brCategoriaProducto:brGeneral
    {
        public beCategoriaProducto obtenerListas()
        {
            beCategoriaProducto obeCategoriaProducto = null;
            string CadenaConexion = ConfigurationManager.ConnectionStrings["conNW"]
                                                     .ConnectionString;
            using (SqlConnection con = new SqlConnection(CadenaConexion))
            {
                try
                {
                    con.Open();
                    daCategoriaProducto odaCategoriaProducto = new daCategoriaProducto();
                    obeCategoriaProducto = odaCategoriaProducto.obtenerListas(con);
                }
                catch (SqlException ex)
                {
                    //grabarLog(ex);
                }
                catch (Exception ex)
                {
                    //grabarLog(ex);
                }
            } //con.Close(); con.Dispose();
            return (obeCategoriaProducto);
        }
    }
}

Nota: También es necesario hacer una referencia a la librería "System.Configuration" para leer la cadena de conexión definida en la aplicación.

Grabar la librería de reglas de negocio creada y compilarla.

Crear una Aplicación Web de ASP.NET MVC4 en C#

Seleccionar un nuevo proyecto de tipo: "Aplicación web de ASP.NET MVC4" y escribir como nombre "Canvas_GraficosDatos", luego seleccionar la opción "Vacio" y como motor de vista "Razor" y "Aceptar".

Crear un Controlador para el Producto

Clic derecho a la carpeta Controllers y seleccionar "Agregar" y luego "Controlador", llamarle al archivo "ProductoController.cs" y en opciones de plantillas dejarlo en plantilla vacía (Vaciar Controlador MVC). Luego escribir el siguiente código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Northwind.LibBusEntities;
using Northwind.LibBusRules;

namespace HT03_Canvas_GraficosDatos.Controllers
{
    public class ProductoController : Controller
    {
        public ActionResult Lista()
        {
            List<string> tipoGrafico = new List<string>();
            tipoGrafico.Add("Barras");
            tipoGrafico.Add("Columnas");
            tipoGrafico.Add("Lineas");
            tipoGrafico.Add("Pie");
            ViewBag.Tipo = tipoGrafico;
            brCategoriaProducto obrCategoriaProducto = new brCategoriaProducto();
            beCategoriaProducto obeCategoriaProducto = obrCategoriaProducto.Listar();
            Session["Productos"] = obeCategoriaProducto.ListaProducto;
            return View(obeCategoriaProducto.ListaCategoria);
        }

        public JsonResult Filtro()
        {
            JsonResult rpta;
            List<beProducto> lbeProducto = (List<beProducto>)Session["Productos"];
            rpta = Json(lbeProducto, JsonRequestBehavior.AllowGet);
            return (rpta);
        }
    }
}

Crear una Hoja de Estilos para la Vista

Primero crear una carpeta llamada "Content", luego clic derecho "Agregar" y luego "Hoja de estilos" y como nombre llamarle "ACME.css" y escribir el siguiente código:

body {
    background-color:lightgray;
}
.Titulo {
    background-color:black;
    color:white;
    font-size:x-large;
    text-transform:uppercase;
}
.Subtitulo {
    background-color:white;
    color:black;
    font-size:large;
    text-transform:capitalize;
    font-weight:bold;
}
.AnchoTotal {
    width:100%;
}
.FilaCabecera {
    background-color:gray;
    color:white;
}
.FilaDatos {
    background-color:white;
    color:black;
}
.Cuadro {
    background-color:white;
    border-style:double;
}

Crear la Vista Productos para mostrar los datos

Ir al controlador y ubicarse sobre el método "Lista" (acción), clic derecho y seleccionar "Agregar vista", seleccionar el check "Crear una vista fuertemente tipada" y escribir el siguiente código:

@using Northwind.LibBusEntities
@model List<beCategoria>
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Lista</title>
    <link href="~/Content/Styles/ACME.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-1.7.1.min.js"></script>
    <script src="~/Scripts/Rutinas.js"></script>
</head>
<body>
    <div>
        <table class="AnchoTotal">
            <tr class="Titulo">
                <td colspan="2">
                   Graficar Datos en el Cliente en ASP.NET MVC con JSON, Canvas y JavaScript
                </td>
            </tr>
            <tr class="Subtitulo">
                <td colspan="2">Gráfico de Precios de Productos x Categoria</td>
            </tr>
            <tr>
                <td style="width:40%">
                    Selecciona una Categoria: @Html.DropDownList("idCategoria",
                    new SelectList(Model,"Codigo","Nombre"))
                </td>
                <td style="width:60%">
                    Selecciona el Tipo de Grafico: @Html.DropDownList("tipo",
                    new SelectList(ViewBag.Tipo))
                </td>
            </tr>
            <tr>
                <td style="vertical-align:top">
                    <table class="AnchoTotal">
                        <thead>
                            <tr class="FilaCabecera">
                                <td style="width:80%">Nombre del Producto</td>
                                <td style="width:20%">Stock</td>
                            </tr>
                        </thead>
                        <tbody id="tbProducto">
                        </tbody>
                    </table>
                </td>
                <td>
                    <canvas id="canvas" width="600" height="400" class="Cuadro"/>
                </td>
            </tr>
        </table>
    </div>
</body>
</html>

Crear el archivo JavaScript con el código cliente

Antes que nada crear una carpeta llamada "Scripts" y arrastrar del explorador de Windows el archivo de jQuery: "jquery-1.7.1.min.js", luego agregar un archivo de JavaScript llamado "Rutinas.js" y escribir el siguiente código:

$(document).ready(function () {
    var rpta;
    var cboCategoria = document.getElementById("idCategoria");    
    cboCategoria.onchange = function () { crearTablaGrafico(); }
    var cboTipo = document.getElementById("tipo");
    cboTipo.onchange = function () { crearTablaGrafico(); }
    $.ajax({
        type: "post",
        url: "/Producto/Filtro",
        contentType: "application/json;charset=utf-8",
        dataType: "json",
        success: exito,
        error: error
    });

    function crearTablaGrafico() {
        var idCategoria = cboCategoria.value * 1;
        var tbProducto = document.getElementById("tbProducto");
        var tipo = cboTipo.value;
        var contenido = "";
        var canvas = document.getElementById("canvas");
        var contexto = canvas.getContext("2d");
        if (contexto != null) graficar(rpta);
        function graficar(rpta) {
            //Crear Degradado
            var deg = contexto.createLinearGradient(0, 0, canvas.width, canvas.height);
            deg.addColorStop(1, "aqua");
            deg.addColorStop(0.1, "blue");
            //Dibujar Rectangulo Degradado
            contexto.fillStyle = deg;
            contexto.fillRect(0, 0, canvas.width, canvas.height);
            //Variables para los calculos
            var x = 10;
            var y = 20;
            var valor = 0;
            var escala = 0;
            var maximo = 0;
            var total = 0;
            if (tipo != "Pie") {
                maximo = calcularMaximo(idCategoria);
                if (tipo == "Barras") escala = Math.abs((canvas.width - 180) / maximo);
                else {
                    escala = Math.abs((canvas.height - 120) / maximo);
                    x = 50;
                }
            }
            else total = calcularTotal(idCategoria);
            var centroX = Math.floor(canvas.width / 2);
            var centroY = Math.floor(canvas.height / 2);
            var radio = Math.floor(canvas.width / 4);
            var anguloInicio = 0;
            var arco = 0;
            var anguloFin = 0;
            //Dibujar el grafico de acuerdo al tipo
            for (i = 0; i < rpta.length; i++) {
                if (rpta[i].IdCategoria == idCategoria) {
                    contenido += "<tr class='FilaDatos'><td>" + rpta[i].Nombre + "</td>" +
                        "<td class='Derecha'>" + rpta[i].PrecioUnitario + "</td></tr>";
                    switch(tipo)
                    {
                        case "Barras":
                            x = 10;
                            contexto.fillStyle = "white";
                            contexto.font = "10px arial";
                            contexto.fillText(rpta[i].Nombre, x, y);
                            x = 150;
                            valor = rpta[i].PrecioUnitario * escala;
                            contexto.fillStyle = "yellow";
                            contexto.fillRect(x, y - 10, valor, 10);
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].PrecioUnitario, x + valor + 5, y);
                            y = y + 20;
                            break;
                        case "Columnas":
                            contexto.save();
                            contexto.translate(x, canvas.height - 10);
                            contexto.rotate(-(Math.PI / 3));
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].Nombre, 0, 0);
                            contexto.restore();
                            contexto.fillStyle = "yellow";
                            valor = (canvas.height - 100 - (rpta[i].PrecioUnitario*escala));
                            contexto.fillRect(x, valor, 10, rpta[i].PrecioUnitario * escala);
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].PrecioUnitario, x, valor - 10);
                            x += 40;
                            break;
                        case "Lineas":
                            contexto.save();
                            contexto.translate(x, canvas.height - 10);
                            contexto.rotate(-(Math.PI / 3));
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].Nombre, 0, 0);
                            contexto.restore();                            
                            contexto.strokeStyle = "yellow";
                            contexto.lineWidth = 3;
                            if (x== 50) {
                                contexto.beginPath();
                                valor = Math.floor(canvas.height - 100 - (rpta[i].PrecioUnitario) * escala);
                                contexto.moveTo(x, valor);
                            }
                            valor = Math.floor(canvas.height - 100 - (rpta[i].PrecioUnitario * escala));
                            contexto.lineTo(x, valor);
                            contexto.stroke();                
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].PrecioUnitario, x, valor - 10);
                            x += 40;
                            break;
                        case "Pie":
                            arco=(rpta[i].PrecioUnitario * 2 * Math.PI) / total;
                            anguloFin = anguloInicio + arco;
                            contexto.save();
                            contexto.beginPath();
                            contexto.moveTo(centroX, centroY);                            
                            contexto.arc(centroX, centroY, radio, anguloInicio, anguloFin, false);
                            contexto.fillStyle = '#' + Math.floor(Math.random() * 16777215).toString(16);
                            contexto.fill();
                            contexto.closePath();
                            contexto.restore();                            
                            contexto.save();
                            contexto.translate(centroX, centroY);
                            contexto.rotate(anguloInicio);
                            x = Math.floor(canvas.width * 0.15) - 10;
                            y = Math.floor(canvas.height * 0.05);
                            contexto.fillStyle = "white";
                            contexto.fillText(rpta[i].PrecioUnitario, x, y);
                            contexto.restore();
                            anguloInicio = anguloFin;
                            break;
                    }
                }
                tbProducto.innerHTML = contenido;
            }
        }
    }

    function calcularTotal(idCategoria) {
        var total = 0;
        for (i = 0; i < rpta.length; i++) {
            if (rpta[i].IdCategoria == idCategoria) {
                total += rpta[i].PrecioUnitario;
            }
        }
        return total;
    }

    function calcularMaximo(idCategoria) {
        var max = 0;
        for (i = 0; i < rpta.length; i++) {
            if (rpta[i].IdCategoria == idCategoria) {
                if (rpta[i].PrecioUnitario>max) max = rpta[i].PrecioUnitario;
            }
        }
        return max;
    }

    function exito(data) {
        rpta = data;
        crearTablaGrafico();
    }

    function error(data) {
        alert(data.status + " - " + data.statusText);
    }
});

Nota: La función asociada al $(document).ready se ejecuta ni bien carga la pagina y se llama en forma asíncrona al método de acción "Filtro" que devuelve un JSON con la lista de productos y por JavaScript se crea el filtro, se muestra la tabla y el gráfico de acuerdo a la categoría seleccionada y al tipo de gráfico.

Modificar el archivo web.config para incluir la cadena de conexión

<configuration>
  <connectionStrings>
    <add name="conNW" providerName="SQLServer"
         connectionString="uid=UsuarioNW;pwd=123456;
         data source=DSOFT\Sqlexpress;database=Northwind"/>
  </connectionStrings>
  .....
</configuration>

Configurar el inicio y ejecutar la aplicación web

Para probar la aplicación web debemos configurar el inicio para lo cual nos vamos a la carpeta "App_Start" y abrimos el archivo "RouteConfig.cs" cambiando el nombre del controlador y la acción tal como sigue: controller = "Producto", action = "Lista".

Ejecución y Pruebas de la Aplicación Web ASP.NET MVC

Finalmente, grabar y pulsar F5 para ejecutar la aplicación, mostrándose el resultado similar a la siguiente figura:


Por defecto se muestra la categoría "Bebidas" y en el tipo de gráfico "Barras", cambiar el tipo de gráfico a "Columnas" y se verá la siguiente figura:


Probar ahora con el gráfico de "Lineas" y se mostrará algo similar a la siguiente figura:


Finalmente, cambiar el tipo de gráfico a "Pie" y se verá lo siguiente:


Comentario Final

En este post hemos visto como trabajar en forma desconectada, y también hemos aprendido a crear grillas (tablas) y gráficos (canvas) sin usar controles, sino mediante código JavaScript.

Además hemos hecho llamada asíncrona mediante Ajax de jQuery y retornado los datos como un arreglo en notación JavaScript (JSON).

Esta forma de trabajo (desconectada) es muy útil en aplicaciones móviles donde la conexión al servidor web es limitada, por lo cual una sola vez se trae los datos y todo se maneja en el cliente (Browser).

Este demo es uno de los tantos que se realiza en el programa Web Developer. Espero les sirva.

Descarga
DemoDia_GraficoDatosCliente

6 comentarios:

  1. Gracias por la Demo Prof, si que esta fuerte....

    ResponderBorrar
  2. Muchisismas gracias por su demo, esta muy interesante.

    ResponderBorrar
  3. Muchas gracias! ¡Es genial! También puede probar la función de gráficos asp.net a partir de shieldui, que yo he descubierto recientemente, mira aquí: https://demos.shieldui.com/web/line-chart/axis-marker. Se puede personalizar tantas veces como quiera.

    ResponderBorrar