jueves, 18 de noviembre de 2010

[WinForms] Realizar Acciones en formularios hijo

 

Introducción

El objetivo del articulo es mostrar como distintas acciones pueden ser ejecutadas en formularios que implementen la interfaz correspondiente.

 

[C#]
[VB.NET]

lunes, 15 de noviembre de 2010

[WinForms] Sumar TextBox asociados a CheckBox

 

Introducción

El ejemplo demuestra como sumar los controles TextBox que están asociados al un checkbox, sumando solo aquellos marcados.

Es importante notar como la propiedad Tag del control checkbox posee el nombre del TextBox relacionado, es por medio de esta propiedad que se puede buscar el control por su nombre para sumarlo

 

[C#]
[VB.NET]

domingo, 14 de noviembre de 2010

[Reporting Service] Campo Imagen desde un DataSet Tipado

 

Introducción


El siguiente ejemplo explica como vincular un campo imagen que se obtiene desde una base de datos.

En este caso se utiliza un reporte de Reporting Service en modo local, por eso se cuanta integrado al VS un archivo de extensión .rdlc

Definición de los datos


Para asignar los datos se ha creado un DataSet Tipado, con dos DataTable, uno que contendrá un campo que representa el logo de la compañía, el segundo DataTable tendrá los datos del listado.

 image

Definición del campo de imagen


Dentro del reporte se ubico un campo de imagen, pero para que este tome la información provista por el datatable debe definirse algunas propiedades

image

Las 3 dentro de la sección “Data” son las que deben tener nuestra atención.

La propiedad Source debe asignarse como “Database” para que tome la info del datatable, y es recomendable además asignar el MiMeType para que sepa con que formato vendrá la imagen.

Pero la propiedad algo mas compleja de definir es “Value”, ya que esta requiere de una expression

image

Se puede hacer uso de la ventana que ayude con esta tarea:

image

En este caso se seleccionando del primer Data Source de Empresa el campo de Logo, pero se hará uso de método First() para tomar solo un valor del mismo, ya que la imagen es solo una.

 

[C#] 
[VB.NET] 

jueves, 11 de noviembre de 2010

Contar Palabras

 

Introducción

El objetivo del articulo es demostrar como Linq puede ayudar en una tarea tan simple como el trabajo con cadenas de texto.

En el ejemplo se contaran las palabras de un Textbox que sea coincidentes, al encontrar una repetida se mantendrá la cuenta y se mostrara al final en una lista

 

private void contar_Click(object sender, EventArgs e)
{
    //separo cada palabra
    string[] palabras = txtTexto.Text.Split(' ');

    //agrupo las palabras que se repiten 
    //y filtro solo las que tengan mas de una coincidencia
    var query = from item in palabras
                group item by item into g
                where g.Count() > 1
                select new
                {
                    Palabra = g.Key,
                    Cantidad = g.Count()
                };

    //limpio los items previos de la lista
    lvListado.Items.Clear();

    //agrego el resultado al listado
    foreach (var item in query)
    {
        ListViewItem row = new ListViewItem(item.Palabra);
        row.SubItems.Add(item.Cantidad.ToString());

        lvListado.Items.Add(row);
    }

}

 

[C#]
 

miércoles, 3 de noviembre de 2010

[ASP.NET][jQuery] Validación sin Postback

 

Introducción


Uno de los principales problemas en un desarrollo web es implementar una confirmación del usuario, especialmente cuando estas necesitas ser realizadas en el servidor.

Acciones implementadas en botones que requieren de una conformación del usuario puede ser un problema, ya que estas necesitan ir al servidor, aplicar la validación, retornar o registrar el script que mostraría al usuario el mensaje javascript de confirmación y ante la aceptación, realizar nuevamente el postback al servidor

 

Solución


El uso de jquery puede aportarnos un alivio a esta problemática, concretamente con el uso de la funcionalidad de $.ajax, el cual no permitirá invocar funcionalidad del servidor para realizar validaciones sin postback de la pagina

El resultado de la validación podrá ser validado y allí mismo mostrar el mensaje de confirmación, si este es aceptado se continua con la operación normalmente.

 

Ejemplo de código


[C#] 
 

sábado, 2 de octubre de 2010

Microsoft MVP C# 2010

 

Aun no puedo creerlo, cuando me llego el mail informándome del nombramiento, por poco se me sale el corazón de la emisión.

No puedo dejar de dar agradecer:

- Juan Carlos Ruiz, sin el apoyo que me brindo desde un primer momento no lo habría conseguido, fue quien me permitió ser parte del grupo de moderadores de los foros de MSDN, además de presentar el reporte de actividades en cada nominación a MVP.

- Felipe Sotelo, con un gesto simple como ser un post en el foro de forma espontanea, logro tanto.

http://social.msdn.microsoft.com/Forums/es-ES/vcses/thread/9142c889-e943-4cca-a3d6-6f7b01d7ebde

A todos los que en este post volcaron sus comentarios, un agradecimiento enorme, la verdad me dejaron sin palabras, la fuerza allí expresadas en cada respuesta es invaluable.

- Fernando García Loera y al comité evaluador, quienes han depositado su confianza al otorgarme este nombramiento.

Espero estar a la altura de lo que un MVP representa, todavía me falta un camino muy largo por recorrer y mucha tecnología por descubrir, pero creo que voy por la senda correcta.

Por si le interesa les dejo el link a mi perfil:

https://mvp.support.microsoft.com/profile/Leandro.Tuttini

También podrán ver este mismo agradecimiento publicado en el foro:

http://social.msdn.microsoft.com/Forums/es-ES/vcses/thread/d52d3fce-52c3-416a-a979-37934a8db491

domingo, 19 de septiembre de 2010

[ADO.NET] Filtrar rango de fechas

 

Introducción


Uno de los principales problemas que se pueden encontrar cuando se confecciona una consulta es el trabajo con campos del tipo fecha.

Por lo general estos campos no solo persisten la fecha, sino que también registran la hora.

Es por eso que ciertas consultas pueden no retornar los valores deseados, aunque se este definiendo correctamente los parámetros de la consulta.

 

Planteo del problema


Para demostrar el problema se plantea el log de actividades del usuario, en donde se registras las acciones que este tiene en la aplicación. Pero el problema se presenta cuando se necesita consultar estos registros para analizar las acciones realizadas en un determinado rango de días, por supuesto se quiere ver los días completo, y es aquí donde nos encontramos con el inconveniente.

Si se analiza los registros de la tabla se ve que el registro de la fecha no solo usa el día, sino también la hora.

imagen1

La aplicación de ejemplo cuenta con dos opciones de búsqueda, pero en este primer análisis nos centraremos en el primer botón de nombre “Buscar (sin usar CONVERT)”, al utilizarlo veremos el resultado de la imagen:

imagen2

Pero con solo comparar los registros obtenido con los que se encuentran en la tabla, nos damos cuenta que hay un problema, esta retornando menos filas de las esperadas, la pregunta seria, porque sucedió esto ?

El código utilizado para obtener los registro es el siguiente :

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRange(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE fecharegistro >= @desde AND fecharegistro <= @hasta";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);

    }

    return dt;
}

El parámetro de la fechas toma el valor directo de los controles que están en la pantalla, pero esta fecha aunque no se conozca también lleva consigo un componente de hora, que puede ser apreciado si se detiene el código y se analiza el valor:

imagen3

El tipo de dato DataTime, lleva una hora aunque esta no se especifique concretamente.

Esto aclara bastante el porque la consulta arroja menos ítems de los esperados, resulta que esta quitando aquellos registros en donde la hora sea menos a las 12 AM

 

Solución del problema


La solución al problema se obtiene usando una funciona en la query, que aplicada sobre los campos de fecha en el filtro quiten la componente de la hora. En realidad no remueve la hora, sino que la normaliza para que esta también tenga las 12 AM, por lo tanto el filtro no descartara ningún registro.

Para esto se hará uso de la función CONVERT, la cual permite convertir entre tipos de datos.

CAST y CONVERT (Transact-SQL)

Si se ejecuta una consulta usando esta función se podría apreciar lo comentado mas arriba:

imagen4

Las fechas de los registros dejan de tener la hora original, ahora todos presentan las 12 AM, esto es justamente lo que ayudara en el filtro.

Entonces si ahora se aplica lo comentado al código se obtiene el resultado esperado (usar el botón “Buscar (usando CONVERT)”):

imagen5

Para que esto resulte se utilizo el siguiente código:

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRangeRemoveHour(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE CONVERT(smalldatetime, CONVERT(char(10), fecharegistro, 103), 103)  >= @desde 
                        AND CONVERT(smalldatetime, CONVERT(char(10), fecharegistro, 103), 103) <= @hasta";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);
    }

    return dt;
}

El uso del CONVERT aplicado en los campo de “fecharegistro”, es quien quita la componente de la hora y permite aplicar el filtro correctamente.

 

Alternativa usando Between


Como alternativa al método anterior se podría lograr usando el Between en la query para filtrar por el rango de fechas.

En el Form2 del ejemplo se podrá encontrar la implementación de este caso.

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRangeWithBetween(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE CAST(CONVERT(CHAR(8), fecharegistro, 112) AS INT) BETWEEN CAST(CONVERT(CHAR(8), @desde, 112) AS INT) 
                                    AND CAST(CONVERT(CHAR(8), @hasta, 112) AS INT)";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);
    }

    return dt;
}

La lógica principal se encontrara en el método creado para realizar la consulta, allí se ha utilizado el BETWEEN para definir el filtro entre un rango, pero este tienen un problema requiere de un formato especial que justamente el CONVERT (con el código 112) nos proporciona.

Esta conversión lleva la fecha al formato yyyyMMdd, o sea si tenemos la fecha 10/06/2010, será formateada a 20100610, claramente un numero, pero primero deberá pasar por un CHAR para adecuar la conversión del formato de la fecha y luego si será convertido a un valor numérico.

 

Ejemplo de Código


El código publicado fue desarrollado con visual Studio 2008.

La base de datos utilizada es el Sql Server Express 2008.

En el proyecto encontraran una carpeta de nombre “Script” que contiene el archivo .sql que podrán usar en el “Sql Server Management Studio” para crear la estructura de la base de datos en caso de tener problemas con el archivo .mdf

En caso de usar la base de datos integrada al servicio de Sql Server, se deberá cambiar la cadena de conexión definida en el App.config

 

[C#] 
[VB.NET] 

sábado, 18 de septiembre de 2010

[WinForms] - Mover UserControl

 

Introducción


El objetivo de este articulo será el revisar algunos aspectos en la creación de User Controls, como ser propiedades, eventos, definición de argumentos en los eventos, propiedades con enumeración.

Consistirá en un control en donde se pueda definir una imagen y a esta asociarle un texto descriptivo, la idea es que el usuario pueda mover el control por la pantalla para posicionarlo de forma correcta.

Contara con al opción para cambiar el texto del label en runtime, además de permitir asignar un estado que bloquear la movilidad.

 

Definición de las propiedades


El control contara con propiedades que definen estados y aspectos visuales, también habrá otras que solo podrán ser usadas solo desde código, como ser el “HasImage”, la cual indica si hay una imagen asociada al control, esta ha sido marcada con el atributo “Browsable” en falso para evitar que sea visualizada en la cuadro de propiedades.

[Browsable(false)]
public bool HasImage
{
    get { return _image != null; }
}


private Image _image;

[Browsable(true)]
[Category("Custom")]
public Image Image 
{
    get { return _image; }
    set { _image = value; }
}

private string _texto;

[Browsable(true)]
[Category("Custom")]
public string Texto
{
    get { return _texto; }
    set 
    { 
        _texto = value;
        lblMensaje.Text = _texto;
    }
}


private EnumEstado _estado;

[Browsable(true)]
[Category("Custom")]
public EnumEstado Estado
{
    get { return _estado; }
    set
    {
        _estado = value;
    }
}

También se definen el atributo “Category”, para que sean agrupadas todas juntas cuando se visualicen en el cuadro de propiedades, lo cual facilita la búsqueda.

 

Evento MouseMove


El permitir que un control tenga la capacidad de movimiento, no es tan directo como parece, con solo asignar el evento MouseMove en el formulario no alcanza para que esto funcione.

O sea hacer esto, no es suficiente:

public void testUserControl1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        TestUserControl us = sender as TestUserControl;
        if (us != null && us.HasImage)
        {
            us.Left += e.X;
            us.Top += e.Y;
        }
    }
}

Si bien al arrastrar el mouse en las zonas libres del User Control se lanzara el evento, si por casualidad el puntero del mouse se sitúa por sobre un control definido en el User Control, el evento se produce en el control interno, sin ser elevado hacia el formulario.

Pero este problema tiene solución, y consiste en atrapar los eventos de los controles internos del User Control que se esta desarrollando y elevar el evento hacia afuera:

public new event MouseEventHandler MouseMove;

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

private void label1_MouseMove(object sender, MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

protected override void OnMouseMove(MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

La primer línea de este código define el evento MouseMove, pero hay que remarcar que el User Control de por si ya posee este evento lo cual trae conflicto al definir uno propio, es por eso que se usa el modificado “new” en la declaración, allí se esta forzando una redefinición del evento.

El ultimo método donde se realiza un override del OnMouseMove, permite tomar la acción cuando el puntero del mouse se sitúa en una zona libre del User Control, y de esta forma agregarle lógica especializada, como es en este caso que al esta en un estado Inactivo no permita el movimiento.

Los dos métodos restantes permiten capturar los eventos MouseMove de los controles internos, en este caso un PictureBox y un Label, también se la aplica cierta lógica de estados y se lanza el evento hacia el formulario, pero en este caso no se usa un override, sino que se adjunta el evento en la inicialización del User Control, asignado el handler del evento:

public TestUserControl()
 {
     InitializeComponent();

     pictureBox1.MouseMove += new MouseEventHandler(pictureBox1_MouseMove);
     lblMensaje.MouseDown += new MouseEventHandler(label1_MouseMove);
     
     ModoEdicion(false);

     this.Estado = EnumEstado.Activo;
     this.Texto = this.Name;

 }

Cambio del Texto


La acción de cambio de Texto implica dos operaciones, manejo de visibilidad de los controles, el envió del evento informando del cambio al formulario.

Para llevar a acabo esta operación se contara un con menú contextual que será desplegado al realizar click con el botón derecho del mouse sobre el User Control.

private void ModoEdicion(bool modo)
{
    txtMensaje.Visible = modo;
    mnuAceptar.Visible = modo;
    mnuAceptar.Visible = modo;
    mnuCancelar.Visible = modo;

    lblMensaje.Visible = !modo;
    mnuCambiarTexto.Visible = !modo;
}

private void mnuCambiarTexto_Click(object sender, EventArgs e)
{
    ModoEdicion(true);

    txtMensaje.Text = _texto;
    txtMensaje.Select();
    txtMensaje.SelectAll();
}

private void mnuAceptar_Click(object sender, EventArgs e)
{
    this.Texto = txtMensaje.Text;

    ModoEdicion(false);

    if (CambioMensaje != null)
        CambioMensaje(this, new MensajeEventArgs(_texto));
}

private void mnuCancelar_Click(object sender, EventArgs e)
{
    ModoEdicion(false);
}

El primer método simplemente cumple la función de implementar como el cambio de estado afecta a la visibilidad de los controles, inicialmente se visualiza lo escrito en el Label, y una modalidad de edición se despliega el TextBox para ingresar la nueva descripción.

Los demás eventos pertenecen a los ítems del menú, que realizaran las acciones necesarias para el edición, por ejemplo la opción “mnuAceptar”, no solo asigna los escrito a la propiedad, sino que además lanza un evento informando al formulario que ha cambiado el mensaje asignado al control.

El formulario por su parte se adjunta al evento expuesto:

private void Form1_Load(object sender, EventArgs e)
{
    testUserControl1.CambioMensaje += new CambioMensajeEventHandler(testUserControl1_CambioMensaje);
}

void testUserControl1_CambioMensaje(object sender, MensajeEventArgs e)
{
    TestUserControl usrcontrol = sender as TestUserControl;

    if (usrcontrol != null)
        MessageBox.Show(string.Format("El texto ha cambiado: {0}", e.Texto));
}

Algo interesante es como se hace uso del argumento “e” den método del evento para tomar el valor ingresado en el control y desplegarlo en el MessageBox, este se logra mediante la definición de un EventArgs creado especialmente para este fin.

La declaración del evento se logra mediante dos simples líneas:

public delegate void CambioMensajeEventHandler(object sender, MensajeEventArgs e);

public partial class TestUserControl : UserControl
{

	public event CambioMensajeEventHandler CambioMensaje;
		
	.
	.
	.
}

Pero como comentamos el delegate hace uso de un argumento en el evento especial, el cual se define al final del control:

public class MensajeEventArgs : EventArgs
{
    public MensajeEventArgs(string texto)
    {
        _texto = texto;
    }

    private string _texto;

    public string Texto
    {
        get { return _texto; }
    }
}

Ejemplo de código


El código fue desarrollado usando Visual Studio 2008.

[C#] 
[VB.NET] 

domingo, 29 de agosto de 2010

[WinForms] - Abrir formulario modal en el evento Load

 

Introducción

En algunas situaciones puede ser necesario realizar la apertura de alguna ventana nueva en forma modal desde un lugar algo particular, como es el evento Load del formulario.

Cualquier proceso que bloque la terminación completa de este evento impedirá que se muestre el formulario que se esta abriendo hasta tanto no se cierre el que se encuentra visualmente activo.

Por ejemplo, si en el código utilizan algunas líneas como estas:

private void Form2_Load(object sender, EventArgs e)
{
    Form3 frm = new Form3();
    frm.ShowDialog();
}

notaran el efecto que se comenta, en donde el Form2 no se visualiza, pero si el Form3, recién cuando este ultimo es cerrado el evento Load termina y se despliega el Form2.

Para evitar este efecto hay algunas técnicas que pueden aplicarse, pero básicamente lo que buscan es realizar la apertura del formulario modal un tiempo después de concluir el evento Load.

 

Utilizando las API de Windows

Mediante el uso de un mensaje a las API se puede enviar un mensajes al proceso de Windows, atapándolo y lanzando allí la ventana.

public partial class Form2 : Form
{
    
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);


    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        PostMessage(this.Handle, 7000, 0, 0);
    }


    protected override void WndProc(ref Message m)
    {
        if (m.Msg == 7000)
        {
            Form3 frm = new Form3();
            frm.ShowDialog();

            m.Result = (IntPtr)0;
            return;
        }

        base.WndProc(ref m);
    }

}
[C#]
 

Utilizando el Timer

Esta otra alternativa declara en muy pocas líneas un objeto Timer, en donde el uso de métodos anónimos permite definir el cuerpo del evento Tick que será lanzado luego de 1 ms, mas que suficiente para que el evento Load termine de ejecutarse.

 

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {

        Timer timer = new Timer();
        timer.Interval = 1;
        timer.Tick += delegate(object s, EventArgs eventarg)
        {
            ((Timer)s).Stop();
            Form3 frm = new Form3();
            frm.ShowDialog();
        };
        timer.Start();

    }
}

La idea es esta alternativa es mostrar además que no es necesario definir evento en métodos separados que harían engorrosa esta implementación, aquí en un bloque de unas pocas líneas se logra el efecto deseado.

[C#]
 

Conclusión

El efecto que debe lograrse es el no bloqueo del evento Load del formulario, estas dos técnicas lo logran, pero seguramente se preguntaran porque no se han usando Thread.

El problema surge porque lo Thread generan un hilo independiente evitando que los formularios actúen como modales, si el Form3 es abierto dentro de un nuevo hilo este ya no seria modal, sino que se abriría como una ventana común a la cual se le aplico el método Show()

[N-Tier] Desarrollo en capas – Transformación de entidades en la Capa de Negocio – Parte 4

 

Introducción


Cuando se desarrolla aplicaciones muy centrada en los datos suele ocurrir que el ida y vuelta de los datos por las capas resulte muy directo, notando que la capa de negocio prácticamente no aporta un valor relevante.

En la mayoría de los casos cuando se confeccionan ABM de entidades sin mayor complejidad, la capa de negocio suele convertirse en un simple pasamanos de entidades y listas sin aportar mayor utilidad, esto puede ser cierto en algunas situaciones, pero igualmente esta capa es necesaria para seguir con una arquitectura adecuada, ya que uno nunca sabe cuando será necesario adaptar la información para disponerlos en un formato distinto.

En este articulo analizaremos la importante la capa de negocio y como puede ser útil para transformar la estructura de los datos, adecuándola a la necesidad de la presentación.

Ejemplo propuesto


Para realizar la demostración se atacara un punto claramente poco practico a la hora de ser usada la aplicación.

Este tiene que ver con la búsqueda de un determinado tema, en la grilla de compra.

Actualmente se agrega una nueva línea y se dispone un combo en la celda para seleccionar un tema, pero esto es poco practico teniendo en cuenta la cantidad de temas disponibles.

Una solución a este problema podrías ser representar la selección en forma de árbol, ya que los datos que disponemos implican que los temas se asocian a Álbumes y Artistas, otorgando una relación jerárquica.

El problema presente aquí es que los datos, tan cual podrían tomarse de la base de datos, serán devueltos como registros sin estructura que permita una simple conversión para ser representada en el control TreeView en el formulario.

Es aquí donde entra en jugo la capa de negocio como mediador / adaptador de la información, facilitando la integración entre las capas.

La capa de datos devolverá los registros planos sin estructura, pero la presentación necesita de una jerarquía, es aquí donde la capa de negocio realizaría la transformación, con la ayuda de Linq.

Estructura de los datos

En la base de datos se cuenta con la siguiente estructura de tablas:

image

Recuperar los datos y armar la jerarquía


Al obtener información proveniente de las tablas esta se estructura en un nivel simple de registros, la información viene de forma plana definida en campos, es por eso que en esta solución se han creado entidades adicionales para trabajar con la información en dos estadios distintos.

El primero cuando se recupera la información directa de la consulta realizada, la cual mapeara a la entidad representada por TrackHierarchicalEntity, esta contiene las propiedades que definen la relación entre varias entidades:

public class TrackHierarchicalEntity
{
    public int ArtistId { get; set; }
    public string ArtistName { get; set; }

    public int AlbumId { get; set; }
    public string AlbumTitle { get; set; }
    
    public int TrackId { get; set; }
    public string TrackName { get; set; }
}

Por otro la se tendrá una estructura diferente de entidades que conformaran la jerárquica de información, es por ello que estas cuentan con propiedades de lista  genérica de su entidad relacionada:

 

public class ArtistEntity
{
  public int ArtistId { get; set; }
  public string Name { get; set; }

  public List<AlbumEntity> Alumns { get; set; }
}

public class AlbumEntity
{
  public int AlbumnId { get; set; }
  public string Title { get; set; }

  public List<TrackEntity> Tracks { get; set; }
}

public class TrackEntity
{
  public int TrackId { get; set; }
  public string Name { get; set; }

}

Capa de Datos


En la clase TrackDAL, se agrego un nuevos métodos que permitirán obtener la información de forma plana y directa mapeando al entidad uno a uno con la query que se utiliza:

public static List<TrackHierarchicalEntity> GetAllHierarchical()
{
    List<TrackHierarchicalEntity> list = new List<TrackHierarchicalEntity>(); ;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string sql = @"SELECT AR.ArtistId, 
                            AR.Name As ArtistName, 
                            A.AlbumId, 
                            A.Title As AlbumTitle, 
                            T.TrackId, 
                            T.Name 
                        FROM Track T 
                            INNER JOIN Album A ON A.AlbumId = T.AlbumId
                            INNER JOIN Artist AR ON AR.ArtistId = A.ArtistId
                        ORDER BY AR.Name, A.Title, T.Name";

        SqlCommand cmd = new SqlCommand(sql, conn);

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadHierarchicalTrack(reader));
        }

    }

    return list;
}

