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] |
Hola Leandro, exelentes tus post, y bueno muchas gracias por compartir tu tiempo y conocimientos, simpre son una guia para los que no dominamos muy bien el desarrollo con .NET.
ResponderEliminarSaludos desde Colombia.
hola
ResponderEliminarMe alegro que haya sido de utilidad.
saludos
Muy bueno Leandro, excelente, ya estoy esperando el proximo capitulo...
ResponderEliminarSaludos,
wow!!!! exelente blog amigo...
ResponderEliminarespero aprender mucho de esto soy un programador que apenas inicia.
gracias exelente trabajo... y gracias por la respuesta en MSDN
Leandro, al cargar tu proyecto en VB me salen varios warnings
ResponderEliminarhttp://i55.tinypic.com/2dl7yom.jpg
¿a que se puede deber?
Un saludo y gracias por tus explicaciones tan claras
hola Enrique
ResponderEliminarEsos warning que estas visualizando son debido a la herramienta de conversion que utilice.
Resulta que los ejemplos los armo en C#, pero los convierto con ayuda de SharpDevelop
a VB.NET
Es por eso que si validas la extension que menciona en el warning, dice .csproj, el cual es la extension que tiene los proyectos de C#, pero me llama la atencion que los marque cuando estas en VB.NET
Seguramente lo que sucede es que la conversion dejo en la solucion refrencia a estos archivos, pero no deberian afectarte en tu caso.
Si quieres removerlos seguramente debas editar el archivo de la solucion de vb.net y buscar la lineas donde declara el .csproj, eliminandola, pero para analizar el ejemplo no haria falta que lo realices, no deberia afectar el funcionamiento.
saludos
Oye amigo muchas gracias mustras muchos ejemplos en tu Blog k son de grn inters, ya revise este ejemplo y funciona a la perfeccion el detalle esk no se como hacer para poner una restriccion para k no puedan seleccionar mas de dos articulos.. en el campo cantidad...
ResponderEliminarGracias xD
Hola Leandro, antes que nada te felicito por el excelente material de tu blog y agradecerte por tu tiempo.
ResponderEliminarLuego del agradecimiento obligado, quisiera saber porque cuando confirmamos la compra queda sin responder el programa sin que se confirme el pedido y se renueve el formulario para un nuevo pedido. Desde ya muchas gracias.
P/D Baje la version para C# y sucede lo mismo con los ejemplos chinook3 y chinook3 objetos complejos.
hola Norimaki88
ResponderEliminarbueno por ahi es algo tarde para contestar pero esta restriccion imagino podrias ser doble, por un lado la agregarias en la presentacion quizas en el mismo boton de agregar cuando cuantas que hay mas dos rows en el grid puedes deshabilitar ese boton
y ademas seria bueno que en el capa de negocio tambien agregues la validacion, pero esta vez verificando que los items del detalle que llegan no superen los dos
Recuerda que las validaciones por defecto siempre deben ir en la capa de negocio, y si puedes la agregasa en al rpsentacion, lo ideal es que sean doble validacion negocio y presentacion pero a veces segun la restriccion no siempre se puede
saludos
hola HJF
ResponderEliminarSimplemente porque es un ejemplo y no previ este escenario, pero si es cierto luego de crear la factura deberia inicializarse la operacion para quedar preparada para una nueva transaccion.
O en caso controlario deberia estar preparada para la edicion de la factura recien creada.
saludos
Hola Leandro me fascina tu forma de explicar cada parte de tu codigo lo haces de manera facil sencilla entendible: muchisimas gracias por este tutorial me es de gran ayuda, espero pronto publiques tu proximo tutorial estoy ansiosa por leerlo. bye y gracias por compartir tu sabiduria en .net
ResponderEliminarBuenas Leandro, gracias por tus aportes.
ResponderEliminarEstoy intentando hacer una transacción donde tengo que utilizar métodos o funciones recursivas, que también tienen que buscar registros y luego guardar en la BD. Si alguno de estos falla no debe de confirmar ningún cambio en la BD. Ya intente realizar esto pero me saca el siguiente error: "Se completó la transacción distribuida. Dé de alta esta sesión en una nueva transacción o en la transacción NULL."
hola Santiago
ResponderEliminarestas usando TrasctionScope para administrar las transacciones?
si es asi has validado que que el Distributed Transaction Coordinator (MSDTC), este activo ?
solo estas creando un unico bloque using que defina al trasactionscope, no se abre ninguno por dentro de este, cosa que se aniden las trasnacciones ?
saludos
Hola Leandro
ResponderEliminarSi, solo estoy utilizando una TransactionScope, el MSDTC no está activo.
Si realizo TransactionScope anidadas estas dependeran de la que esté primero creada, la de mayor nivel?
Gracias por tu ayuda
Hola Leandro,
ResponderEliminarMUCHAS GRACIAS POR TU BLOG.
Mi pregunta/comentario: Las lineas no son necesarias no?
private void InicializarControles()
{
//
// Limpia las lineas del pedido anterior, pero deja preparada
// una nueva linea lista para seleccionar el item
//
List list = dgvLineaCompra.DataSource as List;
list.Clear();
..Ya que crea el objeto "list", lo limpia y nada mas...ademas es local a la funcion.
Muchas gracias.
Eusebio.
hola eestradaa
ResponderEliminaren realidad depende, basicamente esas linea se usan porque no puede limpiar las rows del grid directamente si este esta vinvulado a un origen de datos, deebs recuperar la la info del dataspource y trabajar cn esta
igualmente ahora que reviso el codigo donde tengo dudas si lo implemente correctamente es el metodo NuevaLinea() ese creo que habria que adaptarlo porque no quedo bien, o no recuerdo si habai encontrado alguna situacion por la cual lo implemente de esa frma
saludos
Hola es la primera vez que miro tu Blog y me parece genial y muy recomendable para todos los begginers en la programacion .NET, pero me podrías explicar ¿para que se usan las clases de Entidades, esas clases donde solo hay propiedades con get y set?
ResponderEliminar¡Gracias por la respuesta!
hola Alfonso
ResponderEliminaresas entidades lo que definen son el dominio de tu aplicacion, por lo general mepean uno a uno con las tablas de tu db, pero ojo no es la regla, porque si usarias un ORM como ser Entity Framework podrias aplicar conceptos de POO
basicamente a esa entidades de las conoce como POCO
saludos
hola felicitaciones por el tutorial, tengo una duda estoy haciendo una aplicacion en 3 capas en asp.net con vb, lo unico que hace la aplicacion es seleccionar y mostrar datos, pero cuando quiero actualizar(editar lo que me muestra) no me funciona me podrian pasar algunos ejemplos porfavor o donde los encuentro, parece que no me funciona cuando le paso los parametros de la capa de negocio alguien que pueda ayudarme, gracias por todo de antemano
ResponderEliminarhola joel
ResponderEliminarporque dices que la actualizacion no funciona ? se produce algun error o solo no actualiza
has puesto breakpoint en el codigo y sigues la operacion para determinar que linea podria tener el problema
recuerda ademas inspeccionar el valir devuelto por el ExecuteNonQuery() ya que este informa si hay registros afectados, o sea is haces
Dim cantafectado As Integer = cmd.ExecuteNonQuery()
si evaluas la variable deberia ser distinto a cero para asigurar que actualizo, si devuelve cero entonces el problema esta en el query UPDATE que defines
saludos
Hola Leandro muy buenos los tutoriales, pregunto porque es algo que no me doy cuenta cual sería la forma más eficiente de hacer el proceso de modificar una factura o pedido, ya que podría establecer que una vez facturado, hay que dar de baja la factura y reasignar las cantidades de los items a los respectivos stocks, pero con el pedido NO ME DOY CUENTA... Escucho tus sugerencias. Gracias
ResponderEliminarhola Carlos
ResponderEliminarque relacion tiene la factura con el pedido ? porque si estas facturando y algo se anula primero deberias operar la accion de anular esa facturacion he impactar esto en el stock y luego sigues facturando
en el stock simplemente realizas un UPDATE a al tabla de productos sumando la cantidad de la factura anulada a la cantidad existente en la tabla de productos.
pero el tema del pedido no quedo claro
saludos
Estimado Leandro serias tan hamable de subir nuevamente el codigo de los 3 ejemplos..
ResponderEliminarhola EnZzO
ResponderEliminarpero el codigo esta publicado al final de cada articulo para que puedas descargarlo
saludos
Hola Leandro, pero entonces hay quear una clase por cada accion en la base de datos?? CustomerDAL, InvoiceDAL, TrackDAL y cada uno tiene una conexion con la bd. Osea que en mi caso tendria que crear una clase para crear cliente otra para actualizar cliente, otra para agregar empresa, actualizar empresa y asi susecivamente?
ResponderEliminarOtra Cosa, no se puede crear una clase general que maneje la conexion de la bd para no hacer una conexion en cada clase???
No entendi que se hizo con Chinook.Entities
hola CRISTIAN
ResponderEliminarno es una regla, es mas veras que para las lineas de la factura no cree ningun DAL, sino que lo gestiono mediante el de la factura
no creas una clase para cada accion, sino que creas metodos que cree, actualice, etc sobre una entidad
no recomiendo crear nada general para el acceso a datos, es para problemas
saludos
Excelente pero los links estan rotos, por favor si lo pudieras verificar, seria de gran ayuda
ResponderEliminarhola Osmar
ResponderEliminarya estan actualizados los links
saludos
Hola Leandro! Si quisiera quitar un track de la compra, suponiendo que creo un boton "quitar" como me quedaria el codigo? Saludos
ResponderEliminarhola matias
ResponderEliminardeberia ser tan simple como tomar el id de la linea de la factura InvoiceLineId enviarla a la capa de negocio, o directo a la de persistencia, y ejecutar un DELETE sobre InvoceLine
imagino ejecutar un DELETE en sql sabes como realizarlo
saludos
Hola nuevamente! si, tengo cononocimiento de como hacerlo, de hecho estoy trabajando en un sistema en capas 100% orientado a objetos...para las ventas me base en este ejemplo...y me trabe un poco en lo de clonar las filas...mi duda es quitar una fila de la compra antes de grabar la misma, no creo que deba dirigirme a la capa de persistencia ni de negocios, mas bien quitar un elemento de la lista...espero me entiendas..saludos!
ResponderEliminarhola matias
ResponderEliminarsi es una operacion antes de persistir entonces es correcto lo quitas de la lista en memoria
sino quieres clonar entidades podrias ver de trabjar con el
BindingList
con este tipo de lista podrias operar enlanzado al grid
saludos