domingo, 18 de julio de 2010

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

 

Introducción


Este articulo es la continuación de su predecesor:

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

Donde se había planteado el hecho de confeccionar todo el código dentro del formularios, sin capas, remarcando las desventajas que esto trae a la reutilización y el mantenimiento de la aplicación.

En cambio en esta segunda parte se planteara la aplicación agregando una capa adicional, la de datos.

Estructura del proyecto


En este nueva arquitectura se incluirán dos nuevos proyectos, ambos del tipo “Class Library”, estos compilaran como dll, para ser referenciadas por el proyecto de Presentación. En toda la solución solo habrá un proyecto que genere un ejecutable.

En la siguiente imagen se grafica la relaciones entre los proyectos, las flechas marcan como se han realizado las referencia entre ellos.

 imagen1

El proyecto de entidades es un caso especial que debe analizarse,  en esta oportunidad se ha utilizado DataSet Tipados para crear las clases que representan al negocio.

Se ha creado un único DataSet con varios DataSet tipados, uno por cada entidad.

Se podría haber utilizado clases especiales, pero esto se hará en la próxima iteración cuando se separen verdaderamente en 3 capas, ya que el uso de clase requiere cambiar la lógica en como se carga la información, o sea no se podrá usar mas el DataAdapters, sino que se deberá pasar al DataReader, los cuales son óptimos para la carga secuencial de la información.

La capa de Datos esta formada por una clases estáticas, una por cada entidad que conforma el negocio, se definieron como estática para facilitar el acceso a los métodos, que claramente solo procesan de los datos de la instancias con las cuales trabaja.

En esta capa hay algo interesante para remarcar, como las líneas de facturación de por si solas no existen sin una factura, las líneas no tienen funcionalidad especifica en la cada de datos, o sea no se persisten por si solas, sino que lo hacen cuando se crea una factura. Esto es interesante ya que remarca fuertemente el modelo del negocio, impidiendo que una línea quede disociada del encabezado de factura, que le da sentido de existencia.

Las líneas de facturación son persistidas en la misma operación en que se crea la factura, es por eso que en la clase InvoiceDAL, solo esta el método Create(), cuyo objetico es añadir tanto la factura, como sus líneas.

Este tipo de razonamiento tiene mucho que ver con la notación UML, en donde existe una relación denominada de agregación.

Modelo de Clases

La relación entre las facturas y sus líneas es del tipo Agregación por Valor, o también llamada Composición, en donde el tiempo de vida de las líneas esta condicionado al tiempo en que dure la factura, si esta ultima se elimina las líneas también serán borradas.

Creación de un nuevo ítem en la grilla


En el artículo previo se había utilizado uso de un datatable sin tipo para crear dinámicamente una nueva línea de facturación, en cambio esto se ha cambiado adaptando al uso de una clase.

/// <summary>
/// Crea un item de compra en la grilla
/// </summary>
private void NuevaLinea()
{

    List<DGVLine> newlist = new List<DGVLine>();

    if (dgvLineaCompra.DataSource != null)
    {
        List<DGVLine> list = (dgvLineaCompra.DataSource as List<DGVLine>);

        list.ForEach((item) =>
        {
            newlist.Add((DGVLine)item.Clone());
        });
    }

    newlist.Add(new DGVLine());

    dgvLineaCompra.AutoGenerateColumns = false;
    dgvLineaCompra.DataSource = newlist;

}
public class DGVLine : ICloneable
{
    public int? Track { get; set; }
    public decimal PrecioUnitario { get; set; }
    public int Cantidad { get; set; }

    #region ICloneable Members

    public object Clone()
    {
        DGVLine item = new DGVLine();

        item.Track = this.Track;
        item.PrecioUnitario = this.PrecioUnitario;
        item.Cantidad = this.Cantidad;

        return item;
    }

    #endregion
}

Un detalle importante que implico el cambio del datatable por la clase es la necesidad de clonar los ítems previos que tenia el DataGridView, de no hacerlo el control no detecta las nueva instancia y por lo tanto no se visualizan las nuevas filas agregadas a la grilla.

Por esta razón se implementa de la interfaz ICloneable. El ForEach() aplicado a cada ítem previo de la grilla es pasado a una nueva lista de ítems que al final es asignada como origen de datos nuevamente.

El método ForEach() esta usado una expresión lambda para recorrer cada ítem y facilitar la transformación.

Carga de la datos en la Presentación


El uso de la capa de acceso a datos permite que la presentación quede muy prolija, con una línea se resuelva la toma de los datos necesarios para desplegar la información al usuario.

Un ejemplo claro de esta interactividad es presente en la carga de los ítem del combo de la grilla:

private void frmCompra_Load(object sender, EventArgs e)
{

    //
    // Cargo los items del combo
    //
    DataGridViewComboBoxColumn comboCol = dgvLineaCompra.Columns["Track"] as DataGridViewComboBoxColumn;
    comboCol.ValueMember = "TrackId";
    comboCol.DisplayMember = "Name";
    comboCol.DataSource = TrackDAL.GetAll();

    //
    // 
    //
    NuevaLinea();
}

