Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Thomas Lebrun

Tout sur WPF, LINQ, C# et .NET en général !

Actualités

Nouvelle URL, nouveau blog !

Bonjour à tous,

J'y pensais depuis un moment, c'est maintenant chose faîte ! J'ai décidé de changer de plateforme de blog pour me lancer dans ma "propre" aventure: http://blog.thomaslebrun.net

Les activités prévues au programme seront les mêmes que sur mon précédent blog à savoir WPF, Silverlight, Windows Phone, etc. mais en essayant de varier les plaisirs de temps en temps et surtout, en essayant de retrouver une activité un peu plus dense.

Je tiens à remercier la communauté de CodeS-SourceS, et notamment Nix et Cyril, pour m'avoir hébergé durant toutes ces années !

Vous remarqerez que le design de ce blog n'est, pour le moment, pas tout à fait abouti: j'espère que cela changera rapidement (si d'ailleurs vous souhaitez me filer un coup de main sur la partie design/logo, n'hésitez pas à me le faire savoir, je risque d'en avoir besoin !)

A bientôt pour la suite !

[Silverlight 5] Récupérer le nom de l'utilisateur courant

Lorsque l’on désire récupérer le nom de l’utilisateur courant, dans une application Silverlight, nous avons à notre moyen 2 possibilités.

La première consiste à créer un service WCF qui va contenir une simple méthode renvoyant le nom de l’utilisateur courant:

   1: public string GetLoginName()
   2: {
   3:     string loginName =  System.Web.HttpContext.Current.User.Identity.Name.ToString();
   4:  
   5:     return loginName;
   6: }
   7:  

Une aute possibilité consiste à passer le nom de 'l’utilisateur dans les InitParams de l’application Silverlight:

   1: <param name="initParams" value="username=<%=HttpContext.Current.User.Identity.Name%>"

Avec Silverlight 5, une autre possibilité intervient: l’utilisation du P/Invoke ! Smile

Cette technique consiste à permettre à votre application d’effectuer des appels vers des DLL natives (de l’OS par exemple). Il s’agit là d’une grande nouveauté, surtout lorsque l’on sait que les application Silverlight s’exécutent dans une Sandbox (donc une boite depuis laquelle elles n’ont, en principe, accès à peu de choses).

Pour utiliser le P/Invoke, il est nécessaire de lancer votre application en “Out-Of-Browser” et de lui donner des privilèges élévés, mode dans lequel elle a justement la possibilité d’avoir accès à plus de fonctionnalités.

Une fois la configuration faîte, il ne reste plus qu’à écrire le code qui nous intéresse, en utilisant l’API GetUserName/GetUserNameEx (si l’on souhaite avoir plus d’options):

   1: public static class NativeMethods
   2: {
   3:     public enum EXTENDED_NAME_FORMAT
   4:     {
   5:         NameUnknown = 0,
   6:         NameFullyQualifiedDN = 1,
   7:         NameSamCompatible = 2,
   8:         NameDisplay = 3,
   9:         NameUniqueId = 6,
  10:         NameCanonical = 7,
  11:         NameUserPrincipal = 8,
  12:         NameCanonicalEx = 9,
  13:         NameServicePrincipal = 10,
  14:         NameDnsDomain = 12
  15:     }
  16:  
  17:     [DllImport("secur32.dll", CharSet = CharSet.Auto)]
  18:     public static extern int GetUserNameEx(int nameFormat, StringBuilder userName, ref int userNameSize);
  19: }
  20:  
  21: private void Button_Click(object sender, RoutedEventArgs e)
  22: {
  23:     StringBuilder userName = new StringBuilder(1024);
  24:     int userNameSize = userName.Capacity;
  25:  
  26:     if (NativeMethods.GetUserNameEx((int)NativeMethods.EXTENDED_NAME_FORMAT.NameSamCompatible, userName, 
ref userNameSize) != 0)
  27:     {
  28:         MessageBox.Show(userName.ToString()); 
  29:     }
  30: }

Et le tour est joué Smile

Pour information, sachez que l’utilisation du P/Invoke est identique à celle que l’on retrouve sur Windows (hormis certaines instructions qui ne sont pas (encore ?) supportées). Du coup, si vous souhaitez en mettre en place dans votre application, regarder la documentation associée pour le Compact Framework (s’agissant lui aussi d’un sous-ensemble du Framework .NET, les limitations pour Silverlight sont les mêmes !).

 

Have fun !

[AOP] L’interception avec Unity

Unity est un container IoC très pratique mais qui offre, en plus, la possibilité de réaliser un peu d’AOP (Aspect Oriented Programming) au moyen d’une extension pour l’interception.

En gros, vous allez avoir la possibilité d’intercepter l’exécution de bouts de code (appels de méthodes virtuelles, appels de méthodes définies dans des interfaces, etc.) pour rajouter votre propre logique.

Cela peut s’avérer très pratique lorsque vous avez, par exemple, des objets métiers ou des services mais pour lesquels vous ne souhaitez qu’ils s’occupent de gérer des tâches comme la gestion de logs, la gestion des exceptions, du cache, etc.

Voyons cela avec un exemple simple: imaginer que vous vouliez être en mesure de logger le temps d’exécution des méthodes que vous avez défini dans vos Client Services ou dans vos Helpers. Pour cela, il est d’abord nécessaire de créer la classe correspondante:

   1: public interface ILogger
   2: {
   3:     void Log(string errorMessage);
   4: }
   5:  
   6: public class Logger : ILogger
   7: {
   8:     public void Log(string errorMessage)
   9:     {
  10:         // TODO: Implement method
  11:     }
  12: }

A présent, nous allons ajouter les références nécessaire à notre projet pour pouvoir utiliser Unity et l’interception, à savoir Microsoft.Practices.Unity et Microsoft.Practices.Unity.Interception. Ensuite, nous allons pouvoir écrire notre CallHandler, il s’agit de la classe qui va pouvoir nous permettre de rajouter notre propre comportement:

   1: public class LogExecutionTimeCallHandler : ICallHandler
   2: {
   3:     public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
   4:     {
   5:         var sw = new Stopwatch();
   6:         sw.Start();
   7:  
   8:         IMethodReturn result = getNext()(input, getNext);
   9:  
  10:         sw.Stop();
  11:  
  12:         Debug.WriteLine("Method {0} tooks {1}", 
  13:                 input.MethodBase.Name, 
  14:                 string.Concat(sw.Elapsed.Hours, ":", sw.Elapsed.Minutes, ":",
  15:                 sw.Elapsed.Seconds, ".", sw.Elapsed.Milliseconds));
  16:  
  17:         return result;
  18:     }
  19:  
  20:     public int Order { get; set; }
  21: }
Comme vous pouvez le voir, cette classe se contente de démarrer un chronomètre, appelle la méthode qui est demandée puis affiche le temps d’exécution dans la fenêtre de déboggage de Visual Studio.
 

Cette classe est donc celle que nous devons utilisé mais nous ne pouvons pas le faire tel quel, nous allons essayer de nous simplifier la vie au maximum, en utilisant un attribut. Dès que cet attribut sera rencontré, il utilisera une instance de notre CallHandler et notre logique de log sera appelée:

   1: public class LogExecutionTimeAttribute : HandlerAttribute
   2: {
   3:     public override ICallHandler CreateHandler(Microsoft.Practices.Unity.IUnityContainer container)
   4:     {
   5:         return new LogExecutionTimeCallHandler();
   6:     }
   7: }

Maintenant, il ne nous reste plus qu’à appliquer notre attribut sur chacune de nos méthodes, définies dans notre interface:

   1: public interface ILogger
   2: {
   3:     [LogExecutionTime]
   4:     void Log(string errorMessage);
   5: }

Dès lors, nous pouvons mettre en place notre container Unity et configurer l’interception:

   1: IUnityContainer container = new UnityContainer();
   2: container.AddNewExtension<Interception>();
   3: container.RegisterType<ILogger, Logger>().Configure<Interception>().
   4: SetInterceptorFor<ILogger>(new InterfaceInterceptor());
   5:  
   6: ILogger logger = container.Resolve<ILogger>();
   7: logger.Log("TEST");

Nous pouvons voir ici que nous utilisons un InterfaceInterceptor, afin d’indiquer à Unity que les méthodes que nous voulons intercepter sont présentes dans l’interface que j’ai spécifié (ILogger). Si nous n’avions pas utilisé d’interface pour notre helper, nous aurions pû mettre en place des méthodes virtuelles, et utiliser un VirtualMethodInterceptor.

Lors de l’exécution, la méthode Log est correctement appelé et notre chronomètre est, lui aussi, bien démarré et utilisé:

image

 

Il s’agit donc là d’une technique très pratique lorsque l’on souhaite mettre en oeuvre du log, du cache, etc. et que l’on ne désire pas implémenter ces fonctionnalités directement sur ces objets !

 

Plus d’informations: http://www.palmmedia.de/Blog/2010/9/26/aop-interception-with-unity-2.0

 

 

A+,

[VSTO] Problème de lenteur lors de l’affectation de la propriété Design de l’objet Slide, sous PowerPoint 2007
PowerPoint est un outil très puissant et peu de change savent que, comme ses confrères de la gamme Office (Word, Excel, Outlook, etc…), celui-ci est entièrement extensible, grâce aux VSTO.

J’ai récemment rencontré un problème alors que je voulais faire quelque chose de relativement simple sous PowerPoint 2007. Dans un addin VSTO, je voulais affecter la propriété Design d’un objet de type Slide. Tout se passait correctement mais chaque affectation prenait entre 1 et 3 secondes ce qui, lorsque l’on a 20 ou 30 slides, peut prendre beaucoup (trop !) de temps.

J’ai également remarqué que le code suivant, qui permet de changer l’orientation d’un slide, ne donnait aucun résultat:

   1: var presentation = Globals.ThisAddIn.Application.ActivePresentation;
   2:  
   3: presentation.PageSetup.SlideSize = PpSlideSizeType.ppSlideSizeOnScreen16x9;
   4:  
   5: Marshal.ReleaseComObject(presentation);

Chose encore plus bizarre, je n’ai rencontré aucun problème en faisant tourner le même code sous PowerPoint 2010.

Après avoir jeté un coup d’oeil sur les moteurs de recherche, je suis tombé sur cette KB Microsoft qui décris exactement le 2ème problème que je rencontrais: http://support.microsoft.com/kb/970943/en-us

Après l’avoir appliqué, il s’est avéré que ce hotfix a corrigé non seulement le problème d’orientation mais aussi le problème de lenteur lors de l’affectation de la propriété Design.

Comme quoi, les KB Microsoft peuvent vraiment être utile par moment Winking smile

 

A+,

[Tool] Télécharger les vidéos du Mix 2011 en quelques clics !

Le Mix 2011 est terminé et l’ensemble des vidéos est maintenant téléchargeable en ligne. Seulement, il y a eu beaucoup de sessions donc télécharger les vidéos une à une prendrait énormément de temps.

Pour me faciliter la vie, j’ai écrit un petit outil (en mode “vite fait”), que je profite pour partager à tous: Mix 2011 Videos Downloader

Il vous permettra de sélectionner les vidéos que vous souhaitez télécharger: le reste se fera tout seul (en fonction de votre connexion Internet).

Par défaut, les vidéos sont stockées dans le répertoire “D:\ Mix 11” mais cela peut-être changé avec le bouton “Options”

 

Profitez en bien !

 

A+

[WP7/Silverlight] Détecter la fin du scroll d’une ListBox
Pour les besoins d’un projet récent sur Windows Phone 7, le client souhaitait faire en sorte que lorsque l’utilisateur ait fini de scroller dans sa ListBox, si celle-ci se trouve être à la fin, alors on ré-interroge la source de données pour récupérer d’autres data et les injecter dans le contrôle.
Pour faire simple, imaginer une ListBox infinie mais avec de la pagination In love

Pour mettre en place cela, c’est tout simple. En effet, le code suivant permet d’accéder à la Scrollbar interne à la Listbox et de s’abonner à son évènement ValueChanged. Dans la callback, on regarde si la valeur actuelle correspond bien à la propriété Maximum de la Scrollbar, et le tour est joué:

   1: private double _lastScrollbarValue;
   2:  
   3: void ListBoxLoaded(object sender, RoutedEventArgs e)
   4: {
   5:     if(_scrollBar != null)
   6:         _scrollBar.ValueChanged -= ScrollBarValueChanged;
   7:  
   8:     _scrollBar = _listBox.GetChildrenByType<ScrollBar>(s => s.Name == "VerticalScrollBar").FirstOrDefault();
   9:  
  10:     if (_scrollBar != null)
  11:         _scrollBar.ValueChanged += ScrollBarValueChanged;
  12: }
  13:  
  14: void ScrollBarValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
  15: {
  16:     if (e.NewValue == _scrollBar.Maximum && e.NewValue != _lastScrollbarValue)
  17:     {
  18:         // Here, load more data !
  19:     }
  20:  
  21:     _lastScrollbarValue = e.NewValue;
  22: }
Simple comme tout mais pourtant très pratique Winking smile

 

A+

[WP7] ListBox et ContextMenu: Comment accéder à l’élément sélectionné ?
Hum… Cela fait un petit moment maintenant que je n’ai rien posté sur mon blog: il va être temps de s’y remettre Smile

On commence tout doucement avec une petite astuve si, dans un projet Windows Phone 7, vous utilisez une ListBox et un Context Menu. Pour récupérer l’élément sélectionné dans la ListBox, au moment où l’utilisateur appuie dessus, utilisez le bout de code suivant:

   1: var selectedListBoxItem = this.lbFavorites.ItemContainerGenerator.ContainerFromItem(((MenuItem) sender).DataContext) as ListBoxItem;
   2: if (selectedListBoxItem == null)
   3:     return;
Bien sur, remplacer “lbFavorites” par le nom de votre ListBox Winking smile

 

A bientôt pour la suite !

[Silverlight] Intégrer votre application Silverlight dans la TaskBar de Windows 7

Depuis la version 3 de Silverlight, il est possible d’exécuter des applications Silverlight en mode “Out Of Browser” autrement dit installer localement l’application et l’exécuter directement sur le poste client.

Cette fonctionnalité, bient que très pratique, était assez limité lorsque l’on voulait interopérer avec le système d’exploitation. Certes, Silverlight 4 a permis d’accéder au répertoire Documents de l’utilisateur et Silverlight 5 permettra de faire du P/Invoke mais, pour le moment, qu’avons nous ? Rien !

Enfin, jusqu’à la semaine dernière où Microsoft a mis à disposition un ensemble d’APIs, Native Extensions For Microsoft Silverlight, qui permettent (via l’interop COM, disponible depuis Silverlight 4) d’accéder/utiliser des fonctionnalités natives de Windows 7 comme:

  • l’utilisation des capteurs
  • l’utilisation de l’API “Speech” (Text to Speech et Speech To Text)
  • l’accès aux périphériques portables (caméras, téléphones, lecteurs MP3, etc.)
  • l’intégration avec la barre des tâches
  • Etc.

Nous allons ainsi voir, au travers de ce post, comment profiter des ces nouvelles fonctionnalités pour intégrer votre application Silverlight (qui doit s’exécuter en mode “Out Of Browser” avec des privilèges élèvés) dans la barre des tâches de Windows 7.

 

La première chose à faire est d’installer le runtime qui permet d’interopérer avec le système d’exploitation. Comme on ne sait pas vraiment si l’utilisateur dispose de tous les éléments sur sa machine, il est possible de demander à l’application de lancer l’installation du runtime nécessaire, que celui-ci soit embarqué dans le XAP ou bien situé sur un serveur distant:

   1: private static bool CheckRuntimeInstallation()
   2: {
   3:     try
   4:     {
   5:         if (!Application.Current.HasElevatedPermissions || !AutomationFactory.IsAvailable)
   6:         {
   7:             return false;
   8:         }
   9:  
  10:         if (!Installer.CheckNESLInstalled(1, 0))
  11:         {
  12:             Installer.InstallNESL(new Uri("Setup/NESLSetup.msi", UriKind.Relative), false, true);
  13:             if (!Installer.CheckNESLInstalled(1, 0))
  14:             {
  15:                 return false;
  16:             }
  17:             return true;
  18:         }
  19:         return true;
  20:     }
  21:     catch (Exception Ex)
  22:     {
  23:         return false;
  24:     }
  25: }

Une fois cette étape terminée, il est alors possible de commencer à travailler avec la barre des tâches. La première chose possible est de créer une liste de tâches, accessibles directement lorsque l’utilisateur cliquera avec le bouton droit sur l’icone de l’application, dans la barre des tâches:

   1: private static void BuildJumpList()
   2: {
   3:     JumpListItem firstTask = TaskbarButton.Current.Jumplist.CreateJumpListItem(1);
   4:     firstTask.Title = "Créer une fiche cliente";
   5:     firstTask.Description = "Permet de démarrer l'application sur une nouvelle fiche cliente";
   6:  
   7:     firstTask.Category = "Tâches administratirve";
   8:  
   9:     JumpListItem secondTask = TaskbarButton.Current.Jumplist.CreateJumpListItem(2);
  10:     secondTask.Title = "Ouverture une fiche cliente";
  11:     secondTask.Description = "Permet d'ouvrir l'application sur une nouvelle fiche cliente";
  12:  
  13:     secondTask.Category = "Tâches administratirve";
  14:  
  15:     TaskbarButton.Current.Jumplist.SetJumpListItems(new JumpListItem[] { firstTask, secondTask });
  16: }

A l’exécution, on constate que la JumpList comporte bien 2 nouveaux éléments:

image

Une autre des fonctionnalités que vous pouvez utiliser est la possiblité de rajouter des boutons dans la miniature qui apparait lorsque vous survolez l’icone dans la barre des tâches:

image

Si vous lancez votre application et que vous survolez son icône dans la barre des tâches, vous remarquerez que celle-ci contient 2 icones supplémentaires:

image

Seul hic, pour le moment, je n’ai pas trouvé comment intercepter le clic sur l’un de ces boutons mais je suis sur que cela viendra rapidement dans une prochaine version de l’API (ou alors si quelqu’un sait comment le faire, qu’il n’hésites pas à laisser un commentaire Smile)

Dernière fonctionnalité intéressante, le fait de pouvoir indiquer à l’utilisateur qu’une opération est en cours, à l’aide d’une barre de progression. Tradicitonnellement, ce genre d’action se fait dans l’interface graphique mais imaginer que l’utilisateur ait réduit votre application: à ce moment là, il ne peut plus savoir quand l’opération (potentiellement longue) se termine. Avec cette technique, vous pourrez ainsi le notifier de l’état d’avancement:

image

Si l’on part du principe que la valeur passée en paramètre provient d’un timer, d’un BackgroundWorker, etc., on peut imaginer que la barre de progression change de manière dynamique, dans la barre des tâches:

image

 

Voila pour ce petit tour d’horizon des fonctionnalités des Natives Extensions pour Silverlight. Bien sur, je n’ai couvert ici que la partie barre des tâches, il y a beacoup d’autres focntionnalités à voir mais je vous laisse les découvrir avec les exemples fournis !

 

A+

[WP7] Gestion de l’authentification sous Windows Phone 7

L’une des problématiques que l’on rencontre à l’heure actuelle lorsque l’on développe des applications Windows Phone 7 concerne la gestion de l’authentification. En effet, il s’avère que l’authentification NTLM n’est pas supporté du coup, on est obligé de trouvé une solution de contournement.

Une des solutions possibles consiste à injecter/ajouter, dans les entêtes de la requête qui est envoyé, des entêtes personnalisées. Et pour bien faire, plutôt que de le faire manuellement sur chaque appel du service WCF, on va passer par un petit MessageInpector WCF pour faire en sorte que cela soit fait automatiquement Smile

Pour créer son Message Inspector WCF, il faut implémenter l’interface IClientMessageInspector. Malheurement, cette interface n’est pas disponible dans Silverlight mais , il est possible de télécharger le sample suivant et de récupérer le projet nommé SilverlightMessageInspector pour disposer de là-dîte fonctionnalité au sein de son application (Silverlight / Windows Phone 7).

Une fois que cela est fait, il faut créer le service WCF, en activant la compatibilité ASP.NET et vérifier, dans les méthodes, si les entêtes reçues contiennent bien des login/mot de passe corrects (éventuellement, en s’assurant de cela avec une connexion à une base de données, etc.):

image

A présent, il est temps de créer, côté Windows Phone, le MessageInspector WCF qui va injecter, lors de chaque requête, les entêtes nécessaires (login/mot de passe):

image

Une fois cette étape terminée, il ne reste plus qu’à appeler le service, en utilisant notre nouveau MessageInspector:

image

Et le tour est joué Smile

 

Attention tout de même: avec cette technique, les entêtes sont passées en clair donc il est impératif d’utiliser une liaison sécurisée (SSL) pour éviter que quelqu’un n’intercepte les appels WCF et donc les crédentials.

 

A+

[VSX] Comment rajouter des contrôles Silverlight à la Toolbox de Visual Studio ?

Afin de simplifier la vie des développeurs, il peut-être intéressant de faire en sorte que vos contrôles soient directement accessibles depuis la boite à outils de Visual Studio.

Pour cela, il est nécessaire de rajouter une simple clé dans la base de registre, à l’emplacement suivant:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Silverlight\v4.0\AssemblyFoldersEx

Si vous regardez la base de registre, vous pourrez remarquer que certains utilitaires, tels que le Toolkit Silverlight, ont déjà ajoutés leur propres clés dans la base de registre:

image

Il vous suffit alors de rajouter votre propre noeud, sous AssemblyFolderEx:

image

Ensuite, il faut indiquer, au niveau de la valeur (Default), l’emplacement du répertoire qui contient les binaires que vous voulez mettre à disposition:

image

Si vous relancez votre Visual Studio, vous remarquerez que les contrôles ne sont pas accessibles (encore Wink) dans la boite à outils. Par contre, il sont accessibles directement dans la boite de dialogue “Ajout d’une référence”.

Pour les avoir dans la boite à outils, il faut rajouter un noeud Toolbox à celui que vous avez créé:

image

Si vous voulez que vos contrôles apparaissent dans un onglet spécifique de votre boite à outils, il faut alors rajouter une clé nommé TabName dont la valeur correspond au nom de l’onglet que nous désirez:

image

A partir de là, vos développeurs pourront faire un simple glisser/déposer de vos contrôles Silverlight personnalisés pour les ajouter dans leurs applications !

 

Pour plus d’infos sur le sujet, je vous recommande le blog de Ning Zhang: http://www.ningzhang.org/2009/04/30/register-silverlight-controls-with-visual-studio-and-blend/

 

A+

[VSX] Pourquoi ma ToolWindow n'apparait pas dans mon addin Visual Studio ?

Lorsque l'on développe un addin pour Visual Studio, on apprécie de pouvoir créer des fenêtres qui viennent se rajouter aux fenêtres existantes. Pour cela, il suffit d'utiliser le code suivant qui va rechercher si votre fenêtre est déjà créée (auquel cas, une nouvelle instance va être utilisée):

 

private void ShowToolWindow(IEnumerable<Property> propertiesAndDescription)
{
    // Get the instance number 0 of this tool window. This window is single instance so this instance
    // is actually the only one.
    // The last flag is set to true so that if the tool window does not exists it will be created.
    ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
    if ((null == window) || (null == window.Frame))
    {
                throw new NotSupportedException("Can not create tool window.");
    }

    var windowFrame = (IVsWindowFrame)window.Frame;
    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}

 

Cependant, si vous exécutez ce code dans un de vos package/addin, vous remarquerez que l'exception se déclenche car l'objet window sera null. Comment empéchez cela ? Et bien tout simplement en pensant à rajouter l'attribut suivant sur la classe de votre package:

 

[ProvideToolWindow(typeof(MyToolWindow))]

 

Et là, le tour est joué Smile

 

Bonne extensibilité à vous Wink

 

A+

[Surface] Que faire lorsque le Shell ne démarre plus ?

J’ai récemment rencontré un problème quelque peu dérangeant, sur une table Surface: impossible de lancer le Shell ! En effet, dès que celui-ci était activé (en passant par la connexion de l’utilisateur TableUser ou via l’activation du User Mode), le logo s’affichait, le Shell commencait à se charger puis plus rien, le processus se terminait automatiquement.

Après avoir regardé dans l’Event Viewer, je suis tombé sur plusieurs erreus fatales. J’ai donc cherché sur Internet et ce que je voyais ne me disait rien de bon: corruption du système d’exploitation, etc. Il me restait alors la possibilité de restaurer la table dans son état d’origine mais avant d’en arriver là, j’ai continué à chercher et j’ai trouvé la solution, qui consiste à réparer le Surface SDK 1.0 SP1 !

Pour cela, connectez-vous en administrateur sur la table Surface, lancez le Control Panel et cliquez sur Uninstall a program:

image

Dans la liste des programmes, rechercher “Microsoft Surface SDK 1.0 SP 1”, faîtes un clic droit dessus et sélectionnez “Change”:

image

Cliquez alors sur “Repair”:

image

Patientez jusqu’à la fin de la réparation, relancez le Shell et normalement, celui-ci devrait se lancer correctement Smile

 

Enjoy !

 

A+

[Podcast] Le développement pour Microsoft Surface

Lors du dernier MVP Summit, Simon Ferquel et moi-même avons eu la chance d’être interviewé par Mario Cardinal pour son émission, le Visual Studio Talk Show.

Le thème de notre discussion était le développement pour Microsoft Surface.

Si vous souhaitez retrouver l’émission, c’est par ici: http://www.visualstudiotalkshow.com/Archives/117-14avril2010-SimonFerq.html

Bonne écoute à tous !

 

A+

[MEF/Silverlight] Utiliser des services communs entre le Shell et les différents modules

Dans la lignée de mon post précédent (qui expliquait comment créer une application modulaire avec MEF et Silverlight), nous allons voir maintenant comment faire pour parvenir à utiliser des services qui soient communs au Shell (autrement dit à l’application qui héberge les plugins) et à ces fameux modules/plugins justement.

Tout d’abord, dans le projet qui est commun au Shell et aux modules, nous allons rajouter une interface IPopupService:

image

Ensuite, dans notre Shell, nous allons implémenter cette interface pour créer un service d’affichage de boites de dialogues:

   1: public class PopupService : IPopupService
   2: {
   3:     #region IPopupService Members
   4:  
   5:     public void ShowMessage(string msg)
   6:     {
   7:         MessageBox.Show(msg);
   8:     }
   9:  
  10:     #endregion
  11: }

Notre Shell principal utilisant Unity, nous allons enregistrer notre service dans le container Unity:

   1: instance.RegisterType<IPopupService, PopupService>();

Reste à vérifier maintenant que ce service fonctionne correctement dans notre Shell. Pour cela, dans le ViewModel principal, on va résoudre l’instance de notre service depuis le container Unity:

   1: var popupSvc = UnitySingleton.RootContainer.Resolve<IPopupService>();
   2: popupSvc.ShowMessage("Hello from MainPageViewModel");

A l’exécution, pas de mystères: la référence du service est correctement retrouvée dans le container Unity ce qui permet d’afficher la boite de dialogue:

image

A présent, il va falloir exposer ce service à nos différents modules. Et c’est là que MEF intervient ! On va commencer par exporter, via les attributs de MEF, l’implémentation de notre service:

   1: [Export(typeof(IPopupService))]
   2: public class PopupService : IPopupService

Puis, dans chacun de nos modules, on va importer (toujours via les attributs) ce service. A noter que cela est possible car l’interface IPopupService est définie dans le projet Core, qui est commun à tous les projets de la solutions (Shell + modules):

   1: [Import(typeof(IPopupService))]
   2: public IPopupService PopupSvc { get; set; }

Ensuite, il ne reste plus qu’à utiliser notre propriété qui sera correctement résout grâce à MEF:

   1: if (this.PopupSvc != null)
   2: {
   3:     this.PopupSvc.ShowMessage("Hello from WelcomePageViewModel !");
   4: }

Attention tout de même, pour que l’import puisse fonctionner, il est nécessaire d’appeler la méthode CompositionInitializer.SatisfyImports:

   1: CompositionInitializer.SatisfyImports(this);

Là encore, le résultat parle de lui-même !

image

 

Bonne composition à tous ! Wink

 

A+

[Silverlight / MEF] Développer un système de plugins pour une application Silverlight avec le Microsoft Extensibility Framework

Introduction

MEF (Microsoft Extensibility Framework) est un framework proposé par Microsoft permettant de développer des applications modulaires, composées de plugins qu’il est possible de charger/décharger à la demande de l’utilisateur, en fonction du contenu d’un répertoire, etc.

Les applications modulaires sont de plus en plus répandues car elles offrent plusieurs avantages:

  • Simplicité du code de l’application principale, qui ne sert que de “lanceur” pour les modules
  • Modularité
  • Extensibilité
  • Etc.

Lorsque l’on travaille avec une application “client lourd” (WPF, WindowsForms), ce type d’applications est très simple à réaliser: on dépose ses extensions dans un répertoire physique, l’application est notifée et se met à jour automatiquement.

Cependant, dans le cas de Silverlight, c’est un peu plus compliqué. En effet, Silverlight s’exécutant sur le poste client, mais dans une sandbox (contexte sécurisé), il est impossible de faire un drag/drop de ses extensions sur un répertoire de la machine. Mais vous avez tout à fait la possibilité de mettre vos extensions dans le même répertoire d’où est téléchargée l’application Silverlight (le répertoire ClientBin) puis, avec un peu de code, vous faîtes en sorte de télécharger vos plugins et vous les exécutez ! Ce mécanisme est déjà possible grâce à Prism mais il s’agit d’un framework qui peut paraitre démesuré ou trop complexe à appréhender. Je vous propose donc, au travers de ce post, de vous apprendre comment créer votre propre “framework” de développement d’applications Silverlight modulaires.

Mise en place des bases

Pour commencer, nous allons créer un projet de base, qui contiendra tous les éléments (interfaces, attributs, etc.) qui seront commun aux différents projets de notre solution:

image

La classe ExportModulePageAttribute est en fait un attribut, que l’on appliquera sur chacun de nos modules, et qui servira à identifier nos modules:

   1: [MetadataAttribute]
   2: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
   3: public class ExportModulePageAttribute : ExportAttribute, IModuleMetadata
   4: {
   5:     public ExportModulePageAttribute()
   6:         : base(typeof(Page))
   7:     {
   8:     }
   9:  
  10:     public string XapFilename { get; set; }
  11:     public string NavigateUri { get; set; }
  12:     public string Title { get; set; }
  13:     public int Position { get; set; }
  14: }

Création des plugins/modules

Un plugin, vu par MEF, est simplement un élément décoré avec l’attribut ExprtAttribute. Dans notre cas, nous allons créer 2 modules:

image

Chaque module est en fait une application Silverlight comprendant 1 ou plusieurs Page, que l’on a décoré avec l’attribut créé précédemment:

   1: [ExportModulePage(XapFilename = "TL.Silverlight.Demos.Extensibility.Module1.xap", NavigateUri = "/Welcome", Title = "Welcome Page", Position = 1)]
   2: public partial class WelcomePage : Page
   3: {
   4:     public WelcomePage()
   5:     {
   6:         InitializeComponent();
   7:     }
   8:  
   9:     // Executes when the user navigates to this page.
  10:     protected override void OnNavigatedTo(NavigationEventArgs e)
  11:     {
  12:     }
  13: }

Chacun de ces modules étant indépendant, il est possible d’utiliser toutes les techniques de programmation que l’on connait (pattern MVVM, injection de dépendances, etc.). Une fois les modules créés, il suffit de les déposer dans le répertoire ClientBin du serveur. Cependant, il reste encore à faire en sorte de pouvoir récupérer ces modules, faire la composition MEF et utiliser notre application.

Création du Shell

Le Shell, c’est l’application Silverlight qui va “hoster” les différentes modules qui auront été écrits, afin de pouvoir les exécuter. Pour cela, il va être nécessaire de se créer un petit loader:

   1: public class ModulesLoader : IModulesLoader, IPartImportsSatisfiedNotification
   2: {
   3:     #region Events
   4:  
   5:     public event NotifyOnModulesAvailabilityHandler NotifyOnModulesAvailability;
   6:  
   7:     #endregion
   8:  
   9:     #region Public Contructor
  10:  
  11:     public ModulesLoader()
  12:     {
  13:         DeploymentCatalogService.Instance.Initialize();
  14:  
  15:         CompositionInitializer.SatisfyImports(this);
  16:  
  17:         this.LoadModules();
  18:     }
  19:  
  20:     #endregion
  21:  
  22:     #region Properties
  23:  
  24:     [ImportMany(AllowRecomposition = true)]
  25:     public Lazy<Page, IModuleMetadata>[] Modules { get; set; }
  26:  
  27:     #endregion
  28:  
  29:     #region IPartImportsSatisfiedNotification Members
  30:  
  31:     public void OnImportsSatisfied()
  32:     {
  33:         var handler = this.NotifyOnModulesAvailability;
  34:         if (handler != null)
  35:         {
  36:             handler(this.Modules);
  37:         }
  38:     }
  39:  
  40:     #endregion
  41:  
  42:     #region Private Methods
  43:  
  44:     private void LoadModules()
  45:     {
  46:         var wc = new WebClient();
  47:         wc.OpenReadCompleted += (s, e) =>
  48:         {
  49:             var streamInfo = e.Result;
  50:  
  51:             var xElement = XElement.Load(streamInfo);
  52:  
  53:             var modulesList = from m in xElement.Elements("ModuleInfo")
  54:                               select m;
  55:             if (modulesList.Any())
  56:             {
  57:                 foreach (var module in modulesList)
  58:                 {
  59:                    var moduleName = module.Attribute("XapFilename").Value;
  60:  
  61:                     DeploymentCatalogService.Instance.AddXap(moduleName);
  62:                 }
  63:             }
  64:         };
  65:         wc.OpenReadAsync(new Uri("ModulesCatalog.xml", UriKind.Relative));
  66:     }
  67:  
  68:     #endregion
  69: }

On constate plusieurs chose sur cette classe. Tout d’abord, on fait appel, à plusieurs moments, à une classe nommée DeploymentCatalogService. Il s’agit simplement d’une classe qui se charge de télécharger un module (fichier xap) et de l’ajouter à un DeploymentCatalog si celui-ci n’est pas déjà présent dedans. Ainsi, on dispose d’un container possédant chacun des modules exportés et avec lesquels on peut travailler. De plus, la classe implémente l’interface IPartImportsSatisfiedNotification, offerte par MEF, qui permet d’être notifier lorsque la composition est terminée. Maintenant, il reste nécessaire de savoir quels seront les modules qui devront être chargés ! Et pous cela, on utilise la méthode LoadModules qui télécharge un fichier nommé ModulesCatalog.xml, contenant des informations sur les xap. Une fois le nom des fichiers récupérés, on les ajoute au DeploymentCatalog à l’aide de la méthode AddXap de la classe DeploymentCatalogService.

image

A présent, il est nécessaire d’utiliser notre loader de modules. On peut, par exemple, l’utiliser dans un de nos ViewModels:

   1: var modulesLoader = UnitySingleton.RootContainer.Resolve<IModulesLoader>();
   2: modulesLoader.NotifyOnModulesAvailability += modules =>
   3: {
   4:     this.ModulesList = new ObservableCollection<Module>();
   5:  
   6:     var orderedModulesList = modules.OrderBy(m => m.Metadata.Position).ToList();
   7:  
   8:     for (int i = 0; i <= orderedModulesList.Count() - 1; i++)
   9:     {
  10:         var module = orderedModulesListIdea;
  11:  
  12:         this.ModulesList.Add(new Module { Uri = module.Metadata.XapFilename + module.Metadata.NavigateUri, Title = module.Metadata.Title, IsLast = i == modules.Count() - 1 });
  13:     }
  14: };

Qui, lui-même, sera bindé à notre interface graphique Silverlight:

   1: <ItemsControl ItemsSource="{Binding ModulesList}">
   2:     <ItemsControl.ItemsPanel>
   3:         <ItemsPanelTemplate>
   4:             <StackPanel Orientation="Horizontal" />
   5:         </ItemsPanelTemplate>
   6:     </ItemsControl.ItemsPanel>
   7:     <ItemsControl.ItemTemplate>
   8:         <DataTemplate>
   9:             <StackPanel Orientation="Horizontal">
  10:                 <HyperlinkButton Style="{StaticResource LinkStyle}"
  11:                                  NavigateUri="{Binding Uri}"
  12:                                  TargetName="ContentFrame"
  13:                                  Content="{Binding Title}" />
  14:  
  15:                 <Rectangle Visibility="{Binding IsLast, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}"
  16:                            Style="{StaticResource DividerStyle}" />
  17:             </StackPanel>
  18:         </DataTemplate>
  19:     </ItemsControl.ItemTemplate>
  20: </ItemsControl>

A l’exécution, le loader de module récupère le contenu du fichier ModulesCatalog.xml, télécharge les XAPs, les ajoute au DeployementCatalog et déclenche l’évènement qui lui permet d’indiquer que les modules sont chargés. Du coup, on voit bien le nom de nos modules apparaitre dans notre application:

image

Affichage de la page demandée

Arrivé là, on pourrait se dire que tout est terminé. Mais il reste toujours un problème. A l’heure actuelle, lorsque je clique sur un des éléments de la barre de navigateur, Silverlight affiche la page correspondante, page qui se trouve dans le répertoire Views de l’application principale. Hors, dans le cas présent, les pages ne se trouvent pas dans l’application principale mais dans les plugins ! Comment faire donc pour indiquer au runtime Silverlight l’emplacement de nos pages ? Et bien cela passe par une des nouveautés, pas ou peu connue, qui concernce l’extensibilité du modèle de navigation de Silverlight. Si vous voulez en savoir plus sur le sujet, je vous recommance le blog de David Poll (http://www.davidpoll.com) qui en parle très longuement ! Dans notre cas, on va récupérer le MEFContentLoader mis à disposition par David, afin de permettre de charger les pages depuis les modules exportés/importés par MEF:

image

A l’exécution, les pages sont correctement chargées depuis leur module respectifs, et affichés dans l’application:

image image

Point intéressant, l’historique fonctionne toujours:

image

Et les URLs vous permettent de savoir quel module contient la page demandée:

image

Si l’on veut désactiver un module, il suffit de mettre en commentaire la ligne correspondant dans le fichier ModulesCatalog.xml et de relancer l’application:

image

Conclusions

Avec cet article, vous avez pû avoir un petit aperçu de ce qu’il était possible de faire en mélangeant un peu de MEF avec du Silverlight. Bien sur, des points d’optimisations seraient à envisager mais cela vous permet d’avoir une première idée Wink

 

Les sources de l’article sont ici: http://morpheus.developpez.com/silverlight/TL.Silverlight.Demos.Extensibility.zip

 

A+

[Silverlight 4] Comment mettre, par code, du binding sur un DependencyObject ?

Depuis Silverlight 4, l’une des nouveautés au niveau du binding concerne la possibilité de l’appliquer directement au niveau des DependencyObjects et non plus seulement aux FrameworkElements.

Concrètement, cela signifie qu’il est maintenant possible d’écrire ce genre ce code:

   1: <Rectangle Fill="Red"
   2:                    x:Name="rect"
   3:                    Height="50"
   4:                    Width="100"
   5:                    RenderTransformOrigin="0.5,0.5"
   6:                    Grid.Row="0">
   7:             <Rectangle.RenderTransform>
   8:                 <RotateTransform Angle="{Binding ElementName=slider, Path=Value}" />
   9:             </Rectangle.RenderTransform>
  10:         </Rectangle>
  11:         
  12:         <Slider x:Name="slider"
  13:                 Width="100"
  14:                 Grid.Row="1"
  15:                 Minimum="0"
  16:                 Maximum="180"
  17:                 Value="0" />

Cependant, on peut vouloir réaliser le même genre d’opérations mais directement par code. Traditionnellement, pour faire cela, il faut appeler la méthode SetBinding de l’objet que l’on souhaite binder. Le problème, c’est que les DependencyObjects ne proposent pas cette méthode:

image

Si l’on souhaite faire son binding par code, il est nécessaire de passer par la méthode SetBinding de la classe BindingOperations:

image

Une petit tour dans Reflector nous apprend qu’au final, c’est la même chose car la méthode SetBinding, de FrameworkElement, ne fait qu’appeler la méthode SetBinding de BindingOperations:

image

Tout simple mais bon à savoir Wink

 

A+

[WCF RIA Services] Utiliser un DomainService dans un AuthenticationService

Avec WCF RIA Services, les DomainServices permettent d’exposer des méthodes qui seront accessibles par le client. De plus, les AuthenticationServices permettent de gérer la sécurité: où aller chercher les informations permettant d’identifier les utilisateurs, etc.

On pourrait tout à fait vouloir utiliser son DomainService directement au sein de son AuthenticationService, comme de la façon suivante par exemple:

   1: [EnableClientAccess]
   2: public class AuthenticationService : AuthenticationBase<User> 
   3: {
   4:     protected override User GetAuthenticatedUser(System.Security.Principal.IPrincipal principal)
   5:     {
   6:         var currentUser = base.GetAuthenticatedUser(principal);
   7:             
   8:         var membershipUser = Membership.GetUser(principal.Identity.Name, true);
   9:         currentUser.UserID = (Guid)membershipUser.ProviderUserKey;
  10:         currentUser.NumberOfDedicaces = this.m_Service != null ? this.m_Service.GetDedicacesCountForUser(currentUser.UserID) : 0;
  11:  
  12:         return currentUser;
  13:     }
  14:  }

Le problème, c’est que si vous exécutez ce code, vous obtiendrez l’erreur suivante:

image

Pour corriger ce problème, il est nécessaire d’initialiser le DomainService, en utilisant la méthode Initialize:

   1: // Initialize the DomainService
   2: this.m_Service.Initialize(this.ServiceContext);

D’ailleurs, si l’on regarde la documentation de la méthode Initialize, on voit qu’il est écrit qu’il est nécessaire d’appeler cette méthode avant de faire appel à une méthode du DomainService:

image

Une fois cette méthode appelée, il n’y a plus de problèmes Smile

 

A+

[WPF 4 / Silverlight] Exposer des données en design time

Il y a quelques temps, je vosu avais donné une solution pour disposer de données en design time lorsque vous développez votre application WPF. L’objectif de cette technique était de faciliter la collaboration entre le développeur et le designer car ce dernier dispose désormais de données (fictives) pour convevoir l’interface de l’application.

Dans la Beta 2 de Visual Studio 2010, Microsoft a rajouté de nouvelles propriétés et une nouvelle MarkupExtension permettant d’exposer des données en design time.

Pour cela, il est nécessaire d’utiliser la propriété d:DataContext et la MarkupExtension nommée d:DesignInstance. Pour les utiliser, il faut commencer par rajouter les namespaces suivant à votre fichier XAML:

   1: xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   2: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

Puis, rajouter cette ligne qui permet d’indiquer que le préfixe “d” ne doit pas être compilé/interprétré à l’exécution:

   1: mc:Ignorable="d"

A présent, on peut indiquer que l’on souhaite disposer de données en design-time:

   1: xmlns:DesignTime="clr-namespace:TestDesignTime.ViewModels.DesignTime"
   2: d:DataContext="{d:DesignInstance DesignTime:Demo, IsDesignTimeCreatable=True}">

Ainsi, j’indique qu’en mode design, le DataContext de ma fenêtre est affecté par une instance du type Demo qui est dans le répertoire ViewModels.DesignTime. Par contre, en mode runtime, je lui passe une instance du type Demo, qui est dans le répertoire ViewModels:

   1: private void Grid_Loaded(object sender, RoutedEventArgs e)
   2: {
   3:     this.DataContext = new Demo();
   4: }

Lorsque l’on regarde l’application dans Visual Studio, on voit des données:

image

Qui sont bien différentes de celles que l’on a en mode runtime:

image

On peut même avoir les 2 en même temps Smile

image

Avec cette nouvelle fonctionnalité, Microsoft tend à améliorer encore plus la collaboration entre les développeurs et les désigners, ainsi qu’à augmentez leur productivité !

 

Deux liens pour en savoir plus:

 

A+

[.NET] Appeler un objet COM tout en conservant l’impersonation

Voila une problématique que j’ai rencontré récemment et qui n’est pas aussi simple à régler que ce que l’on pourrait penser. Imaginer que l’on vous demande d’appeler un objet COM (tel que Analysis Services, au moyen de Microsoft.AnalysisServices.AdomdClient) mais qu’on vous impose de faire de l’impersonation (dans un service WCF par exemple) pour que vous puissiez tracer qui exécute la requête MDX.

Ni une, ni deux: vous mettez en place l’impersonation dans votre service WCF. Ainsi, vous commencez par rajouter cette ligne dans votre fichier web.config:

   1: <identity impersonate="true" />

Puis, dans votre code, vous récupérer l’Identity de l’utilisateur courant et vous appelez la méthode Impersonate:

   1: if (HttpContext.Current != null)
   2: {
   3:     if (HttpContext.Current.User != null)
   4:     {
   5:         if (HttpContext.Current.User.Identity != null)
   6:         {
   7:             IIdentity identity = HttpContext.Current.User.Identity;
   8:             WindowsIdentity windowsIdentity = identity as WindowsIdentity;
   9:             if (windowsIdentity != null)
  10:             {
  11:                 using (WindowsImpersonationContext wiCtx = windowsIdentity.Impersonate())
  12:                 {
  13:                     // Call to Analysis Services here
  14:                 }
  15:             }
  16:         }
  17:     }
  18: }

Vous exécutez votre code et là, en traçant les requêtes, vous vous apercevez que c’est un compte système qui exécute les requêtes: il semble donc y avoir un problème avec votre impersonnation…

En fait, il s’avère que votre code est correct. Le problème, expliqué ici, réside dans le fait que les objets COM utilise un modèle de thread STA (Single Thread Apartment) alors que votre service WCF, en .NET, utilise un modèle de thread MTA (Multithread Apartment). Si vous voulez connaitre la différence, jetez donc un oeil ici: http://stackoverflow.com/questions/127188/could-you-explain-sta-and-mta

Pour résoudre notre problème, la solution est simple: il suffit de créer un nouveau thread et de lui imposer de tourner en STA Smile

   1: theThread = new Thread(new ThreadStart(MySTAThreadStart));
   2:  
   3: // Intialize to an STA so that there will be no thread switch to the STA COM object.
   4: theThread.ApartmentState = ApartmentState.STA;

A partir de là, votre code appelera l’objet COM (Analysis Services) avec un modèle de thread STA ce qui permettra de conserver l’impersonnation !

 

Voila qui, je l’espère, permettra de gagner pas mal de temps à certains d’entre vous Smile

 

A+

[WPF] Comprendre le coeur de WPF

Il y a eu beaucoup de sessions lors de la PDC 2009 mais parmis celle-ci, j’en attendais une avec plus d’impatience que les autres:

Windows Presentation Foundation 4 Plumbing and Internals

Ayant enfin eu l’occasion de la regarder, je ne saurais que la conseiller à tous les développeurs WPF. Le speaker explique en détails comment fonctionne le binding, comment sont initialisées les propriétés, pourquoi l’interface peut ne pas être réactive (et comment corriger cela), etc…

Vous développez une application WPF et celle-ci est lente à s'exécuter/se lancer ? Vous vous dîtes qu'en WindowsForms, vous n'aviez pas de problèmes donc que la faute revient à la techno ? Et bien vous découvrirez que, souvent, le problème, c'est vous et non la technologie que vous utilisez Wink (même si, je le conçois, c'est peut-être un peu dur au début).

Bref, une vidéo à ne pas manquer, surtout si vous vous interessez au coeur de la technologie !

 

A+

Plus de Messages Page suivante »


Les 10 derniers blogs postés

- Compte rendu : SharePoint / O365 : des pratiques pour une meilleure productivité par The Mit's Blog le 12-12-2014, 18:11

- [TFS] Suppression des feature SQL Entreprise en masse par Blog de Jérémy Jeanson le 12-06-2014, 09:18

- [Clean Code] règles de nommage par Fathi Bellahcene le 12-04-2014, 22:59

- Windows To Go 10 et Upgrades impossibles par Blog de Jérémy Jeanson le 12-04-2014, 21:38

- SharePoint OnPremise: Statistiques d’utilisation pour traquer les sites fantomes par Blog Technique de Romelard Fabrice le 12-03-2014, 10:28

- SharePoint 2007: Script PowerShell permettant le backup de toutes les collections de sites d’une application Web par Blog Technique de Romelard Fabrice le 12-02-2014, 10:00

- Xamarin : un choix précieux par .net is good... C# is better ;) le 12-01-2014, 15:10

- Office 365: Comparaison des composants pour préparer votre migration de SharePoint 2007 vers Office 365 par Blog Technique de Romelard Fabrice le 11-28-2014, 16:20

- Créer un périphérique Windows To Go 10 ! par Blog de Jérémy Jeanson le 11-21-2014, 04:54

- RDV à Genève le 12 décembre pour l’évènement “SharePoint–Office 365 : des pratiques pour une meilleure productivité !” par Le blog de Patrick [MVP Office 365] le 11-19-2014, 10:40