private static TrackHierarchicalEntity LoadHierarchicalTrack(IDataReader reader)
{
    TrackHierarchicalEntity track = new TrackHierarchicalEntity();


    track.ArtistId = Convert.ToInt32(reader["ArtistId"]);
    track.ArtistName = Convert.ToString(reader["ArtistName"]);

    track.AlbumId = Convert.ToInt32(reader["AlbumId"]);
    track.AlbumTitle = Convert.ToString(reader["AlbumTitle"]);

    track.TrackId = Convert.ToInt32(reader["TrackId"]);
    track.TrackName = Convert.ToString(reader["Name"]);


    return track;
}

La consulta utilizada el INNER JOIN para unir la información de las tres tablas definidas en la base de datos, recuperando la información en una sola consulta.

Capa de Negocio


Sera la responsable de adaptar la información plana devuelta por la Capa de Datos, otorgando una jerarquía.

public static List<ArtistEntity> GetAllHierarchical()
{
    List<TrackHierarchicalEntity> tracks = TrackDAL.GetAllHierarchical();

    var hierarchicalList = from item in tracks
                            group item by new { item.ArtistId, item.ArtistName } into artist
                            select new ArtistEntity()
                            {
                                ArtistId = artist.Key.ArtistId,
                                Name = artist.Key.ArtistName,
                                Alumns = (from artistitem in artist
                                          group artistitem by new { artistitem.AlbumId, artistitem.AlbumTitle } into album
                                          select new AlbumEntity()
                                          {
                                              AlbumnId = album.Key.AlbumId,
                                              Title = album.Key.AlbumTitle,
                                              Tracks = (from trackitem in album
                                                        select new TrackEntity()
                                                        {
                                                            TrackId = trackitem.TrackId,
                                                            Name = trackitem.TrackName  
                                                        }).ToList() 
                                          }).ToList()

                            };

    return hierarchicalList.ToList();
}

