Nov 03 2008

ObsoleteAttribute - Para refactorear y mantener compatibilidad {

Tag: C#, Mini Tips, VB.Net

Algo muy común a la hora de refactorear es que se elimine algún método o alguna clase, pero si estamos trabajando en un grupo de soluciones con referencias entre ellas, eliminar un método puede causar bastantes problemas… y molestias en quienes estén trabajando con nuestro código.

.Net ofrece una ayuda, es un atributo que se puede aplicar a clases, métodos, propiedades, variables y constantes y que sirve de alternativa para no romper compatibilidad, el attributo Obsolete() nos permite marcar como obsoleta una porción del código sin eliminarlo y nos brinda la posibilidad de mostrar un mensaje y generar una advertencia o bien un error a la hora de compilar.

Veamos un pequeño ejemplo en VB.Net y luego en C#

Public Class MiClase
 
	<Obsolete("Este constructor fue reemplazado por New(String, Boolean)", True)> _
	Public Sub New(Arg1 as String, Arg2 as String)
		' Código del ctor obsoleto
		' Ahora genera un error al compilar
	End Sub
 
	Public Sub New(Arg1 as String, Arg2 as Boolean)
		'Código del nuevo ctor
	End Sub
 
	<Obsolete("Esta propiedad está obsolete. Por favor eliminar toda referencia a la misma antes del release 2.0")> _
	Public ReadOnly Property IsObsolete() As Boolean
		Get
			Return True
		End Get
	End Property
 
End Class

class Ejemplo
{
    [Obsolete()]
    public const string MI_CONSTANTE = "NuevosProgramadores.com";
 
    [Obsolete("No usar esta variable. Utilizar la variable FechaHoraActual en su lugar.", false)]
    DateTime Fecha = DateTime.Now;
 
}

A simple vista se puede ver que la única diferencia entre su uso en VB.net y C# es que los atributos se aplican de manera diferente.

Por último veamos las 3 firmas de este atributo:

  • Obsolete() - Genera una advertencia (warning) sin descripción alguna.
  • Obsolete(String) - Genera una advertencia con con la descripción indicada en el parámetro.
  • Obsolete(String, Boolean) - Utiliza el parámetro String como descripción y el Boolean indica si genera una advertencia (false) o un error (true).

}


Ago 29 2008

