Mostrando entradas con la etiqueta WinForm. Mostrar todas las entradas
Mostrando entradas con la etiqueta WinForm. Mostrar todas las entradas

jueves, 22 de mayo de 2014

Comunicar Formularios

 

Introducción


Existen varias formas pasar información entre formularios, especialmente si queremos hacerlo sin generar una fuerte dependencia entre estos.

Este mismo tema lo había abordado en artículos anteriores por lo que retomaremos el análisis de las distintas formas de lograr la comunicación sin generar dependencia entre los forms

Los artículos anteriores en los cuales he tratado el tema:

Comunicar formularios de forma desacoplada

Comunicar formularios MDI

Para ejemplificar haremos uso de un form que permite listar y editar productos de la base de datos Northwind.

image

Analizaremos 3 formas de comunicar formularios y enviar datos entre ellos:

  • Usando Interfaces
  • Usando eventos
  • Usando el evento FormClosed

 

Usando interfaces


Una interfaz permite definir un contrato entre clases, permitiendo que una invoque la funcionalidad de cualquier otra que respete ese contrato definido.

Recordemos que los formularios en definitiva no son mas que clases que se instancian por lo que puede aplicarse los mismos conceptos de POO (Programación orientada a objetos).

La implementación de esta técnica se encuentra en la búsqueda de un proveedor en el form de edición de los productos.

image

El primer paso será definir la interfaz

image

siendo implementada por el formulario padre receptora de la información seleccionada por el usuario

SNAGHTML5a01d235

El form padre será quien instancie el form hijo pasando su instancia por parámetro en el constructor, además de algún otro valor adicional de ser necesario, como en este caso un filtro para limitar los ítems visualizados en el grid de selección.

 image

 

Desde el form hijo (frmFindSupplier) se define el constructor que recibirá los parámetro

SNAGHTML5a09bacf

La variable “caller” contendrá la instancia del form padre que implementa la interfaz, mientras que “_supplierName” tendrá el input del usuario para filtrar las entidades.

Al seleccionar un ítem del grid se dará por seleccionada la entidad y se enviara la acción al form padre a través de la instancia invocando el método definido en la interfaz.

image

Primero se valida que exista selección en el grid, se recupera la entidad seleccionada pasándola como parametro al método Selected() definido en la interfaz, por ultimo se cierra el formulario. El form padre recibe el valor seleccionado y actualiza los controles o entidades que hagan falta.

La secuencia completa quedaría de esta forma

SNAGHTML5b152bbf

  1. El form padre (frmEditProductos) implementa la interfaz.
  2. En el botón de búsqueda se crea la instancia del form hijo (frmFindSupplier), pasando como parámetro en el constructor la instancia del forma padre (utilizando this)
  3. El form hijo recibe en el constructor los valores por parámetro
  4. Se selecciona un ítem del grid y se invoca al método de la interfaz
  5. El form padre recibe la selección y actualiza los datos

Como ventaja se puede mencionar que cualquier form que implemente la interfaz podrá reutilizar la funcionalidad de búsqueda implementada al no quedar acoplados los formularios que interactuan.

 

Usando eventos


Permite enviar una acción directa al form que se subscriba al evento.

El ejemplo va a estar representado en la comunicación del form de edición de productos y la búsqueda de categorías.

image

Partamos analizando la implementación del form hijo (frmFindCategory), este expone el evento, que es lanzado cuando se selecciona una categoría del grid

SNAGHTML5a76cd3

Para definir datos específicos en el intercambio de información se crea una clase que hereda de EventArgs

image

 

Desde el form padre (frmEditProductos) simplemente se debe adjuntar al evento la instancia del form hijo

image

 

Repasemos como seria la ejecución en secuencia

SNAGHTML5c710fe

 

  1. El form hijo expone el evento, utilizado para informar la selección a quien lo haya invocado.
  2. Se define una clase que hereda de EventArgs para poder pasar información adicional como argumento del evento.
  3. El form padre que realiza la búsqueda crea la instancia del forma hijo y se adjunta al evento, además crea el método que recibirá la acción.
  4. Cuando se selecciona un ítem del grid se valida si alguien esta adjunto en el evento y se invoca.

 

Actualizar los datos al cerrar el form (evento FormClosed)


Para actualizar un grid luego de una acción en otro form podría utilizarse eventos existentes, como ser el FormClosed, con este evento se puede realizar una acción al cerrar un form

image

 

Analicemos los pasos

SNAGHTMLaeed38d

 

  1. En el form padre (frmAdminProducts) instancia el form hijo (frmEditProductos) y se adjunta el evento de cierre del form
  2. Cuando se presiona el boton de aceptar o cancelar se devuelve el DialogResult cerrando el form, esto lanza el evento FormClosed
  3. Desde el form padre se atrapa el evento y se realiza la recarga de los datos del grid

 

Descarga


El articulo fue desarrollando usando Visual Studio 2012, la base de datos es Sql Compact (.sdf)

Durante la compilación se descargaran las dependencias por medio de Nuget.