Linq ayuda mucho en esta operación de transformación, el uso de la sentencia “group by” es muy útil para definir los campos usados en cada nivel.

En este caso, al posee dos propiedades para la cada entidad se hizo uso de un “group by” junto al “new” para definir una entidad anónima, lo cual permitió tomar la información de la propiedades Key he ir armando las nuevas entidades.

Capa de Presentación


El contar con información jerárquicamente adaptada por la capa de negocio facilita enormemente la tarea de creación de los nodos en el TreeView, simplemente se recorre de forma anidada cada nivel y se van creando los nodos.

Como verán es muy simple:

private void CargarTree()
{
    List<ArtistEntity> lista = TrackBO.GetAllHierarchical();

    foreach (ArtistEntity artist in lista)
    {
        TreeNode nodeArtist = new TreeNode(artist.Name);
        treeTracks.Nodes.Add(nodeArtist);

        foreach (AlbumEntity album in artist.Alumns)
        {
            TreeNode nodeAlbum = new TreeNode(album.Title);
            nodeArtist.Nodes.Add(nodeAlbum);

            foreach (TrackEntity track in album.Tracks)
            {
                TreeNode nodeTrack = new TreeNode(track.Name);
                nodeTrack.Tag = track;
                nodeAlbum.Nodes.Add(nodeTrack);
            }
        }
    }
}

 

