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] 

domingo, 20 de junio de 2010

ComboBox - DropDownList – Opción “Todos”

 

Introducción

En algunas situaciones se requiere agregar un ítem especial a los controles, como ser en controles del tipo combo, para que el usuario tenga la posibilidad de indicar que no hay un ítem escogido de la lista.

Esto se usa especialmente en circunstancias en donde el campo que representa el combo es opcional, o se quiere inicializar en un ítem que obligue al usuario a elegir un valor de la lista.

Es por esto que según cual sea la funcionalidad del combo, si actuara como filtro, o como lista con obligación de selección del usuario, es que tendrás distintas leyendas.

Si es un filtro podrías decir: “<Todos los items>”, o sea que no se aplicaría el ítem como filtro, o si el control actual sin una selección por defecto podrías decir: “<Seleccione un ítem de la lista>”, por supuesto este ultimo caso se validaría haya elegido un ítem.

En el articulo se intentara explicar las forma en que es posible añadir un ítem extra a los controles ya sea WinForms o Web.

 

WinForms ComboBox

El combobox de una aplicación estilo “Windows Application” puede recibir como origen de datos diferentes fuentes, podrías ser una clase especifica creada por uno, o podrías ser un dataset. Según cual sea el origen de datos cambia la solución que debe aplicarse para agregar este ítem extra opcional de la lista, ya que será el origen de datos donde se agrega la opción especial.

Agregar opción usando clases creadas por uno

En el ejemplo se ha definido una clase de nombre “Producto”, es muy simple, solo cumple la función de definir la información que necesitas el control.

public class Producto
{
    public string Descripcion { get; set; }
    public int Id { get; set; }
    public int Precio { get; set; }
}

El código que carga una lista de tipo Producto y la asigna como origen de datos al combo, es el siguiente:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        
        cbProductos.ValueMember = "Id";
        cbProductos.DisplayMember = "Descripcion";
        cbProductos.DataSource = ObtenerListItemOpcional();

    }

    private List<Producto> ObtenerListItemOpcional()
    {
        List<Producto> listproductos = ObtenerLista();

        listproductos.Insert(0, new Producto() { Id = 0, Descripcion = "<Seleccione un Item>", Precio = 0 });

        return listproductos;
    }

    private List<Producto> ObtenerLista()
    {
        return new List<Producto>()
        {
            new Producto(){ Id = 1, Descripcion = "Notebook HP Athlon Neo MV-40", Precio = 3100},
            new Producto(){ Id = 2, Descripcion = "Notebook ASUS ATOM N450", Precio = 2300},
            new Producto(){ Id = 3, Descripcion = "Notebook ACER N270", Precio = 2000},
            new Producto(){ Id = 4, Descripcion = "Notebook SONY Atom TM", Precio = 2600},
            new Producto(){ Id = 5, Descripcion = "Notebook DELL SU 4100", Precio = 3400},
            new Producto(){ Id = 6, Descripcion = "Notebook ASUS Core i3", Precio = 4500}
             
        };
    }
}

La línea clave en donde se agrega el ítem que permitirá la selección opcional es:

listproductos.Insert(0, new Producto() { Id = 0, Descripcion = "<Seleccione un Item>", Precio = 0 });

Lo importante a remarcar es como el modelo original de datos no es alterado, pudiéndose cargar, si se necesita, un combo simple sin un ítem adicional, se maximiza la reutilización de los métodos separándolos según agregue o no el ítem opcional.

Agregar opción usando un DataTable

En este caso se usara un datatable cuya estructura será definido en runtime, la clave aquí esta en el método ObtenerListItemOpcional() donde se crea la nueva fila en tiempo de diseño.

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        cbProductos.DisplayMember = "Descripcion";
        cbProductos.DataSource = ObtenerListItemOpcional();
    }

    private DataTable ObtenerListItemOpcional()
    {
        DataTable listproductos = ObtenerLista();

        DataRow row = listproductos.NewRow();
        row["Id"] = 0;
        row["Descripcion"] = "<Seleccione un Item>";
        row["Precio"] = 0;
        
        listproductos.Rows.InsertAt(row, 0);

        return listproductos;
    }

    private DataTable ObtenerLista()
    {
        DataTable dt = new DataTable();

        dt.Columns.Add("Id");
        dt.Columns.Add("Descripcion");
        dt.Columns.Add("Precio");

        DataRow row = dt.NewRow();
        row["Id"] = 1;
        row["Descripcion"] = "Notebook HP Athlon Neo MV-40";
        row["Precio"] = 3100;
        dt.Rows.Add(row);

        row = dt.NewRow();
        row["Id"] = 2;
        row["Descripcion"] = "Notebook ASUS ATOM N450";
        row["Precio"] = 2300;
        dt.Rows.Add(row);

        row = dt.NewRow();
        row["Id"] = 3;
        row["Descripcion"] = "Notebook ACER N270";
        row["Precio"] =  2000 ;
        dt.Rows.Add(row);

        row = dt.NewRow();
        row["Id"] = 4;
        row["Descripcion"] = "Notebook SONY Atom TM";
        row["Precio"] = 2600;
        dt.Rows.Add(row);

        row = dt.NewRow();
        row["Id"] = 5;
        row["Descripcion"] = "Notebook DELL SU 4100";
        row["Precio"] = 3400;
        dt.Rows.Add(row);

        row = dt.NewRow();
        row["Id"] = 6;
        row["Descripcion"] = "Notebook ASUS Core i3";
        row["Precio"] = 4500;
        dt.Rows.Add(row);

        return dt;
    }
}

 

En caso de usar un dataset tipado, la técnica es la misma solo que se dispondrá de método específicamente creados por el dataset en tiempo de diseño, en el ejemplo no se contemplo este caso, pero ante la necesidad de usarlo podría ser algo como lo siguiente:

private dtoProducto.ProductosDataTable ObtenerListItemOpcional()
{
    dtoProducto.ProductosDataTable dt = ObtenerLista();

    dtoProducto.ProductosRow row = dt.NewProductosRow();
    row.Id = 0;
    row.Descripcion = "<Seleccione un Item>";
    row.Precio = 0;
    dt.Rows.Add(row);

    return dt;
}

Es prácticamente idéntico solo que hay método especiales, por ejemplo, NewProductosRow() y los campos estas tipados en propiedades.

 

