Mostrando las entradas con la etiqueta Listas Objetos. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Listas Objetos. Mostrar todas las entradas

viernes, 19 de junio de 2015

El Demo del Día: Aumentar Funcionalidad en una Aplicación WinForms usando Reflection (Sin Recompilar)

Aumentar Funcionalidad en una Aplicación WinForms usando Reflection (Sin Recompilar)

El Problema de la Actualización de Aplicaciones

Cada vez que se crea una Aplicación Windows se distribuye el ejecutable en las diferentes PCs de los usuarios que van a usar la aplicación.

Solución Actual al Problema de la Actualización de Aplicaciones

Si deseamos realizar cambios, sobre todo aumentar nueva funcionalidad, existen 2 soluciones comunes:

1. Recuperar los fuentes (código fuente), aumentar la funcionalidad, recompilar y volver a distribuir.

Problema: Si no se tiene los fuentes, porque muchas veces se compra solo el ejecutable (por dismunir costos) no se podría usar esta técnica.

2. Ejecutar desde el Módulo Principal los demás Módulos, es decir una aplicación preparada para ejecutar cualquier exe (los módulos)

Problema: No hay un control interno de las ventanas de los módulos que se ejecuten, por ejemplo que aparezcan dentro del MDI Principal, se puedan organizar y sobre todo el intercambio de datos.

Solución Propuesta al Problema de la Actualización de Aplicaciones

Construir un Sistema Principal que pueda ejecutar cualquier módulo, ya sea una aplicación o una librería, para eso usaremos Reflection.

Las 3 tareas a realizar desde el menú principal del sistema serían:
1. Ejecutar formularios (clases) desde cualquier ensamblado
2. Ejecutar procesos (exe) como por ejemplo utilitarios del sistema
3. Ejecutar métodos (funciones públicas) de cualquier clase de cualquier ensamblado

Con esta aproximación, para aumentar nueva funcionalidad solo basta hacer 2 cosas:
1. Aumentar en tablas (en nuestro caso un archivo de texto) las nuevas opciones del menú
2. Copiar el o los archivos exe (módulos de procesos, usables también independientemente) y archivos dll (módulos de librerías, usables solo por el módulo principal u otras aplicaciones).

Componentes del Demo

Este demo tiene los siguientes componentes (archivos):
1. Menus.txt: Archivo de texto con las opciones del menú principal a cargar.
2. SistemaPrincipal.exe: La aplicación principal (WinForms) que tiene un MDI con el código que permite cargar los menús y a través de este abrir cualquier formulario, ejecutar procesos y ejecutar métodos.
3. ModuloProcesos.exe: Una aplicación WinForms que contiene 2 formularios que van a aumentarse al Sistema Principal. También puede ejecutarse por si sola y muestra un formulario con dos opciones.
4. ModuloReportes.dll: Una librería conteniendo 2 formularios y una clase Rutinas con un método.

Crear la Aplicación Windows Forms con el Sistema Principal

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "SistemaPrincipal", cambiarle de nombre al formulario por "frmPrincipal", configurar su propiedad IsMdiContainer en true y realizar el diseño similar a la figura mostrada:


Agregar 4 formularios al proyecto (configurar su propiedad BackColor en Red), con los siguientes nombres:
- frmConProdCat.cs: Propiedad Text en "Consulta de Productos por Categoria"
- frmConProdProv.cs: Propiedad Text en "Consulta de Productos Proveedor"
- frmManCliente.cs: Propiedad Text en "Mantenimiento de Clientes"
- frmManEmpleado.cs: Propiedad Text en "Mantenimiento de Empleados"

Crear una Clase Entidad de Negocio para el Menú

Agregar una clase al proyecto llamada: "beMenu" y escribir el siguiente código:

using System;
namespace SistemaPrincipal
{
    public class beMenu
    {
        public string IdMenu { get; set; }
        public string Titulo { get; set; }
        public string Ensamblado { get; set; }
        public string Accion { get; set; }
        public string IdPadre { get; set; }
    }
}

Crear una Clase Agente de Servicio para el Menú

Agregar una clase al proyecto llamada: "saMenu" y escribir el siguiente código:

using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace SistemaPrincipal
{
    public class saMenu
    {
        public List<beMenu> Listar(string NombreArchivo)
        {
            List<beMenu> lbeMenu = null;
            if (File.Exists(NombreArchivo))
            {
                using (StreamReader sr = new StreamReader(NombreArchivo, Encoding.Default))
                {
                    string[] menu;
                    lbeMenu = new List<beMenu>();
                    beMenu obeMenu;
                    while (!sr.EndOfStream)
                    {
                        menu = sr.ReadLine().Split(',');
                        obeMenu = new beMenu();
                        obeMenu.IdMenu = menu[0];
                        obeMenu.Titulo = menu[1];
                        obeMenu.Ensamblado = menu[2];
                        obeMenu.Accion = menu[3];
                        obeMenu.IdPadre = menu[4];
                        lbeMenu.Add(obeMenu);
                    }
                }
            }
            return (lbeMenu);
        }
    }
}

Nota: Para los que no conocen la terminología distribuida, si la clase es de datos es da: DataAccess, pero si trabajamos con recursos que no son bases de datos, como archivos de texto, la clase es sa: ServiceAgents (Agentes de Servicio).

Crear una Clase Regla de Negocio para el Menú

Agregar una clase al proyecto llamada: "brMenu" y escribir el siguiente código:

using System;
using System.Collections.Generic;
namespace SistemaPrincipal
{
    public class brMenu
    {
        public List<beMenu> Listar()
        {
            List<beMenu> lbeMenu = null;
            saMenu osaMenu = new saMenu();
            lbeMenu = osaMenu.Listar("Menus.txt");
            return (lbeMenu);
        }
    }
}

Escribir Código para el Formulario frmPrincipal

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Reflection;
using System.IO;

namespace SistemaPrincipal
{
    public partial class frmPrincipal : Form
    {
        List<beMenu> lbeMenu;

        public frmPrincipal()
        {
            InitializeComponent();
        }

        private void llenarMenus(object sender, EventArgs e)
        {
            brMenu obrMenu = new brMenu();
            lbeMenu = obrMenu.Listar();
            List<beMenu> lbeRaiz = lbeMenu.FindAll(x=>x.IdPadre.Equals("0"));
            ToolStripMenuItem mnuRaiz;
            foreach(beMenu obeMenu in lbeRaiz)
            {
                mnuRaiz=new ToolStripMenuItem(obeMenu.Titulo);
                mnuRaiz.Tag = String.Format("{0}|{1}", obeMenu.Accion, obeMenu.Ensamblado);
                if (!obeMenu.Accion.Equals("")) mnuRaiz.Click += new EventHandler(ejecutarAccion);
                mnuPrincipal.Items.Add(mnuRaiz);
                crearSubMenu(obeMenu, mnuRaiz);
            }
        }

        private void crearSubMenu(beMenu obeMenuPadre,ToolStripMenuItem mnuPadre)
        {
            List<beMenu> lbeMenuHijos =
            lbeMenu.FindAll(x=>x.IdPadre.Equals(obeMenuPadre.IdMenu));
            ToolStripMenuItem mnuHijo;
            foreach (beMenu obeMenuHijo in lbeMenuHijos)
            {
                mnuHijo = new ToolStripMenuItem(obeMenuHijo.Titulo);
                mnuHijo.Tag = String.Format("{0}|{1}", obeMenuHijo.Accion,
                obeMenuHijo.Ensamblado);
                if (!obeMenuHijo.Accion.Equals("")) mnuHijo.Click +=
                new EventHandler(ejecutarAccion);
                mnuPadre.DropDownItems.Add(mnuHijo);
                crearSubMenu(obeMenuHijo, mnuHijo);
            }
        }

        private void ejecutarAccion(object sender,EventArgs e)
        {
            ToolStripMenuItem mnu = (ToolStripMenuItem)sender;
            string[] opciones = mnu.Tag.ToString().Split('|');
            string accion = opciones[0];
            string nombreEnsamblado = opciones[1];
            if (accion.StartsWith("frm"))
            {
                Assembly ensamblado = Assembly.LoadFrom(nombreEnsamblado);
                if (ensamblado != null)
                {
                    Type tipo = ensamblado.GetType(String.Format("{0}.{1}",
                    Path.GetFileNameWithoutExtension(nombreEnsamblado),accion));
                    if (tipo != null)
                    {
                        object obj = Activator.CreateInstance(tipo);
                        if (obj != null)
                        {
                            Form frm = (Form)obj;
                            frm.MdiParent = this;
                            frm.Show();
                        }
                    }
                }
            }
            else
            {
                if (accion.EndsWith(".exe"))
                {
                    Process.Start(accion);
                }
                else
                {
                    opciones = accion.Split('.');
                    accion = opciones[0];
                    string clase = opciones[1];
                    if (nombreEnsamblado.Equals("SistemaPrincipal.exe") &&
                    clase.Equals("frmPrincipal"))
                    {
                        MethodInfo mi = this.GetType().GetMethod(accion);
                        var rpta = mi.Invoke(this, null);
                        if (rpta != null) MessageBox.Show(rpta.ToString());
                    }
                    else
                    {
                        Assembly ensamblado = Assembly.LoadFrom(nombreEnsamblado);
                        if (ensamblado != null)
                        {
                            Type tipo = ensamblado.GetType(String.Format("{0}.{1}",
                            Path.GetFileNameWithoutExtension(nombreEnsamblado), clase));
                            if (tipo != null)
                            {
                                object obj = Activator.CreateInstance(tipo);
                                if (obj != null)
                                {
                                    MethodInfo mi = tipo.GetMethod(accion);
                                    if (mi != null)
                                    {
                                        var rpta = mi.Invoke(obj, null);
                                        if (rpta != null) MessageBox.Show(rpta.ToString());
                                    }
                                }
                            }
                        }

                    }
                }
            }
        }

        public void salirApp()
        {
            this.Close();
        }
    }
}

Nota: Asociar al evento "Load" del formulario "frmPrincipal" el método: "llenarMenus", el cual lista las opciones de la raíz o primer nivel, luego la función "crearSubMenu" recursivamente llena los demás niveles.

Crear el Archivo de Texto con las opciones del menu principal

Antes de crear el archivo de texto, hay que grabar y compilar la aplicación y luego ir a la carpeta "bin", "debug" y crear el archivo de texto "Menus.txt" similar a la figura mostrada:


Primera Prueba de la Aplicación Sistema Principal

Si se ejecuta la aplicación se mostrará el MDI con los menús cargados y si abrimos los formularios se mostrará similar a la siguiente figura:


Nota: La primera prueba consiste en mostrar solo las opciones que estan dentro del propio sistema (sin integración).

Crear otra Aplicación Windows Forms con el Módulo de Procesos

Crear una nueva aplicación Windows Forms en C# llamada "ModuloProcesos", cambiarle de nombre al formulario por "frmPrincipal" y configurar su propiedad BackColor en Green, y realizar el diseño similar a la figura mostrada:


Agregar 2 formularios al proyecto (configurar su propiedad BackColor en Green), con los siguientes nombres:
- frmNuevaOrden.cs: Propiedad Text en "Modulo de Procesos - Nueva Orden"
- frmNuevaFactura.cs: Propiedad Text en "Modulo de Procesos - Nueva Factura"

Escribir el siguiente código en el formulario "frmPrincipal":

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ModuloProcesos
{
    public partial class frmPrincipal : Form
    {
        public frmPrincipal()
        {
            InitializeComponent();
        }

        private void btnNuevaOrden_Click(object sender, EventArgs e)
        {
            frmNuevaOrden frm = new frmNuevaOrden();
            frm.WindowState = FormWindowState.Maximized;
            frm.ShowDialog();
        }

        private void btnNuevaFactura_Click(object sender, EventArgs e)
        {
            frmNuevaFactura frm = new frmNuevaFactura();
            frm.WindowState = FormWindowState.Maximized;
            frm.ShowDialog();
        }

        private void btnSalirApp_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

Grabar y probar la aplicación ModuloProcesos.

Crear una Librería de Clases con el Módulo de Reportes

Crear una nueva Librería de Clases en C# llamada "ModuloReportes", cambiarle de nombre a la clase por "Rutinas.cs" y escribir el siguiente código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ModuloReportes
{
    public class Rutinas
    {
        public string procesar()
        {
            return "Procesando";
        }
    }
}

Hacer referencias a 2 librerías: System.Drawing.dll y System.Windows.Forms.dll

Agregar 2 formularios al proyecto (configurar su propiedad BackColor en Blue), con los siguientes nombres:
- frmRepCliente.cs: Propiedad Text en "Modulo de Reportes - Clientes"
- frmRepOrden.cs: Propiedad Text en "Modulo de Reportes - Ordenes"

Grabar y compilar la librería.

Nota: Esta No se puede ejecutar porque es una librería de clases, solo funciona desde una aplicación que la llame.

Segunda Prueba de la Aplicación Sistema Principal

Recuperar la aplicación "SistemaPrincipal", sacar una copia al archivo "Menus.txt" (carpeta bin, debug) y aumentar mas opciones al archivo, tal como se muestra en la siguiente figura:


Copiar los archivos ModuloProcesos.exe y ModuloReportes.dll hacia la carpeta bin, debug donde se encuentra la aplicación SistemaPrincipal.exe y el archivo Menus.txt.

Ejecutar nuevamnete la aplicación y se mostrará el MDI con los menús cargados (los cuales han aumentado) y si abrimos los formularios se mostrará similar a la siguiente figura:


Observe que cada color diferencia el ensamblado donde se encuentra el formulario:
- Rojo: Formularios de la Aplicación Sistema Principal
- Verde: Formularios de la Aplicación Modulo Procesos
- Azul: Formularios de la Librería de Clases Modulo Reportes

También observe que al seleccionar "Reportes", "Graficos", "Estadistica" se ejecuta un método externo "procesar" encontrado en la Librería de Clases "ModuloReportes.dll" y cuando se elige la opción "Salir" se ejecuta el método "salirApp" ubicado en la misma clase del formulario MDI.

Comentario Final

En este post, hemos visto como aumentar funcionalidad a una Aplicación Windows sin Recompilar la Aplicación usando Reflection. Los menús se han cargado usando un archivo de texto, pero en vez de este puede ser una tabla de una base de datos.

Si bien es cierto, el uso intensivo de Reflection (sobre todo llamadas en bucles), no es recomendable, existen muchos casos como en este, que nos sirven de gran ayuda.

Muchos desarrolladores, desconecen la importancia y la gran cantidad de usos que se pueden dar a Reflection, pero en este Blog siempre estamos dando a conocer su importancia para darle "inteligencia" a tus soluciones.

Video del Demo

Aquí les dejo, un video parecido al Demo posteado, con la diferencia que la aplicación SistemaPrincipal se llama Demo12, la aplicación ModuloProcesos se llama Demo13 y la librería ModuloReportes se llama Demo14, pero igual les puede servir de guia.


Descarga del Código

2015_06_19_DemoDia_SistemaPrincipal

jueves, 4 de junio de 2015

El Demo del Día: Performace entre Serializadores .NET

Performace entre Serializadores .NET

El Problema de la Performance

Uno de los problemas principales de los Sistemas Informáticos Modernos es la Performance, sea Windows o Web la Aplicación, con el crecimiento del uso (aumento de usuarios) dentro (Intranet) y fuera de la empresa (Internet) las aplicaciones cada vez son mas lentas.

Esta lentitud es tratada de resolver aumentando Hardware como mas Servidores, mas Memorias, mas CPUs, mas Ancho de Banda, etc. Todo menos hacer una reingeniería del desarrollo que es la principal causa, es decir el problema es la forma cómo se programa (Software) donde lo común es que se realizan muchas conexiones al Servidor de Base de Datos, se usan estructuras de datos pesadas (por ejemplo en .NET los DataSets), se hacen bucles expensivos, etc.

Solución al Problema de la Performance

Aprender las Buenas Prácticas de Programación que permitan consumir la menor cantidad de memoria con el menor tiempo de procesamiento, para lo cual hay diferentes las cuales compartiremos a partir de hoy, empezando por una Comparación de Serializadores para elegir el mejor.

Definición de Serialización

Según Wikipedia, la Serialización (o Marshalling en inglés) es el proceso de codificación de un objeto en un medio de almacenamiento (como puede ser un archivo, o un buffer de memoria) con el fin de transmitirlo a través de una conexión en red como una serie de bytes o en un formato humanamente más legible como XML o JSON, entre otros.

La serie de bytes o el formato pueden ser usados para crear un nuevo objeto que es idéntico en todo al original, incluido su estado interno (por tanto, el nuevo objeto es un clon del original).

La Serialización es un mecanismo ampliamente usado para transportar objetos a través de una red, para hacer persistente un objeto en un archivo o base de datos, o para distribuir objetos idénticos a varias aplicaciones o localizaciones.

Importancia de elegir un Serializador

Es necesario saber elegir adecuadamente un Serializador, ya que cada vez que enviamos datos entre dos procesos se usa la serialización. 

A continuación listamos los Serializadores .NET junto con los tipos de aplicaciones o tecnologías distribuidas que las usan por defecto:

- Serializador Binario: Usado en Sockets y .NET Remoting (dll)
- Serializador SOAP: Usado por Servicios Web XML (asmx)
- Serializador XML: Usado por los Servicios WCF (svc y dll)
- Serializador JSON: Usado en Web API
- Serializador Personalizado: Hay que crearlo uno mismo.

La importancia de seleccionar bien el Serializador es que este es el factor principal de performance al momento de usar un Servicio o una Aplicación Distibuida, es decir, depende de este el tiempo de serialización deserealización y sobre todo el formato y el tamaño de los datos que se pasan por la red.

En este post, comprobaremos cual es el mejor, sin embargo la mayoría siempre usa el peor y después quiere que sus Servicios sean rápidos, es decir, quieren una cosa (Performance) pero hacen otra (Estándar).

Crear una Aplicación Windows Forms en C#

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "Comparacion_Serializacion", cambiarle de nombre al formulario por "frmListaProductos" y realizar el diseño similar a la figura mostrada:


Nota: En el diseño hay un DataGridView llamado "dgvProducto", un menú contextual llamado "mnuContextual" con 6 opciones: mnuCargarDatos, mnuBinario, mnuSOAP, mnuXML, mnuJSON y mnuPersonalizado.

Crear el Procedimiento Almacenado en SQL Server

Crear el Procedimiento llamado "uspProductsListar" en SQL Server:

Create Procedure uspProductsListar
As
Select ProductID, ProductName, SupplierID, 
CategoryID,UnitPrice, UnitsInStock
From Products

Crear la Clase Entidad del Negocio

Agregar una clase al proyecto llamada: "beProducto" y escribir el siguiente código:

using System;
using System.Runtime.Serialization;

namespace Comparacion_Serializacion
{
    //[Serializable]
    //[DataContract]
    public class beProducto
    {
        //[DataMember]
        public int IdProducto { get; set; }
        //[DataMember]        
        public string Nombre { get; set; }
        //[DataMember]
        public int IdProveedor { get; set; }
        //[DataMember]
        public int IdCategoria { get; set; }
        //[DataMember]
        public decimal PrecioUnitario { get; set; }
        //[DataMember]
        public short Stock { get; set; }
    }
}

Nota: Los atributos de serialización (Serializable, DataContract y DataMember) han sido comentados inicialmente para ver que sucede cuando se intenta serializar con cada técnica.

Crear la Clase de Acceso a Datos

Agregar una clase al proyecto llamada: "daProducto" 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

namespace Comparacion_Serializacion
{
    public class daProducto
    {
        public List<beProducto> listar(SqlConnection con)
        {
            List<beProducto> lbeProducto = null;

            SqlCommand cmd = new SqlCommand("uspProductsListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                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);
                }
                drd.Close();
            }

            return (lbeProducto);
        }
    }
}

Crear la Clase de Reglas del Negocio

Agregar una clase al proyecto llamada: "brProducto" y escribir el siguiente código:

using System;
using System.Configuration; //ConfigurationManager
using System.Data.SqlClient; //SqlConnection
using System.Collections.Generic; //List

namespace Comparacion_Serializacion
{
    public class brProducto
    {
        public string CadenaConexion { get; set; }
        private string archivoLog;

        public brProducto()
        {
            CadenaConexion = 
                ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
        }

        public List<beProducto> listar()
        {
            List<beProducto> lbeProducto = null;
            using (SqlConnection con = new SqlConnection(CadenaConexion))
            {
                try
                {
                    con.Open();
                    daProducto odaProducto = new daProducto();
                    lbeProducto = odaProducto.listar(con);
                }
                catch (SqlException ex)
                {
                    //grabarLog(ex);
                }
                catch (Exception ex)
                {
                    //grabarLog(ex);
                }
            } //con.Close(); con.Dispose();
            return (lbeProducto);
        }
    }
}

Nota: Para usar la clase "ConfigurationManager", primero hay que hacer referencia a la librería "System.Configuration.dll"

Crear una Clase para la Serialización Personalizada de Strings

Agregar una clase al proyecto llamada: "CustomSerializer" y escribir el siguiente código:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.IO;

namespace Comparacion_Serializacion
{
    public class CustomSerializer
    {
        public static void Serializar<T>(List<T> lista, string archivo, char separadorCampo, 
        char separadorRegistro)
        {
            StringBuilder sb = new StringBuilder();
            PropertyInfo[] propiedades = lista[0].GetType().GetProperties();
            for (int i = 0; i < propiedades.Length; i++)
            {
                sb.Append(propiedades[i].Name);
                if (i < propiedades.Length - 1) sb.Append(separadorCampo);
            }
            sb.Append(separadorRegistro);
            for (int j = 0; j < lista.Count; j++)
            {
                propiedades = lista[j].GetType().GetProperties();
                for (int i = 0; i < propiedades.Length; i++)
                {
                    if (propiedades[i].GetValue(lista[j], null) != null) 
                    sb.Append(propiedades[i].GetValue(lista[j], null).ToString());
                    else sb.Append("");
                    if (i < propiedades.Length - 1) sb.Append(separadorCampo);
                }
                if (j < lista.Count - 1) sb.Append(separadorRegistro);
            }
            File.WriteAllText(archivo,sb.ToString());
        }

        public static List<T> Deserializar<T>(string archivo, char separadorCampo, 
        char separadorRegistro)
        {
            List<T> lista = new List<T>();
            if (File.Exists(archivo))
            {
                string contenido = File.ReadAllText(archivo);
                string[] registros = contenido.Split(separadorRegistro);
                string[] campos;
                string[] cabecera = registros[0].Split(separadorCampo);
                string registro;
                Type tipoObj;
                T obj;
                dynamic valor;
                Type tipoCampo;
                for (int i = 1; i < registros.Length; i++)
                {
                    registro = registros[i];
                    tipoObj = typeof(T);
                    obj = (T)Activator.CreateInstance(tipoObj);
                    campos = registro.Split(separadorCampo);
                    for (int j = 0; j < campos.Length; j++)
                    {
                        tipoCampo = obj.GetType().GetProperty(cabecera[j]).PropertyType;
                        valor = Convert.ChangeType(campos[j],tipoCampo);
                        obj.GetType().GetProperty(cabecera[j]).SetValue(obj, valor);
                    }
                    lista.Add(obj);
                }
            }
            return (lista);
        }
    }
}

Nota: Para crear mi propio Serializador uso Reflection y Listas Genéricas. 
Una de las claves para hacer la Deserealización es crear una instancia del objeto dinámicamente con Activator.CreateInstance y luego escribir el valor desde la cadena al objeto para lo cual uso Convert.ChangeType.

Escribir el Código del Formulario frmListaProducto

Antes de escribir el código de los Serializadores hay que hacer las siguientes Referencias:
- System.Runtime.Serialization.Formatters.Soap.dll para la Serialización SOAP
- System.Runtime.Serialization para la Serialización JSON

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO; //FileStream
using System.Linq; //ToList
using System.Diagnostics; //Stopwatch
using System.Runtime.Serialization.Formatters.Binary; //BinaryFormatter
using System.Runtime.Serialization.Formatters.Soap; //SoapFormatter
using System.Xml.Serialization; //XmlSerializer
using System.Runtime.Serialization.Json; //DataContractJsonSerializer

namespace Comparacion_Serializacion
{
    public partial class frmListaProducto : Form
    {
        private List<beProducto> lbeProducto;

        public frmListaProducto()
        {
            InitializeComponent();
        }

        private void cargarDatos(object sender, EventArgs e)
        {
            brProducto obrProducto = new brProducto();
            lbeProducto = obrProducto.listar();
            dgvProducto.DataSource = lbeProducto;
        }

        private void medirTiempo(Action metodo)
        {
            Stopwatch cronometro = new Stopwatch();
            cronometro.Start();
            metodo();
            MessageBox.Show(String.Format("Tiempo de Duración: {0:n0} msg", 
            cronometro.Elapsed.TotalMilliseconds));
            cronometro.Stop();
        }

        private void serializarBinario()
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream("Binario.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                bf.Serialize(fs, lbeProducto);
            }
        }

        private void deserializarBinario()
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream("Binario.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=(List<beProducto>)bf.Deserialize(fs);
            }
        }

        private void serializarSOAP()
        {
            SoapFormatter sf = new SoapFormatter();
            using (FileStream fs = new FileStream("SOAP.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                sf.Serialize(fs, lbeProducto.ToArray());
            }
        }

        private void deserializarSOAP()
        {
            SoapFormatter sf = new SoapFormatter();
            using (FileStream fs = new FileStream("SOAP.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=((beProducto[])sf.Deserialize(fs)).ToList();
            }
        }

        private void serializarXML()
        {
            XmlSerializer xs = new XmlSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("XML.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                xs.Serialize(fs, lbeProducto);
            }
        }

        private void deserializarXML()
        {
            XmlSerializer xs = new XmlSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("XML.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto = (List<beProducto>)xs.Deserialize(fs);
            }
        }

        private void serializarJSON()
        {
            DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("JSON.txt",
                FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
            {
                js.WriteObject(fs, lbeProducto);
            }
        }

        private void deserializarJSON()
        {
            DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(List<beProducto>));
            using (FileStream fs = new FileStream("JSON.txt",
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                lbeProducto=(List<beProducto>)js.ReadObject(fs);
            }
        }

        private void serializarString()
        {
            CustomSerializer.Serializar(lbeProducto, "Strings.txt", ',', '|');
        }

        private void deserializarString()
        {
            lbeProducto=CustomSerializer.Deserializar<beProducto>("Strings.txt", ',', '|');
        }

        private void mnuSerializarBinario_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarBinario);
        }

        private void mnuDeserializarBinario_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarBinario);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarSOAP_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarSOAP);
        }

        private void mnuDeserializarSOAP_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarSOAP);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarXML_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarXML);
        }

        private void mnuDeserializarXML_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarXML);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerealizarJSON_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarJSON);
        }

        private void mnuDeserealizarJSON_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarJSON);
            dgvProducto.DataSource = lbeProducto;
        }

        private void mnuSerializarPersonalizado_Click(object sender, EventArgs e)
        {
            medirTiempo(serializarString);
        }

        private void mnuDeserializarPersonalizado_Click(object sender, EventArgs e)
        {
            medirTiempo(deserializarString);
            dgvProducto.DataSource = lbeProducto;
        }
    }
}