[C#] 
[VB.NET] 
 

 

Alternativa en el trabajo de entidades


En el ejemplo planteado hasta el momento la entidad TrackHierarchicalEntity define dos propiedades por cada entidad que involucra en la jerarquía, pero que sucedería si se trata de objetos algo mas complejos.

Esta es justamente la alternativa que se analizaría en esta sección.

La entidad usada para definir la estructura con la cual se trabaja el linq cambiara su aspecto:

public class TrackHierarchicalEntity
{
    public ArtistEntity Artist { get; set; }

    public AlbumEntity Album { get; set; }

    public TrackEntity Track { get; set; }

}

Esto impactara también en la capa de datos, mas que nada en la funcionalidad que crea la entidad:

public static List<TrackHierarchicalEntity> GetAllHierarchical()
{
    List<TrackHierarchicalEntity> list = new List<TrackHierarchicalEntity>(); ;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string sql = @"SELECT AR.ArtistId, 
                            AR.Name As ArtistName, 
                            A.AlbumId, 
                            A.Title As AlbumTitle, 
                            T.TrackId, 
                            T.Name 
                        FROM Track T 
                            INNER JOIN Album A ON A.AlbumId = T.AlbumId
                            INNER JOIN Artist AR ON AR.ArtistId = A.ArtistId
                        ORDER BY AR.Name, A.Title, T.Name";

        SqlCommand cmd = new SqlCommand(sql, conn);

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadHierarchicalTrack(reader));
        }

    }

    return list;
}