[C#]
[VB.NET]

 

ASP.NET DropDownList

La técnicas usadas para añadir un ítem a los controles web, difieren bastante de las usadas en una aplicación winforms.

Tomemos de ejemplo el siguiente html

    <form id="form1" runat="server">
    <div>
        <asp:DropDownList ID="DropDownList1" runat="server">
        </asp:DropDownList>
        
        <br/>
        <br/>
        
        <asp:DropDownList ID="DropDownList2" runat="server" AppendDataBoundItems="true">
            <asp:ListItem Value="0">&lt;Seleccione un Item&gt;</asp:ListItem>
        </asp:DropDownList>
    </div>
    </form>

Este solo contiene dos controles de lista, pero hay una diferencia entre ellos, el segundo hace uso de una propiedad especial que le permite añadir un ítem definido en código html del mismo, me refiero al AppendDataBoundItems="true"

Si ahora se analiza el código .net usado para cargar cada lista

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DropDownList1.DataSource = ObtenerLista();
        DropDownList1.DataTextField = "Descripcion";
        DropDownList1.DataValueField = "Id";
        DropDownList1.DataBind();

        DropDownList1.Items.Insert(0, new ListItem("<Seleccione un Item>", "0"));


        DropDownList2.DataSource = ObtenerLista();
        DropDownList2.DataTextField = "Descripcion";
        DropDownList2.DataValueField = "Id";
        DropDownList2.DataBind();
    }

}

Para el primer dropdownlist se ha agregado el ítem programáticamente usando el método Insert(), indicando en el primer parámetro que el valor estará ubicado en la primer posición.

Mientras que el segundo no requiere del agregado de nada especial, el ítem se había definido en el html.

 

