Silverlight 4: Carga dinámica de aplicaciones XAP usando MEF

Hola a todos!

Por cuestiones laborales he tenido que pelearme con MEF (Managed Extensibility Framework) para, desde una aplicación Silverlight poder cargar bajo demanda del usuario otras aplicaciones Silverlight secundarias e incrustarlas en la aplicación principal como si de UserControls se tratase.

¿Que es MEF?

MEF o Managed Extensibility Framework, es una librería para crear aplicaciones extensibles mediante plugins, sin tener que realizar complicadas operaciones.

MEF establece una forma de descubrimiento implícito de estos plugins. Generalmente una aplicación que use MEF para exponerse a otras aplicaciones, es llamada Part. Define sus dependencias (marcadas con el atributo Imports) y sus capacidades (conocidas como Exports).

De esta forma una aplicación Host puede descubrir y cargar estas partes en tiempo de ejecución, en ese momento el Composition Engine de MEF se encarga de satisfacer las dependencias (Imports) de la parte cargada usando ensamblados o referencias de si misma u otras partes ya cargadas.

MEF no se limita a Silverlight, es parte integral de .NET 4 y puede ser usado desde aplicaciones Winforms, WPF, ASP.NET o cualquier otro tipo de aplicación.

Un ejemplo sencillo

Vamos a realizar un ejemplo sencillo, compuesto por 3 aplicaciones Silverlight (2 aplicaciones para ser inyectadas y una tercera que actuará de Host), solo necesitamos el proyecto web de la aplicación Host, las otras dos no lo necesitan, deberíamos tener una solución con 4 proyectos (3 Silverlight y 1 Web), en mi caso no he creado el proyecto web porque luego he alojado la aplicación Host directamente en IIS:

image

Configurando los proyectos que actuarán de Plugins

Una vez creados nuestros proyectos, debemos empezar a configurar las aplicaciones que se comportarán como Plugins: DynLoadApp1 y DynLoadApp2. Lo primero que debemos hacer en estas dos aplicaciones es añadir las siguientes referencias:

System.ComponentModel.Composition.dll
System.ComponentModel.Composition.Initialization.dll

Podemos encontrarlas en “..Program FilesMicrosoft SDKsSilverlightv4.0LibrariesClient”, es muy importante que en las propiedades de cada una de las referencias le indiquemos el atributo Copy Local a False, para evitar referencias duplicadas que solo harían crecer nuestro paquete XAP.

Para poder tener disponible una clase, usercontrol o página desde estos proyectos en nuestro proyecto Host debemos marcar la clase deseada con un Export, para que el Composition Engine de MEF la pueda localizar y ponerla a disposición del Host, en nuestro caso marcamos MainPage.xaml.cs con este atributo (declarado en System.ComponentModel.Composition):

[Export(typeof(UserControl))]
public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
    }
}

Una vez hecho esto, podemos generar nuestros plugins, que deberemos copiar al directorio Bin de nuestro Host (el mismo donde estará el xap de nuestro host) para poder cargarlos.

Configurando la aplicación Host

Este paso es quizás el más largo y complicado en MEF, puesto que nos exige que llevemos a buen termino varias tareas.

Para empezar, en nuestro proyecto Host referenciaremos de nuevo los ensamblados:

System.ComponentModel.Composition.dll
System.ComponentModel.Composition.Initialization.dll

Esta vez sin embargo debemos dejar el atributo Copy Local a true, pues usaremos las referencias de nuestro host para satisfacer las de nuestros plugins.

Lo siguiente será crearnos una carpeta “Helpers” donde insertaremos dos clases: DeploymentCatalogServices.cs que contendrá el código encargado de Añadir y Eliminar paquetes XAP en nuestro catalogo MEF y LoadHelper.cs que se encargará de cargar los paquetes XAP desde el catalogo de MEF e incrustarlos en nuestra aplicación:

DeploymentCatalogServices.cs se compone de una clase y un interface, el interface se usa para inicializar el servicio de catálogo e indicar el contrato a usar entre el Catálogo de MEF y nuestra clase de carga de paquetes en catalogo:

public interface IDeploymentCatalogService
{
    // The class that will implement this interface will implement these two methods
    void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null);
    void RemoveXap(string uri);
}

[Export(typeof(IDeploymentCatalogService))]
public class DeploymentCatalogService : IDeploymentCatalogService
{
    private static AggregateCatalog _aggregateCatalog;
    Dictionary<string, DeploymentCatalog> _catalogs;

    public DeploymentCatalogService()
    {
        _catalogs = new Dictionary<string, DeploymentCatalog>();
    }

    public static void Initialize()
    {
        _aggregateCatalog = new AggregateCatalog();
        _aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
        CompositionHost.Initialize(_aggregateCatalog);
    }

