Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

[WP7] Interaction entre XNA et Silverlight

Aujourd’hui, il y a deux APIs disponibles pour créer des applications sous Windows Phone 7 : Silverlight et XNA. Toutes deux ont leurs avantages et inconvénients : Silverlight permet de créer rapidement des IHM complexes utilisant des interactions tactiles (par exemple : glissement du doigt), tandis que XNA offre un accès de plus bas niveau aux ressources graphiques. D'une manière générale, on choisira Silverlight pour une application 'générale', tandis que XNA s'imposera pour tout jeu un peu plus complexe qu'un Snake. Je me trouvais dans cette deuxième situation à coder un jeu avec XNA quand est venu le besoin d'ajouter un menu de choix du niveau. Aucun problème à le visualiser dans ma tête : une liste de niveaux avec à chaque fois une image d'aperçu, un titre, et le rappel du meilleur score, et la possibilité de faire défiler le tout verticalement. Temps de développement estimé sous Silverlight : moins de 30 minutes. Problème : il s'agit d'une application XNA. Que suis-je censé faire ? Ecrire from scratch mes propres contrôles tactiles, ou tenter de porter tout mon jeu sous XNA ? ... Et s'il était possible d'utiliser simultanément les deux frameworks ?

 

Note : Moins que le résultat, je souhaite mettre en avant dans cet article le chemin parcouru et les outils utilisés pour y parvenir. Le produit final n’étant de toutes manières pas réutilisable en l’état, je pense que la méthode (ou l’absence de) et les différentes découvertes sur le fonctionnement de XNA justifient à plus forte raison l’écriture de ce billet. Si vous trouvez cet article trop long, trop peu structuré, ou inintéressant, n’hésitez pas à m’en faire part que je sache si je dois renouveler l’expérience à l’avenir ou revenir à un format plus “classique”. 

Exécution du code XNA

L'idée n'est pas venue de nulle part. J'ai déjà pu me rendre compte que les projets XNA pour WP7 généraient des fichiers .zap, ce qui correspond à la même extension que les applications Silverlight. Je me doutais donc déjà que les deux reposaient sur la même base. A partir de ce constat et avec un peu de chance, il doit être possible de faire cohabiter les deux.

Première étape : créer une solution avec deux projets, Silverlight et XNA, et ajouter une référence depuis Silverlight vers le projet XNA. Un problème se pose directement : Visual Studio 2010 ne permet pas de référencer le projet XNA, et il n'est pas possible d'ajouter une référence directement vers le fichier .zap. La solution de contournement est ici vite trouvée : déplacer tout le code du projet XNA dans un projet de type "XNA Game Library", et référencer la dll générée par ce projet. Compilation, lancement : aucun problème ça tourne.

Maintenant, tout ce qu'il reste à faire est de lancer le jeu XNA. J'ajoute un simple bouton à l'application Silverlight, et le fais déclencher le code approprié :

   1: using (var game = new XnaTest.Game1())
   2: {
   3:     game1.Run();
   4: }

 

Bien évidemment, cela aurait été trop simple.

image

 

Exécuter ce code lève une exception 'InvalidOperationException', avec le message : “Run is not supported for this platform.”. La pile d'appel pointe vers la fonction 'MobileGameHost.Run'. C'est déjà une petite victoire, le fait que l'exception soit levée si bas dans la pile d'appel montre que les deux environnements d'exécution sont bien compatibles. Il semblerait donc que d'une manière ou d'une autre XNA parvienne à détecter que le jeu n'est pas lancé d'une manière ordinaire. C'est le moment d'utiliser Reflector pour comprendre exactement ce qu'il se passe et tenter de trouver une solution de contournement.

Un coup d'oeil rapide sur la fenêtre de propriétés dans la liste de références révèle que les assemblies sont stockées dans le répertoire \Program Files (x86)\Reference Assemblies\Microsoft\Framework\Silverlight\v4.0\Profile\WindowsPhone. Pas d'erreur en ouvrant le fichier dans Reflector, dirigeons-nous doit vers la fonction concernée, et...