[C#]
 

domingo, 17 de febrero de 2013

[Winform] Instanciar Form conociendo su nombre

 

Introducción


Conociendo solo el nombre del formulario se abra una instancia del mismo.

Para poder realizar esta acción es necesario conocer el nombre del form, pero también bajo que namespace se ubica.

image

por ejemplo para abrir el form de alta de cliente se identifica

SNAGHTML239b6cc

pero si otro form se encuentra dentro de una carpeta es necesario definir el namespace donde se encuentra

SNAGHTML23d2650

Uso de Reflection


Para poder abrirlo vamos a necesitara Reflection el cual no permitirá descubrir el Type del form basado en la unión del namespace + nombre

es por eso que se define:

using System.Reflection;

 

private void btnAbrirForm_Click(object sender, EventArgs e)
{
    Assembly asm = Assembly.GetEntryAssembly();
    Type formtype = asm.GetType(string.Format("{0}.{1}", txtNamespace.Text, txtNombreForm.Text));

    Form f = (Form)Activator.CreateInstance(formtype);
    f.Show();
}

Se toma el assembly donde se encuentran los formularios y se obtiene el tipo, con el cual se creara la instancia dinámicamente

Código


[C#]
 

domingo, 10 de febrero de 2013

[WinForms] Verificar si el form esta abierto (instancia única)

 

Introducción


Determinar si un form ha sido abierto, evitando que una nueva instancia sea creada.

 

Validar instancia entorno SDI


Para validar la existencia de la instancia de un formulario se hará uso del Application.OpenForms, con este y la ayuda de linq podremos buscar la existencia del form abierto.

image

La selección de botón abrirá una instancia, pero las siguientes pulsaciones solo harán que el mismo form pase al frente.

 

private void btnAbrirFormclientes_Click(object sender, EventArgs e)
{
    //se localiza el formulario buscandolo entre los forms abiertos 
    Form frm = Application.OpenForms.Cast<Form>().FirstOrDefault(x => x is frmEdicionCliente);

    if (frm != null)
    {
        //si la instancia existe la pongo en primer plano
        frm.BringToFront();
        return;
    }
    
    //sino existe la instancia se crea una nueva
    frm = new frmEdicionCliente();
    frm.Show();
}

El truco esta en poder determinar la lista de forma abiertos

image

 

Validar instancia entorno MDI


En un entorno MDI la validación es similar solo cambia la propiedad usada para determinar la lista de forms abiertos

image

 

private void abrirFormClienteToolStripMenuItem_Click(object sender, EventArgs e)
{
    //se localiza el formulario buscandolo entre los forms abiertos 
    Form frm = this.MdiChildren.FirstOrDefault(x => x is frmEdicionCliente);

    if (frm != null)
    {
        //si la instancia existe la pongo en primer plano
        frm.BringToFront();
        return;
    }
    
    //se abre el form de clientes
    frm = new frmEdicionCliente();
    frm.MdiParent = this;
    frm.Show();

}

 

Código


domingo, 3 de febrero de 2013

[WinForms] Listar formulario del proyecto

 

Introducción


Listar los forms del proyecto puede parecer una tarea a simple vista fácil, pero tiene sus particularidades.

En el ejemplo se listara los nombre de los formularios, pudiendo abrirlo mediante la selección en una lista.

SNAGHTML2eee2d7c

 

Listar Formularios


Para listar los formularios podremos ayudarnos de linq, este nos permitirá filtrar por el tipo de clase del cual heredan los forms.

Para obtener las propiedades (como ser el Text) del formulario se requiere una instancia del mismo. Recordemos que en .net todos son objetos, por lo que un form requiere de una instancia para poder acceder a sus propiedades.

 

private void frmPrincipal_Load(object sender, EventArgs e)
{
    
    Assembly asm = Assembly.GetExecutingAssembly();
    
    List<Form> formList = asm.GetTypes()
                                .Where(x => x.IsSubclassOf(typeof(Form)))
                                .Select(x => (Form)Activator.CreateInstance(x))
                                .ToList();


    lstForms.DisplayMember = "Text";
    lstForms.DataSource = formList;
    
}

También se puede realizar la misma tarea sin hacer uso de linq

 

private void frmPrincipal_Load(object sender, EventArgs e)
{

    Assembly asm = Assembly.GetExecutingAssembly();

    List<Form> formList = new List<Form>();

    foreach (Type item in asm.GetTypes())
    {
        if (item.IsSubclassOf(typeof(Form)))
        {
            formList.Add((Form)Activator.CreateInstance(item));
        }
    }

    lstForms.DisplayMember = "Text";
    lstForms.DataSource = formList;

}

Código


jueves, 17 de mayo de 2012

[Winforms] TreeView– Cambiar Estado Node con CheckBox

 

Introducción

Muchas veces es necesario cambiar el estado de ítems predecesores o hijo según ciertos criterios, el control TreeView por posee una estructura jerárquica en sus datos es un caso de esta situación.

 

Validar estado de los nodos


En la implementación de código que se puede observar aplica conceptos simples como de validación de estados de los nodos del treeview.

Básicamente consiste en dos parte

  •    validar si el nodo tiene un nodo padre, para lo cual será necesario verificar si todos los hijos están marcados
  •    cambiar el estado a todos los hijos según la marca de este nodo

Estas acciones se realizan sobre el mismo evento ya que implica de forma contextual al nodo que interviene en la acción

 

private void tvCategoryproducts_AfterCheck(object sender, TreeViewEventArgs e)
{
    //
    // Se remueve el evento para evitar que se ejecute nuevamente por accion de cambio de estado 
    // en esta operacion
    //
    tvCategoryproducts.AfterCheck -= tvCategoryproducts_AfterCheck;

    //
    // Se valida si el nodo marcado tiene presedente
    // en caso de tenerlo se debe evaluar los nodos al mismo nivel para determinar si todos estan marcados, 
    // si lo estan se marca tambien el nodo padre
    //
    if (e.Node.Parent != null)
    {
        bool result = true;
        foreach (TreeNode node in e.Node.Parent.Nodes)
        {
            if (!node.Checked)
            {
                result = false;
                break;
            }
        }

        e.Node.Parent.Checked = result;

    }

    //
    // Se valida si el nodo tiene hijos
    // si los tiene se recorren y asignan el estado del nodo que se esta evaluando
    //
    if (e.Node.Nodes.Count > 0)
    {
        foreach (TreeNode node in e.Node.Nodes)
        {
            node.Checked = e.Node.Checked;
        }
    }


    tvCategoryproducts.AfterCheck += tvCategoryproducts_AfterCheck;

}

 

Optimización usando Linq

Linq nos puede evitar tener que escribir un montón de código, sino lo creen solo basta con dar una mirada a la implementación alternativa que evalúa los nodos marcados

En este caso hice uso de los método de extensión ayudado con lambda, pero se podría haber usado la notación linq sin problemas

 

private void tvCategoryproducts_AfterCheck_UsingLinq(object sender, TreeViewEventArgs e)
{
    //
    // Se remueve el evento para evitar que se ejecute nuevamente por accion de cambio de estado 
    // en esta operacion
    //
    tvCategoryproducts.AfterCheck -= tvCategoryproducts_AfterCheck_UsingLinq;

    //
    // Se valida si el nodo marcado tiene presedente
    // en caso de tenerlo se debe evaluar los nodos al mismo nivel para determinar si todos estan marcados, 
    // si lo estan se marca tambien el nodo padre
    //
    if (e.Node.Parent != null)
    {
        e.Node.Parent.Checked = !e.Node.Parent.Nodes.Cast<TreeNode>().Any(x => !x.Checked);
    }

    //
    // Se valida si el nodo tiene hijos
    // si los tiene se recorren y asignan el estado del nodo que se esta evaluando
    //
    if (e.Node.Nodes.Count > 0)
    {
        e.Node.Nodes.Cast<TreeNode>().ToList().ForEach(x => x.Checked = e.Node.Checked);
    }


    tvCategoryproducts.AfterCheck += tvCategoryproducts_AfterCheck_UsingLinq;

}

Código


Se utilizo Visual Studio 2010 y Sql Compect 3.5

  [C# SkyDrive]

sábado, 17 de septiembre de 2011

Filtros Condicionales (2/2) – Implementar filtros múltiples

 

Introducción


Este artículo representa continuación del anterior

Filtros Condicionales (1/2)

en realidad se podría ver como una extensión, ya que aquí el objetivo consiste en poder aplicar múltiples filtro usando de cursos, y no solo uno como fue en el artículo previo

La interacción en la pantalla, para esta funcionalidad en concreto, esta dada por los siguientes pasos:

image 

Algo que hay que aclarar antes de abordar las soluciones planteadas es que la instrucción IN de sql no soporta de forma directa el uso de parámetros, es por eso que existen actualmente varios caminos a tomar ante esta situación, aquí solo expondré dos de ellos, pero existen algunos otros.

Entre los temas tratados se podrán encontrar

  1. Comunicación entre formularios
  2. Filtro IN, concatenado el string
  3. Filtro por medio de XML

 

1- Comunicación entre formularios


En el formulario de búsqueda (frmBusqueda) encontrar un código como el siguiente

private void btnBuscarCursos_Click(object sender, EventArgs e)
{
    List<CourseEntity> selectedCourses = txtCursos.Tag as List<CourseEntity>;

    using (frmSeleccionarCursos frmcursos = new frmSeleccionarCursos(selectedCourses))
    {
        if (frmcursos.ShowDialog(this) == DialogResult.OK)
        {
            txtCursos.Tag = frmcursos.CursosSeleccionados;
            txtCursos.Text = string.Join(", ", frmcursos.CursosSeleccionados.Select(x => x.Title).ToArray());
        }
    }
}

Como frmSeleccionarCursos se abre de forma modal puede esperarse en el ShowDialog() hasta tanto el Form sea cerrado, cuando esta operación se lleve a cabo y se detecte la aceptación satisfactoria del Form se procede a tomar los cursos seleccionados y asignarlos al control que contendrá la información.

En este caso se hace uso de la propiedad Tag del Textbox para mantener la información seleccionada

En el formulario de selección dispone de una propiedad para que el formulario que lo invoco pueda acceder a la información sin necesidad de recurrir directo de los controles del propio Form. A su vez solo el evento del botón Aceptar es que el cierra el form con una resultado aceptado para procesar la selección.

public List<CourseEntity> CursosSeleccionados
{
    get
    {
        return lstCursosSelected.Items.Cast<CourseEntity>().ToList();
    }
}

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

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

 

2 -Filtro IN, concatenado el string


Esta primera implementación se podría decir que es la mas estándar y directa, aunque hay que remarcar que no es la mas bonita.

En la clase PersonDAL se cuenta el método Select()

public static List<PersonEntity> Select(PersonCriteria filter)
{
    string sql = @"SELECT   P.PersonID,
                            P.LastName,
                            P.FirstName,
                            P.HireDate,
                            P.EnrollmentDate
                   FROM Person P
                        LEFT JOIN CourseInstructor CI 
                        ON P.PersonID = CI.PersonID
                   WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                    AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                    AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                    AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                    AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                    AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                    AND ((@Course IS NULL) OR (CI.CourseID IN ({0})))";

    if (filter.Course != null)
    {
        string courseFilter = string.Join(",", filter.Course.ConvertAll(x => x.CourseID.ToString()).ToArray());
        sql = sql.Replace("{0}", courseFilter);
    }

    List<PersonEntity> list = new List<PersonEntity>();

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

        SqlCommand cmd = new SqlCommand(sql, conn);

        if (string.IsNullOrEmpty(filter.FirstName))
            cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
        else
            cmd.Parameters.AddWithValue("@FirstName",  filter.FirstName);

        cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

        cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@Course", filter.Course == null ? (object)DBNull.Value : "");

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadPerson(reader));
        }

        return list;
    }

}