Nota: Todos las funciones manejadoras de eventos llaman a la función "medirTiempo" para ejecutar la Serialización y Deserealización.

Modificar el Archivo de Configuración

Abrir el archivo app.Config y aumentar la cadena de conexión:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="conNW" providerName="SQLServer"
      connectionString="uid=UsuarioNW;pwd=123456;
      server=DSOFT\Sqlexpress;database=Northwind"/>
  </connectionStrings>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

Ejecutar y Probar la Aplicación Windows Forms

Grabar la aplicación y pulsar F5 para ejecutarla. Aparecerá una ventana similar a la siguiente figura:


Clic derecho y se mostrará el menú contextual, del cual seleccionar la primera opción: "Cargar Datos" y se listarán los productos.


Serialización Binaria

Clic derecho y del menú contextual seleccionar la opción "Binario" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 11 msg y se creará un archivo llamado "Binario.txt" con el siguiente contenido:


Notas: 
- Si se intenta Serializar el archivo a Binario se caerá ya que la Entidad (clase beProducto) no es serializable, proceder a descomentar el atributo [Serializable].
- Observar que el tamaño del archivo Binario.txt es de 5756 bytes (en mi caso).

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 8 msg y se cargarán los datos desde el archivo "Binario.txt" en la lista de objetos y se mostrará en la grilla.

