domingo, 19 de septiembre de 2010

[ADO.NET] Filtrar rango de fechas

 

Introducción


Uno de los principales problemas que se pueden encontrar cuando se confecciona una consulta es el trabajo con campos del tipo fecha.

Por lo general estos campos no solo persisten la fecha, sino que también registran la hora.

Es por eso que ciertas consultas pueden no retornar los valores deseados, aunque se este definiendo correctamente los parámetros de la consulta.

 

Planteo del problema


Para demostrar el problema se plantea el log de actividades del usuario, en donde se registras las acciones que este tiene en la aplicación. Pero el problema se presenta cuando se necesita consultar estos registros para analizar las acciones realizadas en un determinado rango de días, por supuesto se quiere ver los días completo, y es aquí donde nos encontramos con el inconveniente.

Si se analiza los registros de la tabla se ve que el registro de la fecha no solo usa el día, sino también la hora.

imagen1

La aplicación de ejemplo cuenta con dos opciones de búsqueda, pero en este primer análisis nos centraremos en el primer botón de nombre “Buscar (sin usar CONVERT)”, al utilizarlo veremos el resultado de la imagen:

imagen2

Pero con solo comparar los registros obtenido con los que se encuentran en la tabla, nos damos cuenta que hay un problema, esta retornando menos filas de las esperadas, la pregunta seria, porque sucedió esto ?

El código utilizado para obtener los registro es el siguiente :

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRange(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

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

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE fecharegistro >= @desde AND fecharegistro <= @hasta";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);

    }

    return dt;
}

El parámetro de la fechas toma el valor directo de los controles que están en la pantalla, pero esta fecha aunque no se conozca también lleva consigo un componente de hora, que puede ser apreciado si se detiene el código y se analiza el valor:

imagen3

El tipo de dato DataTime, lleva una hora aunque esta no se especifique concretamente.

Esto aclara bastante el porque la consulta arroja menos ítems de los esperados, resulta que esta quitando aquellos registros en donde la hora sea menos a las 12 AM

 

Solución del problema


La solución al problema se obtiene usando una funciona en la query, que aplicada sobre los campos de fecha en el filtro quiten la componente de la hora. En realidad no remueve la hora, sino que la normaliza para que esta también tenga las 12 AM, por lo tanto el filtro no descartara ningún registro.

Para esto se hará uso de la función CONVERT, la cual permite convertir entre tipos de datos.

CAST y CONVERT (Transact-SQL)

Si se ejecuta una consulta usando esta función se podría apreciar lo comentado mas arriba:

imagen4

Las fechas de los registros dejan de tener la hora original, ahora todos presentan las 12 AM, esto es justamente lo que ayudara en el filtro.

Entonces si ahora se aplica lo comentado al código se obtiene el resultado esperado (usar el botón “Buscar (usando CONVERT)”):

imagen5

Para que esto resulte se utilizo el siguiente código:

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRangeRemoveHour(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

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

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE CONVERT(smalldatetime, CONVERT(char(10), fecharegistro, 103), 103)  >= @desde 
                        AND CONVERT(smalldatetime, CONVERT(char(10), fecharegistro, 103), 103) <= @hasta";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);
    }

    return dt;
}

El uso del CONVERT aplicado en los campo de “fecharegistro”, es quien quita la componente de la hora y permite aplicar el filtro correctamente.

 

Alternativa usando Between


Como alternativa al método anterior se podría lograr usando el Between en la query para filtrar por el rango de fechas.

En el Form2 del ejemplo se podrá encontrar la implementación de este caso.

public static LogActividades.RegistroActividadesDataTable GetFilterByDateRangeWithBetween(DateTime desde, DateTime hasta)
{
    LogActividades.RegistroActividadesDataTable dt = new LogActividades.RegistroActividadesDataTable();

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

        string sql = @"SELECT Id, Descripcion, fecharegistro, usuario 
                        FROM RegistroActividades
                        WHERE CAST(CONVERT(CHAR(8), fecharegistro, 112) AS INT) BETWEEN CAST(CONVERT(CHAR(8), @desde, 112) AS INT) 
                                    AND CAST(CONVERT(CHAR(8), @hasta, 112) AS INT)";

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@desde", desde);
        cmd.Parameters.AddWithValue("@hasta", hasta);

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(dt);
    }

    return dt;
}