El mismo sigue todas las reglas mencionadas en la parte 1 del articulo, solo que filtro de cursos tiene una particularidad.

Es preciso notar las líneas 19-23, en estas es cuando se une al string de la consulta principal, la lista de cursos seleccionados.

La línea 46, sigue representando la anulación o no del filtro del cursos, esto es necesario en caso de no enviarse ningún ítem en la selección.

 

3 -Filtro por medio de XML


Si bien esta implementación no es estandar para todas las base de datos, ya que requiere de soporte para xml, si es la que mejor cierra en cuanto al uso de parámetros.

En este caso la lista de cursos seleccionado es convertida a un xml, el cual se asigna al parámetro para luego unirlo al join de la consulta.

public static List<PersonEntity> SelectByXml(PersonCriteria filter)
        {
            string sql = @"
                           DECLARE @idoc  int
                           EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

                           SELECT   P.PersonID,
                                    P.LastName,
                                    P.FirstName,
                                    P.HireDate,
                                    P.EnrollmentDate
                           FROM Person P
                                LEFT JOIN CourseInstructor CI 
                                ON P.PersonID = CI.PersonID
                                    LEFT JOIN OPENXML(@idoc, '/courses/course', 2)
                                    WITH (id  int 'text()') AS CL ON CI.CourseID = CL.id 
                           WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                            AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                            AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                            AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                            AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                            AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                            AND ((@Course IS NULL) OR (CL.id IS NOT NULL))";


            List<PersonEntity> list = new List<PersonEntity>();

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

                SqlCommand cmd = new SqlCommand(sql, conn);

                if (string.IsNullOrEmpty(filter.FirstName))
                    cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
                else
                    cmd.Parameters.AddWithValue("@FirstName", filter.FirstName);

                cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

                cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

                cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

                if (filter.Course != null)
                {
                    XElement root = new XElement("courses");
                    List<XElement> couseList = filter.Course.ConvertAll(x => new XElement("course", x.CourseID));
                    root.Add(couseList.ToArray());

                    cmd.Parameters.AddWithValue("@Course", root.ToString());
                }
                else
                {
                    cmd.Parameters.AddWithValue("@Course", DBNull.Value);
                }

                
                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())
                {
                    list.Add(LoadPerson(reader));
                }

                return list;
            }

        }

Las líneas 47-58, implementan la conversión a xml de los cursos, esta toman la forma

<courses>
  <course>1045</course>
  <course>1061</course>
</courses>

o sea cada curso seleccionado representa un tag en el xml

La consulta tiene algunas particularidades como ser las dos primeras líneas

DECLARE @idoc  int
EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

cuya finalidad es inicializar el xml que luego es usado en el join

LEFT JOIN OPENXML(@idoc, '/courses/course', 2)
WITH (id  int 'text()') AS CL ON CI.CourseID = CL.id

Aquí el text() representa justamente el contenido del tag, y por medio del /courses/course (el cual es un selector de XPath), se toma cada tag de curso.

 

Para mas información sobre como trabajar el xml en T-SQL, un excelente recurso es el MSDN Library

OPEN XML

Usar OPENXML

Se puede además realizar pruebas puntuales del xml para entender su funcionamiento, por ejemplo

DECLARE  @Course As VARCHAR(1000)
SET  @Course = N'<courses>
  <course>1045</course>
  <course>1061</course>
</courses>'

DECLARE @idoc  int
EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

SELECT * FROM OPENXML(@idoc, '/courses/course', 2)
          WITH (id  int 'text()')

Estas podrían ser ejecutadas en el Sql Server Management Studio

 

Links Útiles


Arrays and Lists in SQL Server 2000 and Earlier

 

Ejemplo de Código


La base de datos fue creada con Sql Server Express 2008 R2, en caso de tener problemas con al misma en el proyecto “DataAccess” esta la carpeta “script” con el .sql que puede usar para crear estructura y datos.

[C#] 
 

domingo, 11 de septiembre de 2011

Filtros Condicionales (1/2)

 

Introducción


Al desarrollar una aplicación un aspecto del cual seguramente habrá que pensar es como otorgar al usuario información que el sea útil y simple de analizar.

En este punto juega un papel fundamental como diseñar los filtros aplicado a las entidades, para poder se lo mas preciso posible en la búsqueda.

Es por eso que en este artículo se plantea la implementación de búsqueda por medio de clases que denominare Criteria, ya que esta serán las encargadas de proporcionar los datos seleccionados por el usuario en la pantalla.

Como punto ventajoso de esta técnica se podría mencionar que la clase evita tener que redefinir los parámetros del método de búsqueda cada vez que se quiera extender la funcionalidad. Esto es muy importante si se tiene la idea de aplicar algún patrón como ser el Repository.

Los requerimientos cambiantes en las aplicaciones hacen que en un primer momento un filtro simple de dos campos cubra la necesidad, pero el uso diario por parte del usuario ira cambiando esta visión, solicitando agregar nuevo filtros, el uso de una clase como témplate de filtro facilita esta tarea ya que el método no cambia en sus parámetros, solo la clase Criteria se vera afectada y por supuesto la query que aplique el filtro.

 

Estructura de la solución


Analicemos un poco como esta estructurado el ejemplo.

 image

Solo son dos capas, una de acceso a datos y otro de presentación, se realizo de esta forma para no complicar el desarrollo del ejemplo, porque aquí no se pretendía mostrar una arquitectura completa, sino solo apuntar a entender como aplicar filtros de forma correcta.

Es muy importante remarcar la clase de nombre PersonCriteria, la cual actuara solo cuando los filtros entran en accion, y la clase PersonEntity, quien representa de la entidad de negocio.

Analicemos la diferencia entre las dos entidades:

public class PersonCriteria
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public DateTime? EnrollmentDateFrom { get; set; }
    public DateTime? EnrollmentDateTo { get; set; }

    public DateTime? HireDateFrom { get; set; }
    public DateTime? HireDateTo { get; set; }

    public CourseEntity Curse { get; set; }
}
public class PersonEntity
{
    public int PersonID {get; set;}
    public string LastName { get; set; } 
    public string FirstName { get; set; } 