Serialización SOAP

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "SOAP" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 30 msg y se creará un archivo llamado "SOAP.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo SOAP.txt es de 73586 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 51 msg y se cargarán los datos desde el archivo "SOAP.txt" en la lista de objetos y se mostrará en la grilla.

Serialización XML

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "XML" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 192 msg y se creará un archivo llamado "XML.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo XML.txt es de 22270 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 209 msg y se cargarán los datos desde el archivo "XML.txt" en la lista de objetos y se mostrará en la grilla.

Serialización JSON sin atributos

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "JSON" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 474 msg y se creará un archivo llamado "JSON.txt" con el siguiente contenido:


Nota: Observar que el tamaño del archivo JSON.txt es de 19716 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 347 msg y se cargarán los datos desde el archivo "JSON.txt" en la lista de objetos y se mostrará en la grilla.

Serialización JSON con atributos (DataContract y DataMember)

Detener la ejecución, decomentar los atributos [DataContract] y [DataMamber] de la entidad beProducto, además renombrar el archivo JSON.txt por JSON2.txt y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "JSON" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 27 msg y se creará un archivo llamado "JSON.txt" con el siguiente contenido:



Nota: Observar que el tamaño del archivo JSON.txt es tan solo de 10332 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Binario" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 92 msg y se cargarán los datos desde el archivo "JSON.txt" en la lista de objetos y se mostrará en la grilla.