private static TrackHierarchicalEntity LoadHierarchicalTrack(IDataReader reader)
{
    TrackHierarchicalEntity track = new TrackHierarchicalEntity();

    track.Artist = new ArtistEntity()
    {
        ArtistId = Convert.ToInt32(reader["ArtistId"]),
        Name = Convert.ToString(reader["ArtistName"])
    };

    track.Album = new AlbumEntity()
    {
        AlbumnId =  Convert.ToInt32(reader["AlbumId"]),
        Title = Convert.ToString(reader["AlbumTitle"])
    };

    track.Track = new TrackEntity()
    {
        TrackId = Convert.ToInt32(reader["TrackId"]),
        Name = Convert.ToString(reader["Name"])
    };

    return track;
}

Si bien el query usado para extraer la información continua sin variante, la forma en como se procesa ha cambiado, teniendo que crear instancias por cada entidad que se necesites utilizar.

En la clase TrackBO el linq usado para armar la estructura no ha sufrido cambios notables en el grueso de la lógica aplicada:

public static List<ArtistEntity> GetAllHierarchical()
{
    List<TrackHierarchicalEntity> tracks = TrackDAL.GetAllHierarchical();

    var hierarchicalList = from item in tracks
                            group item by item.Artist into artist
                            select new ArtistEntity()
                            {
                                ArtistId = artist.Key.ArtistId,
                                Name = artist.Key.Name,
                                Alumns = (from artistitem in artist
                                          group artistitem by artistitem.Album into album
                                          select new AlbumEntity()
                                          {
                                              AlbumnId = album.Key.AlbumnId,
                                              Title = album.Key.Title,
                                              Tracks = (from trackitem in album
                                                        select new TrackEntity()
                                                        {
                                                            TrackId = trackitem.Track.TrackId,
                                                            Name = trackitem.Track.Name  
                                                        }).ToList() 
                                          }).ToList()

                            };

    return hierarchicalList.ToList();
}

Pero si hay un detalle no menos a remarcar, en el linq ahora se hacen uso de las propiedades con entidades en las funciones de agrupación, pero estas trabajan por referencia de los objetos, por lo tanto como sabrá linq que entidades son iguales para agruparlas, es aquí donde se genera el principal cambio con la primer parte del articulo.

Las dos entidades involucradas en la agrupación dentro del linq deberás implementar una interfaz IEquatable<>, la cual permite definir los métodos que permitirán diferenciar una instancia con otra cuando se los necesita comparar.

Tanto el método Equals() como el GetHashCode() serán invocados por linq en su operación de agrupación, dentro de estos métodos se hacen uso de la propiedades para poder determinar si se trata de la misma entidad o no, de esta forma se evita evita el método estándar cuando no se implemento la interfaz, en donde solo se comparan las referencias de los objetos.

 

public class AlbumEntity : IEquatable<AlbumEntity>
{
    public int AlbumnId { get; set; }
    public string Title { get; set; }

    public List<TrackEntity> Tracks { get; set; }


    #region IEquatable<ArtistEntity> Members

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

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

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

    public override int GetHashCode()
    {
        return this.AlbumnId.GetHashCode() ^ this.Title.GetHashCode();
    }

    #endregion

}


public class ArtistEntity : IEquatable<ArtistEntity>
{
    public int ArtistId { get; set; }
    public string Name { get; set; }

    public List<AlbumEntity> Alumns { get; set; }


    #region IEquatable<ArtistEntity> Members

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

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

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

    public override int GetHashCode()
    {
        return this.ArtistId.GetHashCode() ^ this.Name.GetHashCode();
    }

    #endregion

}

Si bien esta alternativa no parece muy útil al principio, cuando se trabaja con objetos es muy común encontrarse con situaciones como esta, en donde entidades se encuentren relacionados con otras mas complejas, las cuales hay que trabajarlas y conocer las técnicas que permiten su manipulación.

[C#] 
[VB.NET] 

Conclusión


Si bien la capa de negocio puede resultar muy útil como intermediario para coordinar operaciones que requiere complejas actualizaciones de datos a distintas tablas, definiendo a su vez todo en una misma transacción, esta es solo la operación común para la cual se usaría esta capa.

La transformación de información para adaptarla a la presentación es otra de las tantas responsabilidades que puede tener.

domingo, 15 de agosto de 2010

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 3

Introducción


Hemos entrado en un momento clave en la evolución del presente del articulo, aquí es donde concretamente se verán las 3 capas interactuando entre si.

El mismo fue evolucionado de artículos previos:

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 1

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 2

En la ultima oportunidad se había reestructurado la aplicación para que soportara 2 capas, la presentación accedía directo a la de datos para trabajar con las entidades.

Además se había agregado un proyecto que representa las entidades del negocio, el cual cruzaba todas las capas y era usado como medio de trasporte de datos entre las mismas. Este estaba implementado por medios de dataset tipado para representar las entidades.

Estructura del proyecto


La nueva arquitectura agrego un proyecto adicional del tipo “Class Library”, el cual se ubicara en medio de la capa de presentación y la de datos, desacoplándolas.

Esta nueva capa representara la fachada de entrada al dominio de la aplicación, mas adelante cuando se implementen servicio para distribuir la aplicación cumplirá un papel fundamental para aislar el dominio.

También se reestructuro el proyecto de entidades, ahora ya no se usan dataset tipados para representar las entidades, estas fueron reemplazas por clases custom, es por eso que se verán  nombre como ser:  “CustomerEntity”, “InvoiceEntity”, etc.

El cambio en las entidades afecto la capa de datos, ya no se usa el DataAdapter para cargar los datatable que representaban a la entidad, fue necesario reemplazarlos por DataReader, estos son óptimos para la lectura secuencial de los registros devueltos por la query, y el armado de las instancias de la entidad.

La imagen representa los distintos proyectos y como se referencian entre si:

 image

En esta nueva distribución de capas será imprescindible que la presentación se comunique siempre con la fachada de negocio, la cual abstraerá las operaciones transaccionales, y creara un único punto de entrada al sistema, si bien no se aprecia la importancia de lo dicho con este ejemplo, si a futuro fuera necesario cambiar la presentación, quizás por una web o con WPF, no se perdería todo el trabajo realizado, ya que las reglas de negocio y persistencia quedan intacticas.

Algo que seguro traer molestia al desarrollar aplicando esta técnica es que la mayoría de las operaciones serán un pasamano por la capa de negocio, esta solo tomara lo que la presentación retorne y lo devolverá ala presentación, sin efectuar ninguna operación en medio, para la mayoría de las operaciones de consulta será así, pero en otras circunstancias se vera la importancia de esta capa, sobre todo al persistir entidades complejas.

El uso de entidades con clases en lugar de dataset tipados, también aporta una mejora importante, las clases permiten extender funcionalidad y relacionar entidades fácilmente, como ser el caso del calculo de Total en la entidad de facturación.

A continuación se analizarían las operaciones que han sufrido cambios durante la transformación a las 3 capas.

Grabar/Actualizar un Cliente


Durante la operación de confirmación de la factura se notara el cambio en la técnica utilizada para persistir la información del cliente, anteriormente desde la presentación se decidía si se actualizaba o insertaba la entidad, ahora es la capa de negocio quien decide que operación debe realizarse.

[Presentación]

if (cliente == null)
    cliente = new CustomerEntity();

cliente.FirstName = txtNombre.Text;
cliente.LastName = txtApellido.Text;
cliente.Company = txtCompañia.Text;
cliente.Address = txtDireccion.Text;
cliente.Email = txtEmail.Text;

cliente = CustomerBO.Save(cliente);

[Business Layer]

public static CustomerEntity Save(CustomerEntity customer)
{

    if (CustomerDAL.Exists(customer.CustomerId))
        return CustomerDAL.Update(customer);
    else
        return CustomerDAL.Create(customer);

}

[Data Access]

public static class CustomerDAL
{

    public static bool Exists(int id)
    {
        int nrorecord = 0;

        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sql = @"SELECT Count(*)
                            FROM Customer 
                            WHERE CustomerId = @customerId";

            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("customerId", id);

            nrorecord = Convert.ToInt32(cmd.ExecuteScalar());
        }

        return nrorecord > 0;

    }

    public static CustomerEntity Create(CustomerEntity customer)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            string sql = @"INSERT INTO Customer (FirstName, LastName, Company, Address, Email) 
                                VALUES (@firstName, @lastName, @company, @address, @email)
                           SELECT SCOPE_IDENTITY()";

            SqlCommand cmd = new SqlCommand(sql, conn);

            cmd.Parameters.AddWithValue("@firstName", customer.FirstName);
            cmd.Parameters.AddWithValue("@lastName", customer.LastName);
            cmd.Parameters.AddWithValue("@company", customer.Address);
            cmd.Parameters.AddWithValue("@address", customer.Company);
            cmd.Parameters.AddWithValue("@email", customer.Email);

            customer.CustomerId = Convert.ToInt32(cmd.ExecuteScalar());
        }

        return customer;
    }

    public static CustomerEntity Update(CustomerEntity customer)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sql = @"UPDATE Customer SET  
                                        FirstName = @firstName, 
                                        LastName = @lastName, Company = @company, 
                                        Address = @address,
                                        Email = @email
                                WHERE CustomerId = @customerid";

            SqlCommand cmd = new SqlCommand(sql, conn);

            cmd.Parameters.AddWithValue("@firstName", customer.FirstName);
            cmd.Parameters.AddWithValue("@lastName", customer.LastName);
            cmd.Parameters.AddWithValue("@company", customer.Address);
            cmd.Parameters.AddWithValue("@address", customer.Company);
            cmd.Parameters.AddWithValue("@email", customer.Email);
            cmd.Parameters.AddWithValue("@customerid", customer.CustomerId);


            cmd.ExecuteNonQuery();

        }

        return customer;
    }


}