    public DateTime? HireDate { get; set; }
    public DateTime? EnrollmentDate { get; set; } 
}

Las propiedades de cada clase tienen unas cuantas diferencias porque sus responsabilidades y lo que representantas son diferentes.

Análisis de la Presentación


La pantalla que se presentara al usuario incluirá varios filtros que actúan de forma combinada.

image

El punto clave en al presentación será la lógica encargada de cargar el criterio de búsqueda:

private void btnFiltrar_Click(object sender, EventArgs e)
{
    PersonCriteria filter = new PersonCriteria()
    {
        FirstName = txtNombre.Text,
        LastName = txtApellido.Text,
        EnrollmentDateFrom = chkFechaInscripcionDesde.Checked ? (DateTime?)dtpFechaInscripcionDesde.Value : null,
        EnrollmentDateTo = chkFechaInscripcionHasta.Checked ? (DateTime?)dtpFechaInscripcionHasta.Value : null,
        HireDateFrom = chkFechaContratacionDesde.Checked ? (DateTime?)dtpFechaContratacionDesde.Value : null,
        HireDateTo = chkFechaContratacionHasta.Checked ? (DateTime?)dtpFechaContratacionHasta.Value : null,
        Curse = Convert.ToInt32(cmbCourse.SelectedValue)== -1 ? null : new CourseEntity() { CourseID = Convert.ToInt32(cmbCourse.SelectedValue) }
    };

    dgvPersonList.DataSource = PersonDAL.Select(filter);

}

La presentación conoce como asignar la propiedad con el dato correcto, es porque eso que allí se observan validaciones para determinar si asignar el dato o no.

 

Acceso a Datos


Esta es la capa con mayor responsabilidad, lógicamente porque será la encargada de aplicar el filtro, pero todo se reduce a un simple truco en la query que habilita o no el filtro en el WHERE

 

public static List<PersonEntity> Select(PersonCriteria filter)
{
    string sql = @"SELECT   P.PersonID,
                            P.LastName,
                            P.FirstName,
                            P.HireDate,
                            P.EnrollmentDate
                   FROM Person P
                        LEFT JOIN CourseInstructor CI 
                        ON P.PersonID = CI.PersonID
                   WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                    AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                    AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                    AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                    AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                    AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                    AND ((@Course IS NULL) OR (CI.CourseID = @Course))";

    List<PersonEntity> list = new List<PersonEntity>();

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

        SqlCommand cmd = new SqlCommand(sql, conn);

        if (string.IsNullOrEmpty(filter.FirstName))
            cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
        else
            cmd.Parameters.AddWithValue("@FirstName",  filter.FirstName);

        cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

        cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@Course", filter.Curse == null ? (object)DBNull.Value: filter.Curse.CourseID);


        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadPerson(reader));
        }

        return list;
    }

}

Analicemos una sección pequeña para entenderlo:

((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))

en esta se tienen dos parte, la primera compara el parámetro con NULL, por lo tanto si desde el código .net enviamos un DbNull.Value estaríamos anulando este filtro, porque esta comparación aplicaría siempre para todos los registros.

En la segunda mitad será donde el filtro se aplica, este al tener un valor no se vera afectado por la primer sección.

Además quise mostrar que existen varias formas desde código de asignar el parámetro, es por eso que la línea 27 la asignación del parámetro se efectúa con un if completo, mientras que el resto lo hace en sola linea, esto fue solo para remarcar que no hay una única forma de asignar el parámetro, si la comparación en una línea queda compleja (o poco clara) se puede pasar a varias líneas.

Este es todo el truco, no es nada difícil de implementar, y conocerlo puede ayudar brindar al usuario filtros mas potentes que agreguen valor a la aplicación.

Ejemplo de Código


La base de datos fue creada con Sql Server Express 2008 R2, en caso de tener problemas con al misma en el proyecto “DataAccess” esta la carpeta “script” con el .sql que puede usar para crear estructura y datos.

 