La lógica principal se encontrara en el método creado para realizar la consulta, allí se ha utilizado el BETWEEN para definir el filtro entre un rango, pero este tienen un problema requiere de un formato especial que justamente el CONVERT (con el código 112) nos proporciona.

Esta conversión lleva la fecha al formato yyyyMMdd, o sea si tenemos la fecha 10/06/2010, será formateada a 20100610, claramente un numero, pero primero deberá pasar por un CHAR para adecuar la conversión del formato de la fecha y luego si será convertido a un valor numérico.

 

Ejemplo de Código


El código publicado fue desarrollado con visual Studio 2008.

La base de datos utilizada es el Sql Server Express 2008.

En el proyecto encontraran una carpeta de nombre “Script” que contiene el archivo .sql que podrán usar en el “Sql Server Management Studio” para crear la estructura de la base de datos en caso de tener problemas con el archivo .mdf

En caso de usar la base de datos integrada al servicio de Sql Server, se deberá cambiar la cadena de conexión definida en el App.config

 

[C#] 
[VB.NET] 

sábado, 18 de septiembre de 2010

[WinForms] - Mover UserControl

 

Introducción


El objetivo de este articulo será el revisar algunos aspectos en la creación de User Controls, como ser propiedades, eventos, definición de argumentos en los eventos, propiedades con enumeración.

Consistirá en un control en donde se pueda definir una imagen y a esta asociarle un texto descriptivo, la idea es que el usuario pueda mover el control por la pantalla para posicionarlo de forma correcta.

Contara con al opción para cambiar el texto del label en runtime, además de permitir asignar un estado que bloquear la movilidad.

 

Definición de las propiedades


El control contara con propiedades que definen estados y aspectos visuales, también habrá otras que solo podrán ser usadas solo desde código, como ser el “HasImage”, la cual indica si hay una imagen asociada al control, esta ha sido marcada con el atributo “Browsable” en falso para evitar que sea visualizada en la cuadro de propiedades.

[Browsable(false)]
public bool HasImage
{
    get { return _image != null; }
}


private Image _image;

[Browsable(true)]
[Category("Custom")]
public Image Image 
{
    get { return _image; }
    set { _image = value; }
}

private string _texto;

[Browsable(true)]
[Category("Custom")]
public string Texto
{
    get { return _texto; }
    set 
    { 
        _texto = value;
        lblMensaje.Text = _texto;
    }
}


private EnumEstado _estado;

[Browsable(true)]
[Category("Custom")]
public EnumEstado Estado
{
    get { return _estado; }
    set
    {
        _estado = value;
    }
}

También se definen el atributo “Category”, para que sean agrupadas todas juntas cuando se visualicen en el cuadro de propiedades, lo cual facilita la búsqueda.

 

Evento MouseMove


El permitir que un control tenga la capacidad de movimiento, no es tan directo como parece, con solo asignar el evento MouseMove en el formulario no alcanza para que esto funcione.

O sea hacer esto, no es suficiente:

public void testUserControl1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        TestUserControl us = sender as TestUserControl;
        if (us != null && us.HasImage)
        {
            us.Left += e.X;
            us.Top += e.Y;
        }
    }
}

Si bien al arrastrar el mouse en las zonas libres del User Control se lanzara el evento, si por casualidad el puntero del mouse se sitúa por sobre un control definido en el User Control, el evento se produce en el control interno, sin ser elevado hacia el formulario.

Pero este problema tiene solución, y consiste en atrapar los eventos de los controles internos del User Control que se esta desarrollando y elevar el evento hacia afuera:

public new event MouseEventHandler MouseMove;

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

