MEF Creación de Aplicaciones Extensibles

MEF es un componente que se ha introducido en el framework 4.0 y que en mi opinión es un gran desconocido y que nos va a permitir diseñar aplicaciones extensibles sin ningún esfuerzo extra, de manera que incluso terceros podrán extender la aplicación en tiempo de ejecución sin necesidad de recompilar ni reiniciar la aplicación. Un buen ejemplo es Seesmic con todos sus plugins. No es que Microsoft haya inventado la rueda esto ya lo hacíamos antes con una serie de patrones, en este caso lo que han hecho es facilitar su implementación.

Mucha gente confunde MEF con Ioc y no es lo mismo, MEF utiliza internamente Ioc, en mi opinión , cada uno resuelve problemas distintos. Ioc esta orientado al desacoplamiento entre  componentes y MEF tiene como objetivo la extensibilidad de nuestra aplicación, pero bueno esto daría para muchas discusiones asi que vamos a centrarnos en MEF y para ello lo mejor como siempre un ejemplo.

MEF se basa en un Catalogo, es decir que extensiones (plugins) tengo y un CompositeContainer que es el contenedor de estos plugins y se encarga de instanciarlos y resolver referencias. el ejemplo que propongo es muy sencillo vamos a tener una listbox donde cargaremos nuestras extensiones  y que al seleccionar una de ellos mostrar un mensaje cada una en un idioma diferente, vamos un hola mundo.

Lo primero que tenemos que hacer es definir el contrato que van a tener los plugins que queremos en nuestra aplicación, esto es un requisito importante cada plugin que hagamos, este debe de cumplir una interfaz. En nuestro ejemplo creamos un proyecto donde generaremos esta Interfaz, le daremos de nombre HelloWorldMEFContract que contendrá una interfaz en este caso tan sencilla como