image

Reflector affiche une fonction vide. Soit Reflector n’est pas pleinement compatible avec le CIL utilisé par le .NET Framework de WP7, soit... Ces assemblies ne sont que des bouchons utilisés pour la compilation et l'Intellisense dans Visual Studio. Heureusement, des dumps de la rom WP7 peuvent facilement être trouvées sur le forum XDA-Developpers. Après une rapide recherche au cœur de cette rom, les assemblies recherchées sont situées dans le répertoire \SYS\XNA du téléphone. Est-ce que Reflector est capable de les ouvrir ?

image

Impeccable, nous pouvons maintenant voir le contenu des fonctions. Et il n'y a d'ailleurs pas grand chose à voir dans la fonction MobilgeGameHost.Run :

   1: internal override void Run()
   2: {
   3:     throw new InvalidOperationException(Resources.RunNotSupported);
   4: }

 

Ça n'avait clairement aucune chance de fonctionner. Cela signifie que WP7 utilise un autre point d'entrée pour lancer les applications XNA, reste à trouver lequel. Parcourir la pile d'appel nous mène à la fonction Game.RunGame(bool useBlockingRun), et plus particulièrement ce morceau de code :

   1: if (useBlockingRun)
   2: {
   3:     if (this.host != null)
   4:     {
   5:         this.host.Run();
   6:     }
   7:     this.EndRun();
   8: }

Ici nous devons clairement nous arranger pour que la variable ‘useBlockingRun’ ne soit pas égale à ‘True’. Mais cette fonction est privée, donc nous ne pouvons pas l’appeler directement. Déroulons la pile d’appel un peu plus, et jetons un œil à la fonction Game.Run() :

   1: public void Run()
   2: {
   3:     this.RunGame(true);
   4: }

Aucun moyen de changer la valeur du paramètre en passant par ici, il s'agit d'une impasse. Mais la méthode Game.RunGame est forcément appelée ailleurs, et la fonction 'Analyze' de Reflector est parfaite pour trouver où.

image

 

Jetons un œil à cette méthode StartGameLoop :

   1: internal void StartGameLoop()
   2: {
   3:     this.RunGame(false);
   4: }

 

C'est prometteur, mais nous ne pouvons pas l'appeler directement vu qu'elle est marquée comme 'internal'. Remontons un peu plus loin :

image

 

   1: private void XnaGameApplication_Startup(object sender, StartupEventArgs e)
   2: {
   3:     if (!this.InitializeGame(Deployment.get_Current().get_EntryPointAssembly(), Deployment.get_Current().get_EntryPointType()))
   4:     {
   5:         throw new InvalidOperationException(Resources.CannotCreateGameType);
   6:     }
   7:     this.game.StartGameLoop();
   8: }

 

Voilà qui est intéressant. Le nom 'XnaGameApplication' sonne vraiment comme un point d'entrée, et le contenu de la fonction XnaGameApplication_Startup semble le confirmer. Et pour couronner le tout, la classe est publique ! Essayons de l'utiliser.

 

image

La classe ne s'affiche pas dans l'Intellisense. Serait-il possible que... ?

 

image