La capa de negocio valida si la entidad existe o no, y procede a ejecutar la operación correcta para cada caso, usando como identificador el id de la entidad.

Proceso de Facturación


Con respecto a la implementación del articulo previo el proceso de facturación sufrió un cambio importante, ya no se envía dos entidades separadas para procesar, las cuales representaban al encabezado de la factura y sus líneas, sino que una única entidad posee una colección o lista genérica vinculada que permite cargar los datos de la asociación.

[Presentación]

#region Creo el Encabezado\Linea de la Factura

InvoiceEntity invoice = new InvoiceEntity();

invoice.CustomerId = cliente.CustomerId;
invoice.InvoiceDate = DateTime.Now.Date;
invoice.BillingAddress = txtDireccion.Text;


foreach (DataGridViewRow row in dgvLineaCompra.Rows)
{
    InvoiceLinesEntity invoiceLine = new InvoiceLinesEntity();

    invoiceLine.TrackId = Convert.ToInt32(row.Cells["Track"].Value);
    invoiceLine.UnitPrice = Convert.ToDecimal(row.Cells["PrecioUnitario"].Value);
    invoiceLine.Quantity = Convert.ToInt32(row.Cells["Cantidad"].Value);

    invoice.Lineas.Add(invoiceLine);
}

InvoiceBO.RegistrarFacturacion(invoice);

#endregion

[Business Layer]

public static class InvoiceBO
{
    public static void RegistrarFacturacion(InvoiceEntity invoice)
    {
        //
        // inicializo la transacciones
        //
        using (TransactionScope scope = new TransactionScope())
        {
            //
            // Creo la factura y sus lineas
            //
            InvoiceDAL.Create(invoice);

            //
            // Actualizo el total
            //
            InvoiceDAL.UpdateTotal(invoice.InvoiceId, invoice.Total);
            
            scope.Complete();
        }

    }
}

[Data Access]

public static class InvoiceDAL
{

    public static void Create(InvoiceEntity invoice)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();
            //
            // Creacion de la Factura
            //
            string sqlFactura = @"INSERT INTO Invoice (CustomerId, InvoiceDate, BillingAddress, Total) VALUES (@customerId, @date, @address, @total)
                       SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlFactura, conn))
            {

                cmd.Parameters.AddWithValue("@customerId", invoice.CustomerId);
                cmd.Parameters.AddWithValue("@date", invoice.InvoiceDate);
                cmd.Parameters.AddWithValue("@address", invoice.BillingAddress);
                cmd.Parameters.AddWithValue("@total", 0);

                invoice.InvoiceId = Convert.ToInt32(cmd.ExecuteScalar());
            }


            string sqlLineaFactura = @"INSERT INTO InvoiceLine (InvoiceId, TrackId, UnitPrice, Quantity) 
                                        VALUES (@invoiceid, @trackid, @unitprice, @quantity)
                                        SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlLineaFactura, conn))
            {

                foreach (InvoiceLinesEntity invoiceLine in invoice.Lineas)
                {
                    //
                    // como se reutiliza el mismo objeto SqlCommand es necesario limpiar los parametros
                    // de la operacion previa, sino estos se iran agregando en la coleccion, generando un fallo
                    //
                    cmd.Parameters.Clear();

                    cmd.Parameters.AddWithValue("@invoiceid", invoice.InvoiceId);
                    cmd.Parameters.AddWithValue("@trackid", invoiceLine.TrackId);
                    cmd.Parameters.AddWithValue("@unitprice", invoiceLine.UnitPrice);
                    cmd.Parameters.AddWithValue("@quantity", invoiceLine.Quantity);

                    //
                    // Si bien obtenermos el id de linea de factura, este no es usado
                    // en la aplicacion
                    //
                    invoiceLine.InvoiceLineId = Convert.ToInt32(cmd.ExecuteScalar());
                }

            }

        }

        

    }

    /// <summary>
    /// Actualizacion del Total de la Factura
    /// </summary>
    public static void UpdateTotal(int idInvoice, decimal total)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sqlUpdateTotal = @"UPDATE Invoice SET Total = @total WHERE InvoiceId = @InvoiceId";

            using (SqlCommand cmd = new SqlCommand(sqlUpdateTotal, conn))
            {
                cmd.Parameters.AddWithValue("@total", total);
                cmd.Parameters.AddWithValue("@InvoiceId", idInvoice);

                cmd.ExecuteNonQuery();
            }
        }
    }

}

 

Un punto importante a remarcar es que ahora la capa de negocio orquesta todas las operaciones y por ende es esta la que lleva la transacción de las entidades, en el artículo anterior se había comentado este punto, justamente porque la presentación no debía ser responsable de asegurar la operación. Es mas a la presentación se le ha quitado la referencia a la librería System.Transactions.

Durante la transformación se separo una de las funcionalidades correspondiente a al actualización del total de la factura, en el ejemplo previo se hacia la sumatoria mientras se creaba cada línea, y al final se actualizaba la entidad “invoice” con el valore resultante. Ahora esta se realiza en una operación separada y coordinada por la capa de negocio, se toma el id de factura devuelto por la operación anterior, y se sumando los valores de las línea internamente en la propia entidad que representa la factura de cliente, ya que ahora esta posee la colección relacionada, una operación lambda en el método de extensión de suma fue mas que suficiente.