    public void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null)
    {
        // Add a .xap to the catalog
        DeploymentCatalog catalog;
        if (!_catalogs.TryGetValue(new Uri(App.Current.Host.Source.ToString().Substring(0, App.Current.Host.Source.ToString().LastIndexOf('/') + 1) + uri).ToString(), out catalog))
        {
            catalog = new DeploymentCatalog(new Uri(App.Current.Host.Source.ToString().Substring(0, App.Current.Host.Source.ToString().LastIndexOf('/') + 1) + uri));

            if (completedAction != null)
            {
                catalog.DownloadCompleted += (s, e) => completedAction(e);
            }
            else
            {
                catalog.DownloadCompleted += catalog_DownloadCompleted;
            }

            catalog.DownloadAsync();
            _catalogs[uri] = catalog;
            _aggregateCatalog.Catalogs.Add(catalog);
        }
    }

    void catalog_DownloadCompleted(object sender, AsyncCompletedEventArgs e)
    {
        // Chekcks for errors loading the .xap
        if (e.Error != null)
        {
            throw e.Error;
        }
    }

    public void RemoveXap(string uri)
    {
        // Remove a .xap from the catalog
        DeploymentCatalog catalog;
        if (_catalogs.TryGetValue(uri, out catalog))
        {
            _aggregateCatalog.Catalogs.Remove(catalog);
            _catalogs.Remove(uri);
        }
    }
}

Como podemos ver, nuestra clase está marcada también con un Export para que pueda ser accedida y usada desde MEF.

LoadHelper.cs se encargará de satisfacer las referencias y dependencias (Imports) de nuestros paquetes XAP y de obtener del catalogo e incrustar en nuestra aplicación los diferentes paquetes que tengamos disponibles (o le indiquemos):

public class LoadHelper : IPartImportsSatisfiedNotification
{
    [Import]
    public IDeploymentCatalogService CatalogService { get; set; }

    // Specifies that a property, field, or parameter should be populated with all
    // matching exports by the System.ComponentModel.Composition.Hosting.CompositionContainer.
    [ImportMany(AllowRecomposition = true)]
    public Lazy<UserControl>[] MEFModuleList { get; set; }

    public string XapSelected = "";
    public Grid GridSelected;

    public LoadHelper()
    {
        //Initialize DeploymentCatalogService.
        CompositionInitializer.SatisfyImports(this);
    }
    
    public void LoadSelectedModule()
    {
        // Ensure that we have a Panel to add the .xap to
        if (GridSelected != null)
        {
            // Create a name for the .xap without the ".xap" part
            string strRevisedSelectedXAPName = XapSelected.Replace(".xap", ".");

            // Determine if the .xap is already loaded
            var SelectedMEFModuleInPanel = (from Module in GridSelected.Children.Cast<UserControl>()
                                            where Module.ToString().Contains(strRevisedSelectedXAPName)
                                            select Module).FirstOrDefault();

            // If the .xap is not loaded
            if (SelectedMEFModuleInPanel == null)
            {
                // Clear the panel
                GridSelected.Children.Clear();

                // Get the selected .xap 
                var SelectedMEFModule = (from Module in MEFModuleList.ToList()
                                         where Module.Value.ToString().Contains(strRevisedSelectedXAPName)
                                         select Module).FirstOrDefault();

                // If the .xap is found
                if (SelectedMEFModule != null)
                {
                    // Add the .xap to the main page
                    GridSelected.Children.Add(SelectedMEFModule.Value);
                }
            }
        }
    }
    
    void IPartImportsSatisfiedNotification.OnImportsSatisfied()
    {
        LoadSelectedModule();
    }
}

Lo primero que hacemos en el constructor de esta clase es satisfacer los Imports y una vez hecho esto ejecutamos el método LoadSelectedModule que se encarga de obtener y cargar el paquete XAP indicado, posicionándolo dentro del Panel (Canvas, Grid…) indicado.

Ahora solo nos queda diseñar la UI de nuestro Host:

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width=".5*"></ColumnDefinition>
            <ColumnDefinition Width=".5*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"></RowDefinition>
            <RowDefinition Height="40"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        
        <TextBlock Grid.Row="0" Grid.ColumnSpan="2" 
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   Text="On demand XAP Loader"
                   FontSize="36"
                   FontWeight="Bold">
        </TextBlock>
        
        <Button Name="btnApp1"
                Grid.Row="1" Grid.Column="0" Margin="5,0,5,5" 
                Content="Load DynLoadApp1.XAP Application"
                FontSize="14"
                FontWeight="Bold" Click="btnApp1_Click">
        </Button>

        <Grid Grid.Row="2" Grid.Column="0" Name="MEFPanelApp1"></Grid>

        <Button Name="btnApp2"
                Grid.Row="1" Grid.Column="1" Margin="5,0,5,5"
                Content="Load DynLoadApp2.XAP Application"
                FontSize="14"
                FontWeight="Bold" Click="btnApp2_Click">                
        </Button>
        
        <Grid Grid.Row="2" Grid.Column="1" Name="MEFPanelApp2"></Grid>        
    </Grid>