Oui, la classe n'est pas présente dans l'assembly bouchon. Deux solutions s'offrent à nous : soit essayer de remplacer l'assembly bouchon par la vraie, soit instancier la classe en utilisant la réflexivité. Cette dernière solution devrait être la plus simple dans notre cas.

   1: var applicationType = typeof(Microsoft.Xna.Framework.Game).Assembly.GetType("Microsoft.Xna.Framework.XnaGameApplication");
   2: var obj = applicationType.GetConstructor(System.Type.EmptyTypes).Invoke(null);
   3: var method = obj.GetType().GetMethod("XnaGameApplication_Startup", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
   4:  
   5: method.Invoke(obj, new object[] { null, StartupEventArgs.Empty });

Aucun problème pour récupérer la méthode, malheureusement Silverlight bloque l’exécution de la méthode :

image

Mais il y a encore une piste. XnaGameApplication hérite de Application, qui est une classe Silverlight. Peut-être qu'il y a quelque chose dans le framework Silverlight pour aider à déclencher l'évènement Startup. Mais avant de perdre plus du temps, vérifions ce que la classe XnaGameApplication fait (avec le recul, cela aurait été beaucoup plus censé de commencer par là...).

La méthode XnaGameApplication_Startup charge l'assembly à l'aide de la fonction InitializeGame, en utilisant la réflexion, puis appelle la méthode StartGameLoop() de l'objet Game. Le nom de l'assembly à charger est donné par un objet renvoyé par la méthode 'Deployment.get_Current()', objet qui est créé dès le lancement de l'application. Il est peu probable qu'il existe un moyen de détourner ce mécanisme pour charger notre propre assembly, donc rangeons cette information dans un coin de notre esprit et tentons une autre approche.

A ce point, nous avons trouvé le point d'entrée de l'application XNA, mais nous n'avons aucun moyen de l'utiliser. Peut-être pouvons nous le réécrire ?

Retour à la méthode RunGame(bool useBlockingRun). Que fait-elle ? Elle appelle quelques méthodes protected, puis utilise la méthode MobileGameHost.StartGameLoop. Celle-ci créé un DispatcherTimer et le configure pour appeler périodiquement la fonction RunOneFrame. Donc en résumé, la boucle principale d'un jeu XNA sur WP7 se base tout simplement sur le DispatcherTimer de Silverlight qui se charge d'appeler périodiquement les méthodes de mise à jour et de rendu. J'avoue que je m'attendais à quelque chose de beaucoup plus complexe.

Maintenant que nous savons comment ça marche, nous pouvons écrire notre propre implémentation. Les assemblies Silverlight ne sont pas accessible depuis le projet XNA, donc il va falloir exposer les méthodes nécessaires pour pouvoir les appeler depuis le code Silverlight.

Côté XNA, nous exposons donc les méthodes et réécrivons le code qui n'est pas accessible. A noter que la variable graphicsDeviceManager est initialisée au début de la méthode RunGame(bool useBlockingRun), mais nous ne pouvons faire de même car cette variable est privée. Espérons que ça ne posera pas de problèmes.

   1: public void CallInitialize()
   2: {
   3:     this.Initialize();
   4:  
   5:     this.graphics.DeviceReset += new EventHandler<EventArgs>(graphics_DeviceReset);
   6: }
   7:  
   8: private void graphics_DeviceReset(object sender, EventArgs e)
   9: {
  10:     this.LoadContent();
  11: }
  12:  
  13: public void CallBeginRun()
  14: {
  15:     this.BeginRun();
  16: }
  17:  
  18: public void CallEndRun()
  19: {
  20:     this.EndRun();
  21: }
  22:  
  23: public void CallUpdate(GameTime gameTime)
  24: {
  25:     this.Update(gameTime);
  26: }

Et du côté Silverlight, nous appelons les méthodes exposées précédemment, et créons un DispatcherTimer qui va périodiquement appeler la méthode RunOneFrame :

   1: private void button1_Click(object sender, RoutedEventArgs e)
   2: {
   3:     this.game = new XnaTest.Game1();
   4:  
   5:     game.CallInitialize();
   6:  
   7:     game.CallBeginRun();
   8:  
   9:     GameTime gameTime = new GameTime();
  10:  
  11:     game.CallUpdate(gameTime);
  12:  
  13:     this.gameLoopTimer = new DispatcherTimer();
  14:     this.gameLoopTimer.Tick += new EventHandler(this.gameLoopTimer_Tick);
  15:     this.gameLoopTimer.Interval = game.TargetElapsedTime;
  16:     this.gameLoopTimer.Start();
  17: }
  18:  
  19: private void gameLoopTimer_Tick(object sender, EventArgs e)
  20: {
  21:     this.game.RunOneFrame();
  22: }

 

L'application se lance mais n'affiche qu'un écran noir. Au moins, les contrôles Silverlight ont disparu, donc quelque chose s'est passé. Essayons de comprendre ce qui manque.

 

Initialisation de l’affichage

Reflector nous montre que la méthode RunOneFrame appelle l'évènement OnIdle. En creusant un peu, nous arrivons jusqu'à l'objet Microsoft.Xna.Framework.Game qui souscrit à cet évènement, avec l'event handler HostIdle, qui appelle lui-même la méthode Tick. Il y a pas mal de choses qui se passent dans cette méthode, mais le point qui nous intéresse est l'appel à la fonction DrawFrame à la fin. Celle-ci appelle successivement les méthodes BeginDraw, Draw, et EndDraw.

Que fait BeginDraw ? Son code est très court :

   1: protected virtual bool BeginDraw()
   2: {
   3:     if ((this.graphicsDeviceManager != null) && !this.graphicsDeviceManager.BeginDraw())
   4:     {
   5:         return false;
   6:     }
   7:     return true;
   8: }

 

On retrouve ici le graphicsDeviceManager que nous n'avons pas pu initialiser précédemment. C'est donc vraisemblablement la source du problème. Un détour par le débuggeur de Visual Studio nous confirme que graphicsDeviceManager est null, le code n'appel donc pas la méthode BeginDraw.

image

Nous n'avons pas trop le choix ici, nous allons instancier notre propre graphicsDeviceManager dans une méthode que nous appelerons InitializeGraphics :

   1: private IGraphicsDeviceManager graphicsDeviceManager;
   2:  
   3: public void InitializeGraphics()
   4: {
   5:     this.graphicsDeviceManager = this.Services.GetService(typeof(IGraphicsDeviceManager)) as IGraphicsDeviceManager;
   6:  
   7:     if (this.graphicsDeviceManager != null)
   8:     {
   9:         this.graphicsDeviceManager.CreateDevice();
  10:     }
  11: }

 

Nous appelons InitializeGraphics depuis notre méthode CallInitialize précédemment écrite. Maintenant, il n'y a plus qu'à ajouter les appels à BeginDraw et EndDraw dans notre fonction Draw :

   1: graphicsDeviceManager.BeginDraw();
   2:  
   3: GraphicsDevice.Clear(Color.CornflowerBlue);
   4:  
   5: base.Draw(gameTime);
   6:  
   7: graphicsDeviceManager.EndDraw();

 

On compile, on lance, et...

image

Notre arrière-plan bleu s'affiche bien ! Maintenant dessinons quelque chose dessus pour s'assurer que tout fonctionne correctement.

 

Affichage d’une texture

 

Ajoutons une texture nommée 'dot' dans le content project de l'application XNA, et utilisons là avec un code simple :

   1: protected override void LoadContent()
   2: {
   3:     // Create a new SpriteBatch, which can be used to draw textures.
   4:      spriteBatch = new SpriteBatch(GraphicsDevice);
   5:  
   6:      blueDot = this.Content.Load<Texture2D>("dot");
   7: }
   8:  
   9: protected override void Draw(GameTime gameTime)
  10: {
  11:  
  12:         graphicsDeviceManager.BeginDraw();
  13:  
  14:         GraphicsDevice.Clear(Color.CornflowerBlue);
  15:  
  16:         spriteBatch.Begin();
  17:  
  18:         spriteBatch.Draw(blueDot, new Rectangle(10, 40, 180, 180), Color.White);
  19:  
  20:         spriteBatch.End();
  21:  
  22:         base.Draw(gameTime);
  23:  
  24:         graphicsDeviceManager.EndDraw();
  25:     }
  26: }

 

Le code lève une ContentLoadException avec le message "Error loading 'dot'. File not found.". Il semblerait que notre texture ajoutée au projet XNA ne peut être chargée dans notre application Silverlight. C'est assez logique : l'environnement d'exécution cherche probablement la texture dans l'assembly principale, donc dans notre cas l'assembly Silverlight. En ouvrant le fichier XAP avec un extracteur zip, on se rend compte qu'un dossier 'Content' est créé, et que la texture générée y est copiée. Faisons de même dans notre assembly Silverlight.

Créons juste un répertoire 'Content', ajoutons manuellement la texture XNB générée, et réglons sa 'Compile action' sur 'content'. Victoire, la texture s'affiche !

image

(en bien gros pour être sûr qu’on la voit)

Copier manuellement la texture est un peu fastidieux, mais il y a probablement moyen d'automatiser cela avec des build actions.

 

Epilogue

Résumons un peu : nous avons ajouté une assembly XNA à un projet Silverlight, nous l'avons chargé à la demande, et affiché une texture à l'écran. Avons-nous terminé ?

Hélas, pas complètement. Il reste un problème majeur à régler : comment, depuis le jeu XNA, re-basculer sur l'application Silverlight ? Il y a bien quelques méthodes du genre 'Dispose' ou 'Close', mais elles ferment l'application entière au lieu de juste redonner la main au code Silverlight. C'est là que je me suis arrêté, donc je ne suis pas encore sûr qu'il existe une solution de contournement, mais cela pourrait constituer un plongeon intéressant dans le code de chargement de l'assembly Silverlight. Ou, puisqu'on peut voir une transition au moment de basculer sur le code XNA, peut-être que les contrôles Silverlight sont juste cachés, auquel cas il suffirait de trouver un moyen de les ramener en vue. J'écrirai surement un billet sur le sujet si je trouve quelque chose.

Alors, était-ce inutile ? Au final, nous n'avons rien de réutilisable dans une "vraie" application. Cependant, cet examen en profondeur du code XNA à au moins le mérite d'en démystifier le fonctionnement, et donne un aperçu de ses mécanismes internes ce qui peut s'avérer utile dans le futur pour corriger certains bugs de bas niveau. C'est pourquoi j'ai jugé utile de partager aujourd'hui ces informations et les méthodes qui m'ont permit de les découvrir.

Cependant, je me retrouve quand même au point de départ, à savoir l'impossibilité d'utiliser les contrôles Silverlight au sein de mon application XNA. C'est d'autant plus frustrant qu'il est maintenant clair qu'il ne s'agit pas d'une impossibilité technique, et qu'il suffirait d'exposer quelques mécanismes pour que tout s'emboite. Si par hasard quiconque de l'équipe Silverlight / XNA mobile lisait ce billet, s'il vous plait, considérez l'idée d'ajouter un moyen simple de faire cohabiter Silverlight et XNA sur une application Windows Phone 7 !

 

Vous pouvez télécharger la solution Visual Studio 2010 et le code source utilisé pour cette article en suivant ce lien.

Publié dimanche 6 juin 2010 13:56 par KooKiz
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: [WP7] Interaction entre XNA et Silverlight

Salut,

Et merci pour cet article tres interessant.

Pour rendre le controle a Silverlight, est-ce que tu ne pourrais pas arreter ton timer ?

Tu pourrais rajouter a ta classe Game1 une property publique que l'application xna passe a true quand elle veut se terminer (a la fin du tableau par exemple).

Il suffirait alors de tester cette property depuis gameLoopTimer_Tick() et de suspendre le timer si la property change.

Il reste a trouver le moyen de remettre l'appli XNA dans un etat "stable" si tu veux la relancer mais ca ne devrait pas etre impossible.

Disclaimer: Je n'ai jamais utilise XNA, ni developpe pour WP7 donc je m'excuse d'avance si ma solution est completement foireuse :o)

lundi 7 juin 2010 15:51 by LeFauve42

# re: [WP7] Interaction entre XNA et Silverlight

J'y ai pensé mais l'affichage reste tel qu'il était lors du rendu de la dernière frame. Je ne sais pas exactement ce qui se passe mais j'ai trois hypothèses :

- Soit XNA a acquis un accès exclusif aux ressources graphiques, auquel cas il faut trouver comment le libérer

- Soit XNA a ouvert une fenêtre au premier plan (la présence d'une classe GameWindow me laisse penser qu'il y a une gestion de fenêtres dans WP7, à moins que ce soit là pour assurer la compatibilité avec XNA pour Windows), dans ce cas il faut trouver comment fermer cette fenêtre