public class InvoiceEntity
{
    public InvoiceEntity()
    {
        this.Lineas = new List<InvoiceLinesEntity>();
    }

    public int InvoiceId { get; set; }
    public int CustomerId { get; set; }
    public DateTime InvoiceDate { get; set; }
    public string BillingAddress { get; set; }

    public List<InvoiceLinesEntity> Lineas { get; set; }

    public decimal Total
    {
        get { return this.Lineas.Sum(x => x.UnitPrice * x.Quantity); }
    }
}

Conclusión


Si bien aun quedan muchos puntos por explorar, este simple ejemplo puede servir de guía para comenzar con futuros desarrollos.

Es cierto que algunos aspectos que podrían haberse implementado, como ser:

- La entidad InvoiceEntity aun conserva la propiedad CustomerId, cuando debería reemplazarse por una propiedad del tipo CustomerEntity, lo cual no se realizo para no aumentar la complejidad.

- No se implementaron controles fuerte de errores y como comunicarlos hacia la presentación para ser tratados, este punto se vera en un articulo futuro.

- No se agregaron reglas de negocio restrictivas, como podría ser, por ejemplo, si el cliente supera tiene un monto determinado de facturas impagas no se permita la facturación en curso.

En posteriores artículos tratare estos tema con mas detalle, y otros como ser:

-la utilización de reportadores como Crystal Reports o Reporting Service en aplicación con capas,

-la creación de repositorios de acceso a datos que soporten distintas base de datos,

-el cambio de la presentación a un proyecto web para marcar la reutilización, así como también al desconexión de la capas de presentación y negocio mediante servicios para poder hacer uso de un ambiente distribuido.

 

Ejemplo de código


El proyecto fue desarrollado con Visual Studio 2008

Se debe tener presente Sql Server 2008 Express para poder acceder a la db integrada al proyecto.

 

[C#] 
[VB.NET] 

domingo, 8 de agosto de 2010

[GridView] ITemplate – Columnas definidas en runtime

 

Introducción


Suelen darse los escenarios en donde se necesite editar distintas entidades pero haciendo uso de un único control gridview.

Una respuesta rápida a este problema seria hacer uso de la propiedad AutoGenerateColumns en true, para que los datos que se le proporciona al control defina las columnas que debe mostrar, lo malo de esta opción es que se pierde control sobre la grilla.

Otra alternativa interesante podría ser el uso de la clase BoundField con esta seria posible definir columnas en tiempo de ejecución, si bien podría ser la solución en la mayoría de los caso, esta no permite un control total del témplate que se debe usar en al edición de las columnas.

La solución definitiva al problema esta en la implementación de témplates de columnas, estas clases especializadas contendrán el código del témplate que define, para que esto se posible se necesitara implementar la interfaz ITemplate 

El modelo del ejemplo de código planteado hace referencia a dos listados, uno de notebooks y otro de televisores, ambos con distintas columnas por mostrar, pero haciendo uso de un solo control de grilla y la definición de las columnas de forma explicita en runtime.

 

Uso del BoundField


La definición de las columnas mediante esta clase podrá apreciarse en el formulario de nombre “GridViewBoundField.aspx”

public partial class GridViewBoundField : System.Web.UI.Page
{
    protected void Page_Init(object sender, EventArgs e)
    {
        DefinirColumnasNotebook();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            GridView1.DataSource = DataAccess.ObtenerListaNotebook();
            GridView1.DataBind();
        }
    }

    private void DefinirColumnasNotebook()
    {
        //
        // Se define el campo dentro de la grilla, 
        // para poder identificar cada item 
        //
        GridView1.DataKeyNames = new string[] { "Id" };

        GridView1.Columns.Clear();

        BoundField tempDesc = new BoundField();
        tempDesc.HeaderText = "Descripcion Producto";
        tempDesc.DataField = "Descripcion";
        GridView1.Columns.Add(tempDesc);

        BoundField tempPrecio = new BoundField();
        tempPrecio.HeaderText = "Precio";
        tempPrecio.DataField = "Precio";
        GridView1.Columns.Add(tempPrecio);

    }
}

Se notara en el código que las columnas son definidas en el evento Page_Init, mientras que los datos son cargadas en el Page_Load

Esto es porque la definición de las columnas al ser dinámicas se deberán crear cada vez que se realice un postback, mientras que los datos pueden volver a bindear a la grilla, o no, eso dependerá de la funcionalidad que se quiera lograr

 

Definición de ITemplate


La definición de las columnas por medio de la creación de templetes de columnas, podrá apreciarse en el formulario de nombre “GridViewITemplate.aspx”.

Para poder hacer uso de template de columnas en el control GridView, será necesario la implementación de a interfaz ITemplate, a continuación se vera el código de estas clases:

public class GridViewHeaderTemplate : ITemplate
{
    string text;

    public GridViewHeaderTemplate(string text)
    {
        this.text = text;
    }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        Literal lc = new Literal();
        lc.Text = text;

        container.Controls.Add(lc);

    }
}

public class GridViewEditTemplate : ITemplate
{
    private string columnName;

    public GridViewEditTemplate(string columnName)
    {
        this.columnName = columnName;
    }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        TextBox tb = new TextBox();
        tb.ID = string.Format("txt{0}", columnName);
        tb.EnableViewState = false;
        tb.DataBinding += new EventHandler(tb_DataBinding);

        container.Controls.Add(tb);
    }

    void tb_DataBinding(object sender, EventArgs e)
    {
        TextBox t = (TextBox)sender;

        GridViewRow row = (GridViewRow)t.NamingContainer;

        string RawValue = DataBinder.Eval(row.DataItem, columnName).ToString();

        t.Text = RawValue;
    }
}

public class GridViewItemTemplate : ITemplate
{
    private string columnName;

    public GridViewItemTemplate(string columnName)
    {
        this.columnName = columnName;
    }
    
    public void InstantiateIn(System.Web.UI.Control container)
    {
        Literal lc = new Literal();

        lc.DataBinding += new EventHandler(lc_DataBinding);

        container.Controls.Add(lc);

    }

    void lc_DataBinding(object sender, EventArgs e)
    {
        Literal l = (Literal)sender;

        GridViewRow row = (GridViewRow)l.NamingContainer;

        string RawValue = DataBinder.Eval(row.DataItem, columnName).ToString();

        l.Text = RawValue;
    }
}


public class GridViewItemCheckTemplate : ITemplate
{
    private string columnName;

    public GridViewItemCheckTemplate(string columnName)
    {
        this.columnName = columnName;
    }

    public bool CanEdit { get; set; }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        CheckBox check = new CheckBox();
        check.ID = string.Format("chk{0}", columnName);
        check.Enabled = this.CanEdit;
        check.DataBinding += new EventHandler(check_DataBinding);

        container.Controls.Add(check);

    }

    void check_DataBinding(object sender, EventArgs e)
    {
        CheckBox check = (CheckBox)sender;

        GridViewRow row = (GridViewRow)check.NamingContainer;

        string value = DataBinder.Eval(row.DataItem, columnName).ToString();

        check.Checked = bool.Parse(value);
    }

}

Cada una representa un témplate de visualización y edición dentro del Gridview.

El método principal que debe implementarse es InstantiateIn(), dentro de este se definirá el o los control que conformen el témplate de columna para el estado especifico.