1 public interface HelloWorldContract 2 { 3 //Nombre del plugin 4 string Name { get; } 5 //Metodo que devolvera hola en el idioma del plugin 6 string Hello(); 7 }

Una vez tenemos definida nuestra interface para el plugin vamos a ir construyendo estos plugins , para lo que añadiremos un proyecto por cada uno de ellos, en este caso voy a hacer dos plugins uno para castellano y otro para ingles. A cada proyecto le añadimos como referencia el assembly de la interface anterior y la clase la hacemos que implemente la interface HelloWorldContract.

 

1 public class HolaMundo :HelloWorldContract 2 { 3 #region HelloWorldContract Members 4 5 public string Name 6 { 7 get 8 { 9 return "Plugin Spanish"; 10 } 11 12 } 13 14 public string Hello() 15 { 16 return "Hola"; 17 } 18 19 #endregion 20 }

 

Hasta aquí nada de MEF, pero para que este plugoin sea utilizado por MEF lo primero que tenemos que hacer es añadir la referencia System.ComponentModel.Composition una vez añadimos ponemos como atributo de la clase HolaMundo Export que indicara que sea clase es un plugin que MEF tiene que recoger.

1 [Export(typeof(HelloWorldContract))] 2 public class HolaMundo :HelloWorldContract 3 { 4 #region HelloWorldContract Members 5 6 public string Name 7 { 8 get 9 { 10 return "Plugin Spanish"; 11 } 12 13 } 14 15 public string Hello() 16 { 17 return "Hola"; 18 } 19 20 #endregion 21 }

De esta manera iremos definiendo los plugins que queramos, en nuestro caso el de Ingles quedaría

 

1 [Export(typeof(HelloWorldContract))] 2 public class HelloWorld:HelloWorldContract 3 { 4 #region HelloWorldContract Members 5 6 public string Name 7 { 8 get { return "Plugin English"; } 9 } 10 11 public string Hello() 12 { 13 return "Hello"; 14 } 15 16 #endregion 17 } 18

Ahora vamos a implementar la parte consumidora de estos plugins para lo que crearemos una aplicación WPF la cual tendrá una ventana donde añadiremos un combo y un TextBlock. Para consumir los plugins podemos realizar la carga de un assembly o de varios a la vez. Lo logico es crear una carpeta plugins y copiar todos nuestro plugins en este directorio y realizar la carga indicando ese directorio que es lo que vamos a realizar nosotros.

Lo primero que tenemos que hacer es definir una lista donde vamos a realizar la carga de todos los plugins que consuman ese interface.

 

 

1 [ImportMany(typeof(HelloWorldContract))] 2 private IList<HelloWorldContract> pluginsHello = new List<HelloWorldContract>(); 3

 

Si os fijais hemos puesto el atributo ImportMany de manera que todos los assemblys que contenga ese interfaz los va a cargar en esa lista, el cuando lo vemos mas adelante. Como hemos dicho que vamos a cargar todos los plugins de la carpeta “plugin” utilizaremos el metodo DirectoryCatalog , pero vamos a ver el proceso entero en el código.

 

1 public partial class MainWindow : Window 2 { 3 [ImportMany(typeof(HelloWorldContract))] 4 private IList<HelloWorldContract> pluginsHello = new List<HelloWorldContract>(); 5 6 public MainWindow() 7 { 8 InitializeComponent(); 9 10 InitiateMEF(); 11 Loaded += new RoutedEventHandler(MainWindow_Loaded); 12 } 13 14 private void InitiateMEF() 15 { 16 // crea el catalogo con los tipos que tengan el atributo export de los assemblies de la carpeta plugins 17 DirectoryCatalog catalog = new DirectoryCatalog("Plugins"); 18 19 // Crea el contenedor para el catalogo 20 CompositionContainer container = new CompositionContainer(catalog); 21 22 // Aqui MEF llena la variables pluginsHello con los tipos que cumpla HelloWroldContract 23 24 try 25 { 26 container.ComposeParts(this); 27 } 28 catch (CompositionException compositionException) 29 { 30 MessageBox.Show(compositionException.ToString(), "Error", MessageBoxButton.OK, MessageBoxImage.Error); 31 } 32 }

 

Si os fijáis en el código cuando se llama a ComposeParts es cuando crea todas las clases del catalogo en la lista que hemos declarado con el atributo ImportMany

El siguiente paso es utilizar ya los plugins, en nuestro ejemplo cargaremos el combo con la propiedad Name de los plugins.

1 void MainWindow_Loaded(object sender, RoutedEventArgs e) 2 { 3 foreach (HelloWorldContract plugin in pluginsHello) 4 { 5 cbIdioma.Items.Add(plugin.Name) ; 6 7 } 8 if (cbIdioma.Items.Count > 0) 9 { 10 cbIdioma.SelectedIndex = 0; 11 } 12 13 }

En el evento SelectionChanged invocaremos al método Hello para que muestro Hola en el idioma correspondiente

1 void cbIdioma_SelectionChanged(object sender, SelectionChangedEventArgs e) 2 { 3 foreach (HelloWorldContract plugin in pluginsHello) 4 { 5 if (cbIdioma.SelectedValue.ToString() == plugin.Name) 6 { 7 tbHello.Text = plugin.Hello(); 8 } 9 10 } 11 }

Como veis es muy sencillo utilizar este Framework y nos posibilita de realizar nuestras aplicaciones extensibles.

 

  

Aqui os dejo el código es un zip que contiene un rar, soy un enrevesado

Download File – Code

Pasar parámetros de una aplicación una que esta en ejecución

Después de este titulo tan largo esta un problema que se me planteo la semana pasada y era la necesidad de pasar información a una aplicación WPF que se encontraba en ejecución desde otra aplicación, en este caso eran los parámetros de llamada.

Lo primero que se me vino a la cabeza era realizarlo a través de WCF teniendo un host en la aplicación que esperaba la información y utilizando el binding NetNamedPipeBinding, pero pensaba que se podía realizar de diferente manera.Después de unas cuantas horas pensando, navegando… vi la luz.

Lo primero vamos a definir una clase en este caso ApplicationParametersProxy, lo importante es que esta clase debe de heredar de MarshalByRefObject, si recordamos, esta clase según viene en la MSDN

“MarshalByRefObject es la clase base de los objetos que se comunican a través de los límites de los dominios de aplicación que utilizan un proxy para intercambiar mensajes. Los objetos que no heredan de MarshalByRefObject tienen implícitamente valor de resolución. Cuando una aplicación remota hace referencia a un objeto con valor de resolución, se pasa una copia del objeto a través de los límites del dominio de la aplicación.

Es posible obtener acceso a los objetos MarshalByRefObject directamente dentro de los límites del dominio local de la aplicación. La primera vez que una aplicación en un dominio de aplicación remoto obtiene acceso a un MarshalByRefObject, se pasa un proxy a la aplicación remota. El cálculo de referencias de las siguientes llamadas que se realizan en el proxy, volverá a tener lugar en el objeto que reside en el dominio de la aplicación local.”

Esta clase nos va a permitir comunicarnos entre diferentes dominios que es nuestro caso

No debemos de olvidarnos dar a esta clase permisos de FullTrust a través del atributo

 [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]

A esta clase le añadimos la propiedad estática

public static bool ExistsApplication{ get; internal set; }

Esta propiedad indicara si ya existe una aplicación que ha registrado la clase. Añadimos otra propiedad en la que almacenaremos los argumentos

  public static string[] CommandLineArgs { get; internal set; }

Una vez definida esta clase, lo siguiente que haremos será definir una clase que herede de EventArgs para definir los argumentos que vamos a necesitar para lanzar un evento que mas adelante veremos.

1 public class InstanceCallbackEventArgs : EventArgs 2 { 3 4 internal InstanceCallbackEventArgs(bool existsApplication, string[] commandLineArgs) 5 { 6 ExistsApplication = existsApplication, ; 7 CommandLineArgs = commandLineArgs; 8 } 9 10 11 public bool ExistsApplication { get; private set; } 12 13 14 public string[] CommandLineArgs { get; private set; } 15 }

El siguiente paso es definir la clase donde vamos a controlar si la aplicación esta lanzada y pasarle los parámetros desde la segunda aplicación. Esta clase deberá ser estática y con un método que nos permitirá crear una única instancia utilizando la clase ApplicationParametersProxy.

 

1 public static bool CreateSingleInstance(string name, EventHandler<InstanceCallbackEventArgs> callback) 2 { 3 EventWaitHandle eventWaitHandle = null; 4 string eventName = string.Format("{0}-{1}", Environment.MachineName, name); 5 6 ApplicationParametersProxy.IsFirstInstance = false; 7 ApplicationParametersProxy .CommandLineArgs = Environment.GetCommandLineArgs(); 8 9 try 10 { 11 // intentamos abrir el evento para ver si existe 12 // si no existe excepción 13 eventWaitHandle = EventWaitHandle.OpenExisting(eventName); 14 } 15 catch 16 { 17 // si no existe es la primera vez que se va a crear 18 ApplicationParametersProxy.IsFirstInstance = true; 19 } 20 21 if (ApplicationParametersProxy.IsFirstInstance) 22 { 23 // iniciamos el handle 24 eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); 25 26 // Lo registramos 27 ThreadPool.RegisterWaitForSingleObject(eventWaitHandle, WaitOrTimerCallback, callback, Timeout.Infinite, false); 28 eventWaitHandle.Close(); 29 30 // Registramos el tipo que vamos a compartir 31 RegisterRemoteType(name); 32 } 33 else 34 { 35 // si ya existe una instancia le pasamos los parametro 36 UpdateRemoteObject(name); 37 38 39 if (eventWaitHandle != null) eventWaitHandle.Set(); 40 41 42 // matamos este proceso 43 Environment.Exit(0); 44 } 45 46 return InstanceProxy.IsFirstInstance; 47 }

 

El método OpenExisting intenta abrir un evento del sistema con nombre existente. Si el evento del sistema no existe, este método produce una excepción en lugar de crear el evento del sistema, lo que nos indica que es la primera instancia y debemos registrarlo, Para ello utilizaremos el método RegisterRemoteType.

 

1 private static void RegisterRemoteType(string uri) 2 { 3 // registra el canal remoto (net-pipes) 4 var serverChannel = new IpcServerChannel(Environment.MachineName + uri); 5 ChannelServices.RegisterChannel(serverChannel, true); 6 7 // registra el tipo compartido 8 RemotingConfiguration.RegisterWellKnownServiceType( 9 typeof(ApplicationParametersProxy), uri, WellKnownObjectMode.Singleton); 10 11 // cierra el canal cuando el proceso muera 12 Process process = Process.GetCurrentProcess(); 13 process.Exited += delegate { ChannelServices.UnregisterChannel(serverChannel); }; 14 }

Si es la segunda instancia el método UpdateRemoteObject se encargara de pasar los parámetros de la segunda instancia a la primera.

 

1 private static void UpdateRemoteObject(string uri) 2 { 3 // registramos el canal net-pipe c 4 var clientChannel = new IpcClientChannel(); 5 ChannelServices.RegisterChannel(clientChannel, true); 6 7 // cogemos el objeto compartido por el otro proceos 8 var proxy = 9 Activator.GetObject(typeof(ApplicationParametersProxy), 10 string.Format("ipc://{0}{1}/{1}", Environment.MachineName, uri)) as ApplicationParametersProxy; 11 12 // pasamos los parametroe 13 if (proxy != null) 14 proxy.SetCommandLineArgs(ApplicationParametersProxy.IsFirstInstance, ApplicationParametersProxy.CommandLineArgs); 15 16 // cerramos el canal 17 ChannelServices.UnregisterChannel(clientChannel); 18 } 19 20

 

Por ultimo el método WaitOrTimerCallback invocara el evento en el primer proceso para indicarle que ya se han pasado los argumentos.

 

1 private static void WaitOrTimerCallback(object state, bool timedOut) 2 { 3 4 var callback = state as EventHandler<InstanceCallbackEventArgs>; 5 if (callback == null) return; 6 7 // lanzamos el evento en el otro proceso 8 callback(state, 9 new InstanceCallbackEventArgsApplicationParametersProxy.IsFirstInstance, 10 ApplicationParametersProxy.CommandLineArgs)); 11 }

Espero que os sirva como a mi.