Introducción
Este articulo es la continuación de su predecesor:
[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 1
Donde se había planteado el hecho de confeccionar todo el código dentro del formularios, sin capas, remarcando las desventajas que esto trae a la reutilización y el mantenimiento de la aplicación.
En cambio en esta segunda parte se planteara la aplicación agregando una capa adicional, la de datos.
Estructura del proyecto
En este nueva arquitectura se incluirán dos nuevos proyectos, ambos del tipo “Class Library”, estos compilaran como dll, para ser referenciadas por el proyecto de Presentación. En toda la solución solo habrá un proyecto que genere un ejecutable.
En la siguiente imagen se grafica la relaciones entre los proyectos, las flechas marcan como se han realizado las referencia entre ellos.
El proyecto de entidades es un caso especial que debe analizarse, en esta oportunidad se ha utilizado DataSet Tipados para crear las clases que representan al negocio.
Se ha creado un único DataSet con varios DataSet tipados, uno por cada entidad.
Se podría haber utilizado clases especiales, pero esto se hará en la próxima iteración cuando se separen verdaderamente en 3 capas, ya que el uso de clase requiere cambiar la lógica en como se carga la información, o sea no se podrá usar mas el DataAdapters, sino que se deberá pasar al DataReader, los cuales son óptimos para la carga secuencial de la información.
La capa de Datos esta formada por una clases estáticas, una por cada entidad que conforma el negocio, se definieron como estática para facilitar el acceso a los métodos, que claramente solo procesan de los datos de la instancias con las cuales trabaja.
En esta capa hay algo interesante para remarcar, como las líneas de facturación de por si solas no existen sin una factura, las líneas no tienen funcionalidad especifica en la cada de datos, o sea no se persisten por si solas, sino que lo hacen cuando se crea una factura. Esto es interesante ya que remarca fuertemente el modelo del negocio, impidiendo que una línea quede disociada del encabezado de factura, que le da sentido de existencia.
Las líneas de facturación son persistidas en la misma operación en que se crea la factura, es por eso que en la clase InvoiceDAL, solo esta el método Create(), cuyo objetico es añadir tanto la factura, como sus líneas.
Este tipo de razonamiento tiene mucho que ver con la notación UML, en donde existe una relación denominada de agregación.
La relación entre las facturas y sus líneas es del tipo Agregación por Valor, o también llamada Composición, en donde el tiempo de vida de las líneas esta condicionado al tiempo en que dure la factura, si esta ultima se elimina las líneas también serán borradas.
Creación de un nuevo ítem en la grilla
En el artículo previo se había utilizado uso de un datatable sin tipo para crear dinámicamente una nueva línea de facturación, en cambio esto se ha cambiado adaptando al uso de una clase.
/// <summary> /// Crea un item de compra en la grilla /// </summary> private void NuevaLinea() { List<DGVLine> newlist = new List<DGVLine>(); if (dgvLineaCompra.DataSource != null) { List<DGVLine> list = (dgvLineaCompra.DataSource as List<DGVLine>); list.ForEach((item) => { newlist.Add((DGVLine)item.Clone()); }); } newlist.Add(new DGVLine()); dgvLineaCompra.AutoGenerateColumns = false; dgvLineaCompra.DataSource = newlist; }
public class DGVLine : ICloneable { public int? Track { get; set; } public decimal PrecioUnitario { get; set; } public int Cantidad { get; set; } #region ICloneable Members public object Clone() { DGVLine item = new DGVLine(); item.Track = this.Track; item.PrecioUnitario = this.PrecioUnitario; item.Cantidad = this.Cantidad; return item; } #endregion }
Un detalle importante que implico el cambio del datatable por la clase es la necesidad de clonar los ítems previos que tenia el DataGridView, de no hacerlo el control no detecta las nueva instancia y por lo tanto no se visualizan las nuevas filas agregadas a la grilla.
Por esta razón se implementa de la interfaz ICloneable. El ForEach() aplicado a cada ítem previo de la grilla es pasado a una nueva lista de ítems que al final es asignada como origen de datos nuevamente.
El método ForEach() esta usado una expresión lambda para recorrer cada ítem y facilitar la transformación.
Carga de la datos en la Presentación
El uso de la capa de acceso a datos permite que la presentación quede muy prolija, con una línea se resuelva la toma de los datos necesarios para desplegar la información al usuario.
Un ejemplo claro de esta interactividad es presente en la carga de los ítem del combo de la grilla:
private void frmCompra_Load(object sender, EventArgs e) { // // Cargo los items del combo // DataGridViewComboBoxColumn comboCol = dgvLineaCompra.Columns["Track"] as DataGridViewComboBoxColumn; comboCol.ValueMember = "TrackId"; comboCol.DisplayMember = "Name"; comboCol.DataSource = TrackDAL.GetAll(); // // // NuevaLinea(); }
Es rápidamente visible la mejora que implica usar capas, anteriormente en este misma acción se tenia una buena cantidad de código, ahora solo es una llamada a un método GetAll(), de la clase TrackDAL, el cual devolverá un DataTable especializado del tipo TrackDataTable.
Otro punto importante que recibió una mejora notable es la búsqueda del cliente, tanto al momento de cargar la grilla en el formulario frmBuscarCliente:
private void frmBuscarCliente_Load(object sender, EventArgs e) { // // Se carga la lista de clientes // dgvClientes.AutoGenerateColumns = false; dgvClientes.DataSource = CustomerDAL.GetAll(); }
Como así también al momento del retorno de la información, al cargar por id la información completa de ese cliente en especial:
private void btnBuscarCliente_Click(object sender, EventArgs e) { frmBuscarCliente frm = new frmBuscarCliente(); if (frm.ShowDialog() == DialogResult.OK) { // // Mantengo la entidad cliente seleccionada global al formulario // cliente = CustomerDAL.GetById(frm.IdCliente); // // muestro en pantalla la info del cliente // txtId.Text = Convert.ToString(cliente.CustomerId); txtNombre.Text = cliente.FirstName; txtApellido.Text = cliente.LastName; txtDireccion.Text = cliente.Address; txtCompañia.Text = cliente.Company; txtEmail.Text = cliente.Email; } }
El uso de entidades devueltas por la Capa de Datos agrega prolijidad y facilidad en el acceso a los datos, por ejemplo, la funcionalidad de CustomerDAL.GetById() devuelve una objeto de tipo CustomerRow, o sea un solo registro del CustomerDataTable, lo cual permite acceder directo a los campos de la fila, asignando los valores a los controles del formulario, no hace falta recorrer registros, o controlar si hay datos en un datatable, la capa de datos devuelve directamente la entidad filtrando por el Id.
Proceso de Facturación
Del proceso de Facturación se destaca la creación de las entidades del encabezado y de las líneas dentro del evento de la presentación. Como se habrá observado, en el código la presentación ha perdido todas las consultas sql que anteriormente se ejecutaban, pasando a formar parte de la capa de datos como especialista en procesar la información.
La comunicación entre las capas de Presentación y DataAccess se realiza por medio de entidades definidas en el proyecto que lleva el mismo nombre.
En el proceso de facturación dos entidades son creadas y pasadas por parámetros a la capa de datos para ser procesadas.
private void btnConfirmar_Click(object sender, EventArgs e) { if (!Validaciones()) return; // // inicializo la transacciones // using (TransactionScope scope = new TransactionScope()) { #region Creo\Actualizo la informacion del cliente // // si el cliente se ha seleccionado lo actualizo, sino se crea uno nuevo // if (cliente == null) { cliente = (new ChinookEntity.CustomerDataTable()).NewCustomerRow(); cliente.FirstName = txtNombre.Text; cliente.LastName = txtApellido.Text; cliente.Company = txtDireccion.Text; cliente.Address = txtCompañia.Text; cliente.Email = txtEmail.Text; cliente = CustomerDAL.Create(cliente); } else { cliente.FirstName = txtNombre.Text; cliente.LastName = txtApellido.Text; cliente.Company = txtCompañia.Text; cliente.Address = txtDireccion.Text; cliente.Email = txtEmail.Text; cliente = CustomerDAL.Update(cliente); } #endregion #region Creo el Encabezado\Linea de la Factura ChinookEntity.InvoiceRow invoice = (new ChinookEntity.InvoiceDataTable()).NewInvoiceRow(); invoice.CustomerId = cliente.CustomerId; invoice.InvoiceDate = DateTime.Now.Date; invoice.BillingAddress = txtDireccion.Text; ChinookEntity.InvoiceLineDataTable invoiceLine = new ChinookEntity.InvoiceLineDataTable(); foreach (DataGridViewRow row in dgvLineaCompra.Rows) { ChinookEntity.InvoiceLineRow invoiceLineRow = invoiceLine.NewInvoiceLineRow(); invoiceLineRow.TrackId = Convert.ToInt32(row.Cells["Track"].Value); invoiceLineRow.UnitPrice = Convert.ToDecimal(row.Cells["PrecioUnitario"].Value); invoiceLineRow.Quantity = Convert.ToInt32(row.Cells["Cantidad"].Value); invoiceLine.AddInvoiceLineRow(invoiceLineRow); } InvoiceDAL.Create(invoice, invoiceLine); #endregion scope.Complete(); } InicializarControles(); }
public static class InvoiceDAL { public static void Create(ChinookEntity.InvoiceRow invoice, ChinookEntity.InvoiceLineDataTable invoiceLine) { 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()); } // // Creacion de cada Liena de Factura // decimal total = 0; 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 (ChinookEntity.InvoiceLineRow row in invoiceLine.Rows) { // // 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", row.TrackId ); cmd.Parameters.AddWithValue("@unitprice", row.UnitPrice); cmd.Parameters.AddWithValue("@quantity", row.Quantity); // // Si bien obtenermos el id de linea de factura, este no es usado // en la aplicacion // row.InvoiceLineId = Convert.ToInt32(cmd.ExecuteScalar()); total += row.UnitPrice * row.Quantity; } } // // Actualizacion del Total de la Factura // 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", invoice.InvoiceId); cmd.ExecuteNonQuery(); } } } }
La creación de la factura implica unos cuantos pasos, pero al contar con entidades el procesamiento es mas simple.
Dentro del método Create() ahora solo es necesario iterar por las rows definidas en el datatable InvoiceLineDataTable, cuya información proviene de la presentación.
Ventajas y Desventajas de este nueva implementación
Si bien es cierto que esta nueva implementación requiere codificar un poco mas, ya que es necesario definir clases y proyectos nuevo, también es cierto que estos mejora la reutilización del código, a partir de ahora cualquier otro formulario que necesite, por ejemplo, listar los clientes no será necesario copiar y pegar el fragmento de código usado para conectarse a la base de datos y recuperar los registro, solo se agrega una línea invocando al método GetAll() de la clase CustomerDAL y eso será todo.
Esto implica una gran ventaja que centraliza la funcionalidad, separando todo lo que tenga que ver con la persistencia de los datos, es importante notar que a los métodos de la capa de datos en ningún momento se le paso como parámetro un control de presentación, o sea no se paso una referencia del DataGridView para que tomara las líneas de la factura, sino que se desacopla las capas usando las entidades.
Es por esta razón que en la presentación donde se recorre los ítems del DataGridView y se arman filas del datatable InvoiceLineDataTable, el cual es usado para reflejar las línea de facturación. Las entidades son en todo momento el vinculo de transporte de información entre las capas, y esta regla no se puede romper si es que se quiere mantener las capas desacopladas.
Un punto que aun queda como detalle esta relacionado al manejo de transacciones, el cual aun es controlado por la presentación, pero esto no es correcto, aunque solucionarlo requiere de algún intermediario entre estos componentes que permitan interceder en la transacción, es aquí donde entra en juego la capa de negocio, que se implementara en la próxima iteración.
Ejemplo de código
El mismo fue desarrollado con Visual Studio 2008, y Sql Server 2008 Express.
Durante el desarrollo del articulo no se ha insertado todo el código de los ejemplos, ya que resultaría demasiado extenso, la idea es que puedan descargar el código, ejecutarlo y analizarlo detenidamente, viendo como interactúan y comunican las distintas capas.
[C#] |
[VB.NET] |