Como filtrar las filas de un DataTable usando un RowFilter (VB.Net) {

Tag: VB.Net

Más de una vez me he encontrado con un formulario en el cual muestro datos con un DataGridView, pero los datos a mostrar son demasiados, entonces es bueno agregar algún filtro para minimizar la cantidad de registros mostrados.

Para hacer esto hay 2 opciones, la más lenta sería recargar los datos desde la base aplicando filtros y la más rápida, ya que funciona en memoria es filtrar los datos que ya estamos mostrando en pantalla. Para realizar esto, en este ejemplo utilizaremos un DataTable con datos de alumnos el cual filtraremos dinámicamente.

Para empezar con este ejemplo necesitaremos un Form que tenga un CheckBox para indicar si se debe aplicar el filtro o no, un ComboBox para seleccionar el campo a filtrar, un TextBox para ingresar el valor del filtro y un DataGrid para mostrar los datos.

Lo primero que haremos será llenar el ComboBox con el nombre de las columnas de nuestro DataTable:

Private Sub LlenarComboColumnas()
	If TodosAlumnos IsNot Nothing Then ' TodosAlumnos es el DataTable
		For Each c As DataColumn In TodosAlumnos.Columns
			Me.cmbCampo.Items.Add(c.ColumnName)
		Next
	End If
End Sub

Luego agregaremos un método que se encargará de manejar 3 eventos: CheckBox.CheckedChanged, ComboBox.SelectedIndexChanged y TextBox.TextChanged. En éste método se optará por aplicar el filtro o mostrar todos los registros nuevamente:

Private Sub AplicarFiltro(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles chkFiltro.CheckedChanged, cmbCampo.SelectedIndexChanged, txtValor.TextChanged
	cmbCampo.Enabled = chkFiltro.Checked ' Habilitar o Deshabilitar el ComboBox y TextBox
	txtValor.Enabled = chkFiltro.Checked
 
	If chkFiltro.Checked Then
		Filtrar()
	Else
		Me.cmbCampo.SelectedIndex = 0 ' Resetear el filtro
		Me.txtValor.Text = String.Empty
		AlumnosFiltrados = TodosAlumnos.Copy() ' Mostrar todos los datos nuevamente
		Me.DataGridView1.DataSource = AlumnosFiltrados.DefaultView
	End If
End Sub

Ahora nos resta filtrar, lo que haremos en este caso, ya que el ejemplo es simple, será chequear si el tipo de datos correspondiente a la columna seleccionada es String usaremos como condición LIKE y agregaremos un comodín (*) al final del valor, si en cambio es Integer usaremos = y chequearemos que el valor ingresado sea numérico.

Private Sub Filtrar()
	Try
		AlumnosFiltrados = TodosAlumnos.Copy()
 
		If chkFiltro.Checked AndAlso Me.cmbCampo.SelectedIndex >= 0 AndAlso Not String.IsNullOrEmpty(Me.txtValor.Text) Then
			Dim columna As String = Me.cmbCampo.SelectedItem.ToString()
			Dim condicion As String = "="
			Dim valor As String = txtValor.Text
			' Si no se obtienen todos los datos no se aplica el filtro
			If AlumnosFiltrados.Columns(columna).DataType Is GetType(String) Then
				condicion = "LIKE"
				valor = String.Format("'{0}*'", valor) ' Comodín al final para obtener los valores que empiezan con 'valor'
			ElseIf AlumnosFiltrados.Columns(columna).DataType Is GetType(Integer) Then
				If Not IsNumeric(valor) Then ' Chequear que sea numérico
					Throw New ArgumentException("El valor ingresado no es correcto. Debe ingresar un valor numérico.")
				End If
			End If
 
			Me.AlumnosFiltrados.DefaultView.RowFilter = String.Format("{0} {1} {2}", columna, condicion, valor)
		End If
 
		Me.DataGridView1.DataSource = Me.AlumnosFiltrados.DefaultView ' Mostramos los datos filtrados
	Catch ex As Exception
		MostrarExcepcion(ex)
	End Try
End Sub

Para probar este ejemplo puedes descargar una solución de Visual Studio 2008 (37.03 KB).

}


Ago 21 2008

Métodos de extensión en VB.NET (Extension Methods) {

Tag: VB.Net

Los métodos de extensión, extension methods en inglés, son una de las novedades de la última versión de .Net Framework. Como su nombre lo indica, estos métodos permiten extender la funcionalidad de una clase sin la necesidad de tener el código fuente de la misma.

En VB.NET se utiliza un módulo para escribir los métodos de extensión, y justamente esa es la causa de muchas críticas negativas respecto a la utilización de éstos métodos.

Dentro del módulo necesitaremos definir los métodos y en su firma incluir el atributo que es el que definirá a nuestro método como método de extensión.

Luego hay que tener al menos un parámetro en la firma, que es el que indicará la clase que este método va a extender, por ejemplo hagamos un método para extender la clase String:

<Extension()> _ 
Public Function ReemplazoArroba(ByVal Cadena As String) As String 
	Return Cadena.Replace("@", "(a)") 
End Function

Entonces podremos llamar a nuestro método como un método de instancia:

Me.TextBox1.Text = Me.TextBox2.Text.ReemplazoArroba()

En la firma del método está el parámetro que define la clase que se extiende, pero a la hora de llamarlo, ese parámetro no se utiliza, ahora hagamos que nuestro método de extensión acepte en un parámetro el reemplazo que utilizará para el caracter @.

<Extension()> _
Public Function ReemplazoArroba(ByVal Cadena As String, Reemplazo as String) As String 
	Return Cadena.Replace("@", Reemplazo) 
End Function

Ahora si, a la hora de llamarlo tendremos que pasar el parámetro Reemplazo:

Me.TextBox1.Text = Me.TextBox2.Text.ReemplazoArroba("[arroba]") 

De la misma manera y en el mismo módulo podremos implementar métodos de extensión para varias clases, también se podrían implementar en un módulo ya existente de nuestro proyecto, pero por prolijidad yo prefiero hacerlo en uno aparte.

El siguiente método extiende la clase System.Windows.Forms.Control.ControlCollection y le agrega un método que puede resultar realmente útil, es un método que busca recursivamente todos los controles de tipo TextBox y borra su contenido.

<Extension()> _
Public Sub EmptyAllTextBoxes(ByVal Controls As Windows.Forms.Control.ControlCollection) 
	For Each c As Control In Controls 
		If c.Controls IsNot Nothing AndAlso c.Controls.Count > 0 Then 
			EmptyAllTextBoxes(c.Controls) ' Llamado recursivo 
		ElseIf c.GetType().Equals(GetType(TextBox)) Then 
			CType(c, TextBox).Text = String.Empty 
		End If 
	Next 
End Sub

Pueden descargar una solución de VS2008 con ejemplos de métodos de extensión (55.25 KB) para probar y modificar el código.

}


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)

}


Ago 11 2008

