lunes, 21 de junio de 2010

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

 

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:

imagen1

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:

imagen2

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] 

85 comentarios:

  1. Hola, Gracias segire estos post muy cerca ya que ahora mismo estoy en el desarrollo de una aplicacion de facturacion, solo dos cosas favor de incluir el script de la base de datos algunos no tenemos sql express, por ejemplo yo solo tengo el sql server, favor de comentar mas el codigo esto nos ayuda a entender mejor, gracias

    ResponderEliminar
  2. Hola Leandro, gracias por estos ejemplos, seguire de serca estos post sobre desarrollo en capas ya que me interesa mucho el tema y simplemente gracias por tomarte el tiempo y realizar este tipo de temas.

    ResponderEliminar
  3. Magnifico POST

    Te agradezco que lo publiques en C# y en VB.NET

    He tenido que reconectar la BD no se por que razon. Pensaba que me salia errorprovider porque algo estaba mal y resulta que te lo proporciona windows para control de errores, curioso.

    Voy a la segunda parte a ver con que me sorprendes

    ResponderEliminar
  4. Hola Leandro gracias por el post, me ha sido de mucha utilidad. Tengo una duda, hace algun tiempo tuve una conversacion con un profesor de desarrollo de software en la universidad, respecto a las conexiones a las BDs y donde definir los archivos de conexion a dichas BD. El aseguraba que la mejor practica era definir las propiedades de conexion en un archivo XML aparte. Yo personalmente considero que lo mejor es definir todo en el web.config. ¿Usted que considera?
    Muchas gracias

    Andres Mauricio

    ResponderEliminar
  5. hola Mauro

    Particularmente siempre dejo la cadena de conexion en el .config que se define en la aplicacion, es mas si lo ves bien el app.config es un xml.

    La verdad no me pondria a agregar toda la logica que se necesita en un xml extra solo por el connection string, ademas el .config ya tiene hasta una seccion para esto de nombre, si la idea es no ponerlo alli para que creo .net esa seccion.

    Storing and Retrieving Connection Strings

    Imagino que el planteo lo hizo para agregar algo de seguridad, al no estar alli mismo el xml y poder sacarlo a otro lugar se evitaria una visualizacion rapida y posible inseguridad, pero creo que si es por esto de ultima se podria encriptar una seccion del .config, o sino usar seguridad integrada de windows para autenticar al conexion contra la db.

    saludos

    ResponderEliminar
  6. Hola Leandro, gracias por el POST, como haces para incrustar el codigo en blogspot, me gusta como queda tabulado con la numeración, hay que utilizar alguna herramienta especial para que se maquete así en HTML.
    Gracias

    ResponderEliminar
  7. hola Fernando

    Lo que uso para publicar codigo es una tools que me permita poner syntax highlighting.

    Para escribir en los articulos uso Windows Live Writer, con este plugin

    Code Snippet With Syntaxhighlighter Support for Windows Live Writer

    Para que esto funcione debe modificar con algunas referencas el template del blog.

    saludos

    ResponderEliminar
  8. hola leandro
    es un exelente material
    solo que e tenido unos imcobenientes
    con algo que estoy realizando
    dengo un grid con las columnas
    codigo - cantidad - punitario - total

    estoy trabajando con texbox el grid
    megustaria saber com hacer la consulta de punitario cuando digite el codigo y muestre el punitario de ese producto, cuando ponga la cantida lo multiplique por punitario y muestre el resultado en total
    , y el boton para quitar articulos de la lista del grid

    ResponderEliminar
  9. Tengo un pequeño problema con una palabra que usas frecuentemente que es la de grilla, si me pudieras definir el significado ya que en mi pais tiene otro significado.

    Gracias!

    ResponderEliminar
  10. hola Jesus

    luego de algunas preguntas sobre el mismo tema estoy cambiando el termino grilla, por grid

    hago referencia a este cuando me refiero a controles como ser el DataGridView, o GridView

    la grilla es el grid, o sea un representacion de campos y registros que forman una tabla

    basicamente una cuadricula

    saludos

    ResponderEliminar
  11. Juan
    Hola, Leandro quisiera saber como haces para que las celda de Precio unitario no se pued editar en el diseño de Grid esta False y en ningua parte del codigo veo q le cambien a true,
    yo necesito aser algo parecido pero mi grido si deja editar todo. Saludos y que estes bien..

    ResponderEliminar
  12. hola Juan Manue

    cuando defines la columna asigna la propiedad ReadOnly en true

    esto hara que esa columna no se pueda editar

    saludos

    ResponderEliminar
  13. Amigo Leandro, perdona usar esta vía, pero no encontré como enviar una pregunta sobre un tema distinto (Leer Excel desde ASP VB), agradecería me indicaras por donde y como hacerlo. gracias de antemano.

    ResponderEliminar
  14. Hola Leandro una consulta respecto a C# en N capas entorno web en la datagridview al momento de realizar tus propios metodos se utiliza OnCommand="Editar_Command" y CommandName= <%# Container.DataItem("CODIGO")%> o como se utiliza dichos metodos con respecto al Diseño=?
    (
    Editar )

    ResponderEliminar
  15. Y en la parte programtiva se realiza de la siguiente forma o como se realiza=?
    [public

    void Editar_Command(object sender, CommandEventArgs e)

    {
    string str_modo = "E";
    string str_prueba = e.CommandName;
    Response.Redirect(
    "frmPrueba.aspx?modo=" + str_modo + "&prueba=" + str_prueba);}]

    ResponderEliminar
  16. hola Jenyna ramos

    pero tu desarrollo es web o winforms, porque mencionas el DataGridView, y este es para winforms

    en el CommandName colo debes poner el nombre del campo
    CommandName="CODIGO"

    despues puede hacer

    [ASP.NET] GridView – Edición Empleados

    saludos

    ResponderEliminar
  17. Hola, muchas gracias por los ejemplos
    me sirven de mucho.

    Tengo un problema, como podría actualizar algunas celdas(Ejemplo:precio,cantidad) del gridview al ingresar por caja de texto(TextBox).

    el proceso en resumen:Cargo los productos en un dropdownlist como también precio en un label, ingreso la cantidad en el textbox y lo muestro en el gridview al cliquear el boton agregar.

    agradezco de antemano su respuesta.

    ResponderEliminar
  18. hola henry.iq

    veo que se trata de un desarrollo web

    me pregunto y si tomas la info del combo y del textbox y simplemente haces un INSERT en la tabla ?

    por supuesto luego de insertar podrias recargar los registros del grid para visualizar el ultimo insert

    saludos

    ResponderEliminar
  19. Muy bueno el tutorial. me da un error en el uso de la transaccion. me podria explicar como hace referencia a ella.

    ResponderEliminar
  20. Hola leandro tengo un problemita me da un error porque no se como hacer referencia a la transaccion.

    ResponderEliminar
  21. hola Rosa

    cuals eria el mesnaje de error que recibes ? porque puede ser un problema con alguina query

    recuerda que las transacciones evitan que se ejecuten en forme las queries, sino que lo hacen al momento del complete

    deberias atrapar el exception y ver el mensajes del error

    saludos

    ResponderEliminar

  22. Este es el mensaje:

    Error 1 The type or namespace name 'Transactions' does not exist in the namespace 'System' (are you missing an assembly reference?)

    ResponderEliminar
  23. hola Rosa

    has agregado la referencia a System.Transaction ?

    porque recuerda que sino agregas esa referncia en el proyecto la clase de transaccion no puedes usarla

    saludos

    ResponderEliminar
  24. Lo hice agregando esa referencia al proyecto. pero me aparecen mas errores. lo revisare bien. gracias

    ResponderEliminar
  25. Me pregunto si puedo usar una base de dato local.Database1.sdf

    porque estoy haciendo una aplicacion, y esa es la que estoy usando.

    ResponderEliminar
  26. necesito buscar dato de una tabla en la base de dato a traves de un DataGridView.

    y no me funciona muy bien.

    ResponderEliminar
  27. hola Rosa

    pero el problema de la transaccion continua, porque el lo ultimo que comentas para realizar un busqueda no hace falta ninguna transacion

    puede usar un .sdf si la db sera local a la pc del usuario

    saludos

    ResponderEliminar
  28. Hola el error de la tranasacion me aparece en que esto haciendo pero con la otra base de dato. osea con la aplicacion sin capa, que estoy aprendiendo hacerla.

    y el uso de la base de dato local es otra aplicacion que estoy haciendo, que me pidieron en la universidad.

    ResponderEliminar
  29. Ok. entendi la usare. gracias por la sugerencias.

    ResponderEliminar
  30. Hola Leandro
    Tengo una inquietud.
    Como seria el codigo de c# para cuando elimine un ultimo registro de mi BD, y vuelva a presionar mi boton no provoque un error por falta de registros, sino dado el caso arrojar un mensaje que ya no hay registros en la base de datos.
    Gracias

    ResponderEliminar
  31. hola Alexander

    que operacion es la que falla cuando no hay registros en la db?

    porque en principio no veria que algo tenga que fallar ente esta operacion

    saludos

    ResponderEliminar
  32. hola leandro tengo una duda---

    quiero crear una funcion global en una clase .. esa funcion va a exportar un gridview a excel (es en asp.net(vb)...
    ---pongo esto
    '-----------------
    pagina.RenderControl(htw)
    response.Clear()
    response.Buffer = True
    response.ContentType =
    -----------------------------
    me marka error que dice (la variable se utiliza antes de asignar un valor

    ResponderEliminar
  33. hola germanRive

    esta clase donde la defines ? esta dentro del propio proyecto web

    ese response que se ve alli donde lo declaras ? lo pregunto porque quzias sea el

    HttpContext.Current.Response

    o sea vien de alli?

    saludos

    ResponderEliminar
  34. mira leandro... lo clase(archivo) esta en el mismo proyecto web..

    porque lo que puse anterirmente lo hace en la misma pagina donde tengo la grilla y funciona(y yo lo quiero hacer global o sea una funcion y no tener ke poner todo el codigo(19 lineas) en cada pagina que exporte a excel...

    ResponderEliminar
  35. gracias leaandro ya kedo mi funcion ..muchas gracias., y kedo de la siguiente manera (exporta gridview a excel)por si alguien le sirve y de manera simplificada ...

    Imports System.IO
    Public Class Class1

    Public Shared Function exporToExcel(ByVal Grilla As GridView) As Boolean

    Dim sb As StringBuilder = New StringBuilder()
    Dim sw As StringWriter = New StringWriter(sb)
    Dim htw As HtmlTextWriter = New HtmlTextWriter(sw)

    Grilla.RenderControl(htw)
    With HttpContext.Current.Response
    .Clear()
    .Buffer = True
    .ContentType = "application/vnd.ms-excel" 'vnd.ms-word'exporta a word
    .AddHeader("Content-Disposition", "attachment;filename=ArchivoExcel.xls")
    .Charset = "UTF-8"
    .ContentEncoding = Encoding.Default
    .Write(sb.ToString())
    .End()
    End With
    End Function

    End Class

    ResponderEliminar
  36. leandro otra vez aki preguntado....

    como saber o si hay alguna forma el equivalente a form closing en asp.net....
    lo que pasa que cargo unas imagenes temporales y quiero que alsalir de la pagina o en este casi ir a otra pagina estas imagenes se eliminen para que no se keden el servidor...alguna idea leandro??

    ResponderEliminar
  37. hola

    lo mas parecido es usar

    window.onbeforeunload

    desde javascript, con ese evento podrias controlar cuando se cierra el browser o se sale de una pagina

    saludos

    ResponderEliminar
  38. deja lo pruebo y te digo como me fue...gracias

    ResponderEliminar
  39. leandro hay alguna de forma de apuntar a una imgen en una base de datos.. ejemplo... botonimagen.imageurl="/direccion_foto" sin tener que grabarlo en disco....por ahy lei que si se puede.. pero no dice de ke manera...hay alguna forma leandro??

    ResponderEliminar
  40. hola

    podrias crear un handler

    [ASP.NET] - Guardar Imagen base de datos

    como veras poner el array de byte directo en el Response, sin necesidad de un archivo

    saludos

    ResponderEliminar
  41. ya quedo leandro muchas gracias.. lo probe de las dos maneras y me resulto util...gracias

    ResponderEliminar
  42. hola lenadro tengo el sigueinte problema al imprimir en un aimpresora termica CUSTOM TG02.. al imprimir : CrearLinea(e.Graphics, 9, "FOLIO:", x, y, ancho - 34, 6, StringAlignment.Far) .. me sale el texto reciortado si le dejo mas de 1 espacio entre linea y linea... que podra ser (uso vb.net)

    ResponderEliminar
  43. hola german

    no se que relacion tiene esta pregunta con desarrollo en capas, pero se me ocurre que seria mejor y mas controlado si creras un reporte para imprimir informacion

    con un repote podrias controlar mejor temas de fuentes, espacios y ubicacion del contenido

    saludos

    ResponderEliminar
  44. hola Leandro.
    una consulta e desarrollado una sistema con una tabla que guarda imagenes de diseños creados, todo bien pero solo me permitio guardar hasta 20 diseños y me da error: el grupo primary de la tabla esta lleno estoy usando sql server 2008 y v.net 2005 te mandare el codigo para guardarlo y el error que muestra

    ResponderEliminar
  45. hola Eder

    este es el mensaje del error que obtienes: "el grupo primary de la tabla esta lleno"

    la verdad es la primera vez que veo un mensaje como este, aunque revisando encontre

    http://social.msdn.microsoft.com/Forums/es/netfxwebes/thread/a3f78879-2ecb-4445-93b0-467c3df6b429

    sino pregunta en el foro de sql server (del mismo link que proporciono) para ver si saben a que se debe este mensaje en sql server


    slaudos

    ResponderEliminar
  46. Hola Leandro la verdad agradezco tus aportes son de gran ayuda para iniciar con el aprendizaje. Esta vez me gustaría saber si puedes instruirnos con un tuto que trate de como trabajar con funciones almacenas en otra base de datos como Oracle o MySql y que uno desde C# le pase los paramentos y luego reciba los resultados. No se si me explique bien. Un Saludo des Rep Dom.

    ResponderEliminar
  47. hola Chocobomix

    pero suar mysql u Oracle es muy parecido a sql server porque ado.net que es la base es identico

    solo cambian los objetos y como defines los parametros

    por ejemplo en lugar de usar SqlConnection, usarias MySqlConnection u OracleConnection

    http://social.msdn.microsoft.com/Forums/en-US/netfxwebes/thread/48867ed7-bad2-4c14-835f-3366d0ac2395

    para le parametro en lugar de usar @, seria ? para mysql y : para Oracle
    pero el resto es igual

    saludos

    ResponderEliminar
  48. Hola leandro una pregunta para tener claro el concepto respecto de ntier con base datos chinook, si yo quisiera hacer un calculo algo complejo como descuentos para ciertos productos, o por cierto monto de compra, estos cálculos yo los debería poner en la capa de negocios o business layer?

    ResponderEliminar
  49. hola Unknown

    exacto, asi es

    los calculos o todo lo que tenga que ver con logica que pueda o no trabajar con una determinada entidad del dominio se realiza en el negocio

    ahora bien, recuerda que si este es un simple calculo para mostrar al usuario un dato informativo como se la suma de los montos de compra, esto se pdoria realizar en la presentacion

    pero el calculo de iva de este se realiza en la capa de negocio, porque calcular el iva requiere de un proceso que puede ahcer variar los porcentajes segun el negocio

    saludos

    ResponderEliminar
  50. Hola leandro, teniendo en cuenta la solucion conlabase de datos chinook, y quisiera exponer un proyecto de esta solucion como WebService o quizas como WCF, cual seria?

    ResponderEliminar
  51. hola Unknown

    no se si entendi el planteo, pero si quieres exponer algo como servicio seria la comunicacion de la capa de UI con la de negocio la que debe desacoplarse

    o sea el WCF lo pondrias en medio de estas dos capas, cncretamente lo que haces es poner una capa de servicio remoto que se comunica con el negocio

    saludos

    ResponderEliminar
  52. yendo a un ejemplo concreto, en el proyecto chinook existe este archivo
    CustomerBO.cs qeu es usado por el UI
    con el codigo mostrado abajo, yo pregunto:
    Algo de este archivo deberia convertir a WCF?
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Chinook.Entities;
    using Chinook.DataAccess;

    namespace Chinook.BusinessLayer
    {
    public static class CustomerBO
    {
    public static List GetAll()
    {
    return CustomerDAL.GetAll();
    }

    public static CustomerEntity GetById(int id)
    {
    return CustomerDAL.GetById(id);
    }

    public static CustomerEntity Save(CustomerEntity customer)
    {

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

    }
    }
    }

    ResponderEliminar
  53. hola Unknown

    en ese archivo en concreto no tocas nada, solo pones en medio una capa de servicio, o sea ya no sera la UI quien acceda a esa clase BO sino que sera WCF y la UI referecniara los servicio expuestos

    por supuesto algo que se debera evaluar es como defines los DTO (data transfer object) o sea las clases DataContract, podrian ser las mismas entities o podrian ser clases separadas que conviertes de una a otra usando AutoMapper

    saludos

    ResponderEliminar
  54. Gracias por el codigo, me sirvio mucho. Saludos

    ResponderEliminar
  55. Hola Leandro otra vez molestandote jeje, favor de indicarme por que cuando trato de usar un SqlConnection estatico que debe ser usado con la sentencia using() la cadena se limpia luego de usarla una vez

    Me explico mejor: tengo un combobox en el cual listo los clientes(todo ok), seguidamente con el item seleccionado llamo un procedimiento que me listara sus facturas en otro combobox pero al llegar a la capa de datos en este segundo procedimiento me indica que la connectionstring no ha sido inicializada!!

    Gracias por la atencion y saludos!!

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    using System.Data;
    using System.Configuration;
    using System.Data.SqlClient;
    using Entidad;

    namespace Datos
    {
    public class clsdatos
    {
    static SqlConnection cn = new SqlConnection(
    ConfigurationManager.ConnectionStrings["conex"].ConnectionString.ToString());

    public static DataTable d_trab2_listar_clientes()
    {
    using (cn)
    {
    using (SqlDataAdapter da = new SqlDataAdapter(
    "trab2_listar_clientes", cn))
    {
    DataTable dt = new DataTable();
    da.Fill(dt);
    return dt;
    }
    }
    }

    public static DataTable d_trab2_listar_facturas(clsentidad objEN)
    {
    using (cn)
    {
    using (SqlDataAdapter da = new SqlDataAdapter(
    "trab2_listar_facturas " + objEN.codcli, cn))
    {
    DataTable dt = new DataTable();
    da.Fill(dt);
    return dt;
    }
    }
    }
    }
    }

    ResponderEliminar
  56. hola Franky

    es que nunca deberias definir el objeto de conexion como static, eso es incorrecto

    la cadena de conexion se define cuando las vas a utilizar dentro del bloque using, pero NUNCA como static

    si has analizado el ejemplo del articulo veras que no lo implemento de ea forma

    ademas crear una clase de Datos, no lo veo una buena idea, defina una capa de datos donde te centras en las entidades del dominio y no en encapsular la logia de los componentes de ado.net

    saludos

    ResponderEliminar
  57. Hola Leandro, a continuacion detallo mi estructura:

    1.Capa Datos
    1.1 Acceso a datos ADO.NET
    2.Capa Negocio
    2.1 Logica y algunas validaciones
    3.Capa Entidad
    3.1 Propiedades de bd
    4.Capa Presentacion
    4.1 Interfaz

    Me gustaria saber si debo modificar esa estructura porque e visto que en algunos casos en una clase encapsulas tanto las propiedades como los metodos de un objeto(tabla).

    Lo de la cadena estatica me lo habian dicho por un tema de no estar a cada momento instanciandola, pero de ahora en adelante seguire tus indicaciones pero me agradaria tambien saber cuales son las razones por las cual no debe ser estatica!!

    Gracias

    Saludos

    ResponderEliminar
  58. Ademas e visto que en web algunos no usan mucho la capa de datos, la remplazan con el webservice?? Es una mala practica o no??

    ResponderEliminar
  59. hola Franky

    cuando en la entidad de dominio defienen las propiedades y tambien comportamiento con metodos es que programan usando DDD (Domain driven design)
    si bien es una buena tecnica puede no ser simple de implementar, ya que debes lograr no acoplar la logica de negocio con al entidad usando para esto interfaces y ademas framework de IoC (inversion of control)
    sino usas estos no apliques DDD

    las cadenas de conexion no deberian ser estaticas porque se supone que cuando llevas la aplciacion de un entorno a otro ls info de conexion al servidor cambia y la idea es poder aplciar se cambios sin tener que recompilar

    no tiene nada que ver el uso servicio con reemplazar la cada de datos, quizas lo que viste es WCF Data Services, o quizas algo de RIA Services, WebAPI, al algo como eso
    si bien alli se implementan servicios estos no reemplazas la capa de datos, sino que la hacen remota
    quizas hayas visto que desaparecio porque en estos se usa Entity Framework
    mala practica no es, yo he implementado apliaciones que definen una capa remota, pero era la capa de negocio no la de datos la que implementa servicio

    saludos

    ResponderEliminar
  60. Hola tuttini, baje tu ejemplo donde no tienes nada en capas, y usas un using transactions; ese no me sale, me sale error, osea aparece en rojo subrayado.

    ResponderEliminar
  61. hola CRISTIAN

    cual es el mensaje de error que recibes?

    es al compilar o al ejecutar?

    saludos

    ResponderEliminar
  62. Hola tuttini, gracias por responder. osea ese using me aparece subrayado en rojo, entonces no lo puedo usar.

    Igual me pasa en esta linea "using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()"

    Justo donde dice"ConfigurationManager.ConnectionStrings" no me reconoce ese codigo es como si no existiera o si estuviera mal. pero en tu codigo descargado si funciona y esta igual. no entiendo que pasa.

    ResponderEliminar
  63. hola CRISTIAN

    validaste que el proyecto tenga al referencia a System.Configuration ?

    no solo la referencia tambien tenga definido la linea

    using System.Configuration;

    porque es alli donde esta definida la clase ConfigurationManager

    saludos

    ResponderEliminar
  64. Hola tuttini, es que justo hay esta la falla,escribo using system.configuration y lo subraya en rojo. Como si esa referencia no existiera en visual studio.

    ResponderEliminar
  65. Hola tuttini ya he visto como se agrega, no existe esa libreria asi que toca buscarla, ya la tengo pero lo raro es que de igualmodo no me reconoce le comando configurationmanager en C# que puede ser?

    ResponderEliminar
  66. Buen dia tuttini, tengo una consulta, cuando agrego esto "dgvLineaCompra_SelectedIndexChanged)"

    Aparece como si no existiera, sale subrayado en rojo. sabes que daltaria?

    ResponderEliminar
  67. hola CRISTIAN

    eso seria la declaracion de un evento del DataGridView, pero ese evento no existe en el grid, salvo que en una de las columnas las definas como un combobox

    la verdad no sabria decirte porque lo marca, pero claramente ese evento en el DataGridView no existe

    DataGridView Events

    saludos

    ResponderEliminar
  68. Hola tuttini, pero se supone que en las líneas de arriba se agrego, según entiendo no se hace nada manual para el combo en el datagridview, si no en el código de arriba es que se creo, lo tengo igual pero me sale subrayado en rojo esa línea. me dice que ese nombre no existe en el contexto actual

    ResponderEliminar
  69. Hola tuttini, revise el archivo que das a descargar y vi que habían partes de código que no habías explicado, entonces solucione el error. ya termine todo el tutorial y me quedo un solo error, te agradezco si me colaboras a solucionarlo para poder continuar con la segunda parte.

    "cliente", dice que el nombre 'cliente' no existe en el contexto actual.

    ese lo usamos en private void btnBuscarCliente_Click(object sender, EventArgs e)

    y la verdad no se de donde salio. es el único error que tengo y podría proseguir.

    De antemano Gracias

    ResponderEliminar
  70. hola

    si revisas en el codigo buscando la variable veras que la variable esta definida a nivel del formulario
    en la linea

    DataRow cliente = null;


    saludos

    ResponderEliminar
  71. Hola Leandro, es recomendable usar la clase SqlHelper.

    ResponderEliminar
  72. hola Cleider

    no se si entendi el planteo de la pregunta

    cual seria la responsabilidad de este helper ?

    saludos

    ResponderEliminar
  73. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  74. buenos dias Leandro,

    me han servido de mucho tus ejemplos, que buen trabajo haces.

    Estoy desarrollando una aplicacion en vb.net 2010, tengo un formulario de búsqueda que es el mismo que utilizo en todas los maestros, cuando se abre la pantalla hago una búsqueda y las presento en un datagridview. Hasta ahí no tengo problemas, pero si en el datagridview escojo cualquier fila que no sea la primera, al dar doble click la presento en el formulario de donde abrí la busqueda, con las cajas de texto no tengo problema al presentar los datos con el registro seleccionado, pero cuando quiero presentar los datos en un combobox que esta en el formulario desde donde hago la búsqueda me presenta los datos del primer registro del datagridview (en realidad lo toma desde el dataset), como puedo solucionar esto?

    a las cajas de texto le pongo: txtbox = .Rows(.CurrentCellAddress.Y).Cells("Codigo").Value.

    en el combo tengo:
    With frm1.combo
    .DisplayMember = "nombre"
    .ValueMember = "codigo"
    .DataSource = dataset
    End With

    Agradezco tu respuesta,

    ResponderEliminar
  75. hola roleve

    porque veo alli que usas With frm1 ? no es que el grid esta en el mismo forma qe los controles
    porque si es asi deberias usar Me y no frm1

    para el combo seleccione un valor deberias asignar el codigo que tomarias del grid, pero no se si en elguna columna muestras ese id o solo estas mostrando la descripcion

    quizas la entidad en lugar de tomarla del grid deberias realizar una query nueva para tomar todos los datos de la entidad seleccionada en la row del grid

    saludos

    ResponderEliminar
  76. Muy buen blogg pero tengo una duda como seria el ejemplo anterior si en vez de un combobox fuera un textbox con datos que el usuario agrega como ejemplo el código de barras de un producto y en la siguiente celda aparezca la descripción.

    ResponderEliminar
  77. hola Unknown

    Algo no entendi, dende estarina los checkbox? porque en el ejemplo del articulo no hay ninguno

    En este se van completando items en el grid y luego se persiste

    Ya sea que vayas agreando de forma manual o por medio de un codigo de barra al final obtienes una lista que debes enviar a la capa de negocio

    saludos

    ResponderEliminar
  78. Como así?

    no mira lo que yo quiero hacer es que tengo un DataGridView con las siguientes columnas código descripción Cantidad y precio unitario total lo que quiero es que el usuario en la columna código lo inserte de manera manual ejemplo 01 entonces que automáticamente se llene el campo de descripción ejemplo arroz igual que el de precio unitario y la cantidad también la ingresaría el usuario y el total la multiplicación del precio unitario por la cantidad.

    Gracias por tu ayuda es que soy novato en esto.

    ResponderEliminar
  79. Hola Leandro, estoy haciendo un programa en 3 capas y quisiera volcar en un combobox las provincias (previamente insertadas en SQLServer 2014)...Tengo una tabla PACIENTES donde uno de sus campos es el cod_prov que esta relacionada con la tabla PROVINCIAS... Cuando yo quiera insertar un paciente y especificarle la provincia de donde es quiero que estas se muestren en un combobox, pero no se como hacerlo! Gracias!

    ResponderEliminar
  80. Alejandro, que es mas eficiente , utilizar Listas o datatables?

    Muchas Gracias antemano.

    ResponderEliminar
  81. buenas leandro me gusta la forma que hicieron, solo una pregunta si quiero guardar los detalles de la factura en un datatable o lista para luego acceder e ir guardando, el problema seria que como es con transaccion meter el maestro y los varios detalles de la factura en un procedure con transaccion lo de la transacion esta lista solo es como hacer un procedimiento que guarde el maestro y todos los detalles factura gracias

    ResponderEliminar