Serialización Personalizada con Strings

Detener la ejecución y probar nuevamente, seleccionando la opción "Cargar Datos", luego del menú contextual seleccionar la opción "Personalizado" y luego "Serializar", se mostrará el tiempo de la serialización, en mi caso 15 msg y se creará un archivo llamado "Strings.txt" con el siguiente contenido:



Nota: Observar que el tamaño del archivo Strings.txt es de 3125 bytes (en mi caso)

Detener la ejecución y probar nuevamente, pero esta vez seleccionar directamente del menú contextual "Personalizado" y "Deserealizar", se mostrará el tiempo de deserealización, en mi caso 53 msg y se cargarán los datos desde el archivo "Strings.txt" en la lista de objetos y se mostrará en la grilla.

Resumen Comparativo

A continuación una figura con un cuadro resumen de tiempos y tamaños por serializador (formato):



Esta resaltado los que mejor rendimiento tienen, es decir el tiempo de Serializado el ganador es el Binario, pero en tamaño (ancho de banda) el ganador es el Strings (Personalizado), además para Servicios Web el Binario no es aplicable por tanto, el mejor Serializador en Performance sería el String.

Sin embrago el comportamiento de los desarrolladores ha sido:
- Web Services XML con SOAP (2002-2007)
- WCF Services con XML (2008-2011)
- Web API con JSON (2012-2015)

Comentario Final

Después de haber tratado detalladamente los Serializadores .NET y analizado su tiempo y tamaño, la recomendación final es que si desean consumir el mínimo ancho de banda, hay que crear su propio serializador (yo le estoy dejando el mio, que poco a poco lo voy a optimizar) ya que es el que mejor rendimiento tiene en velocidad, tamaño e inter-operabilidad.

Claro, muchos dirán que No es Estandar el formato creado, y otros dirán y como hago para consumirlos desde los clientes, es decir como muestro estos datos, así como hay Templates para JSON yo tambien tengo mis propias rutinas que muestran en JavaScript cualquier Strings separados por caracteres, los cuales serán motivos de otros post.

Descarga del Código


martes, 20 de enero de 2015

El Demo del Día: TreeView con CheckBox en ASP.NET MVC

TreeView con CheckBox en ASP.NET MVC

Introducción a ASP.NET MVC