Aplicación para calcular la cantidad de billetes a partir del importe en VB.Net {

Tag: VB.Net

Esta es una aplicación muy simple, la misma calcula la cantidad de billetes/monedas de cada denominación necesarios para cubrir un importe tomando en cuenta que siempre vamos a querer completar el importe con los billetes/monedas de mayor denominación posible.

O sea que para cubrir el importe $ 150 vamos a usar un billete de $ 100 y otro de $ 50, pero no uno de $ 100 y 2 de $ 20.

Cabe destacar que como este es sólo un ejemplo, las denominaciones no son configurables, están incluidas en el código fuente y las mismas son correspondientes a la moneda de Uruguay, el Peso Uruguayo.

Para guardar las denominaciones y luego la cantidad necesaria de cada una utilizamos un objeto de tipo Generic.Dictionary(Of Decimal, Integer) en el cual guardaremos como key cada denominación:

Private Sub CargarBilletes(ByRef lista As Generic.Dictionary(Of Decimal, Integer))
    lista = New Generic.Dictionary(Of Decimal, Integer)
    With lista
        .Add(2000, 0) ' Billetes
        .Add(1000, 0)
        .Add(500, 0)
        .Add(200, 0)
        .Add(100, 0)
        .Add(50, 0)
        .Add(20, 0)
        .Add(10, 0) ' Monedas
        .Add(5, 0)
        .Add(2, 0)
        .Add(1, 0)
        .Add(0.5, 0)
    End With
End Sub

La función anterior es llamada cada vez que se hace el cálculo, de esa manera nos aseguramos que nuestros valores han sido reseteados, aunque en una aplicación más compleja éstos valores deberían obtenerse desde una base de datos o un archivo de configuración.

Public Sub DiscriminarBilletes(ByVal Importe As Decimal)
    Dim keys As Decimal() = New Decimal(Billetes.Count - 1) {} ' Creamos un array de Decimal para guardar las denominaciones
    Billetes.Keys.CopyTo(keys, 0) ' Copiamos las keys del objeto Generic.Dictionary(Of Decimal, Integer)
    For Each billete As Decimal In keys
        While Importe >= 0 AndAlso Importe >= billete ' Si el importe > 0 y es > la denominación actual sumamos 1
            Billetes(billete) += 1
            Importe -= billete ' Descontamos del importe el valor de la denominación actual
        End While
    Next
 
    Mostrar() ' Mostramos los resultados
End Sub

La función anterior es la que realiza todo el cálculo, se puede ver que es muy simple. Alguien debe estar preguntándose por qué copiar las keys a otro array en lugar de recorrer el objeto Dictionary directamente, la respuesta es que Visual considera que se modificó el enumerado cada vez que se suma 1, entonces no se puede seguir recorriendo el Dictionary.

Por las dudas aclaro el uso de AndAlso: funciona como And, pero en caso de no cumplirse la primera condición no se chequea la segunda. Es una mejora que se introdujo en VB.Net ya que en VB6 se tenían los operadores And y Or. También se agregó el operador lógico OrElse.

Descargar el código fuente de la aplicación para discriminar billetes (26.01 KB)

}


Ago 06 2008

Capturar una imagen de la ventana activa con VB.Net {

Tag: Mini Tips, VB.Net

Muchas veces nos es útil capturar una imagen del momento en que se produce un error para saber qué sucedió o cómo solucionarlo. Sin ir más lejos, en mi trabajo anterior instalamos una aplicación y a veces teníamos un error que después de hacer guardias de 8hs frente a los usuarios no pudimos encontrar y las explicaciones y suposiciones de los usuarios carecían totalmente de sentido.

Por eso mismo muchas veces es bueno capturar el estado de la aplicación en el momento, en este ejemplo vamos a capturar una imágen de la ventana activa y la vamos a guardar.

Lo primero que haremos será la función que captura la imágen, para eso utilizaremos el viejo y conocido recurso de apretar la tecla PRINT SCREEN, y para eso utilizaremos la función SendKeys.Send.

    Private Sub ImprimirPantalla()
        SendKeys.Send("{PRTSC}")
        GuardarImagenDesdeClipboard()
    End Sub

Lo que hemos hecho hasta este momento es capturar la imagen, la misma queda en el portapapeles de Windows, así que para que nos sea útil necesitamos guardarla en un archivo debidamente identificado, para ello extraeremos la imagen del portapapeles y nos crearemos un archivo cuyo nombre se comprondrá de la fecha-hora actual y el nombre de la clase que generó la excepción.

    Private Sub GuardarImagenDesdeClipboard()
        If Clipboard.ContainsImage() Then
            Dim NombreArchivo As String = String.Format("{0}_{1}.jpg", DateTime.Now.ToString("yyyyMMdd-HHmmss"), Me.Name)
            Dim Path As String = String.Format("{0}\logs\imagenes\{1}", Application.StartupPath, NombreArchivo)
            Dim img As Image = Clipboard.GetImage()
            img.Save(Path)
        End If
    End Sub

Hay que tener en cuenta que para que la imágen se guarde es necesario que el directorio donde se va a guardar exista previamente, por eso sería conveniente chequear su existencia y si es necesario crearlo, antes de intentar guardar la imagen.

}


