Ago 14 2008

Como filtrar una lista de objetos con LINQ y ver las propiedades por Reflection (VB.Net) {

Tag: LINQ, VB.Net

LINQ es un proyecto de Microsoft que agrega a los lenguajes del .Net Framework la capacidad de utilizar consultas de sintáxis parecida a SQL. Inicialmente sólo se itegró a C# y VB.Net.

En el sitio de Microsoft hay una traducción de la página del proyecto LINQ donde se puede leer más al respecto.

En nuestro ejemplo utilizaremos LINQ To Objects, que es el término que se utiliza para definir la utilización de LINQ con cualquier colección que implemente las interfaces IEnumerable o IEnumerable(T).

En nuestro caso hemos definido una clase Objeto que tiene las propiedades ID : Integer, Tipo : String, Tamaño : Decimal y Habilitado : Boolean. Para utilizar LINQ hemos creado una colección del tipo List(Of Objeto) la cual cargamos con varias instancias de nuestra clase Objeto.

La idea es crear un filtro dinámico, para dar la posibilidad al usuario de seleccionar el valor para 2 propiedades y crear un filtro con OR o AND, o sea Propiedad1 = a OR/AND Propiedad2 = b.

Nuestra intención es poder reutilizar el código, entonces no podemos crear 2 ComboBox y cargar las propiedades a mano, por lo que recurriremos al namespace System.Reflection para extraer las propiedades de nuestra clase Objeto.

Según Microsoft: El espacio de nombres System.Reflection contiene clases e interfaces que proporcionan una vista administrada de los campos, los métodos y los tipos cargados, con la posibilidad de crear e invocar tipos dinámicamente.

Private Sub CargarPropiedades()
	Dim propiedades1 As New List(Of String)
	Dim propiedades2 As New List(Of String)
 
	' Recorremos todos los miembros de la clase
	For Each propiedad As System.Reflection.MemberInfo In System.Reflection.Assembly.GetExecutingAssembly.GetType("Objeto").GetMembers()
		' Filtramos para seleccionar solamente las propiedades
		If propiedad.MemberType = System.Reflection.MemberTypes.Property Then
			propiedades1.Add(propiedad.Name)
			propiedades2.Add(propiedad.Name)
		End If
	Next
 
	Me.cmbField1.DataSource = propiedades1 'Asignamos el DataSource a nuestros ComboBox
	Me.cmbField2.DataSource = propiedades2
End Sub

Con ese código tan simple ya tenemos 2 combos cargados con las propiedades de nuestra clase Objeto. En este caso ob tuvimos el tipo Objeto del mismo ensamblado, (Assembly), en el que estamos trabajando, pero pudimos haberlo buscado en otro Assembly.
Cabe aclarar que se utilizan 2 listas, una para cada combo porque si seteamos la misma lista como DataSource para los 2 ComboBox, al cambiar el valor seleccionado en uno, se cambia también en el otro.

Sólo resta armar nuestra sentencia LINQ para filtrar la coleeción de Objeto. Para ello tenemos que tomar: 1) la primera propiedad seleccionada por el usuario, 2) el valor que ingresó para esa propiedad, 3) la condición que seleccionó (AND/OR), 4) la segunda propiedad seleccionada, 5) el valor ingresado para la segunda propiedad.

Lo que haremos será armar la sentencia de la siguiente manera:

  • Seleccionar todos los objetos con PropiedadSeleccionada1 = ValorIngresado1
  • AND Si el usuario seleccionó ‘AND’ PropiedadSeleccionada2 = ValorIngresado2, sino True
  • OR Si el usuario seleccionó ‘OR’ PropiedadSeleccionada2 = ValorIngresado2 sino False.

Entonces la sentencia, en pseudo-código se vería de una se las siguientes maneras:

  • Seleccionar todos los objetos con Propiedad1 = Valor1 AND Propiedad2 = Valor2 OR False
  • Seleccionar todos los objetos con Propiedad1 = Valor2 AND True OR Propiedad2 = Valor2

De esa manera con una misma sentencia se cubren los 2 casos, veamos el código:

Private Sub btnSelect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSelect.Click
	If ValidarEntrada() Then ' Chequeaar que el usuario haya ingresado todos los valores necesarios
		Try
			' LINQ To Objects
			Dim resSelect = From obj In mObjetos _
				Select obj _
				Where obj.GetType().GetProperty(Me.cmbField1.SelectedItem.ToString()).GetValue(obj, Nothing) = Me.txtCond1.Text _
				And (IIf(cmbJoin.SelectedItem = "AND", obj.GetType().GetProperty(Me.cmbField2.SelectedItem.ToString()).GetValue(obj, Nothing) = Me.txtCond2.Text, True)) _
				Or (IIf(cmbJoin.SelectedItem = "OR", obj.GetType().GetProperty(Me.cmbField2.SelectedItem.ToString()).GetValue(obj, Nothing) = Me.txtCond2.Text, False))
 
			Me.lstSelect.Items.Clear() ' Limpiar la lista antes de mostrar el resultado de la consulta
			For Each item In resSelect
				Me.lstSelect.Items.Add(item)
			Next
		Catch ex As Exception
			If MessageBox.Show(String.Format("Error: {0}{1}Desea ver más información acerca de este error?", ex.Message.ToString(), Environment.NewLine), "Error", MessageBoxButtons.YesNo, MessageBoxIcon.Asterisk) = Windows.Forms.DialogResult.Yes Then
				MessageBox.Show(ex.ToString(), "Detalle del error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
			End If
		End Try
	End If
End Sub

Descargar el ejemplo de LINQ y reflection (42.24 KB)

}


Jul 22 2008