El Modelo Vista Controlador es un Modelo o Patrón de Arquitectura de Desarrollo de Software que permite la separación de la lógica en 3 componentes:
- El Modelo: Contiene la lógica del dominio, como las entidades del negocio.
- La Vista: Representa la interface de usuario de la aplicación, por ejemplo las páginas y controles.
- El Controlador: Controla la interacción del usuario con la vista y la asocia a un modelo.

ASP.NET MVC es la implementación de Microsoft de este modelo o patrón como alternativa a los WebForms de ASP.NET, el cual presenta las siguientes ventajas:
- Facilita la administración de la complejidad al separar la lógica en 3 componentes.
- Reduce el HTML enviado al cliente al no usar el estado de vista (ViewState)
- Proporciona una mayor compatibilidad con el desarrollo basado en pruebas (TDD), etc.

A partir de ASP.NET MVC 3 podemos elegir crear las vistas usando 2 motores:
- WebForm: Es el mismo que se usa para crear los Sitios ASP.NET con WebForms, es decir para el código incrustado del servidor se usa: <%= %>, <%: %> o <%# %>.
- Razor: Es un nuevo motor creado especialmente para aplicaciones MVC y usa código incrustado del servidor mediante @.
Nota: Se recomienda usar Razor como motor de vista predeterminado, ya que su sintaxis es mas corta y maneja mejor el tema de codificación y de-codificación.

Para los que recién inician con ASP.NET MVC el inconveniente principal aparte de acostumbrarse al nuevo modelo, es la falta de variedad de controles para crear las vistas, ya que los controles que ofrece Microsoft son los Helpers (no son muchos) que se clasifican en 3:
- HTML Helpers: Form, Label, TextBox, CheckBox, ListBox, DropDownList, ActionLink, etc.
- AJAX Helpers: Form, ActionLink, RouteLink y Script.
- URL Helpers: Action, Content, Encode, HttpRouteUrl.

Quizás los controles que mas extrañen los programadores es los enlazados a datos como GridView, DataList, Repeater, ListView, TreeView, Chart, etc. Aunque si bien es cierto no vienen como Helpers se pueden implementar en forma personalizada, por ejemplo en este primer post de MVC trataremos sobre como crear un TreeView con CheckBox.

Requerimiento

Se desea mostrar una lista de empleados en forma jerárquica de acuerdo a su cargo, para lo cual cada empleado tiene un único jefe. La aplicación debe permitir seleccionar un empleado y automáticamente deberá seleccionarse todos sus dependientes.
Nota: La aplicación debe ser Web y estar en ASP.NET MVC4.

Solución

Para cubrir estos requerimientos se creará un TreeView con CheckBox para lo cual se realizará lo siguiente:
- El el Modelo se creará una Entidad llamada "beEmpleado".
- En el Controlador se creará un método "obtenerEmpleados" que llene una lista con los empleados.
- En el Controlador se creará un método de acción llamado "Lista" que devuelva una vista (ViewResult).
- En el Controlador se creará un método de acción llamado "Listar" que devuelva la lista en formato JSON (JsonResult).
- Desde la Vista "Lista", al cargar la página usando jQuery Ajax se llamará al método "Listar" que devuelve los datos como JavaScript Object Notation (JSON).
- Mediante JavaScript mostraremos los datos en una Tabla
- Mediante jQuery mostraremos los datos en un div creando una lista con viñetas (ul - li) conteniendo CheckBoxs.
- Usando jQuery se programará la selección jerárquica, es decir al marcar o desmarcar un empleado, debe expandirse a todos sus subordinados.
- Usando jQuery se programará el expandir o colapsar un nodo (empleado) del TreeView.

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

Abrir el Visual Studio 2012 y seleccionar un nuevo proyecto de tipo: "Aplicación web de ASP.NET MVC4", tal como se muestra en la siguiente figura:


Se mostrará una ventana para seleccionar la plantilla de proyecto, similar a la mostrada en la siguiente figura:


Seleccionar la opción "Vacio" y como motor de vista "Razor" y "Aceptar". En la ventana del Explorador de Soluciones se verá la siguiente estructura de carpetas:


Nota: Apreciar como viene predefinida las carpetas para los Controladores, los Modelos y las Vistas.
Además viene una carpeta App_Start para configurar los inicios de la aplicación.

Crear un Modelo con la Entidad del Empleado

Clic derecho a la carpeta Models y seleccionar "Agregar" y luego "Clase", llamarle al archivo "beEmpleado.cs" y escribir el siguiente código:

namespace TreeView_CheckBox.Models
{
    public class beEmpleado
    {
        public int IdEmpleado { get; set; }
        public string Nombres { get; set; }
        public int IdJefe { get; set; }
    }
}

Crear un Controlador para el Empleado

Clic derecho a la carpeta Controllers y seleccionar "Agregar" y luego "Controlador" y se mostrará una ventana similar a la siguiente figura:


Llamarle al archivo "EmpleadoController.cs" y en opciones de plantillas dejarlo en plantilla vacia (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 TreeView_CheckBox.Models;

namespace TreeView_CheckBox.Controllers
{
    public class EmpleadoController : Controller
    {
        List<beEmpleado> lbeEmpleado;

        private void obtenerEmpleados()
        {
            lbeEmpleado = new List<beEmpleado>();
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 1, Nombres = "Luis Dueñas",
                                          IdJefe = 0 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 2, Nombres = "Arturo Castañeda",
                                          IdJefe = 1 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 3, Nombres = "Enrique Espinal",
                                          IdJefe = 1 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 4, Nombres = "Maria Rojas",
                                          IdJefe = 2 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 5, Nombres = "Juan Perez",
                                          IdJefe = 2 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 6, Nombres = "Rosa Toro",
                                          IdJefe = 3 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 7, Nombres = "Pedro Vasquez",
                                          IdJefe = 3 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 8, Nombres = "Cesar Ramirez",
                                          IdJefe = 5 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 9, Nombres = "Ana Rodriguez",
                                          IdJefe = 6 });
        }

        public ViewResult Lista()
        {
            return View();
        }

        public JsonResult Listar()
        {
            JsonResult rpta;
            obtenerEmpleados();
            rpta = Json(lbeEmpleado, 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;
}
.AnchoTotal {
    width:100%;
}
.FilaCabecera {
    background-color:gray;
    color:white;
}
.FilaDatos {
    background-color:white;
    color:blue;
}

Crear la Vista para mostrar los datos

Ir al controlador y ubicarse sobre el método "Lista" (acción), clic derecho y seleccionar "Agregar vista", aparecerá un diálogo como se muestra en la siguiente figura:


Desmarcar todas las casillas (checkboxs) y agregar la vista llamada "Lista", luego escribir el siguiente código:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Lista</title>
    <link href="~/Content/ACME.css" rel="stylesheet" />
    <link href="~/Content/TreeView.css" rel="stylesheet" />
</head>
<body>
    <div>
        <table class="AnchoTotal">
            <tr class="Titulo">
                <td colspan="2">TreeView con CheckBox en ASP.NET MVC usando
                                             JSON, jQuery y Ajax</td>
            </tr>
            <tr class="Subtitulo">
                <td colspan="2">Lista de Empleados</td>
            </tr>
            <tr>
                <td style="width:50%">
                    <table class="AnchoTotal">
                        <thead>
                            <tr class="FilaCabecera">
                                <td style="width:100px">Id Emp</td>                        
                                <td style="width:300px">Nombres</td>
                                <td style="width:100px">Id Jefe</td>
                            </tr>
                        </thead>
                        <tbody id="tblEmpleado">
                        </tbody>
                    </table>
                </td>
                <td style="width:50%; vertical-align:top">
                    <div id="divEmpleado">
                        <ul id="ulEmpleado"></ul>
                    </div>
                </td>
            </tr>
        </table>
    </div>
    <script src="~/Scripts/jquery-1.8.2.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
    <script src="~/Scripts/Rutinas.js"></script>
    <script>cargarLista();</script>
</body>
</html>

Crear el archivo JavaScript con el código cliente para el TreeView

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

function cargarLista() {
    $.ajax(
        {
            url: "Empleado/Listar/",
            type: "post",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: exito,
            error: error
        });
}

function mostrarTabla(rpta) {
    var tabla = "";
    for (i = 0; i < rpta.length; i++) {
        tabla += "<tr class='FilaDatos'><td>" + rpta[i].IdEmpleado + "</td><td>" +
            rpta[i].Nombres + "</td><td>" + rpta[i].IdJefe + "</td></tr>";
    }
    var tbl = document.getElementById("tblEmpleado");
    tbl.innerHTML = tabla;
}

function mostrarTreeCheck(rpta) {
    var ul = $("#ulEmpleado");
    for (var i = 0; i < rpta.length; i++) {
        var li = "<li data-id='" + rpta[i].IdEmpleado + "'><input type='checkbox' />
                      <span style='cursor:pointer'>" + rpta[i].Nombres + "</span></li>";
        var padre;
        if (rpta[i].IdJefe == "0") padre = ul;
        else {
            var $elem = ul.find("li[data-id='" + rpta[i].IdJefe + "']");
            if ($elem.find("ul").length == 0) $elem.append("<ul>");
            padre = $elem.find("ul:first");
        }
        padre.append(li);
    }
}

function seleccionarTreeCheck() {
    $(function () {
        $("input[type='checkbox']").change(function () {
            $(this).siblings('ul')
                   .find("input[type='checkbox']")
                   .prop('checked', this.checked);
        });
    });
}

function expandirColapsarTreeCheck() {
    $(function () {
        $('#ulEmpleado').find('span').click(function (e) {
            $(this).parent().children('ul').toggle();
        });
    });
}

function exito(rpta) {
    mostrarTabla(rpta);
    mostrarTreeCheck(rpta);
    seleccionarTreeCheck();
    expandirColapsarTreeCheck();
}

function error(rpta) {
    alert(rpta.statusText);
}

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 se muestra en el siguiente código:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace JS01_TreeView_CheckBox
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Empleado", action = "Lista", id = UrlParameter.Optional }
            );
        }
    }
}

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


Seleccionar el primer empleado y ver como se marcan todos los empleados, tal como se muestra en la siguiente figura:


Dar clic sobre el segundo empleado y notar como se colapsa (oculta) sus subordinados, también colapsar el tercer empleado y el resultado será similar a la siguiente figura:


Comentario Final

En este post, un poco grande, hemos querido iniciar los Demos dedicados a ASP.NET MVC, empezando por la creación de un control TreeView con CheckBox que seleccione en forma jerárquica y expanda y colapse sus elementos.

Un camino hubiera sido usar Librerías de Controles de terceros como jQueryUI, Telerik KendoUI, Infragistics, DevExpress, etc. Pero como siempre el objetivo de este blog es enseñar a hacerlo uno mismo, para lo cual usamos JSON, JavaScript, jQuery y Ajax.

Espero les guste y si es así lo pueden re-postear en sus redes sociales: Google+, Facebook, Twitter, etc.

Descarga
2015_01_20_DemoDia_TreeView_CheckBox