Jul 30 2008

Ordenar una lista con expresiones Lambda (múltiples criterios) VB.Net {

Tag: VB.Net

Este post se trata de como ordenar una lista de objetos de acuerdo a varios criterios utilizando una sola expresión lambda. El ejemplo está hecho utilizando el .net framework 3.5 (Visual Studio 2008).

Tenemos una lista de objetos con la siguiente estructura:

Public Class Elemento
    Public Sub New(ByVal mNum As Integer, ByVal mPrioridad As Prioridades, ByVal mValor As Integer)
        Numero = mNum
        Prioridad = mPrioridad
        Valor = mValor
    End Sub
 
    Public Enum Prioridades
        Alta = 0
        Media = 1
        Baja = 2
    End Enum
 
    Public Numero As Integer
    Public Prioridad As Priorities
    Public Valor As Integer
End Class

Para este ejemplo utilizaremos un objeto List(Of Elemento) que llenaremos con valores aleatorios y necesitaremos ordenarla de acuerdo a los siguientes criterios:

  • Primero: Numero en orden ascendente
  • Segundo: Prioridad de mayor a menor
  • Tercero: Valor en orden ascendente

Supongamos que nuestra lista se completó con los siguientes elementos:

Num Prioridad Valor
4 Baja 16
1 Alta 26
3 Media 87
4 Media 10
2 Baja 3
5 Alta 4
4 Media 134
2 Baja 342

Nuestro resultado debe ser el siguiente:

Num Prioridad Valor
1 Alta 26
2 Baja 3
2 Baja 342
3 Media 87
4 Media 10
4 Media 134
4 Baja 16
5 Alta 4

Por lo tanto nuestra expresión lambda será la siguiente:

Public Sub LambdaSort(ByVal lista As List(Of Element))
        lista.Sort(Function(x, y) _
                x.Numero.CompareTo(y.Numero) Or _
                (x.Numero.Equals(y.Numero) And x.Prioridad.CompareTo(y.Prioridad)) Or _
                (x.Numero.Equals(y.Numero) And x.Prioridad.Equals(y.Prioridad) And x.Valor.CompareTo(y.Valor)))
End Sub

La expresión está hecha en una sola línea que fue separada para comodidad en la lectura, por lo que iremos viendo linea a linea, en la primera declaramos la expresión lambda que va a ordenar la lista:

lista.sort(Function(x,y)

luego vamos ennumerando nuestras condiciones:

x.Numero.CompareTo(y.Numero) Comparamos el valor de x.Numero con el de y.Numero, esta comparación nos devuelve el orden ascendente o sea x < y.
(x.Numero.Equals(y.Numero) And x.Prioridad.CompareTo(y.Prioridad)) Si x.Numero = y.Numero comparamos la Prioridad de ambos al igual que lo hicimos en el punto anterior con Numero.
(x.Numero.Equals(y.Numero) And x.Prioridad.Equals(y.Prioridad) And x.Valor.CompareTo(y.Valor)) Si x.Numero = y.Numero y también x.Priridad = y.Pririodad comparamos el Valor.

Separamos cada uno de los criterios de ordenación con OR para que se aplique uno u otro y listo.

Sin duda una forma simple y rápida de ordenar una lista.

}


Jul 25 2008

Como obtener el directorio de ejecución con VB.Net {

Tag: Mini Tips, VB.Net

Hay varias formas de llegar al mismo resultado, la idea es obtener la ruta del directorio donde se está ejecutando nuestra aplicación, a continuación dejo 3 funciones que nos llevan al mismo resultado.

Para probarlas, basta con crear una nueva aplicación de consola, agregar las 3 funciones y llamarlas:

   Sub Main()
        Console.WriteLine(ObtenerDirectorio_1())
        Console.WriteLine(obtenerDirectorio_2())
        Console.WriteLine(obtenerDirectorio_3())
        Console.Read()
    End Sub

    Function ObtenerDirectorio_1() As String
        Return System.Threading.Thread.GetDomain().BaseDirectory()
    End Function

    Function ObtenerDirectorio_2() As String
        Return Application.ExecutablePath.Substring(0, Application.ExecutablePath.LastIndexOf("\") + 1)
    End Function

    Function ObtenerDirectorio_3() As String
        Return System.Reflection.Assembly.GetExecutingAssembly().Location.Substring(0, Application.ExecutablePath.LastIndexOf("\") + 1)
    End Function

}


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