- Soit les contrôles Silverlight sont tout bêtement cachés (visibility hidden ou position en dehors de l'écran), simple à vérifier et simple d'y remédier.

Dans tous les cas, ça promet encore quelques heures à farfouiller dans les assemblies de XNA. Quand on lance la boucle principale de XNA, une transition se déclenche qui décale la fenêtre Silverlight vers la gauche, à mon avis c'est là qu'il faut chercher.

A suivre :P

lundi 7 juin 2010 16:05 by KooKiz

# re: [WP7] Interaction entre XNA et Silverlight

Et une question à 2 cents, faire l'inverse ne serait-il pas + judicieux dans le cadre de l'interopérabilité entre XNA et Silverlight.

Héberger/Executer l'application Silverlight au sein d'un contrôle WebBrowser (ou assimilé) depuis le code XNA et développé la couche d'interop en fonction.

Je sais que cette solution fonctionne en WPF (j'écris un billet que je publie très prochainement sur ce sujet), je n'ai pas particulièrement testé en XNA.

C'est peut être une piste, mais ça prend ton article dans l'autre sens.

lundi 7 juin 2010 23:14 by nicoboo

# re: [WP7] Interaction entre XNA et Silverlight

Le problème est qu'il n'y a pas de contrôle WebBrowser ou assimilé dans XNA. On n'est pas dans une logique d'arborescence de contrôles : on indique ce que l'on doit afficher (formes, textures, ...), le tout est traduit en "ordres" qui sont transmis à je ne sais quoi (je ne peux pas aller plus loin avec Reflector, on passe dans du code natif), mais probablement un service qui se charge directement du rendu.

Le seul endroit où je pourrais ajouter mon contrôle WebBrowser-like serait dans l'arborescence de contrôles de Silverlight... Qui est justement celle que je cherche à afficher :P

mardi 8 juin 2010 09:48 by KooKiz

# re: [WP7] Interaction entre XNA et Silverlight

que pensez vous de l'approche proposée dans cette discussion

http://forums.silverlight.net/forums/p/168531/381489.aspx#381489

et démontrée par ce code source:

http://www.uta.fi/%7Etp79800/Silverlight/WindowsPhoneApplicationXNA.zip

consistant à transférer le contenu d'une texture XNA vers un WriteableBitmap SL?

Elle permettrait de piloter le rendu de scènes XNA via une interface SL, puis d'y rendre visible ces scènes XNA....

Il ne manque qu'une fonction efficace de transfert de texture XNA vers un bitmap SL....

Philippe Monteil

jeudi 1 juillet 2010 11:55 by pmonteil

# re: [WP7] Interaction entre XNA et Silverlight

Ca a le mérite de la simplicité, et le fait de pouvoir mêler Silverlight et XNA dans la même vue offre des possibilités intéressantes. Reste le problème de la performance, qui est difficile à évaluer dans un émulateur. Vivement qu'on puisse tester ça sur de vraies machines.

jeudi 1 juillet 2010 12:10 by KooKiz

# re: [WP7] Interaction entre XNA et Silverlight

La question de la performance de cette solution pourrait être réglée par une fonction permettant de transférer directement le contenu d'une texture XNA vers un objet SL ImageSource. Il me semble que cela suffirait à pleinement connecter SL et XNA....

Il ne resterait ensuite plus qu'à fournir une version de XNA pour SL5, et la question de l'intégration d'un moteur 3D dans les diverses déclinaisons de SL serait ainsi résolue :-)!

jeudi 1 juillet 2010 16:37 by pmonteil

# re: [WP7] Interaction entre XNA et Silverlight

Faudra que je jette un oeil au code des WriteableBitmap et des Render2D, mais je doute hélas qu'en l'état des choses il y ait une solution plus performante que la copie pixel à pixel.

Vivement SL5 effectivement, en espérant que Microsoft entendre notre voix :p

jeudi 1 juillet 2010 16:41 by KooKiz
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01