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]