[C#] 
[VB.NET] 

domingo, 17 de julio de 2011

[Winforms] Singleton - Pasar datos entre formularios

 

Introducción


En mas de una oportunidad surgió el hecho de comunicar formulario y pasar información entre los mismos, cuando estos se invocan uno a otra la solución es relativamente simple, pero que sucede cuando las distintas invocaciones deben capturar y conservar los datos en cada paso que realizan.

Es aquí donde entra en juego un patrón bien simple de aplicar pero no por eso menos potente, hago referencia al singleton.

Para el presente artículo platearemos un escenario nada complicado, simplemente algunos forms donde se capturan datos números, pero esta información deberá ser usada en la ultima pantalla, la cual realiza el calculo con los datos recolectados en los pasos previos.

image

 

1- Singleton,  Pasar datos entre formularios


Implementar el patrón es muy simple, solo se deben aplicar ciertas reglas que abrigan a tomar la misma instancia.

public class RecolectarDatos
{
    private static RecolectarDatos datos;

    private RecolectarDatos()
    {
    }

    public static RecolectarDatos Instance()
    {
        if (datos == null)
        {
            datos = new RecolectarDatos();
        }

        return datos;
    }

    public int Dato1 { get; set; }
    public int Dato2 { get; set; }
    public int Dato3 { get; set; }

    public int Dato4 { get; set; }
    public int Dato5 { get; set; }
}

El primer punto es definir una instancia static del propia clase singleton (línea 3)

Segundo, definir el constructor de la clase como privado (línea 5), de esta forma no se podrán crear instancia de la clase, salvo desde dentro de la propia clase.

Tercero, definir un método o propiedad static, la cual será el punto central por donde se podrá recuperar una instancia de la clase (línea 9), en este punto es donde se valida y crea la instancia de la variable static definida mas arriba, es por esto que siempre se retorna esta misma instancia.

El resto del código pueden ser método, propiedades, otras instancias de clases, o sea se puede aplicar cualquier concepto de POO sin problemas, porque el patron singleton devuelve una única instancia, pero esta tiene comportamiento como cualquier otro objeto.

 

2 – Eventos, Informar de cambios entre formulario


Al llegar al ultimo formulario era necesario informar al primero que las operaciones se realizaron correctamente, pero entre estos formulario no hay visibilidad alguna, no se aplico ninguna pasamano de instancias entre los formularios, o algo que los comunique.

Es por eso que la subscripción a un evento genérico resulta ideal para avisar de una determinada acción sin ser necesario pasar instancias.

 

public static class CompleteEvents
{
    public delegate void CompleteHandler(CompleteEventArgs args);
    public static event CompleteHandler Complete;

    public static void RaiseEvent(int calculo)
    {
        if(Complete != null)
            Complete(new CompleteEventArgs(calculo));
    }
}

public class CompleteEventArgs : EventArgs
{
    public CompleteEventArgs(int calculo)
    {
        this.Calculo = calculo;
    }

    public int Calculo { get; set; }
}

El formulario principal se adjunta al evento definido en la clase, mientras que en el ultimo formulario es invocado el método RaiseEvent() que lanzara la acción.

Adicionalmente se define un argumento del evento para informar el resultado del calculo, igualmente en este caso por aplicar singleton, quizás no era necesario, ya que se podría haber tomado directamente los valores de cada dato recolectado y realizar nuevamente el calculo.

 

Código del ejemplo


[C#] 
[VB.NET] 

sábado, 2 de julio de 2011

[WinForm] Listar Archivos del Directorio seleccionado

 

Introducción

En el presente artículo se implementa un ejemplo sencillo de como listar directorios con su contenido, incluyendo además el icono asociado a la extensión del archivo.

Algunas pruebas

Si bien durante las primeras pruebas se hizo uso de la funcionalidad

Icon.ExtractAssociatedIcon

para obtener la imagen relacionada con la extensión del archivo, note que no siempre se recuperaba el icono correcto, es por eso que investigando algo mas en detalle encontré que por medio de las api se puede realizar esto mismo.

La implementación de esta se puede hallar en el archivo ExtractIcon.cs

Si bien en el código solo he dejado una de estas alternativas

private void LoadFileList(string path)
        {
            string[] files = Directory.GetFiles(path);

            lvFiles.Items.Clear();

            foreach (var item in files)
            {
                string extension = Path.GetExtension(item);

                if (!imlSmall.Images.ContainsKey(extension))
                {
                    Icon iconSmall = FileExplorer.ExtractIconClass.GetIcon(item, true);
                    imlSmall.Images.Add(extension, iconSmall);
                    Icon iconLarge = FileExplorer.ExtractIconClass.GetIcon(item, false);
                    imlLarge.Images.Add(extension, iconLarge);
                }

                ListViewItem listviewItem = new ListViewItem(Path.GetFileName(item), extension);
                lvFiles.Items.Add(listviewItem);

            }
        }

podrían cambiarse las líneas 13 a 16 para hacer uso del

Icon.ExtractAssociatedIcon()

y comprobar por uno mismo la diferencia en el icono que se obtiene.

 

Ejemplo de código

Debo remarcar que la implementación que se encuentre en la clase ExtractIcon.cs, no es de mi autoría, sino que use de guía ejemplo como ser

Extracting Icons from Files

ExtractIconClass.cs

Building a Better ExtractAssociatedIcon

 

[C#]
[VB.NET]

lunes, 20 de junio de 2011

[Winforms] Control global de Errores – Implementar Log

 

Introducción

He notado en varias oportunidades que en ciertas circunstancias surgen errores no controlador que son difíciles de rastrear

Estos es consecuencia de un descuido, al no agregar correctamente en todos los puntos importantes el try..catch, o simplemente por la lógica de la aplicación por se grande implicaría un gran esfuerzo poner en cada evento el control de errores.

Es por eso que definir un control global a nivel de aplicación podría ayudar, atrapando el problema y registrando que sucedió y donde, esta información es muy preciada cuando se esta perdido y no se sabe que produce el problema, mas cuando se esta en el entorno de producción y no se cuenta con una herramienta de debug

En este artículo se trataran los siguientes temas:

  1. Control global de errores
  2. Log usando System.IO.Log

 

1- Control Global Errores

El implementar la lógica para controlar globalmente los errores no es nada difícil, el truco esta en adjuntarse a un evento, concretamente:

Application.ThreadException

Un buen lugar para realizar esta asignación del evento es el archivos Program.cs, dentro del método Main()

[C#]

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);

    Application.Run(new Form1());
}

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
    MessageBox.Show(e.Exception.Message);
}

Para lograr esto mismo en vb.net requiere de algunos paso adicionales, ya que vb.net no brinda un acceso transparente al método Main, ni permite que este sea asignado como inicio de la aplicación, pero en este articulo

Winforms, realizar tareas antes de inicializar aplicación

explico como destrabar esta situación para poder así implementarlo como lo harían en c#, definiendo el Main() como inicio de la aplicación.

[VB.NET]

<STAThread()> _
Friend Shared Sub Main()

    Application.EnableVisualStyles()
    Application.SetCompatibleTextRenderingDefault(False)

    AddHandler Application.ThreadException, AddressOf Application_ThreadException

    Application.Run(New Form1())

End Sub

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    MessageBox.Show(e.Exception.Message)

End Sub

El código en ambos lenguajes preserva el mismo concepto, pero la forma en como se adjunta el evento difiera bastante.

Entonces, ahora si sucediera algo como lo reflejado en la imagen

imagen1

O sea, si se ingresara caracteres en el calculo, el no atrapar el error en un bloque try..catch, haría que la aplicación finalice de forma brusca cerrándose, pero al tener definido el evento ThreadException, entraría en acción tomando el error y mostrando el mensaje.

Con estos simples paso ya tenemos el control global de errores implementado.

 

2- Log usando System.IO.Log

Si bien en una primer instancia hacer uso de un mensaje podría ayudar a detectar el problema en la aplicación, este podría evolucionar en un log a un archivo para dejar tracking de lo sucedido, en este caso no solo se pondría el mensaje del problema, sino que además se podría agregar el StackTrace para poder analizar que métodos se fueron ejecutando hasta causar el fallo.

En este caso el log a un archivos será muy simple

[C#]

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            using(FileRecordSequence record = new FileRecordSequence("application.log", FileAccess.Write))
            {

                string message = string.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, 
                                                                                    e.Exception.Message, 
                                                                                    e.Exception.StackTrace);

                record.Append(CreateData(message), SequenceNumber.Invalid, 
                                                            SequenceNumber.Invalid, 
                                                            RecordAppendOptions.ForceFlush);
            }
        }


        private static IList<ArraySegment<byte>> CreateData(string str)
        {
            Encoding enc = Encoding.Unicode;

            byte[] array = enc.GetBytes(str);

            ArraySegment<byte>[] segments = new ArraySegment<byte>[1];
            segments[0] = new ArraySegment<byte>(array);

            return Array.AsReadOnly<ArraySegment<byte>>(segments);
        }

[VB.NET]

Private Shared Sub Application_ThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)

    Using record As New FileRecordSequence("application.log", FileAccess.Write)

        Dim message As String = String.Format("[{0}]Message::{1} StackTrace:: {2}", DateTime.Now, e.Exception.Message, e.Exception.StackTrace)

        record.Append(CreateData(message), SequenceNumber.Invalid, SequenceNumber.Invalid, RecordAppendOptions.ForceFlush)
    End Using

End Sub


Private Shared Function CreateData(ByVal str As String) As IList(Of ArraySegment(Of Byte))

    Dim enc As Encoding = Encoding.Unicode

    Dim _array As Byte() = enc.GetBytes(str)

    Dim segments As ArraySegment(Of Byte)() = New ArraySegment(Of Byte)(0) {}
    segments(0) = New ArraySegment(Of Byte)(_array)

    Return Array.AsReadOnly(Of ArraySegment(Of Byte))(segments)

End Function

Ahora el evento global de errores tiene bastante mas código, en donde se arma un mensaje bastante mas útil, el cual será enviado a la funcionalidad de log para registrar el suceso.

Para recuperar el archivo, solo deben ir a la ubicación donde esta el .exe, en este caso debería esta en la carpeta \bin\Debug del proyecto.

Lo mas probable es que el archivo no se legible a simple vista porque este sistema de log trabaja con el concepto de entradas de registros, es por eso que se confecciono un formulario muy simple para poder recuperar la información de forma visual.

