Les coulisses du Techdays 2014 : Développer une application Modern UI avec MEF 2.0

Bonjour à tous,

2 décembre 2013, J – 71 avant l’édition 2014 des Techdays et je m’attèle à développer une application afin d’illustrer les nouvelles APIs disponibles avec le Windows Runtime 8.1.

Les fonctionnalités de cette application vont sans doute évoluer avec le temps, mais le but ultime, c’est de faire une application, qui permet d’illustrer, d’expliquer, de stocker et donc rechercher des petits exemples de code rapidement.

Dans sa 1ere mouture je me suis fixé les objectifs suivants pour cette application

  1. Chaque exemple devra être chargé dynamiquement.
  2. Le code source de l’exemple devra être affiché à l’écran et colorisé pour une meilleur compréhension.
  3. On devra pouvoir effectuer une recherche, autant “in-app” , qu’externe à l’application.
  4. Et d’autres fonctionnalités qui viendront avec le temps.

Managed Extensibility Framework 2.0 (MEF)

Pour pouvoir charger dynamiquement une Librairie, je vais utiliser le Framework .NET MEF 2.0, que vous pouvez obtenir directement à partir de Visual Studio 2013. C’est un package “nuget”, il suffit de taper “composition” dans la boite de recherche et installer le package.

image

Je ne vais pas détailler MEF 2.0, vous trouverez un guide, pour ceux qui voudraient aller un peu plus loin.

Sinon, voici la manière dont je l’ai implémenté.

Il faut une application Hôte qui aura pour rôle

  1. De découvrir les exemples.
  2. De les charger dynamiquement.
  3. Et d’afficher leur interface utilisateur sous forme d’un UserControl.

Pour que l’hôte puisse être couplé faiblement avec nos différents exemples, il faut définir un contrat entre l’hôte et les exemples. Ce contrat expose les méthodes ou les propriétés qui seront invoquées.

Dans une librairie nommée Ultimate.Interfaces, je déclare une interface comme suit :

public interface IUltimateDemo
{
Lazy<UserControl> View { get; set; }
}

Pour l’instant une seule propriété View qui retourne un UserControl encapsulé dans le type générique Lazy<T> . J’expliquerai pourquoi j’utilise ce dernier type un petit plus tard.

Ensuite, j’aimerai pouvoir exposer des informations utiles comme le titre de la démo, l’auteur, une description, le langage utilisé, le niveau de la démo et d’autres informations qui seront réutilisées dans l’hôte.

J’aurais pu définir dans mon contrat une propriété pour chaque information à diffuser, mais en l’occurrence, j’ai préféré utiliser un mécanisme de méta-données incorporées dans un attribut personnalisé.

Pour ce faire, je définie une interface IUltimateDemoMetadata

  1. public interface IUltimateDemoMetadata
  2.     {
  3.         UInt16 Level { get; }
  4.         String Region { get; }
  5.         String Author { get; }
  6.         String Title { get; }
  7.         Category Group { get; }
  8.         Language Language { get; }
  9.         String Namespaces { get; }
  10.         String Description { get; }
  11.     }

Puis l’attribut en lui même qui dérive de la classe Attribut, et de mon interface IUltimateDemoMetadata.

[MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class)]
    public class UltimateDemoMetadataAttribute : Attribute,IUltimateDemoMetadata
    {
      public UInt16 Level { get; set; } //i.e 100->400
      public String Region { get; set; } //i.e fr-fr, en-us, etc...
      public String Author { get; set; }
      public String Title { get; set; }
      public Category Group { get; set; } //i.e Devices, Fundamentals, Graphics and media, Communications and data, Services, User Interface, and Miscelaneous (Default
                                          // Priphriques, Graphiques et mdia, communication et donnes, services, INterface utilisateur, Divers (Default)
      public Language Language { get; set; }
      public String Namespaces { get; set; } //Ie Windows.
      public String Description { get; set; }
        public UltimateDemoMetadataAttribute(String title,
                                             String author,                                            
                                             String description,
                                             UInt16 level=100,
                                             String region="fr-fr",
                                             String namespaces="Windows",
                                             Category group = Category.Miscelaneous,
                                             Language language=Language.CS)
        {
            this.Title = title;
            this.Author = author;
            this.Description = description;
            this.Level=level;
            this.Region = region;
            this.Namespaces=namespaces;
            this.Group = group;
            this.Language = language;
        }  
    }

Ensuite la subtilité réside dans une classe UltimateDemoMetadataView qui fera la liaison entre les composants à charger dynamiquement et l’hôte, comme nous le verrons également par la suite. En faite c’est cette classe qui créera mon attribut personnalisé à fournir à l’hôte.
Notez qu’elle expose une propriété UltimateDemoInfos que j’utiliserai par la suite dans l’hôte pour le mapping avec mon modèle de données.

public class UltimateDemoMetadataView
    {
        public IUltimateDemoMetadata UltimateDemoInfos {get; set;}
        public UltimateDemoMetadataView(IDictionary<String, Object> dico)
        {
            String Title = dico["Title"] as String;
            String Author =dico["Author"] as String ;
            String Description = dico["Description"] as String;
            UInt16 Level =(UInt16)dico["Level"] ;
            String Region = dico["Region"] as String;
            String Namespaces = dico["Namespaces"] as String;
     ��      Category Group =(Category)dico["Group"];
            Language Language = (Language)dico["Language"];
            UltimateDemoInfos = new UltimateDemoMetadataAttribute(Title, Author, Description, Level, Region, Namespaces, Group,Language);

        }
    }

Maintenant nous allons à titre d’exemple, implémenter un code simple.
Pour ce faire, il faut un projet de type Class Library (Windows Store Apps) dans Visual Studio.

   

Ensuite il faut ajouter à cette librairie les références à MEF et une référence à notre contrat Ultimate.Interfaces comme illustré sur la figure suivante :
image
 

Puis créer un UserControl, qui illustrera l’API à démontrer.

Enfin, il faut créer une classe qui exporte et implémente notre contrat IUltimateDemo, qui expose ses informations à l’aide de l’attribut personnalisé UltimateDemoMetadata. En tout dernier , il faut exporter notre classe avec l’attribut Export de MEF et le type à exporter, afin que l’infrastructure de MEF puisse l’importer dans l’hôte.

[System.Composition.Export(typeof(IUltimateDemo))]
[UltimateDemoMetadata("Manipulation de port USB", "Eric Verni", "Utilisation de USB.SYS ....",
Namespaces = "Windows.Devices.USB", Group = Category.Devices,Language=Language.XAML)]
public class USBDemo : IUltimateDemo
{
static UserControl InitControl()
{
return new USBControlView();
}
public Lazy<Windows.UI.Xaml.Controls.UserControl> View
{
get
{

return Helper.InitLazyControl(InitControl, true);

}
set
{
throw new NotImplementedException();
}
}

   

Vous noterez également que je fais appelle à la méthode InitLazyControl() qui instancie le type Lazy<UserControl>, en lui passant comme paramètre, la méthode statique InitControl().

public static Lazy<UserControl> InitLazyControl(Func<UserControl> Func,bool isThreadSafe)
         {             
              return new Lazy<UserControl>(Func, isThreadSafe);
         }

Un petit mot d’explication s’impose. Le faite d’utiliser le type Lazy<UserControl> ici, me permet de différer l’instanciation du contrôle via la méthode statique InitControl. En d’autre terme, je ne veux pas que le contrôle sous jacent soit instancié n’importe quand, et surtout pas dans un thread différent de celui de l’interface utilisateur. En effet, l’hôte, pour des raisons de fluidité de l’interface, peut très bien appeler (comme nous le verrons plus tard) son mécanisme de découverte et d’instanciation dynamique dans une méthode asynchrone, qui par définition, peut démarrer dans un autre thread. Or vous devez savoir maintenant qu’un contrôle d’interface, ne peut être manipulé que par le thread qui le crée.

Il nous reste maintenant à ajouter le code dans l’hôte qui permet de découvrir et d’instancier notre composant.

Dans un 1er temps nous allons définir une classe qui permettra d’importer mes composants.

class UltimateDemoImport
    {
        [ImportMany]
        public Lazy<IUltimateDemo, UltimateDemoMetadataView>[] DemoViews { get; set; }
    }

On retrouve ici notre contrat IUltimateDemo, ainsi que la classe UltimateDemoMetadataView qui créera l’attribut personnalisé UltimateDemoMetadataAttribut et ses données associées..

Ensuite on défini la méthode LoadViews qui prend en paramètre une liste de noms d’assemblies à charger, et qui retourne un tableau de type Lazy<IUltimateDemo,UltimateDemoMetadataView>[]

class UltimateDemoComposition
    {
        public static Lazy<IUltimateDemo, UltimateDemoMetadataView>[] LoadViews(List<String> listofassembliesnames)
        {
            UltimateDemoImport importations = null;
            try
            {
                var configuration = new ContainerConfiguration();

                List<Assembly> assemblies = new List<Assembly>();
                foreach(var assemblieName in listofassembliesnames)
                {
                    assemblies.Add(Assembly.Load(new AssemblyName(assemblieName)));
                }
               
                configuration.WithAssemblies(assemblies);
                importations = new UltimateDemoImport();
                var compositionHost = configuration.CreateContainer();
                compositionHost.SatisfyImports(importations);

            }
            catch (Exception ex)
            {
                throw new  Exception("UltimateDemoComposition_error",ex);
            }

            return importations.DemoViews;
        }

 

  

Ensuite il suffit de mapper les données à un POCO qui me sert de modèle dont voici la définition

public class ModelAPIDemo :Ultimate.Interfaces.IUltimateDemo,Ultimate.Interfaces.IUltimateDemoMetadata
    {
        Lazy<UserControl> m_view;
        public Lazy<UserControl> View
        {
            get
            {
                return m_view;
            }
            set
            {
                m_view = value;
            }
        }
        public UInt16 Level { get; set; }
        public String Region { get; set; }
        public String Author { get; set; }
        public String Title { get; set; }
        public Category Group { get; set; }
        public Language Language { get; set; }        
        public String Namespaces { get; set; }
        public String Description { get; set; }
    }

var demosViews = Ultimate.Host.Services.UltimateDemoComposition.LoadViews(assemblies);
                    foreach (var demoView in demosViews)
                    {
                        ModelAPIDemo api = new ModelAPIDemo();
                        api.Group = demoView.Metadata.UltimateDemoInfos.Group;
                        api.Level = demoView.Metadata.UltimateDemoInfos.Level;
                        api.Description = demoView.Metadata.UltimateDemoInfos.Description;
                        api.Author = demoView.Metadata.UltimateDemoInfos.Author;
                        api.Namespaces = demoView.Metadata.UltimateDemoInfos.Namespaces;
                        api.Region = demoView.Metadata.UltimateDemoInfos.Region;
                        api.Title = demoView.Metadata.UltimateDemoInfos.Title;
                        api.Language = demoView.Metadata.UltimateDemoInfos.Language;
                        api.View = demoView.Value.View;

Vous noterez ici que dans le modèle j’utilise pour la propriété View de mon model le type Lazy<UserControl> .

Enfin j’utilise le mécanisme de DataBinding traditionnel dans ma page XAML.

<TextBlock Grid.Column="0" x:Name="txtDescription" Margin="120,0,0,0" Grid.Row="2" TextWrapping="Wrap"
                   Text="{Binding Description}" Style="{StaticResource BasicTextBlockStyle}"/>       
        <Canvas Grid.Row="1" x:Name="canvas" Margin="120,0,0,0" Width="auto" Height="auto" >           
            <UserControl Content="{Binding View,Converter= {StaticResource LazyViewConverter}}"
                        Width="auto" Height="auto" Background="Red"/>       
        </Canvas>

Le converter LazyViewConverter permet de faire la liaison avec le UserControl. En faisant référence à la propriété Value du type Lazy<UserControl>, c’est la méthode InitControl vue plus haut qui sera appelée, et qui instanciera dans le bon thread le contrôle.

class LazyViewConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            Lazy<UserControl> view=(Lazy<UserControl>)value;

            return view.Value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

 

    

A bientôt

Eric