Es rápidamente visible la mejora que implica usar capas, anteriormente en este misma acción se tenia una buena cantidad de código, ahora solo es una llamada a un método GetAll(), de la clase TrackDAL, el cual devolverá un DataTable especializado del tipo TrackDataTable.

Otro punto importante que recibió una mejora notable es la búsqueda del cliente, tanto al momento de cargar la grilla en el formulario frmBuscarCliente:

private void frmBuscarCliente_Load(object sender, EventArgs e)
{
    //
    // Se carga la lista de clientes
    //
    dgvClientes.AutoGenerateColumns = false;
    dgvClientes.DataSource = CustomerDAL.GetAll();

}

Como así también al momento del retorno de la información, al cargar por id la información completa de ese cliente en especial:

private void btnBuscarCliente_Click(object sender, EventArgs e)
{
    frmBuscarCliente frm = new frmBuscarCliente();

    if (frm.ShowDialog() == DialogResult.OK)
    {
        //
        // Mantengo la entidad cliente seleccionada global al formulario
        //
        cliente = CustomerDAL.GetById(frm.IdCliente); 

        //
        // muestro en pantalla la info del cliente
        //
        txtId.Text = Convert.ToString(cliente.CustomerId);
        txtNombre.Text = cliente.FirstName;
        txtApellido.Text = cliente.LastName;
        txtDireccion.Text = cliente.Address;
        txtCompañia.Text = cliente.Company;
        txtEmail.Text = cliente.Email; 
    }

}

El uso de entidades devueltas por la Capa de Datos agrega prolijidad y facilidad en el acceso a los datos, por ejemplo, la funcionalidad de CustomerDAL.GetById() devuelve una objeto de tipo CustomerRow, o sea un solo registro del CustomerDataTable, lo cual permite acceder directo a los campos de la fila, asignando los valores a los controles del formulario, no hace falta recorrer registros, o controlar si hay datos en un datatable, la capa de datos devuelve directamente la entidad filtrando por el Id.

Proceso de Facturación


Del proceso de Facturación se destaca la creación de las entidades del encabezado y de las líneas dentro del evento de la presentación. Como se habrá observado, en el código la presentación ha perdido todas las consultas sql que anteriormente se ejecutaban, pasando a formar parte de la capa de datos como especialista en procesar la información.

La comunicación entre las capas de Presentación y DataAccess se realiza por medio de entidades definidas en el proyecto que lleva el mismo nombre.

En el proceso de facturación dos entidades son creadas y pasadas por parámetros a la capa de datos para ser procesadas.

private void btnConfirmar_Click(object sender, EventArgs e)
        {
            if (!Validaciones())
                return;

            //
            // inicializo la transacciones
            //
            using (TransactionScope scope = new TransactionScope())
            {

                #region Creo\Actualizo la informacion del cliente

                //
                // si el cliente se ha seleccionado lo actualizo, sino se crea uno nuevo
                //
                if (cliente == null)
                {
                    cliente = (new ChinookEntity.CustomerDataTable()).NewCustomerRow();

                    cliente.FirstName = txtNombre.Text;
                    cliente.LastName = txtApellido.Text;
                    cliente.Company = txtDireccion.Text;
                    cliente.Address = txtCompañia.Text;
                    cliente.Email = txtEmail.Text;

                    cliente = CustomerDAL.Create(cliente);

                }
                else
                {
                    cliente.FirstName = txtNombre.Text;
                    cliente.LastName = txtApellido.Text;
                    cliente.Company = txtCompañia.Text;
                    cliente.Address = txtDireccion.Text;
                    cliente.Email = txtEmail.Text;

                    cliente = CustomerDAL.Update(cliente);
                }
                #endregion

                #region Creo el Encabezado\Linea de la Factura

                ChinookEntity.InvoiceRow invoice = (new ChinookEntity.InvoiceDataTable()).NewInvoiceRow();

                invoice.CustomerId = cliente.CustomerId;
                invoice.InvoiceDate = DateTime.Now.Date;
                invoice.BillingAddress = txtDireccion.Text;


                ChinookEntity.InvoiceLineDataTable invoiceLine = new ChinookEntity.InvoiceLineDataTable();

                foreach (DataGridViewRow row in dgvLineaCompra.Rows)
                {
                    ChinookEntity.InvoiceLineRow invoiceLineRow = invoiceLine.NewInvoiceLineRow();
 
                    invoiceLineRow.TrackId = Convert.ToInt32(row.Cells["Track"].Value);
                    invoiceLineRow.UnitPrice = Convert.ToDecimal(row.Cells["PrecioUnitario"].Value);
                    invoiceLineRow.Quantity = Convert.ToInt32(row.Cells["Cantidad"].Value);

                    invoiceLine.AddInvoiceLineRow(invoiceLineRow);
                }

                InvoiceDAL.Create(invoice, invoiceLine);

                #endregion
                
                scope.Complete();

            }

            InicializarControles();

        }
public static class InvoiceDAL
{