Algo que seguramente llamara la atención es el uso del evento DataBinding, el cual es usada para tomar los datos al momento de bindear cada fila de la grilla, este evento será ejecutado tantas veces como filas tenga.

Estas clases serán usadas para definir cada témplate de columna:

private void DefinirColumnasNotebook()
{
    //
    // Se define el campo dentro de la grilla, 
    // para poder identificar cada item 
    //
    GridView1.DataKeyNames = new string[] { "Id" };

    GridView1.Columns.Clear();

    //
    // Columna Descripcion
    //
    TemplateField tempDesc = new TemplateField();
    tempDesc.HeaderTemplate = new GridViewHeaderTemplate("Descripcion Producto");
    tempDesc.ItemTemplate = new GridViewItemTemplate("Descripcion");
    tempDesc.EditItemTemplate = new GridViewEditTemplate("Descripcion");
    GridView1.Columns.Add(tempDesc);

    //
    // Columna Precio
    //
    TemplateField tempPrecio = new TemplateField();
    tempPrecio.HeaderTemplate = new GridViewHeaderTemplate("Precio");
    tempPrecio.ItemTemplate = new GridViewItemTemplate("Precio");
    tempPrecio.EditItemTemplate = new GridViewEditTemplate("Precio");
    GridView1.Columns.Add(tempPrecio);
}

private void DefinirColumnasTelevisores()
{
    GridView1.DataKeyNames = new string[] { "Id" };

    GridView1.Columns.Clear();

    //
    // Columna Descripcion
    //
    TemplateField tempDesc = new TemplateField();
    tempDesc.HeaderTemplate = new GridViewHeaderTemplate("Descripcion Televidor");
    tempDesc.ItemTemplate = new GridViewItemTemplate("Descripcion");
    tempDesc.EditItemTemplate = new GridViewEditTemplate("Descripcion");
    GridView1.Columns.Add(tempDesc);

    //
    // Columna PrecioUnitario
    //
    TemplateField tempPrecio = new TemplateField();
    tempPrecio.HeaderTemplate = new GridViewHeaderTemplate("Precio Unitario");
    tempPrecio.ItemTemplate = new GridViewItemTemplate("PrecioUnitario");
    tempPrecio.EditItemTemplate = new GridViewEditTemplate("PrecioUnitario");
    GridView1.Columns.Add(tempPrecio);

    //
    // Columna EsPlasma
    //
    TemplateField tempEsPlasma = new TemplateField();
    tempEsPlasma.HeaderTemplate = new GridViewHeaderTemplate("Plasma");

    GridViewItemCheckTemplate esPlasmaItem = new GridViewItemCheckTemplate("EsPlasma");
    tempEsPlasma.ItemTemplate = esPlasmaItem;

    GridViewItemCheckTemplate esPlasmaEdit = new GridViewItemCheckTemplate("EsPlasma");
    esPlasmaEdit.CanEdit = true;
    tempEsPlasma.EditItemTemplate = esPlasmaEdit;

    GridView1.Columns.Add(tempEsPlasma);
}

 

Se define tanto el témplate del Ítem, como el de edición y encabezado, usando para ello el témplate que corresponda, se debe tener presente que también el tipo de dato a mostrar influye en la decisión de que témplate utilizar, un ejemplo muy claro lo representa el checkbox que marca si el televisor es de plasma o no, representado por un témplate que justamente dibuja un check en la celda.

También hay que comentar que no hay una forma única de crear las clases de témplate, estas podrían tomar la info mediante propiedades o pasarlas en el constructor. Un ejemplo de esto es la clase “GridViewItemCheckTemplate” la cual asigna el nombre del campo al cual vincula los datos, pero si debe permitir la edición o no, es asignada mediante una propiedad, tomando un valor por defecto en caso de no asignar valor.

La edición de un registro en la grilla implica todo un tema:

protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e)
{
    GridView1.EditIndex = e.NewEditIndex;
    DataBindGrid();
}

protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
    GridView1.EditIndex = -1;
    DataBindGrid();
}

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    int Id = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value);
    
    GridViewRow row = GridView1.Rows[e.RowIndex];

    if (Session["datos"] is List<Notebook>)
    {
        Notebook notebookActualizada = (from item in (List<Notebook>)Session["datos"]
                                         where item.Id == Id
                                         select item).FirstOrDefault();


        TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;
        notebookActualizada.Descripcion = Convert.ToString(txtDescripcion.Text);

        TextBox txtPrecio = row.FindControl("txtPrecio") as TextBox;
        notebookActualizada.Precio = Convert.ToInt32(txtPrecio.Text);

    }
    else if (Session["datos"] is List<Televisor>)
    {
        Televisor televisorActualizado = (from item in (List<Televisor>)Session["datos"]
                                            where item.Id == Id
                                            select item).FirstOrDefault();


        TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;
        televisorActualizado.Descripcion = Convert.ToString(txtDescripcion.Text);

        TextBox txtPrecio = row.FindControl("txtPrecioUnitario") as TextBox;
        televisorActualizado.PrecioUnitario = Convert.ToInt32(txtPrecio.Text);

        CheckBox chkEsPlasma = row.FindControl("chkEsPlasma") as CheckBox;
        televisorActualizado.EsPlasma = chkEsPlasma.Checked;
    }

    GridView1.EditIndex = -1;
    DataBindGrid();
}

Mediante los evento RowEditing y RowCancelingEdit, se controla que la fila este o no en estado de edición, esto indica a la grilla cuando debe cambiar el témplate de edición que se ha definido.

El evento RowUpdating actuara cuando se acepta la edición, es en este momento donde controla que tipo de lista se esta visualizando, como primer paso se localiza la entidad dentro de la colección que se había usado para bindear la grilla, para esta tarea se hizo uso de Linq.

Luego se toma la información de los controles que genero cada template, hay que remarcar en este punto que los template usan internamente el agregado de un prefijo con respecto al tipo de control que agregan, agregando este al nombre de la columna que se le asigno, por ejemplo:

tb.ID = string.Format("txt{0}", columnName);

Esta línea agrega el prefijo “txt” al nombre del campo, es por esto que luego al buscar el control se uso

TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;

En donde “Descripcion” es el nombre del campo, y “txt” el prefijo.

Al igual que se hizo con el BoundField, en este caso la definición de las columnas se realiza en el Page_Init

protected void Page_Init(object sender, EventArgs e)
{
    if (Session["datos"] == null || Session["datos"] is List<Notebook>)
    {
        DefinirColumnasNotebook();

    }
    else if (Session["datos"] is List<Televisor>)
    {
        DefinirColumnasTelevisores();
    }
}

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        Session["datos"] = DataAccess.ObtenerListaNotebook();
        DataBindGrid();
    }
}

private void DataBindGrid()
{
    GridView1.DataSource = Session["datos"];
    GridView1.DataBind();
}

Hay un método adicional que por ahí no este tan claro, DataBindGrid(), este método simplemente toma la info de session y bindea la grilla, este cache de información en session me pareció importante ya que por cada accion que se realiza la grilla debe ser bindeada a los datos, lo cual podría producir una sobrecarga de comunicación si en todo momento debe ir controla la db para buscar la información.

 

Ejemplos de Código


El ejemplo fue confeccionado con Visual Studio 2008.

 

[C#] 
[VB.NET]