private void label1_MouseMove(object sender, MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

protected override void OnMouseMove(MouseEventArgs e)
{
    if (this.Estado == EnumEstado.Inactivo)
        return;

    if (this.MouseMove != null)
        this.MouseMove(this, e);
}

La primer línea de este código define el evento MouseMove, pero hay que remarcar que el User Control de por si ya posee este evento lo cual trae conflicto al definir uno propio, es por eso que se usa el modificado “new” en la declaración, allí se esta forzando una redefinición del evento.

El ultimo método donde se realiza un override del OnMouseMove, permite tomar la acción cuando el puntero del mouse se sitúa en una zona libre del User Control, y de esta forma agregarle lógica especializada, como es en este caso que al esta en un estado Inactivo no permita el movimiento.

Los dos métodos restantes permiten capturar los eventos MouseMove de los controles internos, en este caso un PictureBox y un Label, también se la aplica cierta lógica de estados y se lanza el evento hacia el formulario, pero en este caso no se usa un override, sino que se adjunta el evento en la inicialización del User Control, asignado el handler del evento:

public TestUserControl()
 {
     InitializeComponent();

     pictureBox1.MouseMove += new MouseEventHandler(pictureBox1_MouseMove);
     lblMensaje.MouseDown += new MouseEventHandler(label1_MouseMove);
     
     ModoEdicion(false);

     this.Estado = EnumEstado.Activo;
     this.Texto = this.Name;

 }

Cambio del Texto


La acción de cambio de Texto implica dos operaciones, manejo de visibilidad de los controles, el envió del evento informando del cambio al formulario.

Para llevar a acabo esta operación se contara un con menú contextual que será desplegado al realizar click con el botón derecho del mouse sobre el User Control.

private void ModoEdicion(bool modo)
{
    txtMensaje.Visible = modo;
    mnuAceptar.Visible = modo;
    mnuAceptar.Visible = modo;
    mnuCancelar.Visible = modo;

    lblMensaje.Visible = !modo;
    mnuCambiarTexto.Visible = !modo;
}

private void mnuCambiarTexto_Click(object sender, EventArgs e)
{
    ModoEdicion(true);

    txtMensaje.Text = _texto;
    txtMensaje.Select();
    txtMensaje.SelectAll();
}

private void mnuAceptar_Click(object sender, EventArgs e)
{
    this.Texto = txtMensaje.Text;

    ModoEdicion(false);

    if (CambioMensaje != null)
        CambioMensaje(this, new MensajeEventArgs(_texto));
}

private void mnuCancelar_Click(object sender, EventArgs e)
{
    ModoEdicion(false);
}

El primer método simplemente cumple la función de implementar como el cambio de estado afecta a la visibilidad de los controles, inicialmente se visualiza lo escrito en el Label, y una modalidad de edición se despliega el TextBox para ingresar la nueva descripción.

Los demás eventos pertenecen a los ítems del menú, que realizaran las acciones necesarias para el edición, por ejemplo la opción “mnuAceptar”, no solo asigna los escrito a la propiedad, sino que además lanza un evento informando al formulario que ha cambiado el mensaje asignado al control.

El formulario por su parte se adjunta al evento expuesto:

private void Form1_Load(object sender, EventArgs e)
{
    testUserControl1.CambioMensaje += new CambioMensajeEventHandler(testUserControl1_CambioMensaje);
}

void testUserControl1_CambioMensaje(object sender, MensajeEventArgs e)
{
    TestUserControl usrcontrol = sender as TestUserControl;

    if (usrcontrol != null)
        MessageBox.Show(string.Format("El texto ha cambiado: {0}", e.Texto));
}

Algo interesante es como se hace uso del argumento “e” den método del evento para tomar el valor ingresado en el control y desplegarlo en el MessageBox, este se logra mediante la definición de un EventArgs creado especialmente para este fin.

La declaración del evento se logra mediante dos simples líneas:

public delegate void CambioMensajeEventHandler(object sender, MensajeEventArgs e);

public partial class TestUserControl : UserControl
{

	public event CambioMensajeEventHandler CambioMensaje;
		
	.
	.
	.
}

Pero como comentamos el delegate hace uso de un argumento en el evento especial, el cual se define al final del control:

public class MensajeEventArgs : EventArgs
{
    public MensajeEventArgs(string texto)
    {
        _texto = texto;
    }

    private string _texto;

    public string Texto
    {
        get { return _texto; }
    }
}

Ejemplo de código


El código fue desarrollado usando Visual Studio 2008.

[C#] 
[VB.NET]