Desarrollo de plugins con .Net

Desarrollo de plugins con .Net

October 11, 2019

El concepto de plugin o extensión es bien conocido para todo desarrollador: un componente dentro de una aplicación que se puede cargar y utilizar en tiempo de ejecución sin vincularlo directamente en tiempo de diseño.

Los plugins nos permiten añadir funcionalidades a nuestra aplicación para las que no estaba preparada en un principio e incluso cargarlos o descargarlos sin necesidad de configuración.

En.Net, existen varias formas de añadir plugins a un ejecutable. En teoría podemos gestionarlos directamente utilizando Reflection y carga dinámica de ensamblados con el método LoadAssembly aunque es mucho más sencillo si utilizamos las librerías de MEF.

Introducción a MEF

MEF o Managed Extensibility Frameworks (no confundir con EF - Entity Frameworks) es una librería para la creación de aplicaciones que usen extensiones o plugins. Esta librería permite descubrir o utilizar extensiones evitando la inclusión de dependencias, es decir, sin añadir referencias a otras librerías dentro de nuestra aplicación principal.

Se puede usar MEF prácticamente con cualquier tecnología de.Net, es decir, tanto WPF como Windows Forms, consolas de Windows, servicios o ASP.Net, de hecho, se podría usar como sistema de inyección de dependencias aunque le faltan algunas de las características a las que estamos acostumbrados y que tendríamos que implementar a mano.

Nota: A los programadores que utilicen Visual Studio quizá les suene MEF por una de las ventanas que aparecen al abrir la aplicación que dice algo así como ‘Creando el árbol de componentes MEF’. Cierto, es la misma tecnología que usa Visual Studio para sus extensiones.

Para utilizar MEF simplemente debemos añadir una referencia al ensamblado System.ComponentModel.Composition tanto en la aplicación que vamos a utilizar como host como en las que van a actuar como plugin o extensión y añadir atributos al código para indicar las clases que se exportan e importan.

Nota: para liar un poco las cosas, Microsoft decidió portar MEF en un ensamblado similar llamado Microsoft.Composition dedicado a las aplicaciones de Windows Store con añadidos para MVVM. Este ensamblado no es compatible con el anterior, de hecho el código a utilizar es diferente en ambos casos.

Veremos cómo utilizar estas librerías para crear nuestro sistema de plugins en este artículo.

Definiendo los interfaces de los plugins

MEF nos ayuda a descubrir y añadir plugins a nuestras aplicaciones pero no impone ninguna restricción a los interfaces de comunicación entre las diferentes librerías, de hecho, lo más complicado en este caso es definir los métodos que vamos a utilizar y crear un sistema robusto que nos permita añadir ‘cualquier’ tipo de librería a nuestra aplicación principal.

Para este artículo, he definido una serie de interfaces básicos tanto para las clases que vamos a utilizar como plugin como para las clases que van a trabajar como host, es decir, las clases que se van a utilizar desde nuestra aplicación principal.

Los interfaces son intencionadamente simples, por supuesto habría que ampliarlos para una aplicación real pero pretendo que el ejemplo sea lo más sencillo posible para no confundir la base de MEF con la implantación en nuestra aplicación.

En primer lugar, vamos a ver el interface de la clase Host, es decir, la clase de nuestra aplicación principal con la que se van a comunicar los plugins:

namespace Bau.Libraries.LibPlugins.Core.Host
{
	// Interface para la aplicación Host
	public interface IHostController
	{ 
		//  Muestra un mensaje
		void ShowMessage(string strMessage);
	
		// Nombre de la aplicación
		string ApplicationName { get; }
	}
}

Un interface simple: una propiedad con el nombre de la aplicación y un método que muestra un mensaje.

El interface de las clases que funcionan como plugins es igual de simple:

namespace Bau.Libraries.LibPlugins.Core.Plugins
{
	// Interface para los plugins
	public interface IPlugin
	{
		// Inicializa las librerías del plugin
		void InitLibraries(Host.IHostController objHostController);