Como puedes ver es muy simple, hemos dividido la pantalla en dos columnas, tenemos dos botones, cada uno de los cuales realiza la petición de carga de un paquete xap y dos grids (MEFPanelApp1 y MEFPanelApp2) que contendrán su correspondiente paquete XAP.

En los eventos Click de los botones realizamos la petición de carga de los paquetes:

private void btnApp1_Click(object sender, RoutedEventArgs e)
{
    MefLoader.CatalogService.AddXap("DynLoadApp1.xap");
    MefLoader.XapSelected = "DynLoadApp1.xap";
    MefLoader.GridSelected = MEFPanelApp1;
    MefLoader.LoadSelectedModule();
}

private void btnApp2_Click(object sender, RoutedEventArgs e)
{
    MefLoader.CatalogService.AddXap("DynLoadApp2.xap");
    MefLoader.XapSelected = "DynLoadApp2.xap";
    MefLoader.GridSelected = MEFPanelApp2;
    MefLoader.LoadSelectedModule();
}

Pasos Finales

Para que todo esto funcione sin mayor problema, deberemos tener en cuenta las siguientes instrucciones:

  • Debemos copiar los paquetes XAP de los plugins al directorio Bin donde se encuentra el paquete XAP del Host.
  • Si lanzamos la aplicación desde una ruta física, obtendremos una excepción NotSupportedException al cargar los plugins, debemos realizar la carga desde un servidor IIS o un servidor de desarrollo de Visual Studio.
  • Una vez que hayamos ejecutado desde un servidor web, podemos instalar la app Out Of the Browser y funcionará perfectamente.

Si tenemos estos pasos en cuenta y ejecutamos nuestra aplicación, presionando los botones deberíamos ser capaces de cargar los paquetes XAP como si de UserControls se tratasen:

image

image

image

Con esto ya tenemos MEF funcionando a nuestra disposición y la posibilidad de, de manera sencilla, realizar aplicaciones extensibles mediante Plugins.

Os dejo el código fuente de la solución y cualquier duda, aquí estoy para ayudaros.

Un saludo y Happy Coding

15 comentarios sobre “Silverlight 4: Carga dinámica de aplicaciones XAP usando MEF”

  1. Hola jmmartinez

    La respuesta corta: NO

    La respuesta larga: No, por ahora al menos. Silverlight Mobile (la versión que usamos para desarrollar en WP7) se corresponde con Silverlgiht 3 + algún pequeño añadido de Silverlight 4, lamentablemente entre esos añadidos no se encuentra MEF, aunque en el site de codeplex de MEF comentan que están trabajando en ello.

    Un gran saludo y gracias por leerme!

  2. ¿Que debo hacer para implementar una clase que reciba unos datos de iniciales para que el xap dinamico muestre un mensaje que le envia el xap que lo llamo?

  3. Hola este comando no lo he podido transcribir a VB, me gen era error y no puedo probar el ejemplo te agradeceria haber si es q me falta algo

    catalog.DownloadCompleted += (s, e) => completedAction(e);

    Alex

  4. Alex, transcribir ese evento (en C# para hacer el AddHandler de VB se hace de la manera que has escrito) quedaría:

    AddHandler catalog.DownloadCompleted, Sub(s, e) completedAction(e)

  5. Hola amigo esta excelente tu post, tengo una pregunta, es posible en vez de usar objetos como Panel (Canvas, Grid…), usar ChildWindow??

  6. Hola, excelente post muy claro.. tengo una duda, es posible invocar una aplicacion .xap que corre fuera del navegador desde otra que corre dentro del navegador ? y si es posible que la aplicacion que se invoque me devuelva unos datos que requiere la otra apliacion ?

  7. Lamentablemente no funciona, me marca errores: Excepción durante una solicitud WebClient para ser exacto en: void catalog_DownloadCompleted(object sender, AsyncCompletedEventArgs e), siguí todos los pasos que indicas, pero no funcionó.

  8. Buenas! Debe existir algún error en tu código que está generando la excepción, ¿que error te da exactamente? ¿Cual es la innerexception?
    Un saludo!

  9. Pensé que no responderías, gracias.
    En esta línea MefLoader.CatalogService.AddXap(«DynLoadApp1.xap»);
    la cual provoca que en .AddXap en la línea: _aggregateCatalog.Catalogs.Add(catalog);
    mande error:
    Referencia a objeto no establecida como instancia de un objeto.

  10. Buenas, tendrías que revisar tu código, has probado con el ejemplo que tienes para descargar aquí?

    La verdad es que ando muy liado, pero si tengo un momento reviso que puede estar pasando.

    Un saludo!

  11. es posible invocar una aplicacion .xap que corre fuera del navegador desde otra que corre dentro del navegador ? de ser posible puedes dar un ejemplo como seria?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *