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]