		// Devuelve un mensaje
		string GetHelloMessage();

		// Nombre del plugin
		string Name { get; }
	}
}

Sólo hay un par de métodos: uno que informa al plugin de cuál va a ser su controlador principal (InitLibraries) y otro utilizado para obtener un mensaje de saludo (GetHelloMessage). Por último, le añadimos una propiedad con el nombre del plugin (Name).

En MEF tenemos la oportunidad de asociar atributos con metadatos a nuestras clases exportadas, el interface que he utilizado en este caso para los metadatos es el siguiente:

namespace Bau.Libraries.LibPlugins.Core.Plugins
{
	// Interface para los datos de un plugin
	public interface IPluginMetaData
	{
		// Nombre del plugin
		string Name { get; }

		// Descripción del plugin
		string Description { get; }
	}
}
``

con dos propiedades para el nombre y la descripción del plugin.

Por facilitar las cosas, también creamos una clase abstracta para los plugins. Esto no es completamente necesario, pero cuando
ampliemos los interfaces, es posible que nos sea de utilidad para implementar métodos comunes para todos los plugins:

```csharp
// Clase base para los plugins
public abstract class AbstractBasePlugin: IPlugin
{
	public AbstractBasePlugin(string strName)
	{ 
		Name = strName;
	}

	// Inicializa las librerías del plugin
	public abstract void InitLibraries(Host.IHostController objHostController);

	// Obtiene el mensaje de saludo
	public abstract string GetHelloMessage();

	// Nombre del plugin
	public string Name { get; }

	// Controlador de la aplicación principal
	public Host.IHostController HostController { get; protected set; }
}

Como podemos observar, aún ni siquiera hemos comenzado a utilizar MEF, simplemente hemos creado interfaces y clases abstracta que utilizaremos como base en nuestras aplicaciones de plugin.

Nota: estos interfaces se han definido en una librería llamada LibPlugins.Core en la solución. Debemos añadir referencias a esta librería tanto en nuestras librerías de plugin como en las librerías o aplicaciones host para que compartan sus métodos.

Implementando un plugin

Para la implementación de una aplicación como plugin, nos creamos otro proyecto (en este caso lo he llamado PluginSampleHello) y una clase que implemente el interface IPlugin:

using System;
using System.ComponentModel.Composition;

using Bau.Libraries.LibPlugins.Core.Host;
using Bau.Libraries.LibPlugins.Core.Plugins;

namespace PluginSampleHello
{
	//	Controlador del plugin "SampleHello"
	[Export(typeof(IPlugin))]
	[ExportMetadata("Name", "SampleHello")]
	[ExportMetadata("Description", "Prueba de plugin")]
	public class PluginSampleHelloController: AbstractBasePlugin
	{	
		public PluginSampleHelloController(): base("SampleHello") {}

		// Inicializa las librerías
		public override void InitLibraries(IHostController objHostController)
		{ 
			System.Timers.Timer tmrMessage = new System.Timers.Timer(5000);

			// Inicializa el controlador
			HostController = objHostController;
			// Inicializa un temporizador para enviar un mensaje al controlador
			tmrMessage.Start();
			tmrMessage.Elapsed += (objSender, objEventArgs) =>
				{ HostController.ShowMessage("Mensaje enviado desde PluginSampleHelloController");
				};
			// Muestra el mensaje en la consola
			Console.WriteLine("Inicializando las librerías de SampleHello");
		}

		// Obtiene el mensaje de saludo
		public override string GetHelloMessage()
		{ 
			return $"Hello desde PluginSampleHelloController. Mi host es {HostController.ApplicationName}";
		}
	}
}

Siguiendo con la filosofía del artículo, no he hecho más que implementar la interface a través de la clase abstracta, es decir, hemos definido los métodos InitLibraries y GetHelloMessage.

Además, al método InitLibraries le hemos añadido un temporizador para que cada cierto tiempo mande un mensaje a la aplicación Host para así demostrar que podemos llamar tanto desde el plugin al host como desde el host al plugin.

De cualquier forma, ya hemos comenzado a utilizar MEF en esta clase añadiendo el atributo Export a la cabecera de la clase indicando que exportamos o implementamos el interface IPlugin que definimos en la sección anterior.

Los otros dos atributos ExportMetadata nos sirven para añadir metadatos al plugin que podemos utilizar desde la aplicación host. Si nos fijamos, son las mismas propiedades que definimos en IPluginMetaData y que no hemos implementado en ninguna clase real.

Hasta aquí la configuración del plugin: no hace falta nada más para tener una extensión con MEF, simplemente nos queda pasar a la aplicación que funcionará como host.

Implementando una aplicación host

La aplicación host es aquella que va a importar y utilizar los plugins. En algunos casos también se encargará de cargarlos y descargarlos o de las comunicaciones entre ellos.

Para implementarlo nos creamos otro proyecto: una aplicación WPF llamada TestPlugins. Podríamos haber elegido cualquier otro tipo de aplicación pero considero que una aplicación de escritorio es más adecuada para este tipo de pruebas.

La clase que importa y controla los plugins es la siguiente:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

using Bau.Libraries.LibPlugins.Core.Host;
using Bau.Libraries.LibPlugins.Core.Plugins;

namespace TestPlugins.Plugins
{
  // Manager de plugins
  [Export(typeof(IPluginManager))]
  public class PluginsManager: IPluginManager
  {
    #pragma warning disable 0649
    //Plugins
    [ImportMany]
    private IEnumerable<Lazy<IPlugin, IPluginMetaData>> objColPlugins;
    #pragma warning restore 0649

    // Inicializa los datos
    public void Initialize(Type objTypeMainAssembly, string strPathsPlugins)
    { 
    	CompositionContainer objContainer;
		AggregateCatalog objCatalog = new AggregateCatalog();
		string [] arrStrPaths = strPathsPlugins.Split(';');
		
		// Añade todas las secciones localizadas en el ensamblado principal
		objCatalog.Catalogs.Add(new AssemblyCatalog(objTypeMainAssembly.Assembly));
		// Añade los directorios de plugins
		if (arrStrPaths.Length > 0)
			foreach (string strPath in arrStrPaths)
				if (System.IO.Directory.Exists(strPath))
					objCatalog.Catalogs.Add(new DirectoryCatalog(strPath));
		// Crea el CompositionContainer con las secciones del catálogo
		objContainer = new CompositionContainer(objCatalog);
		// Rellena los datos importados de este objeto
		try
		{  
			objContainer.ComposeParts(this);
		}
		catch (CompositionException objException)
		{ 
			Console.WriteLine(objException.ToString());
		}
    }

  // Inicializa los plugins
  public void InitPlugins(IHostController objHostController, out string strError)
  { 
  	// Inicializa los argumentos de salida
    strError = "";
    // Indica a los plugins que inicialicen sus librerías
    if (Plugins != null)
      foreach (Lazy<IPlugin, IPluginMetaData> objPlugin in Plugins)
        try
          { 
          	objPlugin.Value.InitLibraries(objHostController);
          }
        catch (Exception objException)
          { 
          	strError += $"Error en el método InitLibraries del plugin {objPlugin.Metadata.Name}.\nExcepción: {objException.Message}" + Environment.NewLine;
          }
    }

  // Plugins
  public IEnumerable<Lazy<IPlugin, IPluginMetaData>> Plugins 
  { 
  	get { return objColPlugins; }
  }

  // Lista de plugins
  public List<IPlugin> PluginsController
  { get 
      { 
      	List<IPlugin> objColPlugins = new List<IPlugin>();

        // Crea la lista de plugins
        if (Plugins != null)
          foreach (Lazy<IPlugin, IPluginMetaData> objPlugin in Plugins)
            objColPlugins.Add(objPlugin.Value);
        // Devuelve la lista de plugins
        return objColPlugins;	
      }
    }
  }
}

Al ser la clase que vamos a utilizar como host, indicamos a MEF que queremos exportarla utilizando el atributo Export e indicando que implementamos el interface IPluginManager que hemos definido al principio del artículo.

También nos definimos una variable global que va a almacenar nuestros plugins y la marcamos con otro atributo de MEF, en este caso ImportMany indicando que vamos a importar no un objeto si no varios y de tipo IPlugin con metadatos de tipo IPluginMetadata.

El método que realiza la importación en este caso es Initialize. Existen varias formas de importar extensiones en MEF, la más cómoda para mí es indicar una serie de directorios donde buscar nuestros ensamblados, para ello, a este método le pasamos una cadena con directorios separados por punto y coma.

El método Initialize se crea un contenedor y le va añadiendo los ensamblados de los directorios. Automáticamente MEF nos deja los plugins en la colección de plugins que definimos al principio (objColPlugins).

El resto de métodos únicamente nos sirven para llamar al método InitLibraries de cada uno de los plugins que tengamos cargado y preparan una interface para que podamos acceder desde el resto de las clases de nuestra aplicación.

Nos queda por implementar la clase HostController que en teoría es la encargada de la comunicación entre las clases de plugins y las ventanas de la aplicación. En nuestro caso sólo contiene el nombre de la aplicación y un método al que llaman los plugins para mostrar un mensaje:

// Controlador del host de Plugins
public class HostController: Bau.Libraries.LibPlugins.Core.Host.IHostController
{ 
	// Eventos públicos
	public event EventHandler<string> MessageAdded;

	public HostController()
	{ 
		ApplicationName = "TestPlugins";
	}
	
	// Muestra un mensaje
	public void ShowMessage(string strMessage)
	{ 
		if (MessageAdded != null)
			MessageAdded(this, strMessage);
	}
	
	// Nombre de la aplicación
	public string ApplicationName { get; }
}

Para terminar, he creado una ventana de WPF que presenta un cuadro de texto donde podemos introducir los directorios de los que vamos a cargar los plugins y una lista con los plugins cargados.

El único método interesante de esta ventana es el que llama a la clase PluginsManager para cargar los plugins y añade un manejador de eventos para recoger los mensajes enviado con el temporizador de la clase PluginSampleHelloController que vimos en la sección anterior:

// Carga los plugins
private void LoadPlugins()
{	
	Plugins.HostController objHostController = new Plugins.HostController();
	Plugins.PluginsManager objPluginsManager = new Plugins.PluginsManager();
	string strError;

	// Inicializa el manager
	objPluginsManager.Initialize(typeof(MainWindow), txtPath.Text);
	// Inicializa los plugins
	objPluginsManager.InitPlugins(objHostController, out strError);
	// Añade los plugins a la lista
	lstPlugins.Items.Clear();
	foreach (IPlugin objPlugin in objPluginsManager.PluginsController)
		lstPlugins.Items.Add($"Plugin {objPlugin.Name} - {objPlugin.GetHelloMessage()}");
	// Añade el manejador de mensajes
	objHostController.MessageAdded += (objSender, strMessage) =>
		{ Dispatcher.Invoke(() => lstPlugins.Items.Add($"Mensaje recibido en el host: {strMessage}"),
													   System.Windows.Threading.DispatcherPriority.Normal);
		};
}

Resumen

Es muy sencillo utilizar MEF para implementar extensiones en una aplicación. Como hemos visto, sólo tenemos que utilizar una serie de atributos y cargar los plugins utilizando un contenedor.

Lo realmente complicado es definir lo que van a hacer nuestras extensiones y ampliar los interfaces de forma que sean lo más flexibles posible.

Como siempre, el código fuente de las pruebas lo podeis encontrar en GitHub.