[C#] 
[VB.NET] 

domingo, 13 de febrero de 2011

[WinForms] Edición Empleados – Grabar imagen en base de datos

 

Introducción


El objetivo de este articulo será reflejar los siguiente puntos:

- Grabar una imagen en un campo de una base de datos

- Recuperar la imagen visualizándola en un celda del DataGridView y en un PictureBox

- Comunicar un formulario pasándole información en el constructor para la edición de una entidad

- Detectar el cierre del formulario para actualizar la información de los empleados

 

Dominio de la aplicación


Bien empezaremos definiendo al estructura de datos de la aplicación.

imagen1

 

El dominio consiste en Empleados, con información básica del mismo, mas un campo de imagen el cual es opcional por lo cual esta marcado para permitir nulos.

El atributo Estado Civil tomara los valores de 1 a 4, los cuales fueron definido en un enumerado.

Adicionalmente el empleado podrá tener Estudios realizados, los cuales se asignan marcando de una lista.

El diseño de la interfaz es simple, un formulario principal que lista los usuarios.

imagen2

Y otro que edita el usuario cuando se realiza un doble click en la fila de la lista.

imagen3

La arquitectura de la aplicación es en dos capas, la presentación se comunica con DataAccess pasándole la información que necesita persistir o recuperar, el medio de comunicación son entidades definidas por medio de clases que modelan el dominio de la aplicación.

imagen4

Si bien el esquema de estas entidades son muy parecido al de la base de datos ,se debe apreciar que la entidad “Empleado” posee una relación con la de estudios, lo cual posibilita la navegación de la información asociada.

 

Cargar lista de empleados (con imágenes)


En esta sección analizaremos como cargar las imágenes en el DataGridView, tomando esta información desde la base de datos.

El primer paso será recuperar los registros de la tabla, incluyendo la imagen.

[C#]

public static List<EmpleadoEntity> ObtenerTodos()
{
    List<EmpleadoEntity> lista = new List<EmpleadoEntity>();

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

        string query = @"SELECT IdEmpleado, Nombre, Apellido, FechaNacimiento, EstadoCivil, Imagen
                         FROM Empleados";

        SqlCommand cmd = new SqlCommand(query, conn);

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            lista.Add(ConvertirEmpleado(reader, false));
        }

    }

    return lista;
}

private static EmpleadoEntity ConvertirEmpleado(IDataReader reader, bool cargarRelaciones)
{
    EmpleadoEntity empleado = new EmpleadoEntity();

    empleado.IdEmpleado = Convert.ToInt32(reader["IdEmpleado"]);
    empleado.Nombre = Convert.ToString(reader["Nombre"]);
    empleado.Apellido = Convert.ToString(reader["Apellido"]);
    empleado.FechaNacimiento = Convert.ToDateTime(reader["FechaNacimiento"]);
    empleado.EstadoCivil = Convert.ToInt16(reader["EstadoCivil"]);

    if (reader["Imagen"] != DBNull.Value)
        empleado.Imagen = (byte[])reader["Imagen"];

    if (cargarRelaciones)
    {
        empleado.Estudios = EstudiosDAL.ObtenerAsignadoEmpleado(empleado.IdEmpleado);
    }

    return empleado;
}		

[VB.NET]

Public Shared Function ObtenerTodos() As List(Of EmpleadoEntity)

    Dim lista As New List(Of EmpleadoEntity)()

    Using conn As New SqlConnection(ConfigurationManager.ConnectionStrings("default").ToString())
        conn.Open()

        Dim query As String = "SELECT IdEmpleado, Nombre, Apellido, FechaNacimiento, EstadoCivil, Imagen" & _
                              "FROM Empleados"

        Dim cmd As New SqlCommand(query, conn)

        Dim reader As SqlDataReader = cmd.ExecuteReader()

        While reader.Read()
            lista.Add(ConvertirEmpleado(reader, False))
        End While

    End Using

    Return lista

End Function


Private Shared Function ConvertirEmpleado(ByVal reader As IDataReader, ByVal cargarRelaciones As Boolean) As EmpleadoEntity

    Dim empleado As New EmpleadoEntity()

    empleado.IdEmpleado = Convert.ToInt32(reader("IdEmpleado"))
    empleado.Nombre = Convert.ToString(reader("Nombre"))
    empleado.Apellido = Convert.ToString(reader("Apellido"))
    empleado.FechaNacimiento = Convert.ToDateTime(reader("FechaNacimiento"))
    empleado.EstadoCivil = Convert.ToInt16(reader("EstadoCivil"))

    If reader("Imagen") IsNot DBNull.Value Then
        empleado.Imagen = DirectCast(reader("Imagen"), Byte())
    End If

    If cargarRelaciones Then
        empleado.Estudios = EstudiosDAL.ObtenerAsignadoEmpleado(empleado.IdEmpleado)
    End If

    Return empleado

End Function	

Es preciso notar como se asigna la imagen a la entidad