    public static void Create(ChinookEntity.InvoiceRow invoice, ChinookEntity.InvoiceLineDataTable invoiceLine)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();
            //
            // Creacion de la Factura
            //
            string sqlFactura = @"INSERT INTO Invoice (CustomerId, InvoiceDate, BillingAddress, Total) VALUES (@customerId, @date, @address, @total)
                       SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlFactura, conn))
            {

                cmd.Parameters.AddWithValue("@customerId", invoice.CustomerId);
                cmd.Parameters.AddWithValue("@date", invoice.InvoiceDate);
                cmd.Parameters.AddWithValue("@address", invoice.BillingAddress);
                cmd.Parameters.AddWithValue("@total", 0);

                invoice.InvoiceId = Convert.ToInt32(cmd.ExecuteScalar());
            }

            //
            // Creacion de cada Liena de Factura
            //
            decimal total = 0;

            string sqlLineaFactura = @"INSERT INTO InvoiceLine (InvoiceId, TrackId, UnitPrice, Quantity) 
                                        VALUES (@invoiceid, @trackid, @unitprice, @quantity)
                                        SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlLineaFactura, conn))
            {

                foreach (ChinookEntity.InvoiceLineRow row in invoiceLine.Rows)
                {
                    //
                    // como se reutiliza el mismo objeto SqlCommand es necesario limpiar los parametros
                    // de la operacion previa, sino estos se iran agregando en la coleccion, generando un fallo
                    //
                    cmd.Parameters.Clear();

                    cmd.Parameters.AddWithValue("@invoiceid", invoice.InvoiceId);
                    cmd.Parameters.AddWithValue("@trackid", row.TrackId );
                    cmd.Parameters.AddWithValue("@unitprice", row.UnitPrice);
                    cmd.Parameters.AddWithValue("@quantity", row.Quantity);

                    //
                    // Si bien obtenermos el id de linea de factura, este no es usado
                    // en la aplicacion
                    //
                    row.InvoiceLineId = Convert.ToInt32(cmd.ExecuteScalar());

                    total += row.UnitPrice * row.Quantity;
                }

            }

            //
            // Actualizacion del Total de la Factura
            //
            string sqlUpdateTotal = @"UPDATE Invoice SET Total = @total WHERE InvoiceId = @InvoiceId";

            using (SqlCommand cmd = new SqlCommand(sqlUpdateTotal, conn))
            {
                cmd.Parameters.AddWithValue("@total", total);
                cmd.Parameters.AddWithValue("@InvoiceId", invoice.InvoiceId);

                cmd.ExecuteNonQuery();
            }


        }

        

    }

}

La creación de la factura implica unos cuantos pasos, pero al contar con entidades el procesamiento es mas simple.

Dentro del método Create() ahora solo es necesario iterar por las rows definidas en el datatable InvoiceLineDataTable, cuya información proviene de la presentación.

Ventajas y Desventajas de este nueva implementación


Si bien es cierto que esta nueva implementación requiere codificar un poco mas, ya que es necesario definir clases y proyectos nuevo, también es cierto que estos mejora la reutilización del código, a partir de ahora cualquier otro formulario que necesite, por ejemplo, listar los clientes no será necesario copiar y pegar el fragmento de código usado para conectarse a la base de datos y recuperar los registro, solo se agrega una línea invocando al método GetAll() de la clase CustomerDAL y eso será todo.

Esto implica una gran ventaja que centraliza la funcionalidad, separando todo lo que tenga que ver con la persistencia de los datos, es importante notar que a los métodos de la capa de datos en ningún momento se le paso como parámetro un control de presentación, o sea no se paso una referencia del DataGridView para que tomara las líneas de la factura, sino que se desacopla las capas usando las entidades.

Es por esta razón que en la presentación donde se recorre los ítems del DataGridView y se arman filas del datatable InvoiceLineDataTable, el cual es usado para reflejar las línea de facturación. Las entidades son en todo momento el vinculo de transporte de información entre las capas, y esta regla no se puede romper si es que se quiere mantener las capas desacopladas.

Un punto que aun queda como detalle esta relacionado al manejo de transacciones, el cual aun es controlado por la presentación, pero esto no es correcto, aunque solucionarlo requiere de algún intermediario entre estos componentes que permitan interceder en la transacción, es aquí donde entra en juego la capa de negocio, que se implementara en la próxima iteración.

Ejemplo de código


El mismo fue desarrollado con Visual Studio 2008, y Sql Server 2008 Express.

Durante el desarrollo del articulo no se ha insertado todo el código de los ejemplos, ya que resultaría demasiado extenso, la idea es que puedan descargar el código, ejecutarlo y analizarlo detenidamente, viendo como interactúan y comunican las distintas capas.

 