[C#]
[VB.NET]

Conclusión

Si se compara las técnicas usadas para cargar el ComboBox de una aplicación WinForms y el DropDownList de una aplicación asp.net, se notara rápidamente que el primer caso el ítem adicional se agrega a los datos antes de bindear el control

Mientras que el el desarrollo con asp.net permite agregar un ítem directo al control, ya sea que se defina el en html, o se use el Insert() en su colección de ítems

martes, 1 de junio de 2010

[Linq to XML] Unir Documentos – Uso del Join

 

Introducción

El articulo muestra la forma en que se puede hacer uso de linq to xml para unir dos documentos xml diferentes, pero que poseen un dato en común como vinculo

Se podría decir que la unión seria similar a un “join” entre tablas, pero la diferencia esta en que es xml.

El resultado será la creación de una lista (de uan clase concreta) que podrá ser bindeada a una grilla.

 

Unión de las listas

Para el ejemplo se ha creado una clase custom que será usada en la defunción del tipo a devolver.

Esto es necesario ya que un tipo anónimo definido como “var”, no puede ser devuelto en una función, es necesario definir el tipo concreto.

public class Producto
{
    public string NumeroSerie {get; set;}
    public string Nombre {get; set;}
    public string Alquilado {get; set;}
    public string Modelo { get; set; }
}

El código linq usado es el siguiente:

private List<Producto> ProcesarXML()
        {
            string xml1 = @" <Nodos>
                             <Nodo s='122531715' m='0'>
                              <Model id='029A' m='210ED441A02' vn='2.27' vp='1.94' fd='12/11/2007 00:00:00' dad='0107' pass='2' />
                              <Info s='8' lpt='15/04/2010 01:00:00' let='01/04/2010 19:36:25' lc='29/05/2010 13:09:45' in='False' iim='True' />
                              <En E0='00000515' />
                              <ICP I0='0' />
                             </Nodo>
                             <Nodo s='122531717' m='0'>
                              <Model id='0290' m='210ED441A02' vn='2.27' vp='1.94' fd='23/10/2007 00:00:00' dad='0119' pass='2' />
                              <Info s='8' lpt='15/04/2010 01:00:00' let='02/04/2010 09:24:16' lc='29/05/2010 16:09:47' in='False' iim='True' />
                              <En E0='00000859' />
                              <ICP I0='0' />
                             </Nodo>
                            </Nodos>";



             string xml2 =@"<Parcelas>
                             <Parcela trafo='1' Sector='K'>
                              <ID>1</ID>
                              <Nombre>Par 1 Sec. K</Nombre>
                              <Numero>122531715</Numero>
                              <Alquilada>No</Alquilada>
                             </Parcela>
                             <Parcela trafo='1' Sector='K'>
                              <ID>2</ID>
                              <Nombre>Par 2 Sec. K</Nombre>
                              <Numero>122531717</Numero>
                              <Alquilada>No</Alquilada>
                             </Parcela>
                            </Parcelas>";

            XElement xmldoc1 = XElement.Parse(xml1);
            XElement xmldoc2 = XElement.Parse(xml2);
            

            List<Producto> query = (from item1 in xmldoc1.Elements("Nodo")
                                    join item2 in xmldoc2.Elements("Parcela") 
                                    on item1.Attribute("s").Value equals item2.Element("Numero").Value
                                    select new Producto()
                                    {
                                        NumeroSerie = item1.Attribute("s").Value,
                                        Nombre = item2.Element("Nombre").Value,
                                        Alquilado = item2.Element("Alquilada").Value,
                                        Modelo = item1.Element("Model").Attribute("m").Value
                                    }).ToList<Producto>();

            return query;
            
        }

Si se observa en la consulta como se ha definido el join, se vera que se une el valor de un atributo del primer xml, con el valor de un nodo del segundo, al usarse:

on item1.Attribute("s").Value equals item2.Element("Numero").Value

 

Aclaraciones

En el ejemplo se hace uso del método XElement.Parse() para cargar desde variables del tipo string el xml

En caso de ser desde un archivo debería usarse: XElement.Load()

 

[C#]
 

[DataGridView] – Texto Celdas en Mayúscula

 

Introducción

En algunos casos es necesario que la entrada de datos sea siempre en mayúscula, lograr esto en un control Textbox es relativamente simple, ya que se dispone del evento KeyPress para detectar y convertir el valor ingresado.

Pero que sucede si esto mismo se quiere aplicar a las celdas de un DataGridView, bien este articulo demuestra como poder lograrlo.

Usando el evento EditingControlShowing

En el ejemplo se hará uso del evento que permitirá detecta cuando una celda entra en modo de edición.

Dentro del evento EditingControlShowing, se detecta si la columna que entro en modo de edición corresponde a la que se quiere controlar. En este ejemplo solo la columna “Descripcion” será afectada con el control en el input de datos.

Es necesario remarcar que al momento de adjunta el evento KeyPress al objeto e.Control, (que ha sido casteado a DataGridViewTextBoxEditingControl, para su correcta utilización), el handler del evento se aplicaría a todas las celdas de este mismo tipo. También a las de las columnas que no se quiere aplicar el control de mayúsculas, en este caso la de  “Cuenta”.

Es por eso que en el evento KeyPress también se control la columna que esta activa en ese momento.

El handler del evento queda adjunto aun cuando se sale de modo edición.

La utilización de la primer línea que quita el hadler al entrar en edición:

dText.KeyPress -= new KeyPressEventHandler(dText_KeyPress);

Solo sirve para que se adjunte un único evento, ya que sino estuviera se adjuntarían una detrás de otro produciéndose múltiples llamadas al evento KeyPress.

[C#]

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    int columnIndex = dataGridView1.CurrentCell.ColumnIndex;

    if (dataGridView1.Columns[columnIndex].Name == "Descripcion")
    {
        DataGridViewTextBoxEditingControl dText = (DataGridViewTextBoxEditingControl)e.Control;

        dText.KeyPress -= new KeyPressEventHandler(dText_KeyPress); 
        dText.KeyPress += new KeyPressEventHandler(dText_KeyPress);
    }
}

void dText_KeyPress(object sender, KeyPressEventArgs e)
{
    int columnIndex = dataGridView1.CurrentCell.ColumnIndex;

    if (dataGridView1.Columns[columnIndex].Name == "Descripcion")
    {
        e.KeyChar = char.ToUpper(e.KeyChar);
    }
}

[VB.NET]

 

Private Sub dataGridView1_EditingControlShowing(ByVal sender As Object, ByVal e As DataGridViewEditingControlShowingEventArgs)

    Dim columnIndex As Integer = dataGridView1.CurrentCell.ColumnIndex

    If dataGridView1.Columns(columnIndex).Name = "Descripcion" Then

        Dim dText As DataGridViewTextBoxEditingControl = DirectCast(e.Control, DataGridViewTextBoxEditingControl)

        RemoveHandler dText.KeyPress, AddressOf dText_KeyPress
        AddHandler dText.KeyPress, AddressOf dText_KeyPress

    End If

End Sub

Private Sub dText_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs)

    Dim columnIndex As Integer = dataGridView1.CurrentCell.ColumnIndex

    If dataGridView1.Columns(columnIndex).Name = "Descripcion" Then

        e.KeyChar = Char.ToUpper(e.KeyChar)

    End If

End Sub

 

[C#]
[VB.NET]

 

Resolución del problema en al asignación del evento

El problema comentado anteriormente podrías resolverse fácilmente con solo declarar la variable que contendrá la celda en edición de forma global al evento.

De esta forma se podrías hacer uso del evento que detecta cuando una celda ha dejado de editarse, removiendo el handler del evento.

Ahora el evento KeyPress no controla que columna se esta editando, esto solo se hace cuando se entra o sale del modo edición de la celda.

[C#]

DataGridViewTextBoxEditingControl dText = null;

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    int columnIndex = dataGridView1.CurrentCell.ColumnIndex;

    if (dataGridView1.Columns[columnIndex].Name == "Descripcion")
    {
        dText = (DataGridViewTextBoxEditingControl)e.Control;
        
        dText.KeyPress += new KeyPressEventHandler(dText_KeyPress);
    }
}

private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
    int columnIndex = dataGridView1.CurrentCell.ColumnIndex;

    if (dataGridView1.Columns[columnIndex].Name == "Descripcion")
    {
        dText.KeyPress -= new KeyPressEventHandler(dText_KeyPress);
    }
}

void dText_KeyPress(object sender, KeyPressEventArgs e)
{
   e.KeyChar = char.ToUpper(e.KeyChar);
}

[VB.NET]

Private dText As DataGridViewTextBoxEditingControl = Nothing

Private Sub dataGridView1_EditingControlShowing(ByVal sender As Object, ByVal e As DataGridViewEditingControlShowingEventArgs) Handles dataGridView1.EditingControlShowing

    Dim columnIndex As Integer = dataGridView1.CurrentCell.ColumnIndex

    If dataGridView1.Columns(columnIndex).Name = "Descripcion" Then

        dText = DirectCast(e.Control, DataGridViewTextBoxEditingControl)

        AddHandler dText.KeyPress, AddressOf dText_KeyPress

    End If
End Sub

Private Sub dataGridView1_CellEndEdit(ByVal sender As Object, ByVal e As DataGridViewCellEventArgs) Handles dataGridView1.CellEndEdit

    Dim columnIndex As Integer = dataGridView1.CurrentCell.ColumnIndex

    If dataGridView1.Columns(columnIndex).Name = "Descripcion" Then
        RemoveHandler dText.KeyPress, AddressOf dText_KeyPress
    End If

End Sub

Private Sub dText_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs)

    e.KeyChar = Char.ToUpper(e.KeyChar)

End Sub

 

[C#]
[VB.NET]

miércoles, 12 de mayo de 2010

[WinForms] ComboBox Add ítems - SelectedValue

 

Introducción


Suele suceder cuando se quiere representar información en ciertos controles como ser combos, o listas, que no se conozca cual es la mejor forma para trabajarlos obteniendo un acceso rápido a sus ítems.

Una claro caso es el uso de combo cuando se necesite agregar ítems de forma manual, y no se dispone de una lista u objetos de datos para bindearlo.

Como proceder en estos casos? bueno este articulo intentara mostrar como trabajar con este control, mostrando como bindearlo y como seleccionar un valor programáticamente.

 

Cargar ítems al ComboBox


El primer paso será la carga de los ítems al combo, en este caso serán valores manuales los que se dispones, estos no proceden de una consulta a una tabla, o de algún servicio para recuperarlos, sino que son ítems que uno quieres desplegar en el control.

La mejor forma para hacerlo es usando una clase, específicamente creada para este caso, podría ser algo como esto:

public class Item
{
    public string Name {get; set;}
    public int Value{get; set;}

    public Item(string name, int value)
    {
        Name = name; 
        Value = value;
    }
    public override string ToString()
    {
        return Name;
    }
}

Es una clase muy simple que solo tiene dos propiedades, las cuales recibirá en el constructor los valores que uno decida cuando cree cada instancia.

La carga de cada ítem será realizada en el Load del formulario:

private void Form1_Load(object sender, EventArgs e)
{
    List<Item> lista = new List<Item>();

    lista.Add(new Item("Blue", 1));
    lista.Add(new Item("Red", 2));
    lista.Add(new Item("Green", 10));
    lista.Add(new Item("Yellow", 5));

    comboBox1.DisplayMember = "Name";
    comboBox1.ValueMember = "Value";
    comboBox1.DataSource=lista;

    //
    // Se asigna el evento para control el cambio de seleccion
    //
    comboBox1.SelectedIndexChanged += new System.EventHandler(this.comboBox1_SelectedIndexChanged);

}

Como se visualiza la carga no respeta un orden especifico, ni sus valores de clave están en orden, esto es porque uno decide como se crea cada valor.

Lo que si es importante es que cada ítem no se agrega directo en el ComboBox, sino que se añade a la lista genérica definida del tipo de la clase creada anteriormente.

Al final se procede a bindear la lista al combo y se define los nombres de las propiedades que representan la visualización y el valor.

 

Asignar la selección de un ítem especifico


Un punto interesante es que será posible asignar directamente un valor al combo de forma programática y este seleccionara el ítem en la lista si es que esta presente.

private void btnSeleccionarValor_Click(object sender, EventArgs e)
{
    int valor = 0;
    if (!int.TryParse(txtValor.Text, out valor))
        return;

    comboBox1.SelectedValue = valor;
}

private void btnSeleccionarTexto_Click(object sender, EventArgs e)
{
    comboBox1.Text = txtTexto.Text;
}

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    Item seleccion = comboBox1.SelectedItem as Item;

    if (seleccion == null)
        return;

    MessageBox.Show(string.Format("Se ha seleccionado Color: {0} Valor: {1}", 
                                                seleccion.Name , 
                                                seleccion.Value ));

}

En el código se aprecia dos métodos de selección, ya sea por valor o texto, pero ambos trabajan de forma distintas.

En la selección por valor, si es asignado número que no en encuentra en la lista, el combo pasara a tomar el el SelectedIndex = –1, o sea no tendrá ningún valor activo, el SelectedItem pasara a devolver un null.

En cambio en la por Texto, si el valor no se encuentra en la lista dejara el que esta, no cambiara la selección.

 

[C#]
[VB.NET]

miércoles, 5 de mayo de 2010

Crystal Reports – Parameters – Como asignarlos desde código .net

 

Introducción


Un problema frecuente cuando se usa Crystal Reports es el pasaje de información por medio de parámetros al reporte.

El uso de parámetros es un aspecto importante para la creación de reportes, y suele suceder que no es tan intuitiva, ni directa la forma en como se debe programar esta funcionalidad.

 

Definición del parámetro en Crystal


En este ejemplo se usara el parámetro como filtro de los datos del reporte, por lo tanto se creara en el mismo momento en que se define la información.

Para esto se usara la opción “DataBase Expert”:

En el cuadro de dialogo que se despliega, se deber crear una nueva conexión a la base de datos Access usada en el reporte:

Haciendo uso en este cuadro, no directamente la tabla mostrada, sino la opción “Add Command”, la cual permitirá definir una consulta personalizada.

Es por eso que desplegara el cuadro:

 

Y se definirá un parámetro mediante la opción de la derecha.

Solo queda definir la consulta, haciendo uso del parámetro:

Seguramente cuando se acepte el dialogo, mostrara el mensaje pidiendo el ingreso de un valor al parámetro creado, se puede ingresar un valor cualquiera, o simplemente aceptar.

En cualquier momento la consulta escrita podrá ser modificada.

Luego de arrastrar los campos al diseñador del Crystal, podría visualizarse algo como lo siguiente:

En el “Field Explorer” se puede inspeccionar tanto los campos de la consulta, como los parámetros definidos para el reporte.

 

Pasaje de parámetros desde código


Al momento de ejecutar la aplicación será necesario pasarle al reporte desde el código el filtro que se ha definido, sino se hace seguramente el propio Crystal desplegara un cuadro pidiendo el ingreso de un valor.

En el siguiente código se muestra como llevar a cabo esta operación:

 

private void Form1_Load(object sender, EventArgs e)
{
          
		  //
          // Creo el parametro y asigno el nombre
          //
          ParameterField param = new ParameterField();
          param.ParameterFieldName = "CargoParam";

          //
          // creo el valor que se asignara al parametro
          //
          ParameterDiscreteValue discreteValue = new ParameterDiscreteValue();
          discreteValue.Value = "Developer";
          param.CurrentValues.Add(discreteValue);

          //
          // Asigno el paramametro a la coleccion
          //
          ParameterFields paramFiels = new ParameterFields();
          paramFiels.Add(param);

          //
          // Asigno la coleccion de parametros al Crystal Viewer
          //
          crystalReportViewer1.ParameterFieldInfo = paramFiels;

          //
          // Creo la instancia del reporte
          //
		  crListado report = new crListado();

          //
          // Cambio el path de la base de datos
          //
		  string rutadb = Path.Combine(Application.StartupPath, "TestDb.mdb");
		  report.DataSourceConnections[0].SetConnection("", rutadb, false);

          //
          // Asigno el reporte a visor
          //
		  crystalReportViewer1.ReportSource = report;

}

 

[C#]
[VB.NET]

martes, 4 de mayo de 2010

[Linq] DataSet – Agrupar y totalizar

 

Introducción


En este articulo analizaremos como trabajar con los datos procedentes de un datatable, realizando operaciones por medio de la ayuda de linq, y luego confeccionado un datatable distinto, cuya estructura de campos difiere de la original.

Para esta operación se contara con un datatable con documento de clientes, y un campo total que informa cuantos de estos se tienen registrados.

La idea es obtener en otro datatable la cantidad total agrupando todos los clientes.

 

Agrupar con Linq


Para esta operación se realizaría una agrupación mediante la funcionalidad que provee linq.

private void button1_Click(object sender, EventArgs e)
{
    IEnumerable<IGrouping<string, DataRow>> query = from item in Datos().AsEnumerable()
                                                    group item by item["Documento"].ToString() into g
                                                    select g;


    DataTable resultado = Transformar(query);

    dataGridView1.AutoGenerateColumns = true;
    dataGridView1.DataSource = resultado;

}

Como se observa en la query de linq se agrupa por medio del campo “Documento”, y el resultado de esta operación se alojo en “g”, valor que es devuelto en la consulta.

Si bien podría haberse igualado toda la consulta a una variable definida como “var”, esto hubiera imposibilitado el pasaje de la información a un método de transformación, para la creación del nuevo datatable, es por eso que se crea el IEnumerable<>, que contiene el IGrouping<> también genérico.

 

Creación del nuevo DataTable


En esta operación en una primera implementación es algo manual, ya que toma el resultado de la consulta linq e itera para cargar el nuevo datatable, y en su camino realiza las operaciones de calculo.

private DataTable Transformar(IEnumerable<IGrouping<string, DataRow>> datos)
{
    //
    // Se define la estructura del DataTable
    //
    DataTable dt = new DataTable();
    dt.Columns.Add("Documento");
    dt.Columns.Add("CantRegistros");
    dt.Columns.Add("Total");

    //
    // Se recorre cada valor agruparo por linq y se vuelca el resultado 
    // en un nuevo registro del datatable
    //
    foreach (IGrouping<string, DataRow> item in datos)
    {
        DataRow row2 = dt.NewRow();
        row2["Documento"] = item.Key;
        row2["CantRegistros"] = item.Count();
        row2["Total"] = item.Sum<DataRow>(x => Convert.ToInt32(x["Total"]));

        dt.Rows.Add(row2);
    }

    return dt;
}

Debe observarse como se trabaja con los métodos extendidos, y en ellos se aplica la funcionalidad Lambda, puntualmente al indica que campo debe sumarse.

 

[C#]
 

 

Creación del DataTable con CopyToDataTable()


Una alternativa algo mejor a la transformación de datos podrías ser por medio del uso del método de extensión CopyToDataTable()

DataTableExtensions.CopyToDataTable<(Of <(T>)>)

Hay que remarcar un punto con este método, ya que de forma estándar solo permite convertir a DataTable aquellas consulta linq que devuelvan como resultado una DataRow.

Pero en nuestro ejemplo planteado esto es diferente ya que el datatable a devolver requiere de una transformación de los registro con respecto a los datos originales.

Pero esta situación esta contemplada, ya que es posible implementar un método CopyToDataTable<> genérico, en donde a base de un tipo de dato se arme un datatable.

Para esta implementación, se hará uso de la explicación proporcionada por siguiente articulo:

Cómo implementar CopyToDataTable<T> donde el tipo genérico T no es un objeto DataRow

El código del link de msdn, se ha volcado al archivo de nombre “CopytoDataTable.cs”, se podrá observar en el código de ejemplo adjunto en el articulo.

La implementación de la funcionalidad quedo reducida al siguiente código:

private void button1_Click(object sender, EventArgs e)
{

    var query = from item in Datos().AsEnumerable()
                group item by item["Documento"].ToString() into g
                select new DocumentoResult
                {
                    Documento = g.Key,
                    CantRegistros = g.Count(),
                    Total = g.Sum(x => Convert.ToInt32(x["Total"]))
                };


    DataTable resultado = query.CopyToDataTable<DocumentoResult>();

    dataGridView1.AutoGenerateColumns = true;
    dataGridView1.DataSource = resultado;

}

Esta nueva alternativa ya no necesita iterar por cada registro que dio como resultado la consulta linq, es mas se puede hacer uso del “var”, puesto que no es necesario pasar por parámetro la consulta para trabajarla.

Pero si se necesito de un pequeño cambio, en donde se ha definido una clase, de nombre “DocumentoResult”, la cual define los campos del nuevo DataTable.

public class DocumentoResult
{
    public string Documento { get; set; }
    public int CantRegistros { get; set; }
    public int Total { get; set; }

}

Siendo esta la clase usada en la definición del método genérico CopyToDataTable<>, cuando se usa en la línea:

DataTable resultado = query.CopyToDataTable<DocumentoResult>();

 

[C#]
 

domingo, 2 de mayo de 2010

C# - [Winforms] UserControl - Focus()

 

Introducción

Suele suceder que la implementación de cierta funcionalidad cuando se desarrollan componentes no sea tan obvia como uno se imagina.

Este es el caso de la implementación del método Focus() en un User Control.

User Control - Método Focus

La clase base de la cual hereda el componente es UserControl, y este método en particular no esta marcado para poder realizar un override, es por eso que hay que redefinirlo mediante el uso del modificador new

public new bool Focus()
{
    //
    // Es necesario activar el control para que tome el foco
    //
    this.Select();
    return textBox1.Focus();
}

Un detalle a remarcar en el código es el uso del método Selected(), el cual activa el User Control y permite así que un control contenido en este tome el foco.

 

[C#]
 

C# - [DataGridView] DataGridViewComboBoxColumn – Variar contenido del combobox respecto a la fila

 

Introducción

El objetivo del articulo es demostrar algunos problemas que pueden surgir cuando se necesita que el ComboBox ubicado en las celdas del DataGridView, varíe su contenido de acuerdo al registro que se visualiza.

Para demostrar el problema, se ha definido un dominio del ejemplo intentara cargar una grilla de sucursales, de los cuales mostraran el listado de los teléfonos disponibles.

Por supuesto para cada sucursal los teléfonos disponibles varían, por lo tanto definir el contenido del combo como una única lista no es posible.

Carga del contenido variable en ComboBox

En el evento Load del formulario se procede a la carga de las sucursales, e inmediatamente después de esta operación se invoca a un método creado con el fin de la carga de los combos de cada registro de a grilla.

Este método recorre en un ciclo cada registro, toma el id de la sucursal que se encuentra en la celda del registro actual, y también toma la celda que representa el combo.

Con las anteriores dos selecciones procede a realizar una consulta a los datos y cargar en el combo los teléfonos de esa sucursal.

 

private void frmSucursales_Load(object sender, EventArgs e)
{
    SucursalesEntities sucursalesContext = new SucursalesEntities();

    dgvScursales.AutoGenerateColumns=false;
    dgvScursales.DataSource = sucursalesContext.Sucursales;

    //
    // Luego de bindear la grilla, cargo el contenido de cada fila
    // en el combo de telefonos para cada sucursal
    //
    LoadGrillaComboBoxTelefonos();

}

private void LoadGrillaComboBoxTelefonos()
{
    foreach (DataGridViewRow row in dgvScursales.Rows)
    {
        int idSucursal = Convert.ToInt32(row.Cells["IdSucursal"].Value);

        DataGridViewComboBoxCell comboboxCell = row.Cells["Telefonos"] as DataGridViewComboBoxCell;
        
        SucursalesEntities sucursalesContext = new SucursalesEntities();

        comboboxCell.DataSource = sucursalesContext.Telefonos.Where(x => x.IdSucursal == idSucursal);
        comboboxCell.ValueMember = "Id";
        comboboxCell.DisplayMember = "Numero";

    }
}

Algunos Comentarios

Durante la realización del ejemplo se realizaron varias pruebas, con algunos eventos que se adaptaran a la situación y evitaran tener que recorrer en un “foreach” cada registro del DataGridView.

Después de investigar un poco los únicos dos eventos que se adaptarían a este caso serian el RowsAdded y el CellFormatting, pero ambos trajeron problemas, lo que impidió su uso.

El CellFormatting, tiene como problema que se ejecuta contantemente, ya que es lanzado al redibujar la celda, y como esta operación requiere la consulta a los datos, generaba un efecto de bloqueo en la pantalla, el cual no es deseado.

El RowsAdded, si bien se ejecuta por cada fila agregada en la operación de bindeo de los datos, no había forma interna en el evento de detectar que fila se estaba agregando, ya que el argumento del evento e.RowIndex o e.RowCount, no devuelven un valor correcto.

Como conclusión se dejo de lado los eventos y se hizo uso del “foreach”, implementado esto en un método, cuya utilización se realiza luego que se bindearon los datos en el control.

 

[C#]
[VB.NET]

[Crystal Reports] Exportar DataGridView a Crystal

 

Introducción


Comúnmente cuando uno confecciona un reporte la información que se usara proviene de una tabla o consulta a una base de datos, pero suele suceder que a veces se necesite información de orígenes distintos, como ser el caso de controles del propio formulario, donde el usuario cargado información.

Este el caso planteado en el articulo, como generar un reporte en Crystal Reprots, pero utilizando información que no se encuentra en una base de datos, sino que esta aun contenida en los controles del formulario, no hay una tabla a la cual se puede vincular el reporte.

Para resolver este problema se hará uso de DataSet Tipados, los cuales no solo brindaran el esquema necesario para definir la estructura de datos que usara el reporte, sino que además será contenedor de los datos que luego se asignaran como origen de datos.

 

Información Adicional


Como referencia adicional podría mencionar dos excelente artículos que han sido de mucha ayuda, los cuales detallan muy bien como hacer uso de los reportes vinculados a DataSet Tipados, pero en este caso si toman la información mediante una consulta a la db.

Informes Crystal Reports

Creación de Reportes con Crystal Reports en Visual Studio 2005/2008

 

Creación del reporte


Uno de los primeros paso consiste en la creación del reporte, para el mismo es necesario definir los datos o mejor dicho el esquema de los mismos, tarea que será responsabilidad del DataSet Tipado.

Es por ello que en el ejemplo podrán ver un archivo de nombre dtCompra.xsd, este es precisamente el DataSet que se usara en el reporte y definirá la estructura de datos del mismo.

crystal4

En este se han creado manualmente dos DataTable que representan los datos del cliente y los ítems que ha seleccionado en la compra.

Ambas tablas se han creado de forma manual, arrastrando los datatable de la toolbox de la izquierda, y luego agregando sus columnas. Al no existir base de dato alguna, es necesario crear estas de forma manual, no se tiene ayuda del “Solution Explorer” para arrastrar tablas y crearlas dinámicamente, justamente porque no existen.

Una vez creado el esquema de los datos se procede con el reporte, para ello se agrega uno nuevo en el proyecto, en este caso con el nombre de Factura.rpt, y se configuran los datos mediante la opciones del “Field Explorer”, agregando el origen de datos, como muestra la imagen:

crystal1

Del cuadro de dialogo siguiente se selecciona el DataSet creado en el paso previo:

crystal2

Por ultimo, el asistente de Crystal permitirá redefinir como se vinculan las tablas, en este caso por representar información a distinto nivel no existe una relación, pero si se desarrollara algún reporte del estilo maestro-detalles, podría existir un vinculo entre las tablas.

crystal3

Una vez concluidos los pasos para configurar el esquema de datos del reporte, arrastrando los campos al reporte, podría armar un reporte similar a este:

crystal5

 

Carga de los datos en el DataSet Tipado


El siguiente paso consiste en volcar la información ingresada por el usuario en los controles al dataset tipado, creando en dtComrpas.xsd

Para esto se codifico un método responsable de esta tarea:

private dtCompra GenerarFactura()
{
    dtCompra facturacion = new dtCompra();

    //
    // Agrego el registro con la info del cliente
    //
    dtCompra.DatosClienteRow rowDatosCliente = facturacion.DatosCliente.NewDatosClienteRow();
    rowDatosCliente.Nombre = txtNombre.Text;
    rowDatosCliente.Direccion = txtDireccion.Text;
    rowDatosCliente.Telefono = txtTelefono.Text;
    rowDatosCliente.DNI = txtDni.Text;

    facturacion.DatosCliente.AddDatosClienteRow(rowDatosCliente);

    //
    // Itero por cada fila del DataGridView creando el registro 
    // en el DataTabla 
    //
    foreach (DataGridViewRow row in dgvCompras.Rows)
    {
        dtCompra.ComprasRow rowCompra = facturacion.Compras.NewComprasRow();  
        rowCompra.Descripcion = Convert.ToString(row.Cells["Descripcion"].Value);
        rowCompra.PrecioUnitario = Convert.ToInt32(row.Cells["PrecioUnitario"].Value);
        rowCompra.Cantidad = Convert.ToInt32(row.Cells["Cantidad"].Value);

        facturacion.Compras.AddComprasRow(rowCompra);
    }


    return facturacion;
}

Este método es fundamental ya que será el encargado de cargar los datos en una instancia del dataset tipado, para ello tomara el contenido de los controles y los agregara como registros en el DataTable que corresponda.

Es de apreciar como opera con el DataGridView, recorriendo cada fila del mismo, tomando los valores de sus celdas y creando nuevos registros en el datatable que representa las Compras.

 

Visualización del Reporte


Una vez que tenemos los datos cargados en una instancia del DataSet, solo queda asignarlos como origen de datos del reporte.

Para esta operación el reporte será mostrador en un formulario especialmente creado para tal fin.

Pero hay un detalle, los datos han sido creado en un formulario distinto, es por eso usa la técnica por medio del constructor para pasar la información de un formulario a otro. Esto podrá visualizarse en el formulario frmVisorFactura.

public partial class frmVisorFactura : Form
{
    dtCompra _datosreporte;

    private frmVisorFactura()
    {
        InitializeComponent();
    }

    public frmVisorFactura(dtCompra datos): this()
    {
        _datosreporte = datos;
    }

    private void frmVisorFactura_Load(object sender, EventArgs e)
    {
        Factura _factura = new Factura();
        _factura.SetDataSource(_datosreporte);

        crwFactura.ReportSource = _factura;
    }
}

Allí se ha agregado un constructor que acepta como parámetro el dataset que requiere el reporte para visualizarse.

También puede verse como en el evento Load se hace uso de los datos pasados al formulario y como son asignados al DataSource de la instancia del reporte.

Desde el frmCompra (donde el Usuario de la aplicación ingresa los datos), solo basta con invocar al formulario de la siguiente manera:

private void btnReporte_Click(object sender, EventArgs e)
{
    dtCompra datos = GenerarFactura();

    frmVisorFactura frm = new frmVisorFactura(datos);
    frm.Show();
}

En al primer línea se invoca al método que carga la estructura de datos, y en las siguiente se crea la instancia del formulario que contienen el control Crystal Report Viewer, al cual se le pasa en el constructor la información a visualizar.

Hay que remarcar en este punto el porque de todo esta lógica en al comunicación de los formularios. Resulta que uno de ellos es el que contiene la información y el otro es el que permite la visualización, la idea es mantener bien clara la separación de responsabilidades de cada uno, y no tener que acceder desde un formulario a los controles del otro, eso no es correcto, pero si se usa un medio que permita la comunicación, como ser en este caso el dataset tipado ,se puede trabajar en cada formulario de forma transparente y prolija.

 

[C#]
[VB.NET]

sábado, 1 de mayo de 2010

Login – Usando Password con Hash

 


Introducción


Este artículo intentara plantear la forma en que se puede crear un login que pueda ser usado tanto para un entorno Web, como para uno WinForms.

Se vera como mediante la definición de funcionalidad en una capa de Servicio el código de autenticación puede ser reutilizado, en realidad este nombre que le he dado a la capa podría llevar cualquier otro nombre, podrías ser directo la capa de Datos, o un Repository, la idea es mantener la técnica que permita reutilizar la funcionalidad.

Algo particular que encontraran en el ejemplo esta referido al password no se guarda de forma plana en la db, sino que por motivos de seguridad se aplica una función de hash.

La misma tiene una particularidad interesante, ya que el algoritmo solo se puede aplicar en un solo sentido, siempre obteniendo el hash de un texto o mensaje, pero nunca de un valor hash obtener el mensaje original.

Al aplicar el algoritmo hash a un mensaje, el resultado será siempre el mismo, por lo tanto esta técnica utilizada en el login permite que el password este siempre seguro. En el ejemplo verán como en el momento de login se aplica el algoritmo al la entrada del usuario y este valor se busca como password en la tabla, al asegurarse que un mismo texto produce el mismo resultado de hash, si se ingreso correctamente usuario y password este debe coincidí con el presente en la base de datos.


Definición de la capa de Servicio



El primer paso que explicaremos en el articulo es el código común que será reutilizado tanto por la capa web como winform, se trata de la denominada capa de servicio.

La misma básicamente se compone de una fachada estática, con métodos para autenticar o insertar un usuario, por supuesto la funcionalidad adicional para actualizar no se incluyo en el ejemplo, pero podrías agregarse si fuera necesario.

public static bool Autenticar(string usuario, string password)
{
    string sql = @"SELECT COUNT(*)
                      FROM Usuarios
                      WHERE NombreLogin = @nombre AND Password = @password";


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand command = new SqlCommand(sql, conn);
        command.Parameters.AddWithValue("@nombre", usuario);

        string hash = Helper.EncodePassword(string.Concat(usuario, password));
        command.Parameters.AddWithValue("@password", hash);

        int count = Convert.ToInt32(command.ExecuteScalar());

        if (count == 0)
            return false;
        else
            return true;

    }
}
public static UsuarioEntity Insert(string nombre, string apellido, string nombreLogin, string password)
{
    UsuarioEntity usuario = new UsuarioEntity();

    usuario.Nombre = nombre;
    usuario.Apellido = apellido;
    usuario.NombreLogin = nombreLogin;
    usuario.Password = password;

    return Insert(usuario);
}

public static UsuarioEntity Insert(UsuarioEntity usuario)
{

    string sql = @"INSERT INTO Usuarios (
                           Nombre
                          ,Apellido
                          ,NombreLogin
                          ,Password)
                      VALUES (
                            @Nombre, 
                            @Apellido, 
                            @NombreLogin,
                            @Password)
                    SELECT SCOPE_IDENTITY()";


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {

        SqlCommand command = new SqlCommand(sql, conn);
        command.Parameters.AddWithValue("Nombre", usuario.Nombre);
        command.Parameters.AddWithValue("Apellido", usuario.Apellido);
        command.Parameters.AddWithValue("NombreLogin", usuario.NombreLogin);

        string password = Helper.EncodePassword(string.Concat(usuario.NombreLogin, usuario.Password));
        command.Parameters.AddWithValue("Password", password);

        conn.Open();

        usuario.Id = Convert.ToInt32(command.ExecuteScalar());

        return usuario;
    }
}

El método importante en estos dos métodos es el siguiente:

string password = Helper.EncodePassword(string.Concat(usuario.NombreLogin, usuario.Password));

Esta línea es la que arma el password real que se salvara en el campo de la tabla, como se observa para una seguridad aun mayor se une el nombre del usuario y el password, pasándolos luego por la función que aplica el hash.

internal class Helper
{
    public static string EncodePassword(string originalPassword)
    {
        SHA1 sha1 = new SHA1CryptoServiceProvider();

        byte[] inputBytes = (new UnicodeEncoding()).GetBytes(originalPassword);
        byte[] hash = sha1.ComputeHash(inputBytes);

        return Convert.ToBase64String(hash);
    }
}

En el método Autenticar() como se había comentado no se obtiene el password que originalmente el usuario tienen, sino que se aplica el hash sobre los valores ingresados al momento de realizar el login, ejecutando el query con esta información como filtro.

Si el usuario y password ingresados en la autenticación son los correctos al aplicar el hash retornara la misma cadena que se tiene en la tabla, por lo tanto al ejecutar el query debería devolver el registro del usuario.


Autenticación desde una aplicación Winform



En el método Main() del archivo Program.cs se encontrara la llamada a el formulario que será responsable de al autenticación.

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        
        frmLogin frm = new frmLogin();
        frm.ShowDialog();

        if(frm.DialogResult == DialogResult.OK)
            Application.Run(new frmPrincipal());

    }
}

El formulario de login (frmLogin.cs), tendrá un código muy simple, solo tomara el usuario y password invocara al servicio y según el resultado retornara la respuesta al método Main() para que se abra o no el formulario principal.

private void btnAceptar_Click(object sender, EventArgs e)
{
    string nombre = txtNombre.Text;
    string password = txtPassword.Text;

    if (LoginService.Autenticar(nombre, password))
        this.DialogResult = DialogResult.OK;
    else
        this.DialogResult = DialogResult.Abort;

}

private void btnCancelar_Click(object sender, EventArgs e)
{
    this.DialogResult = DialogResult.Cancel;
}

Adicionalmente se agrego un formulario para la creación de nuevos usuarios, si bien en este no se ha programado la edición completa del usuario, o sea toda la funcionalidad de ABM, el Insert permitirá analizar como se registra un nuevo usuario y se guarda su password.

private void btnAceptar_Click(object sender, EventArgs e)
{
    UsuarioEntity usuario = new UsuarioEntity();

    usuario.Nombre = txtUsuario.Text;
    usuario.Apellido = txtApellido.Text;
    usuario.NombreLogin= txtNombreLogin.Text;
    usuario.Password = txtPassword.Text;

    usuario = LoginService.Insert(usuario);

    MessageBox.Show(string.Format("Se ha creado el usuario, ID: {0}", usuario.Id));
}

Nuevamente gracias a la funcionalidad encapsulada del Servicio, el código del formulario es muy simple.

 


Autenticación desde una aplicación Web



La autenticación en un entorno web es algo distinta a una aplicación Windows, en este caso se ha configurado en el web.config, las líneas necesarias para que todo el sitio este bajo una autenticación por medio de “Forms”

<authentication mode="Forms">
	<forms name="appNameAuth" path="/" loginUrl="frmLogin.aspx" defaultUrl="Default.aspx" protection="All" />
</authentication>
  
  <authorization>
    <deny users="?" />
  </authorization>

En el archivo de configuración del sitio se especifica cual será la pagina por defecto y cual la de login, de esta forma si no hay usuario autenticado, solo el sitio redirección a estas url.
El formulario web frmLogin.aspx tiene un código muy simple:

protected void ProcessLogin(object sender, EventArgs e)
{
    if (LoginService.Autenticar(txtUser.Text, txtPassword.Text))
    {
        FormsAuthentication.RedirectFromLoginPage(txtUser.Text, chkPersistLogin.Checked);
    }
    else
        ErrorMessage.InnerHtml = "<b>Usuario o contraseña incorrectos...</b> por favor re-ingrese las credenciales...";
}

Como se observa luego de la autenticación se hace uso de los métodos provistos por .net en el namespace System.Web.Security, para trabajar con al seguridad.
Aquí se esta indicando el nombre del usuario que paso la validación, y esto hace que solo el sitio se redireccione a la pagina marcada por defecto en el web.config.

En la pagina principal del sitio se ha agregado algo de código para mostrar el nombre del usuario autenticado, y dar la posibilidad de un logout.

protected void Page_Load(object sender, EventArgs e)
{
    Label1.Text = string.Format("Bienvenido al Sistema: {0}", Thread.CurrentPrincipal.Identity.Name); 
}

protected void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
    if (Menu1.SelectedValue == "Salir")
    {
        FormsAuthentication.SignOut();
        FormsAuthentication.RedirectToLoginPage();
    }
}

También se ha creado una pagina para crear nuevos usuarios (Usuarios.aspx)

protected void btnAceptar_Click(object sender, EventArgs e)
{
    UsuarioEntity usuario = new UsuarioEntity();

    usuario.Nombre = txtNombre.Text;
    usuario.Apellido = txtApellido.Text;
    usuario.NombreLogin = txtLogin.Text;
    usuario.Password = txtPassword.Text;

    usuario = LoginService.Insert(usuario);

    ClearControls();
    lblMessage.InnerHtml = string.Format("Se ha creado el usuario, ID: {0}", usuario.Id);

}

protected void btnCancelar_Click(object sender, EventArgs e)
{
    Response.Redirect("~/Default.aspx");
}

private void ClearControls()
{
    txtNombre.Text="";
    txtApellido.Text="";
    txtLogin.Text="";
    txtPassword.Text="";
}

Código de ejemplo


Como requerimiento será necesario contar con al menos Sql Server Express 2008, para que pueda ejecutarse la aplicación.

La tabla de usuario ya posee un registro para ingresar la primera vez, la información es la siguiente:
Usuario: admin
Password: pass123

Para no tener dos base de datos iguales, se agrego un Build Event en el proyecto web para que copie la db desde el proyecto de Class Library a la carpeta App_Data del sitio Web.

Puede que a veces si se compila repetida veces la compilación lance un error al intentar copiar la db a la carpeta App_Data, cuya operación no pueda hacerlo porque el servicio de sql server la tenga loqueada, en ese caso simplemente ejecuten a pesar de este mensaje de error, ya que funcionara sin problemas.

[C#] 
[VB.NET]