Aplicación con plugins en VB.Net {

Tag: VB.Net

Este ejemplo muestra una forma sencilla de crear aplicaciones cuya funcionalidad se puede extender con el uso de plugins o agregados.
En este caso, como lo que importa es mostrar como se hace la carga mediante herramientas de Reflection, nuestros plugins son sólo forms con una funcionalidad muy simple, un web browser y un reloj… queda en la imaginación de cada uno crear algún nuevo plugin.

Para la carga de los plugins, o sea buscar los archivos y cargar los menús correspondientes utilizaremos un nuevo thread, pero como no es el propósito de este post mostrar el manejo de threads, utilizaremos un Background Worker que automatiza el proceso.

    Private Sub bgwCargaPlugins_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgwCargaPlugins.DoWork
        Dim DirectorioPlugins As String = String.Format("{0}{1}", Threading.Thread.GetDomain().BaseDirectory, "plugins")
        If Directory.Exists(DirectorioPlugins) Then
            For Each _dir As String In Directory.GetDirectories(DirectorioPlugins)
                Try
                    Dim aux As System.Collections.Generic.KeyValuePair(Of String, String) = PluginLoader.CargarPluginDesdeDirectorio(_dir)
                    _pluginsEncontrados.Add(aux.Key, aux.Value)
                Catch ex As Exception
                    Trace.WriteLine(ex.ToString())
                End Try
            Next
        Else
            Trace.WriteLine("El directorio de plugins no existe.")
        End If
    End Sub
 
    Private Sub bgwCargaPlugins_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgwCargaPlugins.RunWorkerCompleted
        If _pluginsEncontrados.Count > 0 Then
            mostrarMenuDePlugins()
        End If
    End Sub

Lo primero que hacemos es chequear que el directorio de plugins exista, en este caso es un directorio hard-coded llamado plugins que se ubica en el directorio de instalación, pero en una aplicación real deberíamos podríamos hacer que esa ruta sea configurable.
Luego recorreremos los subdirectorios y pasaremos la ruta de cada uno a nuestra clase PluginLoader para guardar el resultado en un objeto del tipo Dictionary(Of String, String), que a grandes rasgos es una colección ordenada que guarda un valor asociado a una determinada clave; en este caso, la clave será el nombre del tipo a cargar, o sea el form del plugin y el valor será la ruta a la dll que contiene el plugin.

Una vez que el background worker ha finalizado su ejecución, llamamos a un método que lo que hará es recorrer nuestro Dictionary cargado de plugins y agregar un item al menú plugins y agregarle un handler genérico para el evento click a cade item que agregue.

    ' Método que carga los items en el menú
    Public Sub mostrarMenuDePlugins()
        For Each p As System.Collections.Generic.KeyValuePair(Of String, String) In _pluginsEncontrados
            Dim aux As System.Windows.Forms.ToolStripMenuItem = Me.PluginsToolStripMenuItem.DropDownItems.Add(p.Key)
            AddHandler aux.Click, AddressOf PluginMenuClick
        Next
        Me.PluginsToolStripMenuItem.Enabled = True
    End Sub
 
    ' handler para el evento click de cada item del menú plugins
    Public Sub PluginMenuClick(ByVal sender As Object, ByVal e As System.EventArgs)
        Try
            Dim plugin As Form = PluginLoader.InstanciarPlugin(_pluginsEncontrados.Item(sender.ToString()), sender.ToString())
            plugin.MdiParent = Me
            plugin.Show()
        Catch ex As Exception
 
        End Try
    End Sub

Lo que haremos en el click de cada item del menú plugins será pedirle a nuestra clase PluginLoader que nos instancie el plugin para luego mostrarlo como MDI Child.

Ahora sólo nos resta ver el código de la clase PluginLoader:

Imports System.IO
 
Public Class PluginLoader
    Public Shared Function CargarPluginDesdeDirectorio(ByVal Directorio As String) As System.Collections.Generic.KeyValuePair(Of String, String)
        Dim aux As System.Collections.Generic.KeyValuePair(Of String, String) = Nothing
 
        'Busco solo los archivos .dll en el directorio especificado
        Dim archivos() As String = Directory.GetFiles(Directorio, "*.dll")
        If archivos.Length > 0 Then
            For Each dll As String In archivos
                Try
                    Dim assembly As System.Reflection.Assembly = System.Reflection.Assembly.LoadFrom(dll)
                    'Recorro los tipos que se encuentran en la dll cargada
                    For Each t As Type In assembly.GetTypes()
                        Try
                            'cargo solo los que son derivados de Form
                            If t.BaseType Is GetType(Form) Then
                                aux = New System.Collections.Generic.KeyValuePair(Of String, String)(t.Name, assembly.Location)
                            End If
                        Catch ex As Exception
                            Trace.WriteLine(ex.ToString())
                        End Try
                    Next
                Catch ex As Exception
                    Trace.WriteLine(ex.ToString())
                End Try
            Next
        End If
        Return aux
    End Function
 
    Public Shared Function InstanciarPlugin(ByVal RutaDll As String, ByVal PluginName As String, Optional ByVal ExtraData As Object = Nothing) As Form
        Dim TipoPlugin As Type
        Dim frm As Form
 
        Dim PluginAssembly As System.Reflection.[Assembly] = System.Reflection.[Assembly].LoadFrom(RutaDll)
        TipoPlugin = PluginAssembly.GetType(PluginName, False, True)
 
        If Not TipoPlugin Is Nothing Then
            Dim Instancia As Object
            'ExtraData se puede utilizar para pasar argumentos al constructor
            If ExtraData Is Nothing Then
                Instancia = Activator.CreateInstance(TipoPlugin)
            Else
                Dim arg() As Object = {ExtraData}
                Instancia = Activator.CreateInstance(TipoPlugin, arg)
            End If
 
            frm = CType(Instancia, Form)
        Else
            'En caso de error devuelvo un form cualquiera con el texto de error
            frm = New Form()
            frm.Text = "Error cargando el plugin"
        End If
 
        Return frm
    End Function
End Class

El código de esta clase no es muy complejo y con los comentarios se puede entender lo más relevante de la misma, al final del artículo está el enlace para descargar la solución, que está hecha con Visual Studio 2005.

Con respecto a los plugins, son proyectos de tipo Class Library, los mismos tienen referencia a System.Windows.Forms y el proyecto de la aplicación principal no tiene ninguna referencia a éstos, de manera que el proyecto no tiene por qué estar incluido en la misma solución.

Descargar el código fuente de la aplicación con plugins en VB.Net (124.71 KB)

}


Página 1 de 11