[C#] 
[VB.NET] 

viernes, 16 de julio de 2010

[ASP.NET] Mantener la Session Activa Indefinidamente

 

Introducción


El objetivo que persigue el articulo es demostrar como mediante llamadas asíncronas realizadas por medio de la librería de jquery se puede mantener la Session activa en el servidor de forma indefinida sin que este expire en un tiempo determinado.

 

Mantener la Session Activa


Asp.net expira la Session de un usuario cuando un tiempo prolongado de inactividad indicaría que ya no se esta usando la pagina, pero a veces es necesario permitir que el usuario tome varias horas sin actividad, aunque en la configuración del servidor el timeout este definido en un tiempo reducido.

La idea del ejemplo es realizar de forma transparente para el usuario, y sin que se refresque la pagina, una invocación al servidor, para que este registre dicha actividad.

Para lograr el objetivo se hará uso de dos librerías de jquery:

- la principal realizará las invocaciones a los WebMethod definidos en la pagina, usando la funcionalidad de $.ajax

- la otra librería permitirá especificar desde javascript el intervalo de tiempo en que debe invocarse al método expuesto en el servidor web, jQuery Timers 

[javascript]

<script language="javascript" type="text/javascript">

        $().ready(function() {

        $(document).everyTime(3000, function() {
        
                $.ajax({
                    type: "POST",
                    url: "ValidarSession1.aspx/KeepActiveSession",
                    data: {},
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    async: true,
                    success: VerifySessionState,
                    error: function(XMLHttpRequest, textStatus, errorThrown) {
                        alert(textStatus + ": " + XMLHttpRequest.responseText);
                    }
                });
                
            });

     
        });

        var cantValidaciones = 0;

        function VerifySessionState(result) {

            if (result.d) {
                $("#EstadoSession").text("activo");
            }
            else
                $("#EstadoSession").text("expiro");

            $("#cantValidaciones").text(cantValidaciones);
            cantValidaciones++;

        }

        function SessionAbandon() {

            $.ajax({
                type: "POST",
                url: "ValidarSession1.aspx/SessionAbandon",
                data: {},
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                async: true,
                error: function(XMLHttpRequest, textStatus, errorThrown) {
                    alert(textStatus + ": " + XMLHttpRequest.responseText);
                }
            });
            
        }
    </script>

 

[C#]

public partial class ValidarSession1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Session["datos"] = true;
    }


    [WebMethod()]
    public static bool KeepActiveSession()
    {
        if (HttpContext.Current.Session["datos"] != null)
            return true;
        else
            return false;
    }

    [WebMethod()]
    public static void SessionAbandon()
    {
        HttpContext.Current.Session.Remove("datos");
    }

}

En el código puede observarse los WebMethod expuestos en la página, y como son invocados desde javascript de forma asíncrona, usando una llamada ajax, mediante el uso de $.ajax

 

Consideraciones de los Ejemplos


Dentro de ejemplo de código se encontraran dos páginas aspx, ambas realizan la misma operación, solo que aplican distintas técnicas.

En la pagina ValidarSession1.aspx se verifica explícitamente que determinada key de la session esta disponible. La idea de este ejemplo es demostrar que tanto la pagina como los servicios web que se están usando comparten la misma Session y se puede acceder a los datos allí guardados

La pagina ValidarSession2.aspx, si bien la forma de comunicarse es la misma, la validaciones de la session ha cambiado, aquí no se usa una key determinada, sino que se valida el objeto de forma general.

En el ejemplo para que sea practico se estableció un tiempo de 3000 ms (3 seg), para que valide la Session, pero si la idea es mantener la session activa con especificar un tiempo cercano al definido en el expiración de la sesión alcanzaría.

Por ejemplo si se determino en el IIS que el sitio tiene un timeout de session en 20min, especificando un tiempo de 15000 ms (15 min) seria suficiente para evitar que la session expire.

La solución fue desarrollada con Visual Studio 2008.

 