[C#]

if (reader["Imagen"] != DBNull.Value)
    empleado.Imagen = (byte[])reader["Imagen"];

[VB.NET]

If reader("Imagen") IsNot DBNull.Value Then
    empleado.Imagen = DirectCast(reader("Imagen"), Byte())
End If

lo cual resulta muy simple.

En el formulario “ListaEmpleados” se usa esta información para cargar el DataGridView

[C#]

private void CargarListaEmpleados()
{
    dgvEmpleados.AutoGenerateColumns = false;
    dgvEmpleados.DataSource = EmpleadosDAL.ObtenerTodos();

    foreach (DataGridViewRow row in dgvEmpleados.Rows)
    {
        //se asigna el alto de la fila para poder ver la imagen correctamente
        row.Height = 120; 

        //se recupera la entidad que es asignada como dato
        EmpleadoEntity empleado = row.DataBoundItem as EmpleadoEntity;

        if (empleado.Imagen == null)
            row.Cells["Imagen"].Value = ImageHelper.ObtenerImagenNoDisponible();
        else
            row.Cells["Imagen"].Value = ImageHelper.ByteArrayToImage(empleado.Imagen); 
    }

}

[VB.NET]

Private Sub CargarListaEmpleados()
	dgvEmpleados.AutoGenerateColumns = False
	dgvEmpleados.DataSource = EmpleadosDAL.ObtenerTodos()

	For Each row As DataGridViewRow In dgvEmpleados.Rows
		'se asigna el alto de la fila para poder ver la imagen correctamente
		row.Height = 120

		'se recupera la entidad que es asignada como dato
		Dim empleado As EmpleadoEntity = TryCast(row.DataBoundItem, EmpleadoEntity)

		If empleado.Imagen Is Nothing Then
			row.Cells("Imagen").Value = ImageHelper.ObtenerImagenNoDisponible()
		Else
			row.Cells("Imagen").Value = ImageHelper.ByteArrayToImage(empleado.Imagen)
		End If
	Next

End Sub

seguramente una pregunta que se hagan es porque en este método se esta recorriendo las filas del DataGridView después de asignar los datos al mismo ?

bien esto tiene una explicación, resulta que la celda de imagen del DataGridView requiere un tipo Image para poder trabajar, pero la entidad que proporcionamos devuelve un array de byte, lo cual no es compatible, entonces es que se aprovecha la capacidad del DataGridView para retornar la entidad que bindea a cada fila para obtener la entidad “Empleado” y convertir el array de byte al tipo Image.

También debo aclara que esto se intento hacer en un evento, como ser el CellFormatting, pero resulto un desastre porque este evento repintaba constantemente la información generando un parpadeo molesto en el control, se podría haber realizado esto mismo en el evento DataBindingComplete, pero para este caso no valía la pena separa la funcionalidad, por eso se dejo allí mismo.

En ese método se hace uso del la funcionalidad de “helper” que no es mas que un grupo de funciones que facilitan ciertas tareas, en este caso convierten el array de byte en Image.

 

Comunicar Formulario y detectar cierre del form hijo


Al realizar un doble click en la celda del DataGridView este entra en modo edición, esto implica simplemente toma la información y pacérsela al formulario

Desde “ListaEmpleados” se trabaja con el evento tomando solo el id del empleado, y en la instancia del formulario se lo pasa en el constructor. También en la misma operación se adjunta el evento FormClosing para poder detectar el cierre el form y proceder a la actualización del la lista de empleados.

[C#]

private void dgvEmpleados_CellContentDoubleClick(object sender, DataGridViewCellEventArgs e)
{
    int IdEmpleado = Convert.ToInt32(dgvEmpleados.Rows[e.RowIndex].Cells["IdEmpleado"].Value);

    //
    // al pasarle un id de empleado este lo cargara para su edicion
    //
    EditarEmpleado frmEditar = new EditarEmpleado(IdEmpleado);
    frmEditar.FormClosing += new FormClosingEventHandler(frmEditar_FormClosing);

    frmEditar.Show();
}

void frmEditar_FormClosing(object sender, FormClosingEventArgs e)
{
	//
	// al cerrarse el form de edicion se ingresa a este evento 
	// para actualizar la informacion del listado
	//

	EditarEmpleado frmEdit = sender as EditarEmpleado;

	if(frmEdit.DialogResult == DialogResult.OK)
		CargarListaEmpleados();

}

[VB.NET]

Private Sub dgvEmpleados_CellContentDoubleClick(ByVal sender As Object, ByVal e As DataGridViewCellEventArgs)

    Dim IdEmpleado As Integer = Convert.ToInt32(dgvEmpleados.Rows(e.RowIndex).Cells("IdEmpleado").Value)

    '
    ' al pasarle un id de empleado este lo cargara para su edicion
    '
    Dim frmEditar As New EditarEmpleado(IdEmpleado)
    AddHandler frmEditar.FormClosing, New FormClosingEventHandler(AddressOf frmEditar_FormClosing)

    frmEditar.Show()

End Sub

Private Sub frmEditar_FormClosing(sender As Object, e As FormClosingEventArgs)
	'
	' al cerrarse el form de edicion se ingresa a este evento 
	' para actualizar la informacion del listado
	'

	Dim frmEdit As EditarEmpleado = TryCast(sender, EditarEmpleado)

	If frmEdit.DialogResult = DialogResult.OK Then
		CargarListaEmpleados()
	End If

End Sub

En el formulario que recibe esta información “EditarEmpleado” solo utiliza el id del empleado, porque con este ira a la db y recuperara la entidad para cargar el formulario, no hace falta enviar la entidad completa, solo la mínima información como para que este form pueda recuperar el resto.

[C#]

  public partial class EditarEmpleado : Form
   {
       private int? _idEmpleado = null;

       public EditarEmpleado()
       {
           InitializeComponent();
       }

       public EditarEmpleado(int idEmpleado)
           : this()
       {
           _idEmpleado = idEmpleado;
       }
	
	.
	.
	.
}	

[VB.NET]

 

Public Partial Class EditarEmpleado
    Inherits Form

    Private _idEmpleado As Nullable(Of Integer) = Nothing

	Public Sub New()
		InitializeComponent()
	End Sub

	Public Sub New(idEmpleado As Integer)
		Me.New()
		_idEmpleado = idEmpleado
	End Sub
	
	.
	.
	.
End Class

Cuando se termina la edición se retorna como un resultado del formulario que indicara si se grabo o se cancelo el formulario, esto se logra por medio del DialogResult.

[C#]

		
private void btnGuardar_Click(object sender, EventArgs e)
{
	.
	.
	.

	//
	// se graba 
	//
	EmpleadosDAL.Save(empleado);

	this.DialogResult = DialogResult.OK;
	this.Close();
}

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

 

[VB.NET]

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

	.
	.
	.

	'
	' se graba 
	'
	EmpleadosDAL.Save(empleado)

	Me.DialogResult = DialogResult.OK
       Me.Close()

End Sub

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

       Me.DialogResult = DialogResult.Cancel
       Me.Close()

   End Sub

Editar Empleado (mostrando la imagen en un Picturebox)


Esta tarea es bastante simple, solo implica recuperar una entidad en concreto haciendo uso de la cada de datos y cargar la entidad en los controles para presentar esta al usuario.

[C#]

public static EmpleadoEntity ObtenerById(int idEmpleado)
{
    EmpleadoEntity empleado = null;

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

        string query = @"SELECT IdEmpleado, Nombre, Apellido, FechaNacimiento, EstadoCivil, Imagen
                         FROM Empleados WHERE IdEmpleado = @idEmpleado";

        SqlCommand cmd = new SqlCommand(query, conn);
        cmd.Parameters.AddWithValue("@idEmpleado", idEmpleado);

        SqlDataReader reader = cmd.ExecuteReader();

        if (reader.Read())
        {
            empleado = ConvertirEmpleado(reader, true);
        }

    }

    return empleado;
}

[VB.NET]

Public Shared Function ObtenerById(ByVal idEmpleado As Integer) As EmpleadoEntity

    Dim empleado As EmpleadoEntity = Nothing

    Using conn As New SqlConnection(ConfigurationManager.ConnectionStrings("default").ToString())
        conn.Open()

        Dim query As String = "SELECT IdEmpleado, Nombre, Apellido, FechaNacimiento, EstadoCivil, Imagen " & _
                              "FROM Empleados WHERE IdEmpleado = @idEmpleado"

        Dim cmd As New SqlCommand(query, conn)
        cmd.Parameters.AddWithValue("@idEmpleado", idEmpleado)

        Dim reader As SqlDataReader = cmd.ExecuteReader()

        If reader.Read() Then
            empleado = ConvertirEmpleado(reader, True)
        End If

    End Using

    Return empleado

End Function

Una vez que se tiene la entidad solo se carga esta en los contoles.

[C#]

private void EditarEmpleado_Load(object sender, EventArgs e)
{
    CargarEstadoCivil();
    CargarEstudio();

    //por defecto se carga una imagen de no disponible
    picImagenEmpleado.Image = ImageHelper.ObtenerImagenNoDisponible();

    //
    // se carga la informacion del empleado que se quiere editar
    //
    if (_idEmpleado.HasValue)
    {
        EmpleadoEntity empleado = EmpleadosDAL.ObtenerById(_idEmpleado.Value);

        _idEmpleado = empleado.IdEmpleado;
        txtNombre.Text = empleado.Nombre;
        txtApellido.Text = empleado.Apellido;
        dtpFechaNacimiento.Value = empleado.FechaNacimiento;

        cbEstadoCivil.SelectedValue = Convert.ToInt32(empleado.EstadoCivil);

        if (empleado.Imagen == null)
            picImagenEmpleado.Image = ImageHelper.ObtenerImagenNoDisponible();
        else
            picImagenEmpleado.Image = ImageHelper.ByteArrayToImage(empleado.Imagen);

        //
        // Se obtienen los estudios del empleado
        //
        AsignarEstudios(empleado);

    }

}

private void CargarEstadoCivil()
{
    cbEstadoCivil.DisplayMember = "Descripcion";
    cbEstadoCivil.ValueMember = "IdEstadoCivil";
    cbEstadoCivil.DataSource = EstadoCivilDAL.ObtenerTodos();
}

private void CargarEstudio()
{
    dgvEstudios.AutoGenerateColumns = false;
    dgvEstudios.DataSource = EstudiosDAL.ObtenerTodos();
}

private void AsignarEstudios(EmpleadoEntity empleado)
{
    
    List<DataGridViewRow> rows = (from row in dgvEstudios.Rows.Cast<DataGridViewRow>()
                                    let idEstudio = Convert.ToInt32(row.Cells["IdEstudio"].Value)
                                  join dd in empleado.Estudios on idEstudio equals dd.IdEstudio
                                  select row).ToList();


    rows.ForEach(o => o.Cells["Seleccion"].Value = true);
    
}

[VB.NET]

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

       CargarEstadoCivil()
       CargarEstudio()

       'por defecto se carga una imagen de no disponible
       picImagenEmpleado.Image = ImageHelper.ObtenerImagenNoDisponible()

       '
       ' se carga la informacion del empleado que se quiere editar
       '
       If _idEmpleado.HasValue Then
           Dim empleado As EmpleadoEntity = EmpleadosDAL.ObtenerById(_idEmpleado.Value)

           _idEmpleado = empleado.IdEmpleado
           txtNombre.Text = empleado.Nombre
           txtApellido.Text = empleado.Apellido
           dtpFechaNacimiento.Value = empleado.FechaNacimiento

           cbEstadoCivil.SelectedValue = Convert.ToInt32(empleado.EstadoCivil)

           If empleado.Imagen Is Nothing Then
               picImagenEmpleado.Image = ImageHelper.ObtenerImagenNoDisponible()
           Else
               picImagenEmpleado.Image = ImageHelper.ByteArrayToImage(empleado.Imagen)
           End If

           '
           ' Se obtienen los estudios del empleado
           '

           AsignarEstudios(empleado)
       End If


   End Sub

   Private Sub CargarEstadoCivil()

       cbEstadoCivil.DisplayMember = "Descripcion"
       cbEstadoCivil.ValueMember = "IdEstadoCivil"
       cbEstadoCivil.DataSource = EstadoCivilDAL.ObtenerTodos()

   End Sub

Private Sub CargarEstudio()
	dgvEstudios.AutoGenerateColumns = False
	dgvEstudios.DataSource = EstudiosDAL.ObtenerTodos()
End Sub

Private Sub AsignarEstudios(empleado As EmpleadoEntity)

       Dim rows As List(Of DataGridViewRow) = (From row In dgvEstudios.Rows.Cast(Of DataGridViewRow)() _
                                                Let idEstudio = Convert.ToInt32(row.Cells("IdEstudio").Value) _
                                                Join dd In empleado.Estudios On idEstudio Equals dd.IdEstudio _
                                                Select row).ToList()


       'rows.ForEach(Function(o) o.Cells("Seleccion").Value = True)
       rows.ForEach(Function(o) InlineAssignHelper(o.Cells("Seleccion").Value, True))

End Sub

La asignación de la imagen es muy simple y similar a como se trabajo en el DataGridview, solo se convierte el array de byte recuperado de la tabla en la base de datos y se carga directo en el PictureBox.

Algo interesante a remarcar es la forma en como se carga la lista de Estudios, en esta se usan dos consultas separadas, por un lado la primera que carga todos los estudios existentes, si se ingresara en modo de alta de empleado solo esta lista seria cargada, pero si se esta editando se recupera la lista de estudios asignados a ese empleado en particular y por medio de una consulta linq (método AsignarEstudios) se realiza la unión entres ambas listas aquellas filas del DataGridView que coincidan serán marcadas porque son las que el empleado tiene registradas en la tabla.

 

Búsqueda de una imagen (cargar archivo a un PictureBox)


Un punto que debería ser menor pero que he visto con mucha dificultad en preguntas en los foros, es como cargar seleccionar un archivo y cargarlo en un PictureBox?

El código es muy simple pero suele generar problemas.

[C#]

private void btnBuscarImagen_Click(object sender, EventArgs e)
{
    OpenFileDialog fileDialog = new OpenFileDialog();
    fileDialog.Filter = "Archivo JPG|*.jpg";

    if (fileDialog.ShowDialog() == DialogResult.OK)
    {
        picImagenEmpleado.Image = Image.FromFile(fileDialog.FileName);
    }
}

[VB.NET]

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

    Dim fileDialog As New OpenFileDialog()
    fileDialog.Filter = "Archivo JPG|*.jpg"

    If fileDialog.ShowDialog() = DialogResult.OK Then
        picImagenEmpleado.Image = Image.FromFile(fileDialog.FileName)
    End If

End Sub

Persistir la edición


La operación de persistir los datos o guardarlos requiere dos etapas, una implica tomar la información de los controles y generar los objetos de dominio que la capa de datos requiere para llevar a cabo la actualización en la base de datos, la segunda será simplemente procesar la entidad generando los INSERT y UPDATE necesario.

[C#]

private void btnGuardar_Click(object sender, EventArgs e)
{
    //
    // Se crea la entidad
    //
    EmpleadoEntity empleado = new EmpleadoEntity() 
    { 
        IdEmpleado = _idEmpleado.GetValueOrDefault(),
        Nombre = txtNombre.Text,
        Apellido= txtApellido.Text,
        FechaNacimiento = dtpFechaNacimiento.Value,
        EstadoCivil = Convert.ToInt16(cbEstadoCivil.SelectedValue),
        Imagen = ImageHelper.ImageToByteArray(picImagenEmpleado.Image)
    };

    //
    // Se asignan los estudios seleccionados, se unicializa la lista 
    // para cargar la selecciond el usuario
    //
    empleado.Estudios = new List<EstudioEntity>();

    IEnumerable<DataGridViewRow> rowsSelected = dgvEstudios.Rows.Cast<DataGridViewRow>()
                                                                .Where(o => Convert.ToBoolean(o.Cells["Seleccion"].Value));

    foreach (DataGridViewRow row in rowsSelected)
    {
        EstudioEntity estudio = new EstudioEntity()
        {
            IdEstudio = Convert.ToInt32(row.Cells["IdEstudio"].Value)
        };
        
        empleado.Estudios.Add(estudio);
    }

    //
    // se graba 
    //
    EmpleadosDAL.Save(empleado);

    this.DialogResult = DialogResult.OK;
    this.Close();
}

[VB.NET]

Private Sub btnGuardar_Click(sender As Object, e As EventArgs)
	'
	' Se crea la entidad
	'
       Dim empleado As New EmpleadoEntity() With { _
                                                  .IdEmpleado = _idEmpleado.GetValueOrDefault(), _
                                                  .Nombre = txtNombre.Text, _
                                                  .Apellido = txtApellido.Text, _
                                                  .FechaNacimiento = dtpFechaNacimiento.Value, _
                                                  .EstadoCivil = Convert.ToInt16(cbEstadoCivil.SelectedValue), _
                                                  .Imagen = ImageHelper.ImageToByteArray(picImagenEmpleado.Image) _
                                                 }

	'
	' Se asignan los estudios seleccionados, se unicializa la lista 
	' para cargar la selecciond el usuario
	'
	empleado.Estudios = New List(Of EstudioEntity)()

       Dim rowsSelected As IEnumerable(Of DataGridViewRow) = dgvEstudios.Rows.Cast(Of DataGridViewRow)().Where(Function(o) Convert.ToBoolean(o.Cells("Seleccion").Value))

       For Each row As DataGridViewRow In rowsSelected

           Dim estudio As New EstudioEntity() With { _
                                                    .IdEstudio = Convert.ToInt32(row.Cells("IdEstudio").Value) _
                                                   }

           empleado.Estudios.Add(estudio)

       Next

	'
	' se graba 
	'
	EmpleadosDAL.Save(empleado)

	Me.DialogResult = DialogResult.OK
       Me.Close()

End Sub

Es preciso remarcar como en la entidad “Empleado” es cargada la lista de Estudios que este posee y solo una entidad es enviada desde la Presentación a la capa de datos. Durante esta operación también se toma la imagen del PictureBox y se convierte en un array de byte.

La actualización realiza varias operaciones las cuales son muy extensas en código, por lo tanto no será puesta directa en el articulo, pero si podrán verse en el código de ejemplo que puede descargarse.

 

Código de Ejemplo


La base de datos utilizada en el ejemplo es la Sql Server Express 2008 R2, como ver en la solución el mdf esta integrado al Visual Studio, por lo tanto con solo tener el sql server express instado esta debería funciona adjuntándose sola al servicio.

En la carpeta “script” del proyecto “DataAccess”  se encuentra un archivo .sql con las instrucciones para crear la estructura de tablas y datos que se requieren para este articulo.

 

[C#] 
[VB.NET]