Introducción
Este articulo es el primero de una serie que tratara temas relacionados con el desarrollo en capas
La idea original es empezar desde el principio e ir paulatinamente agregando funcionalidad mientras se analiza las mejoras involucradas en el diseño
Es por eso que este primer articulo no mostrara nada en la separación en capas, sino que se desarrollara íntegramente en la presentación, todo el código estará en los eventos o método privados de los formularios
Esto será útil para poder comparar la evolución en la codificación empleada cuando no se tienen capas, y cuando se define una arquitectura que si la tiene.
Ejemplo de código
Como se comento al principio del articulo este ejemplo no reflejara una arquitectura en capas, sino que estará toda la lógica en los formularios.
Además se notara que en el ejemplo se hace uso de datatable dinámicos, o sea no se usan objetos tipados.
Sera al momento de armar una arquitectura con capas en donde esto se modificara para dar lugar a una correcta comunicación.
En la siguiente imagen se puede apreciar el diseño del formulario:
Este permitirá seleccionar un cliente, mediante el botón de Búsqueda, mostrando un dialogo con una grilla con el listado de cliente.
Al costado derecho de la grilla se encuentra el botón con un signo de “+”, este permitirá agregar un nueva ítem de compra.
En la parte inferior un botón de confirmar procesara el pedido, previa validación la información ingresada.
Carga de los datos de inicialización del formulario
Durante la carga del formulario de compra, se tiene lugar varias acciones.
La primer tiene que ver con una consulta a la Base de Datos en donde se recuperan el listado de “temas” que serán cargados en el combo del DataGridView que representa las líneas de compra.
Además por defecto se crea una línea de compra, usando el método NuevaLinea()
private void frmCompra_Load(object sender, EventArgs e) { // // Obtengo la lista de Tracks // DataTable dt = new DataTable(); using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString())) { conn.Open(); string sql = @"SELECT TrackId, Name FROM Track ORDER BY Name"; SqlCommand cmd = new SqlCommand(sql, conn); SqlDataAdapter da = new SqlDataAdapter(cmd); da.Fill(dt); } // // Cargo los items del combo // DataGridViewComboBoxColumn comboCol = dgvLineaCompra.Columns["Track"] as DataGridViewComboBoxColumn; comboCol.ValueMember = "TrackId"; comboCol.DisplayMember = "Name"; comboCol.DataSource = dt; // // // NuevaLinea(); }
/// <summary> /// Crea un item de compra en la grilla /// </summary> private void NuevaLinea() { DataTable dt = null; if (dgvLineaCompra.DataSource == null) { dt = new DataTable(); dt.Columns.Add("Track", typeof(int)); dt.Columns.Add("PrecioUnitario"); } else dt = dgvLineaCompra.DataSource as DataTable; DataRow row = dt.NewRow(); dt.Rows.Add(row); dgvLineaCompra.DataSource = dt; }
Este método valida si la grilla posee un origen da datos previamente asignado, en caso de contar con uno lo recupera y procede a agregar un ítem nuevo.
Sino hay datos presentes en la grilla, crea una nueva instancia de datos y procede de igual forma.
Al final concluye con la asignación del datatable a la grilla, para desplegar la información.
Obtener el precio desde un combo en el DataGridView
Para poder operar correctamente con el ComboBox contenido en una celda de la grilla, es necesario contar con mas de un evento.
La explicación del uso de estos eventos, esta detallada en este otro artículo:
[DataGridView] - Parte 6 - ComboBox y evento SelectedIndexChanged
Lo interesante es ver como se recuperar un valor especifico de la consulta haciendo uso del ExecuteScalar(), el cual devuelve contenido del primer registro y la primer columna de la consulta, en este caso por ser solo un valor cierra perfecto su uso.
DataGridViewComboBoxEditingControl dgvCombo; private void dgvLineaCompra_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) { dgvCombo = e.Control as DataGridViewComboBoxEditingControl; if (dgvCombo != null) { dgvCombo.SelectedIndexChanged += new EventHandler(dgvLineaCompra_SelectedIndexChanged); } } private void dgvLineaCompra_SelectedIndexChanged(object sender, EventArgs e) { ComboBox combo = sender as ComboBox; // // Se obtiene el precio // en base a la seleccion de combo en la celda de la grilla // decimal precio = 0; using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString())) { conn.Open(); string sql = @"SELECT UnitPrice FROM Track WHERE TrackId = @idTrack"; SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@idTrack", Convert.ToInt32(combo.SelectedValue)); precio = Convert.ToDecimal(cmd.ExecuteScalar()); } DataGridViewRow row = dgvLineaCompra.CurrentRow; row.Cells["PrecioUnitario"].Value = precio; } private void dgvLineaCompra_CellEndEdit(object sender, DataGridViewCellEventArgs e) { if (dgvCombo != null) dgvCombo.SelectedIndexChanged -= new EventHandler(dgvLineaCompra_SelectedIndexChanged); }
Validación del formulario
La validación de los campos del formulario es un punto importante, en el método Validaciones(), no solo se verifican los campos individuales, sino que también se itera por las líneas de la grilla verificando que las celdas estén completas.
Se puede remarcar que al usuario se le informa de los puntos detectados durante la validación mediante el uso del control ErrorProvider, desplegándose así un icono al costado de cada control o línea de la grilla donde se detecta el problema.
/// <summary> /// Valida que los controles contengan con informacion correcta /// antes de procesar la transaccion /// </summary> /// <returns></returns> private bool Validaciones() { bool result = true; // // inicializo los mensajes de validaciones que pudiera haber // errorProvider.Clear(); // // verifico los campos del cliente // if (string.IsNullOrEmpty(txtNombre.Text)) { errorProvider.SetError(txtNombre, "El nombre es obligatorio"); result = false; } if (string.IsNullOrEmpty(txtApellido.Text)) { errorProvider.SetError(txtApellido, "El apellido es obligatorio"); result = false; } if (string.IsNullOrEmpty(txtEmail.Text)) { errorProvider.SetError(txtEmail, "El Mail es obligatorio"); result = false; } // // valido las lineas de compra // foreach (DataGridViewRow row in dgvLineaCompra.Rows) { // // inicializo las linea de error // en caso de tener un mensaje de error previo // row.ErrorText = ""; // // se validan los campos de la fila // if (string.IsNullOrEmpty(Convert.ToString(row.Cells["Track"].Value))) { row.ErrorText = "Debe seleccionar in item de compra."; result = false; } if (string.IsNullOrEmpty(Convert.ToString(row.Cells["Cantidad"].Value))) { row.ErrorText = "Debe ingresar una cantidad"; result = false; } else { int cantidad = 0; if (!Int32.TryParse(Convert.ToString(row.Cells["Cantidad"].Value), out cantidad)) { row.ErrorText = "La cantidad ingresada debe ser un valor numerico"; result = false; } } } return result; }
Proceso de Confirmación de Compra
Este es el código mas extenso de todo el formulario, ya que allí se tomara la información y se procederá toda la transacción de compra realizando los INSERT o UPDATE que hagan falta para lleva a cabo la operación.
private void btnConfirmar_Click(object sender, EventArgs e) { if (!Validaciones()) return; // // inicializo la transacciones // using (TransactionScope scope = new TransactionScope()) { using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString())) { conn.Open(); #region Creo\Actualizo la informacion del cliente int idCliente = 0; // // si el cliente se ha seleccionado lo actualizo, sino se crea uno nuevo // if (cliente == null) { 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", txtNombre.Text); cmd.Parameters.AddWithValue("@lastName", txtApellido.Text); cmd.Parameters.AddWithValue("@company", txtDireccion.Text); cmd.Parameters.AddWithValue("@address", txtCompañia.Text); cmd.Parameters.AddWithValue("@email", txtEmail.Text); idCliente = Convert.ToInt32(cmd.ExecuteScalar()); } else { 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", txtNombre.Text); cmd.Parameters.AddWithValue("@lastName", txtApellido.Text); cmd.Parameters.AddWithValue("@company", txtDireccion.Text); cmd.Parameters.AddWithValue("@address", txtCompañia.Text); cmd.Parameters.AddWithValue("@email", txtEmail.Text); cmd.Parameters.AddWithValue("@customerid", Convert.ToInt32(cliente["CustomerId"])); cmd.ExecuteNonQuery(); idCliente = Convert.ToInt32(cliente["CustomerId"]); } #endregion #region Creo el Encabezado de la Factura int idEncabezadoFactura = 0; 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", idCliente); cmd.Parameters.AddWithValue("@date", DateTime.Now.Date); cmd.Parameters.AddWithValue("@address", txtDireccion.Text); cmd.Parameters.AddWithValue("@total", 0); idEncabezadoFactura = Convert.ToInt32(cmd.ExecuteScalar()); } #endregion #region Creo cada linea de la facturacion 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 (DataGridViewRow row in dgvLineaCompra.Rows) { // // como se reutiliza el mismo objeto SqlCommand es necesario limpiar los parametros // de la oepracion previa, sino estos se iran agregando en la coleccion, generando un fallo // cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@invoiceid", idEncabezadoFactura); cmd.Parameters.AddWithValue("@trackid", Convert.ToInt32(row.Cells["Track"].Value)); cmd.Parameters.AddWithValue("@unitprice", Convert.ToDecimal(row.Cells["PrecioUnitario"].Value)); cmd.Parameters.AddWithValue("@quantity", Convert.ToInt32(row.Cells["Cantidad"].Value)); // // Si bien obtenermos el id de linea de factura, este no es usado // en la aplicacion // int idLineaFactura = Convert.ToInt32(cmd.ExecuteScalar()); } } #endregion } scope.Complete(); } InicializarControles(); }
Los bloques de código principales incluyen:
- Creación o actualización de los datos del cliente
- Creación de la cabecera de la compra
- creación de las líneas de compra, asociadas a la cabecera.
En este punto sea interesante contar con la estructura de tablas con las cuales se trabajara:
Si se analiza el código se vera que los id de las tablas son auto numéricos, por lo tanto en la misma operación de INSERT se adiciona la operación que permite recuperar el id generado por la tabla, haciendo uso del SCOPE_IDENTITY()
Para mas información sobre el uso de los campos auto numéricos revisar este otro articulo:
[ADO.NET] – Parte 6 - Ejemplos simples – Campos Auto numéricos (Identity)
Algo a destacar es el uso de transacciones, las cuales son muy simples de implementar al contar con la clase TransactionScope, la cual engloba toda la transacción, y por medio del método Complete() se confirma la operación, cualquier error que se produjera anularía las operaciones realizadas dejando las tablas con datos consistentes.
Conclusiones
Si bien el ejemplo se puede considerar prolijo y ordenado en el código, esto es solo porque se trata de una aplicación pequeña que forma parte de un ejemplos, aplicar esta misma técnica en aplicaciones de varios formulario y operaciones que interactúan puede resultar verdaderamente complejas.
Las principales desventajas de esta forma de codificar serian:
- No fomentan la reutilización, si se analiza el código cada consulta o conexión a la base de datos se realiza en los eventos, si fuera necesario en algún otro punto del código obtener la misma información abría que replicar el código.
Este punto se podría solucionar si se crean métodos que encapsulen esta funcionalidad, pero al estas usando formulario estos estarían local al mismo, no podrías accederse desde otros puntos de la aplicación, es necesario contar con otro sitio que permita acceder sin generar acoplamiento.
- El procesamiento de las transacciones generan código muy extenso, este punto es claramente visible en el botón de confirmación de la compra, esta es una operación simple en comparación con las que podrían crearse, sin embargo creo una buena cantidad de líneas de código.
- No hay separación de las responsabilidades, en un solo punto se crean cliente, y se graban la facturación, esto debería poder separarse para dividir la complejidad y aumentar la mantenibilidad de la aplicación.
Tener código que hace muchas operaciones distintas no es bueno, mas que nada para su mantenimiento futuro.
Consideración para el código
Los ejemplos fue desarrollado usando VS 2008 y SQL Server 2008 Express.
Es necesario tener el servicio de Sql Express iniciado localmente para poder ejecutar la solución.
[C#] |
[VB.NET] |