[C#] 
[VB.NET] 

jueves, 15 de julio de 2010

[Linq] Join DataTable


Introducción


Si bien el objeto DataSet es muy completo y brinda una funcionalidad muy amplia hay que reconocer que ciertas operación con la información pueden estar fuera del alcance del mismo, por lo tanto se debe recurrir a otras técnicas.

Es bien conocido que el DataSet posee un método de nombre Merge(), el cual resulta muy práctico para unir información proveniente de diversas consultas, siempre y cuando se respete la misma estructura en los datos, pero este solo une información, no permite obtener obtener diferencias entre las dos fuentes de datos.

Que sucede si es necesario cruzar registros para obtener diferencias entre ellos y trabajar con esta información, es aquí donde entra en juego Linq, proveyendo una herramienta muy amplia de posibilidades.

 

Join entre DataSet, devolviendo la lista de string


En el ejemplo se devolverá una lista de string con el resultado del join de ambos datatable. Una de las listas representan una relojes de registro de ingreso de personal, mientras que la otra devuelve cuales de estoy hay que controlar.

[C#]

private void btnListaString_Click(object sender, EventArgs e)
{
    #region Defino Columnas DataGridView
    dataGridView1.Columns.Clear();

    DataGridViewColumn col = new DataGridViewColumn(new DataGridViewTextBoxCell());
    col.HeaderText = "Relojes ha Controlar";
    col.Name = "Relojes";
    col.DataPropertyName = "Value";
    col.Width = 160;
    dataGridView1.Columns.Add(col);

    #endregion


    List<string> lista = (from dt1 in ObtenerListadoRelojes().AsEnumerable()
                          join dt2 in ObtenerRelojesControlar().AsEnumerable() on dt1.Field<int>("Numero") equals dt2.Field<int>("NumControl")
                          select dt1.Field<string>("Nombre")).ToList();


    dataGridView1.AutoGenerateColumns = false;
    dataGridView1.DataSource = lista.Select(x => new { Value = x }).ToList(); 
}

[VB.NET]

Private Sub btnListaString_Click(ByVal sender As Object, ByVal e As EventArgs)

    'Defino Columnas DataGridView
    dataGridView1.Columns.Clear()

    Dim col As New DataGridViewColumn(New DataGridViewTextBoxCell())
    col.HeaderText = "Relojes ha Controlar"
    col.Name = "Relojes"
    col.DataPropertyName = "Value"
    col.Width = 160
    dataGridView1.Columns.Add(col)


    Dim lista As List(Of String) = (From dt1 In ObtenerListadoRelojes().AsEnumerable() _
                                     Join dt2 In ObtenerRelojesControlar().AsEnumerable() _
                                        On dt1.Field(Of Integer)("Numero") Equals dt2.Field(Of Integer)("NumControl") _
                                     Select dt1.Field(Of String)("Nombre")).ToList()


    dataGridView1.AutoGenerateColumns = False

    dataGridView1.DataSource = lista.[Select](Function(x) New With {.Value = x}).ToList()

End Sub


Es interesante marcar como el “join” de linq, en la parte de “on” se asemeja a una consulta SQL, igualando que campo se comparan en la operación.

En la sección del “select” de la consulta, solo se define un campo el tipo string, el cual es utilizado para conformar cada ítem de la lista devuelta por la consulta linq.


Join entre DataSet - mapeando con una clase


El uso de clases para convertir datos en una estructura diferente puede resultar útil cuando se requiere obtener datos complejos con los cuales trabajar.

En este ejemplo era necesario obtener dos atributos provenientes de los datos originales, usar un string era imposible ya que este solo representa un dato, por lo tanto el uso de la clase permitió lograr el objetivo.

[C#]

private void btnlistaClase_Click(object sender, EventArgs e)
{
   
    #region Defino Columnas DataGridView
    dataGridView1.Columns.Clear();

    DataGridViewColumn col1 = new DataGridViewColumn(new DataGridViewTextBoxCell());
    col1.HeaderText = "Relojes ha Controlar";
    col1.Name = "Relojes";
    col1.DataPropertyName = "Nombre";
    col1.Width = 160;
    dataGridView1.Columns.Add(col1);

    DataGridViewColumn col2 = new DataGridViewColumn(new DataGridViewTextBoxCell());
    col2.HeaderText = "Horario";
    col2.Name = "Horario";
    col2.DataPropertyName = "Hora";
    col2.Width = 50;
    dataGridView1.Columns.Add(col2);

    #endregion

    List<Datos> lista = (from dt1 in ObtenerListadoRelojesConHorarios().AsEnumerable()
                         join dt2 in ObtenerRelojesControlar().AsEnumerable() on dt1.Field<int>("Numero") equals dt2.Field<int>("NumControl")
                         select new Datos
                         {
                             Nombre = dt1.Field<string>("Nombre"),
                             Hora = dt1.Field<string>("Hora")
                         }).ToList();


    dataGridView1.AutoGenerateColumns = false;
    dataGridView1.DataSource = lista;
}

[VB.NET]

Private Sub btnlistaClase_Click(sender As Object, e As EventArgs)

       'Defino Columnas DataGridView
	dataGridView1.Columns.Clear()

	Dim col1 As New DataGridViewColumn(New DataGridViewTextBoxCell())
	col1.HeaderText = "Relojes ha Controlar"
	col1.Name = "Relojes"
	col1.DataPropertyName = "Nombre"
	col1.Width = 160
	dataGridView1.Columns.Add(col1)

	Dim col2 As New DataGridViewColumn(New DataGridViewTextBoxCell())
	col2.HeaderText = "Horario"
	col2.Name = "Horario"
	col2.DataPropertyName = "Hora"
	col2.Width = 50
	dataGridView1.Columns.Add(col2)


       Dim lista As List(Of Datos) = (From dt1 In ObtenerListadoRelojesConHorarios().AsEnumerable() _
                                        Join dt2 In ObtenerRelojesControlar().AsEnumerable() _
                                           On dt1.Field(Of Integer)("Numero") Equals dt2.Field(Of Integer)("NumControl") _
                                        Select New Datos() With { _
                                               .Nombre = dt1.Field(Of String)("Nombre"), _
                                               .Hora = dt1.Field(Of String)("Hora") _
                                           }).ToList()


	dataGridView1.AutoGenerateColumns = False
       dataGridView1.DataSource = lista

End Sub


Hay que remarcar como el la sección del “select” se crea un nueva instancia de la clase por cada ítem resultante de la query, en esta es donde se especifica como mapean las propiedades con los campos originales.

 

Ejemplos de código


El código ha sido desarrollado usando Visual Studio 2008

 

[C#] 
[VB.NET] 

miércoles, 14 de julio de 2010

[WinForms] FormBase – Control de tiempo de actividad

 

Introducción


La idea de este articulo surge con la necesidad de implementar código común para los formulario.

El requerimiento apunta a que los formularios después de cierto tiempo deben deshabilitar sus controles.

 

Definición del Form Base


todo el truco en esta implementación esta en la definición del la clase Base, la cual será usada por todos los formularios de la aplicación para controlar las actividades.

[C#]

public class FormBase : Form
    {
        private Timer _timer;

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            HandlerActitivy(this.Controls);

            _timer = new Timer();
            _timer.Interval = 5000; //se establecen 5 seg
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.Start();
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            _timer.Stop();

            DisableControls();

        }

        public void Restart()
        {
            EnableControls();

            _timer.Start();

        }

        protected virtual void DisableControls()
        {
        }

        protected virtual void EnableControls()
        {
        }

        private void HandlerActitivy(Control.ControlCollection col)
        {
            foreach (Control ctrl in col)
            {
                if (ctrl is TextBox)
                {
                    TextBox txt = ctrl as TextBox;
                    txt.TextChanged += new EventHandler(txt_TextChanged);
                }
                if (ctrl is ComboBox)
                {
                    ComboBox cmb = ctrl as ComboBox;
                    cmb.SelectedIndexChanged += new EventHandler(cmb_SelectedIndexChanged);
                }
                if (ctrl is CheckBox)
                {
                    CheckBox chk = ctrl as CheckBox;
                    chk.CheckedChanged += new EventHandler(chk_CheckedChanged);
                }
                if (ctrl is GroupBox)
                {
                    if (ctrl.HasChildren)
                    {
                        HandlerActitivy(ctrl.Controls);
                    }
                }
            }
        }

        void chk_CheckedChanged(object sender, EventArgs e)
        {
            TimerRestart();
        }

        void cmb_SelectedIndexChanged(object sender, EventArgs e)
        {
            TimerRestart();
        }

        void txt_TextChanged(object sender, EventArgs e)
        {
           TimerRestart();
        }

        private void TimerRestart()
        { 
            _timer.Stop();
            _timer.Start();  
        }
    }

[VB.NET]

Public Class FormBase
    Inherits Form

	Private _timer As Timer

	Protected Overloads Overrides Sub OnLoad(e As EventArgs)
		MyBase.OnLoad(e)

		HandlerActitivy(Me.Controls)

		_timer = New Timer()
        _timer.Interval = 5000 'se establecen 5 seg

        AddHandler _timer.Tick, AddressOf _timer_Tick

		_timer.Start()
	End Sub

	Private Sub _timer_Tick(sender As Object, e As EventArgs)
        _timer.Stop()

		DisableControls()

	End Sub

	Public Sub Restart()
		EnableControls()

		_timer.Start()

	End Sub

	Protected Overridable Sub DisableControls()
	End Sub

	Protected Overridable Sub EnableControls()
	End Sub

    Private Sub HandlerActitivy(ByVal col As Control.ControlCollection)

        For Each ctrl As Control In col

            If TypeOf ctrl Is TextBox Then
                Dim txt As TextBox = TryCast(ctrl, TextBox)
                AddHandler txt.TextChanged, AddressOf txt_TextChanged
            End If

            If TypeOf ctrl Is ComboBox Then
                Dim cmb As ComboBox = TryCast(ctrl, ComboBox)
                AddHandler cmb.SelectedIndexChanged, AddressOf cmb_SelectedIndexChanged
            End If

            If TypeOf ctrl Is CheckBox Then
                Dim chk As CheckBox = TryCast(ctrl, CheckBox)
                AddHandler chk.CheckedChanged, AddressOf chk_CheckedChanged
            End If

            If TypeOf ctrl Is GroupBox Then
                If ctrl.HasChildren Then
                    HandlerActitivy(ctrl.Controls)
                End If
            End If

        Next
    End Sub

	Private Sub chk_CheckedChanged(sender As Object, e As EventArgs)
		TimerRestart()
	End Sub

	Private Sub cmb_SelectedIndexChanged(sender As Object, e As EventArgs)
		TimerRestart()
	End Sub

	Private Sub txt_TextChanged(sender As Object, e As EventArgs)
		TimerRestart()
	End Sub

	Private Sub TimerRestart()
        _timer.Stop()
		_timer.Start()
    End Sub

End Class

Los puntos a remarcar serian:

- Se define en esta clase un objeto timer que actuaria como controlador del tiempo transcurrido sin actividad.

- En el Load de formulario, el cual es tomado por la clase base al utilizar el OnLoad(), lo cual es posible ya que esta clase hereda de Form.

- En el Load se invoca un método de nombre HandlerActitivy(), cuya funcionalidad es tomar los controles definidos en el formulario y adjunta un evento estándar a cada control del formulario.

Este método si bien cubre el objetivo del ejemplo, puede que se necesite extender para controlar otros controles que pudieran usarse en el formulario.

 

Definición del formulario, usando la clase base


[C#]

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

        private void Form1_Load(object sender, EventArgs e)
        {

        }


        protected override void DisableControls()
        {
            txtNombre.Enabled = false;
            txtDireccion.Enabled = false;
            txtMail.Enabled = false;

            btnConfirmar.Enabled = false;

            confirmarToolStripMenuItem.Enabled = false; 

        }

        protected override void EnableControls()
        {
            txtNombre.Enabled = true;
            txtDireccion.Enabled = true;
            txtMail.Enabled = true;

            btnConfirmar.Enabled = true;

            confirmarToolStripMenuItem.Enabled = true; 
        }


        private void btnSalir_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void btnRestart_Click(object sender, EventArgs e)
        {
            base.Restart(); 
        }

        private void restartToolStripMenuItem_Click(object sender, EventArgs e)
        {
            base.Restart(); 
        }

        private void salirToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }

    }

 

[VB.NET]

Public Partial Class Form1
    Inherits FormBase

	Public Sub New()
		InitializeComponent()
	End Sub


	Private Sub Form1_Load(sender As Object, e As EventArgs)

	End Sub


	Protected Overloads Overrides Sub DisableControls()
		txtNombre.Enabled = False
		txtDireccion.Enabled = False
		txtMail.Enabled = False

		btnConfirmar.Enabled = False

		confirmarToolStripMenuItem.Enabled = False

	End Sub

	Protected Overloads Overrides Sub EnableControls()
		txtNombre.Enabled = True
		txtDireccion.Enabled = True
		txtMail.Enabled = True

		btnConfirmar.Enabled = True

		confirmarToolStripMenuItem.Enabled = True
	End Sub


	Private Sub btnSalir_Click(sender As Object, e As EventArgs)
		Me.Close()
	End Sub

	Private Sub btnRestart_Click(sender As Object, e As EventArgs)
		MyBase.Restart()
	End Sub

	Private Sub restartToolStripMenuItem_Click(sender As Object, e As EventArgs)
		MyBase.Restart()
	End Sub

	Private Sub salirToolStripMenuItem_Click(sender As Object, e As EventArgs)
		Me.Close()
	End Sub

End Class

Los puntos a remarcar aquí podría ser

- En la definición del formulario no se hereda mas de Form, ahora se utiliza FormBase

- Se implementaron los método definidos como virtual en la clase base, DisableControls() y EnableControls(), estos métodos permites especificar concretamente que controles se deshabilitan cuando se vence el tiempo, y son invocados directamente desde FormBase.

 

Acerca del ejemplo


Cuando se abra por primera vez el código, si se realiza un doble clik en alguno de los formularios puede que el diseñador muestre un error, ya que requiere de la implementación de FormBase para poder resolver el diseño del formulario

Es por esto que lo primero que se debería hacer es compilar la solución, de esta formas e evita el problema que se comenta con el diseñador de los formularios

 

[C#] 
[VB.NET] 

lunes, 12 de julio de 2010

[ASP.NET][GridView] Edición usando DropDownList

 

Introducción


La edición de entidades directamente en el control GridView puede requerir algunas adaptaciones que adecuen la grilla a la funcionalidad requerida dependiendo de la información que se quiera mostrar.

Dos puntos importantes cuando uno utiliza el control GridView son:

- Adecuar el control usado para desplegar la información al usuario.

En el ejemplo se explicara como cambiar el control de edición por defecto, o sea un TextBox, que se desplegaría al usuario, por otro especialmente definido para la selección por medio de un ítem concreto, por medio de un DropDownList.

- La definición de un identificador para cada registro de la grilla, sin necesidad de desplegar este al usuario.

Se suele necesitar un id o clave que permita identificar el registro, pero que este no se muestre al usuario, sino que sea de uso interno.

 

Definición del ejemplo


El mismo consiste en una lista de contactos a los cuales se le podrá asignar el país de nacimientos.

Durante la edición se desplegara al usuario un cuadro de texto en donde podrá cambiar el nombre, pero los países estarán definidos en una lista desplegable desde la cual serán seleccionados.

Imagen1

 

Edición mediante un DropDownList


Si se analiza la definición del Gridview se notara que es por medio de un control DropDownList en el tag <EditItemTemplate> que se define la lista de países.

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
     AutoGenerateEditButton="True" CellPadding="4" ForeColor="#333333" GridLines="None"
     DataKeyNames="id" 
     onrowediting="GridView1_RowEditing"
     onrowcancelingedit="GridView1_RowCancelingEdit" 
     onrowupdating="GridView1_RowUpdating">
     <RowStyle BackColor="#EFF3FB" />
     <Columns>
         <asp:BoundField DataField="nombre" HeaderText="Nombre Usuario" />
         <asp:TemplateField HeaderText="Pais Nacimiento">
             <EditItemTemplate>
                 <asp:DropDownList ID="ddlPaises" runat="server">
                 </asp:DropDownList>
             </EditItemTemplate>
             <ItemTemplate>
                 <asp:Label ID="Label1" runat="server" Text='<%# Bind("descripcion") %>'></asp:Label>
             </ItemTemplate>
         </asp:TemplateField>
     </Columns>
     <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
     <PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
     <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
     <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
     <EditRowStyle BackColor="#2461BF" />
     <AlternatingRowStyle BackColor="White" />
 </asp:GridView>

Además la columna completa ha cambiado ya no es es mas BoundField, el cual mostraría un simple TextBox durante la edición, sino que se define todo un témplate, mediante el tag <TemplateField>.

Mediante la propiedad AutoGenerateEditButton se indica que en la grilla estén definidos las opciones de edición, por lo tanto habrá ciertos eventos que será necesario agregar en el código.

Es muy importante tener en cuenta que la carga de la información de la grilla solo se realice una vez al inicio de la pagina.

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        BindData();
    }
}

private void BindData()
{
    GridView1.DataSource = DataAccess.GetAllUsuario();
    GridView1.DataBind();
}

Es por eso que el evento Load de la pagina define la carga de los datos de la grilla dentro del bloque IsPostBack, le cual tiene que estar negado, ya que solo durante la carga inicial del formulario será requerida esta operación.

La primer acción que se expone al usuario es la edición, que efectuara el cambio al edittemplate, pero antes ejecutara el evento:

protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e)
{
    GridView1.EditIndex = e.NewEditIndex;

    int id = Convert.ToInt32(GridView1.DataKeys[e.NewEditIndex].Value);
    DataRow row = DataAccess.GetUsuarioById(id);

    BindData();

    DropDownList combo = GridView1.Rows[e.NewEditIndex].FindControl("ddlPaises") as DropDownList;

    if (combo != null)
    {
        combo.DataSource = DataAccess.GetAllPaises();
        combo.DataTextField = "descripcion";
        combo.DataValueField = "id";
        combo.DataBind();
    }

    combo.SelectedValue = Convert.ToString(row["pais"]);
}

En este evento se recupera el id del contacto que representa la fila en edición, se cargan los ítems en el combo para esa fila, y por ultimo se asigna la selección del país que actualmente esta definido la entidad del contacto. Un link interesante que puede ayudar es el siguiente: GridView.RowEditing Event

Debe recordarse que en principio el DropDownList no tiene registros u opciones definidas, es en este evento donde se asignan.

Es importante notar que por medio de la propiedad EditIndex del GridView, se define que fila cambia de estado.

Luego de editar el registro, se disponen de dos acciones, actualizar o cancelar, para lo cual se disponen los eventos:

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    int id = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value);

    DropDownList combo = GridView1.Rows[e.RowIndex].FindControl("ddlPaises") as DropDownList;
    int pais = Convert.ToInt32(combo.SelectedValue);

    TextBox text = GridView1.Rows[e.RowIndex].Cells[1].Controls[0] as TextBox;
    string nombre = text.Text;

    DataAccess.UpdateUsuario(id, nombre, pais);

    GridView1.EditIndex = -1;
    BindData();

}

protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
    GridView1.EditIndex = -1;
    BindData();
}

La edición recupera la información asignada a los controles de la fila activa, y se procede a invocar a la capa de datos para actualizar la entidad.

Conociendo la fila que lanza el evento de actualización, por medio de e.RowIndex, y pudiendo buscar el control mediante su nombre, es que se recupera el valor seleccionado del combo.

En cambio el nombre al estar definido en un BoundField, se recupera mediante el acceso a la celda y a la colección de controles definidos en este, pero como es sabido que solo contendrá un control, o sea el TextBox donde se edita el nombre.

Por ultimo mediante la propiedad EditIndex de la grilla, se deja de editar la fila, es preciso notar que luego de cada operación la información se vuelve a bindear.

 

Definición del Identificador de la fila


Una característica importante del GridView es la posibilidad de definir que propiedad del origen de datos que actuara como identificador de la entidad.

En el ejemplo seguramente se habrá notado la definición de la propiedad DataKeyNames, la cual define el “id” como identificador de cada registro.

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
       AutoGenerateEditButton="True" CellPadding="4" ForeColor="#333333" GridLines="None"
      
DataKeyNames="id"
       onrowediting="GridView1_RowEditing"
       onrowcancelingedit="GridView1_RowCancelingEdit"
       onrowupdating="GridView1_RowUpdating">

Esto es clave para poder recuperar la información del contacto en cada acción realizada por el control. Por medio de DataKeys, es que se obtiene el valor asociado a una fila en particular.

En el código de cada evento se uso de la siguiente forma:

int id = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value);

Es necesario conocer que fila que se esta editando para recuperar de la colección de claves el valor concreto.

 

Acerca del ejemplo


El mismo fue creado usando VS2008 y Sql Server Express 2008

Sera necesario tener el servicio de Sql Server local para que pueda ejecutarse la aplicación

 

[C#] 
[VB.NET] 

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]