[CRM 2011] Débugguer un plugin dans CRM 2011 avec Visual Studio 2010

26. September 2011 09:09 by Renaud in   //  Tags:   //   Comments (0)

À ce stade, vous avez certainement déjà déployé votre premier plugin. J'espère que pour vous que tout s'est bien déroulé du premier coups. Mais dans ce comme dans l'autre, il peut être très intéressant de pouvoir débugguer votre plugin.

Pré-requis: Pour ce qui va suivre, il est nécessaire d'avoir Visual Studio installé sur le serveur sur lequel se trouve l'installation de CRM. Je n'ai pas encore utilisé le remote debugging, mais ça ne saurait tarder, et je vous ferai alors un autre article à ce sujet ;)

Vous pourrez ainsi faire des tests, explorer les données dont vous disposez, et apprendre à mieux utiliser les plugins.

Pour débugguer, rien de très compliqué: rappelez-vous l'enregistrement de votre assembly dans CRM.  Aviez-vous fait attention aux remarques concernant les fichiers .pdb? Ces grâce à eux que vous pourrez suivre l'exécution de vos plugins dans Visual Studio.

Commencez par placer le fichier .pdb correspondant à votre assembly dans le dossier server/bin/assembly. Attention de bien copier la bonne version du fichier, sinon le debug ne fonctionnera pas. C'est pour cela que la solution du stockage sur disque de votre assembly est pratique en développement. Vous pouvez, après chaque nouveau build de votre assembly, remplacer les deux fichiers (.dll et .pdb) en une seule opération, ce qui évitera toute erreur.

Ensuite, dans Visual Studio, allez dans le menu Debug et choisissez Attach to Process...

Dans la liste des processus actifs, cherchez w3wp.exe. S'il n'apparaît pas, c'est peut qu'il faut cocher la case "Show processes in all sessions". [caption id="attachment_554" align="aligncenter" width="738" caption="Dans mon cas, le process qui m'intéresse est celui lié à l'utilisateur "crm""][/caption]

Il se peut aussi que vous ayez plusieurs fois ce processus dans la liste. Si vous n'êtes pas sûr de quel processus vous intéresse réellement, ouvrez une page web pour vous connecter à votre organisation CRM. Ouvrez également le Task Manager pour observer les process w3wp.exe. Rafraîchissez la page affichée dans Internet Explorer, et vous verrez l'un des process consommer d'avantage de ressources pendant un bref instant. C'est lui le coupable :) Retenez le PID un cours instant, le temps de le retrouver dans Visual Studio!

Cliquez ensuite sur "Attach". Vous serez prévenu que cela peut endommage votre système, mais pas de panique, on ne fait rien de mal.  Confirmez!

Il ne reste plus maintenant qu'à mettre un point d'arrêt quelque part dans votre code, vous rendre sur la page concernée par votre plugin, et si tout à été fait correctement, l'exécution s'interrompra comme prévu! :)

[caption id="attachment_702" align="aligncenter" width="449" caption="Image thématique :)"][/caption]

Attention, lorsque vous voudrez remplacer l'assembly et le pdb par un nouveau build sur votre serveur, il se peut que les fichiers soient en cours d'utilisation et donc inaccessibles. Il suffit de tuer le process de crm utilisé précédemment via le Task manager, le temps de remplacer les fichiers. Ensuite, rafraîchissez la page dans CRM pour créer un nouveau process avec un nouveau PID auquel vous pourrez de nouveau attacher Visual Studio!

Bon débug ;)

[CRM 2011] Update the display of a lookup field [part 2/2]

19. September 2011 17:00 by Renaud in   //  Tags:   //   Comments (1)

Before we start

Here is the second part of this solution concerning lookup fields. Just to remind you, the goal of those articles is to display a different field than the primary field of a related entity in a lookup field.

In the following example, the interface is in french, but the "Discipline" field contains an english value. This is because the lookup field displays the new_name attribute of the Discipline entity. And CRM 2011 doesn't allow us to map this field to an other property of the related entity. That's why we have to build our own solution.

In the first article, we have build a plugin that replaces the actual value by its french traduction which is stored in the new_french attribute. This plugins works both for Create and Update message.

But there is another problem: if we change the current Discipline to which a student has subscribe, then the field will be updated, but it will show an english term again.

This window allows you to browse Discipline entities that you could associate with a student.

The point is that there isn't no request between the moment you click the Ok button and the moment the field is updated. The new value directly comes from the previous research dialog. This is a built-in behavior of CRM.

We will try to modify the data in that window, so that CRM will continue working the same way and will return the correct value without noticing that we have change something.

What are the possibilities...

We know that before opening the lookup dialog of Discipline entities, the RetrieveMultiple message is launched for the new_discipline entity. One possibility would be to display only one column in that window with the generic title "Name" instead of two columns "English name" and "French name". This column will be able to handle both languages, and we can create a plugin in post-operation that check for the user language and then, if it is french, retrieve the french values to replace the english ones.

In my case, the client wants to aways see both columns. So it's easier because we can just create a plugin in the post-operation pipeline of the RetrieveMultiple message to just switch the values of both columns! :)

Prerequisites

If you haven't read the first article yet, maybe you should have a look. So you could download the sources and do the lab with me in the best conditions! :)

Customize CRM view

Nothing complicated here: I'll simply do some modifications as explained above.

For this to work  fine, there is a little trick: The name of the colums in english will be "English" and "French" and the translations will be respectively "Français" and "Anglais" to fits the columns content!

To modify those titles, go into the customization form of the Discipline entity.

Then select a field, let's say English:

In the Ribbon menu, click on Change Properties. Go into Details, and then clik Edit. There, you'll be able to change the Display Name value. Do the same for the second field. The names you are writing here aren't those which are displayed directly on the form but those who will appear on grid views! If you want to change the display label of a field, you can do it directly in the properties of this field. Keep it in mind when you'll do the translations!

The source code

Here is the interesting part :) Take back the plugin library from the first article. You'll need the helper class to check the language! Create a new plugin:

RetrieveMultipleDiscipline.cs
using Microsoft.Xrm.Sdk;
using System;

namespace Plugins
{
    public class RetrieveMultipleDiscipline: IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            // get the context
            IPluginExecutionContext context = (IPluginExecutionContext)
                serviceProvider.GetService(typeof(IPluginExecutionContext));

            EntityCollection collection;

            // Check if the input parameters property bag contains a BusinessEntityCollection
            // key of type EntityCollection
            if (context.OutputParameters.Contains("BusinessEntityCollection") &&
              context.OutputParameters["BusinessEntityCollection"] is EntityCollection)
            {
                // Obtain the EntityCollection from the output parmameters.
                collection = (EntityCollection)context.OutputParameters["BusinessEntityCollection"];

                // Verify that the collection contains Discipline entities.
                if (collection.EntityName == "new_discipline")
                {
                    // TODO: checker la langue et switcher les valeurs.
                }
            }
        }
    }
}

This time, we will look for an EntityCollection! The EntityName attribute contains the LogicalName of the entity that this collection contains.

You will now use the method from the PluginHelper class to check wether the user has set the language to french or not. If he has, then you can start the process of switching the values.  Place the following code instead of the TODO.

RetrieveMultipleDiscipline.cs
                    // the factory will allow us to create a new IOrganizationService
                    IOrganizationServiceFactory factory = (IOrganizationServiceFactory)
                        serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    // Creates a new IOrganizationService to retrieve the user language.
                    IOrganizationService orgService = factory.CreateOrganizationService(context.UserId);

                    if (orgService.RetrieveLanguageForUser(context.UserId) == 1036)
                        SwitchFields(collection, "new_name", "new_french");

And last but not least, the SwitchFields method:

RetrieveMultipleDiscipline.cs
        /// <summary>
        /// Switches the specified fields of the given collection.
        /// </summary>
        /// <param name="collection">The entity collection.</param>
        /// <param name="firstField">The first field.</param>
        /// <param name="secondField">The second field.</param>
        private void SwitchFields(EntityCollection collection, string firstField, string secondField)
        {
            foreach (Entity targetEntity in collection.Entities)
            {
                string temp = targetEntity.Attributes[firstField] as string;

                targetEntity.Attributes[firstField] = targetEntity.Attributes[secondField];

                targetEntity.Attributes[secondField] = temp;
            }
        }

Register the plugin

For the registring, you just have to update the assembly because you had already registered it in the previous lab. Then, add a new step for the RetrieveMultiple message of the new_discipline entity. Use the post-operation event pipeline.

Now you can check the result by yourselves!

Whatever the language is, french or english, the first column of the Discipline lookup dialog will always display the value corresponding to the language. CRM will automatically return the value from the first column to the Student Form when you click ok, because it will suppose that it is the primary field! :)

You can download the sources here:

Visual Studio 2010 project

CRM 2011 Solution, with
entities + assembly + plugin

[CRM 2011] Modifier l’affichage d’un champs lookup [partie 2/2]

19. September 2011 16:59 by Renaud in   //  Tags:   //   Comments (0)

Prologue

Nous voici dans la deuxième partie de la solution consacrée aux lookup fields. Pour rappel, l'idée de cette série d'articles est d'arriver à afficher autre chose que le primary field d'une entité dans un champs lookup.

Dans l'exemple ci-dessous, l'interface est en français, mais le champs "Cours" est en anglais. En fait, c'est la propriété new_name du cours, qui est affichée. Ceci est dû à une limitation de CRM 2011, qui n'offre pas la possibilité de mapper un champs sur n'importe quelle propriété d'une entité relative. A moins donc de développer sa propre solution, vous verrez toujours le champs primaire de l'entité relative.

Dans le premier article, nous avons créé un plugin pour faire en sorte de remplacer cette valeur par sa traduction française à l'ouverture ou au rafraîchissement de la fenêtre ci-dessus.

Mais il reste un problème: en sélectionnant un cours parmi la liste ci-dessous, et en cliquant sur Ok, le champs de la Form étudiant est bien mis à jour, mais apparaît de nouveau en anglais!

Cette fenêtre permet de rechercher une entité à associer à un étudiant

En fait, il n'y a pas de requête effectuée vers le serveur entre le moment où l'on clique sur Ok et celui où le champs Cours est mis à jour. La nouvelle valeur provient directement de la fenêtre précédente. C'est donc là qu'il faut traiter nos entités.

On va modifier un petit peu les données de la fenêtre de sélection d'un cours. CRM pourra continuer à fonctionner de la même manière, et la fenêtre de sélection d'un cours renverra une valeur à afficher, mais la bonne cette fois-ci! C'est-à-dire celle correspondant à la langue de l'utilisateur :)

Solutions envisageables

Avant l'ouverture de la fenêtre ci-dessus, le message RetrieveMultiple est déclenché pour l'entité new_discipline. Une des possibilités serait de n'afficher qu'une seule colonne avec pour titre Name ou Intitulé, selon la langue de l'utilisateur. Avec un plugin, on check la langue de l'utilisateur. S'il s'agit du français, alors il suffit de remplacer les données de la propriété OutputParameters du IPluginExecutionContext par les valeurs françaises. Mais cette solution nécessite un deuxième accès à la base de données pour récupérer les infos manquantes!

Dans mon cas, l'utilisateur souhaite toujours voir les noms des cours dans les deux langues! Nous allons donc adapter les labels des attributs de l'entité cours. Et plutôt que de faire une deuxième requête après que le message RetrieveMultiple ait été traité, on va tout simplement switcher le contenu de chaque colonne pour s'adapter au scénario :)

Pré-requis

Si vous n'avez pas lu le premier article, vous pouvez allez y faire un tour, histoire de télécharger les sources et de pouvoir suivre cette solution dans les meilleures conditions! :)

Customization CRM

Rien de très compliqué ici, je vais simplement effectuer les modifs expliquées quelques lignes plus haut.

Bon, il y a quand même une petite astuce! Comme on va simplement switcher les données françaises et anglaises, il faudra que les titres des colonnes switchent également. Ainsi, la traduction de English sera Français, et la traduction de French sera Anglais ;) vous suivez?

Les titres de ces colonnes viennent des metadata des attributs. Pour les modifier, ouvrez la fenêtre de customization de la Form Discpline.

Sélectionnez l'un des deux champs à modifier:

Dans le menu ribbon, cliquez sur Change Properties. Allez dans Details, et cliquez sur Edit. Vous pourrez alors modifier le Display Name de l'attribut. Faites pareil pour le deuxième. Les noms que vous venez de modifier ne sont pas ceux affichés dans l'image ci-dessus, mais bien ceux qui seront affichés dans les colonnes des gridviews! Faites donc attention pour les traductions de ne pas confondre les titres des tableaux avec les labels de la Form.

Le code

Nous voilà dans la partie intéressante. On va la faire courte puisque vous êtes supposés avoir quelques notions maintenant.

Reprenez donc la librairie avec le premier plugin et la classe PluginHelper. Nous allons en avoir besoin :) Créez un nouveau plugin:

RetrieveMultipleDiscipline.cs
using Microsoft.Xrm.Sdk;
using System;

namespace Plugins
{
    public class RetrieveMultipleDiscipline: IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            // get the context
            IPluginExecutionContext context = (IPluginExecutionContext)
                serviceProvider.GetService(typeof(IPluginExecutionContext));

            EntityCollection collection;

            // Check if the input parameters property bag contains a BusinessEntityCollection
            // key of type EntityCollection
            if (context.OutputParameters.Contains("BusinessEntityCollection") &&
              context.OutputParameters["BusinessEntityCollection"] is EntityCollection)
            {
                // Obtain the EntityCollection from the output parmameters.
                collection = (EntityCollection)context.OutputParameters["BusinessEntityCollection"];

                // Verify that the collection contains Discipline entities.
                if (collection.EntityName == "new_discipline")
                {
                    // TODO: checker la langue et switcher les valeurs.
                }
            }
        }
    }
}

Cette fois-ci on va chercher une EntityCollection! On peut connaître le type d'entité qu'elle contient grâce à l'attribut EntityName.

On va utiliser l'unique méthode de la classe PluginHelper pour vérifier si l'utilisateur utilise ou non CRM en français. Si c'est le cas, on traite les données. Le bout de code suivant est à placer à l'endroit du TODO.

RetrieveMultipleDiscipline.cs
                    // the factory will allow us to create a new IOrganizationService
                    IOrganizationServiceFactory factory = (IOrganizationServiceFactory)
                        serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    // Creates a new IOrganizationService to retrieve the user language.
                    IOrganizationService orgService = factory.CreateOrganizationService(context.UserId);

                    if (orgService.RetrieveLanguageForUser(context.UserId) == 1036)
                        SwitchFields(collection, "new_name", "new_french");

Et pour finir, la méthode SwitchFields:

RetrieveMultipleDiscipline.cs
        /// <summary>
        /// Switches the specified fields of the given collection.
        /// </summary>
        /// <param name="collection">The entity collection.</param>
        /// <param name="firstField">The first field.</param>
        /// <param name="secondField">The second field.</param>
        private void SwitchFields(EntityCollection collection, string firstField, string secondField)
        {
            foreach (Entity targetEntity in collection.Entities)
            {
                string temp = targetEntity.Attributes[firstField] as string;

                targetEntity.Attributes[firstField] = targetEntity.Attributes[secondField];

                targetEntity.Attributes[secondField] = temp;
            }
        }

Enregistrement du plugin

Pour l'enregistrement, commencez par mettre à jour l'assembly. Ensuite choisissez le message RetrieveMultiple de l'entité new_discipline. Utilisez le mode post-operation.

Vous pouvez maintenant tester le résultat par vous-même! :) Les sources sont téléchargeables:

Le projet Visual Studio.

La solution CRM 2011, avec
entités + assembly + plugin

Premier boulot: le bien contre le mal?

16. September 2011 10:09 by Renaud in   //  Tags:   //   Comments (7)

Une fois qu'un développeur termine ses études, il arrive à ce moment angoissant de la vie, où il doit choisir un premier boulot. Angoissant parce qu'il sera contacté de tous les côtés par des recruteurs tous plus friendly les uns que les autres, qui lui proposeront des boulots fondamentalement différents, avec des salaires variables. A côté de ça, il y a également les jobs que ses contacts (ceux créés durant les études, un stage en entreprise, ...) lui recommanderont et ceux pour lesquels il aura été recommandé!

Chacun à son avis sur la question du choix du boulot: la famille, les amis, les contacts professionnels. Et tout le monde y va de son conseil!

Prends celui avec la voiture de société!
Si tu vas dans une grosse boîte, tu resteras un numéro.

Il y a clairement deux courants. D'une part il y a ceux qui vous envient pour le côté financier, pour qui le boulot n'est qu'un gagne-pain. Et puis, il y a ceux qui ont choisi un boulot qui leur convient, au détriment du package salarial.

Alors quoi? Ayant été élevé dans un milieu modeste, notre développeur fini par se persuader qu'une grosse boîte, c'est le mal! Son dark-side prendrait-il le dessus? Est-ce qu'il peut, sans honte, après avoir pris le train toutes ces années en bon prolétaire, simplement capituler et accepter un salaire un peu plus élevé que d'autres? Alors que ces autres, eux, ont résisté.

C'est vraiment tout ce qu'on a comme choix? Ce discours intellectualisant est dans l'air du temps. Mais faut-il vraiment généraliser à ce point? Est-ce qu'un boulot bien payé est forcément un boulot dans lequel on va gâcher son temps? Est-ce qu'il faut travailler dans une start-up pour réussir sa vie?

Le critère salarial devient alors un obstacle dans le choix d'un boulot. De l'idée saine de départ "l'argent n'est pas un critère", on passe à un réducteur "l'argent est mauvais signe". On en finirait presque par refuser un boulot, juste parce qu'il rapporte, pour ne pas avoir mauvaise conscience. Et puis, on a peur que les autres aient raison au final, ou qu'ils voient en nous une personne dont le seul leitmotiv est l'argent. Comme si ce n'était pas déjà assez compliqué.

Mais maintenant je voudrais rétablir la vérité, en espérant déculpabiliser les futures générations. Non, il ne faut pas absolument travailler dans une start-up pour être heureux dans la vie. Le plus dur est évidemment d'arriver à faire la part des choses entre le plaisir que vous avez à faire votre travail, la qualité de vie que vous attendez,  et les objectifs que vous vous fixez sur le long terme.

L'idéal étant de trouver un environnement qui vous aide à atteindre vos objectifs. Et vos objectifs, vos motivations, personne ne peut les comprendre mieux que vous! Posez-vous ces quelques questions: votre travail est-il en lien direct avec vos besoins? Si non, combien de temps y consacrerez-vous et que vous restera-t-il pour vos propres projets? Est-ce que vous pourrez profiter de votre environnement de travail et des ressources (humaines, matérielles) pour votre épanouissement personnel?

J'ai eu plusieurs fois l'opportunité de travailler pour des start-up, toutes très intéressantes. Mais j'ai choisi une autre direction. Et pour terminer, je citerai encore une fois cet axiome: "Dans la vie, il ne faut pas attendre les opportunités. Il faut se les créer soi-même!" C'est comme cela que je fonctionne depuis quelques temps, et peu importe dans quel milieu je travaillerai, cela sera toujours vrai. Dans toute communauté, toute société, il y a des gens qui seront plus entreprenant que d'autres. Et travailler pour une boîte de plus de 10 personnes ne signifie pas se ranger. Il faut juste savoir où l'on veut aller, et se donner les moyens d'y arriver.

Sur ce, j'vous souhaite un bon vendredi! :D

Le b.a.-ba du debug avec Visual Studio.

16. September 2011 09:09 by Renaud in   //  Tags:   //   Comments (5)

On a reçu pas mal de commentaires et de questions via le formulaire de contact sur le blog WP7Team, et en y répondant je me suis rendu compte que beaucoup de développeurs débutants se retrouvent coincés face à des problèmes simplement parce qu'ils ne savent pas débugger. Cet article est une adaptation d'un des mails que j'ai écrit pour répondre à l'une des questions reçues.

Coder sans débugger, c'est un peu comme avancer dans un labyrinthe les yeux bandés! Dès que vous vous retrouvez face à un mur, vous êtes incapable de dire comment vous êtes arrivés là.

Sans être un expert en la matière, je pense pouvoir vous donner quelques conseils très très basiques qui pourront peut-être vous éviter des crises de nerfs!

1/ Lancer l'appli en Debug.

Premièrement, passez en mode Debug. Pour lancer l'exécution de votre application en Debug, cliquez sur la flèche verte ou appuyez sur F5 (Start Debugging).

[caption id="attachment_241" align="aligncenter" width="265" caption="Configuration: Debug"]Debug[/caption]

2/ Placer des breakpoints.

[caption id="attachment_243" align="aligncenter" width="750" caption="Breakpoints: points d'arrêt"]breakpoint[/caption]

Dans la barre de gauche il y a des points rouges. Ce sont des points d’arrêts, et quand je lance mon appli en mode debug, comme c’est le cas ici, l’exécution s’arrêtera à chaque point d’arrêt !

Donc, lancez l’application, et vous verrez le premier point d'arrêt rencontré se surligner en jaune :

[caption id="attachment_563" align="aligncenter" width="1030" caption="La ligne surlignée en jaune est celle sur le point d'être exécutée!"][/caption]

Notez bien que la ligne surlignée n'a pas encore été exécutée!

Si vous vous arrêtez sur la ligne suivante, myObject ne sera pas encore initialisé et aura null pour valeur.

MyObject myObject = new MyObject();

3/ Naviguer en debug

Le bouton « play »  (F5) permet de relancer l’exécution jusqu’à ce qu’on rencontre un nouveau point d’arrêt. Vous pouvez stopper ou redémarrer l'application: 

Pas loin, vous trouverez des contrôles assez intéressants: 

Le premier, StepInto (F11), va passer à la ligne suivante où rentrer à l’intérieur d’une méthode si la ligne sur laquelle on se trouve fait appel à une méthode !

Celui du milieu, StepOver (F10), va passer à la ligne suivante, sans s'occuper des appels de méthodes.

Et le dernier, StepOut (Maj + F11), va relancer l’exécution jusqu’à ce qu’on remonte d'un niveau, c'est à dire jusqu'à ce que l'on sortant de la méthode dans laquelle on se trouve pour arriver au code ayant fait appel à cette méthode.

4/ Observer

Un des avantages des points d'arrêts est qu'ils vous permettent à un moment donné d'observer la valeur des variables que vous avez créées. Cela vous aidera peut-être à comprendre pourquoi votre programme ne se comporte pas comme vous l'espériez!

Il existe plus moyens pour observer vos variables, comme cette vue, que vous devriez apercevoir lorsque vous lancez votre application en debug:

Si cette vue n’est pas affichée chez vous, allez dans le menu Debug > Windows.

Affichez « Locals » et dans « Watch » affichez « Watch1 » (On parlera de celle-ci plus bas ;) ).

Une autre possibilité pour espionner vos variables consiste simplement à placer le curseur de votre souris par dessus un nom de variable.

Sur l'image ci-dessus, en passant par-dessus le« e », vous pouvez voir grâce au menu contextuel que cette variable représente un DownloadStringCompletedEventArgs. Même pas besoin de connaître le début du code! Si l'on avait placé le curseur par dessus le "Cancelled", vous pourriez voir apparaître [ e.Cancelled |false ] (ou true évidemment :) c'est selon! )

Regardons un exemple un peu plus complexe, tiré d'un code de MSDN:

Ici, l'objet this est un objet de type MainForm de l'espace de nom PluginRegistrationTool. Les valeurs affichées juste en dessous représentent les valeurs des propriétés , et les champs private de l'objet.

La première ligne, "base", permet d'accéder aux propriétés et champs hérités de la super-classe qui est ici System.Windows.Forms.Form.  On peut décider d'aller voir ce qu'il s'y cache:

On peut voir que cette classe hérite elle-même de System.Windows.Forms.ContainerControl, qui possède une quantité de propriétés. Vous pouvez bien évidemment décortiquer chacune de ces propriétés exactement de la même manière :)

5/ Evaluer des expressions

Explorer à la souris, c'est pratique et rapide, mais parfois ce ne sera pas suffisant. C'est le cas dans l'exemple suivant:

J'aimerais beaucoup savoir ce que vaut Math.Pow(z, y) à ce moment du programme, parce que ça m'aiderait à comprendre pourquoi je n'ai pas de chance et ne satisfait jamais la condition! Mais je suis nul en math :/ Et en fait, j'aimerais aussi savoir combien vaut mon opération si vraiment ce n'est pas égal à 100! :)

C'est là qu'intervient la vue Watch que je vous ai fait ajouter tout à l'heure!

Pour l'utiliser, rien de très compliqué, il suffit de sélectionner une expression, et de la glisser sur la vue :) On sait maintenant que le résultat est...

Et puis rien ne nous empêche de mettre le premier membre de l'expression tout entier!

[caption id="attachment_579" align="aligncenter" width="472" caption="Effectivement, on est loin du compte..."][/caption]

Et ce qui est tout aussi pratique, c'est que vous pouvez finalement taper n'importe quelle expression directement dans la colonne Name de la vue Watch:

5/ Les exceptions

Pour terminer, parlons d'un des points essentiels: les exceptions! Je vais reprendre le problème rencontré par le développeur qui nous avons contacté et qui a fait que j'écris ce post aujourd'hui.

Cette personne développait une application pour Windows Phone et utilisait un HyperlinkButton, avec pour valeur de la proprité NavigateUri un lien vers une page web. Dans son dernier mail, cette personne me disait:

[...] je suis incapable de comprendre pourquoi lorsque je clique sur le titre du post, sur lequel j'ai fait pointer l'url de la vidéo désormais, fait planter l'application, au lieu d'ouvrir IE [...]

Là, j'ai compris que je ne pouvais pas le laisser continuer à développer sans lui apprendre à lire et comprendre les exceptions.

Je ne vais pas faire ici un cours complet sur ce qu'est une exception. Le but n'est pas que vous sachiez comment créer les vôtres, mais bien de comprendre ce qu'elles veulent dire. Sachez déjà que lorsqu'une exception est lancée dans votre application, cela signifie que quelque chose ne s'est pas passé comme prévu, et cela empêche votre programme de continuer à s'exécuter! En général, chez un débutant, cela signifie tout simplement le plantage et la fermeture de l'application illico!

Si toutefois vous ne savez pas comment gérer ses exceptions, je vous invite grandement à lire ceci: try-catch (C#).

Revenons au problème:

J'ai mon HyperlinkButton, et sa propriété NavigateUri pointe vers une url tout à fait correcte. Lorsque je lance l'application en debug, et que je clique sur le lien, il y a effectivement une erreur, et je me retrouve dans cette partie du code :

 

Ici, l’exception est traitée par le code généré par visual studio quand vous créez un projet (c'est un peu un cas particulier) mais en général quand il y a une exception, l’application s’arrête et un pop-up apparaît dans visual studio en pointant sur la ligne de code qui a déclenché l’exception !

[caption id="attachment_584" align="aligncenter" width="731" caption="NullReferenceException signifie que vous essayez d'appeler une méthode sur une référence null. Généralement, le type de l'exception est assez explicite pour comprendre quel est le problème."][/caption]

Dans le cas du problème de navigation, ce n'est à première vue pas aussi simple. Mais les techniques apprises plus haut vont vous permettre de vous en sortir sans difficulté!

Si vous exploriez le paramètre « e », vous verriez dans e.Exception.Message la valeur : {"Navigation is only supported to relative URIs that are fragments, or begin with '/', or which contain ';component/'.rnParameter name: uri"}.

On comprends donc bien que l’HyperlinkButton ne permet tout simplement pas de naviguer vers une page web mais uniquement vers des pages de l'application même.

Matthieu Vandenhende apporte une petite correction dans les commentaires! N'hésitez pas à visiter son blog. C'est plein de bons conseils sur le développement Windows Phone!

 Il est tout à fait possible de naviguer vers une page web depuis un HyperLinkButton. La solution est de préciser comment va s’ouvrir la page avec l'attribut TargetName. Ex:
<HyperlinkButton Content="Devillu BLOG"
                 NavigateUri="http://blog.devillu.com" TargetName="_blank" />

Voilà, donc, comment Visual Studio peut vous permettre de mieux comprendre les problèmes rencontrés, et vous aider à vous corriger :)

La solution, en bonus! (pour ceux que ça intéresse :) )

Pour naviguer vers une page web sur Windows Phone 7, il faut utiliser l'un des nombreux launchers disponibles dans le SDK. Il s'agit du WebBrowserTask.

WebBrowserTask webBrowserTask = new WebBrowserTask();

webBrowserTask.Uri = new Uri("http://msdn.microsoft.com", UriKind.Absolute);

webBrowserTask.Show();

[CRM 2011] Modify the display of a lookup field [part 1/2]

14. September 2011 16:53 by Renaud in   //  Tags:   //   Comments (1)

In Microsoft Dynamics CRM 2011, lookup fields always display the primary field of the entity to which it is related. In some cases, you may want to display another property of this entity because the primary field isn't always enough explicit. Look at this example:

Each student can suscribe to one disciple. A disciple has two required properties: the english name (new_name) and the french name (new_french). Now, when you open the student form, you'd like to see the name in the correct language. I mean, the language that the user has selected in the settings! But CRM doesn't allow you to do this easily. There isn't any lookup mapping functionnality. In this post, we will see how to do this using a simple plugin:

Interface en anglais

Interface en français

"English UI: displays the new_name property

French UI: displays the new_french property

You can directly go to the solution if you follow this link! :)

Let's go back to the beginning:

If you want to do this lab with me, you can download this CRM 2011 solution. It contains the two entities described above.

Starter kit

So, in this example, the value displayed in the "Discipline" field isn't a proper noun like the "Owner" field, but it is a common noun. The difference is that it need to be translated to fits with the user's prefered language.

Let's look at what happens if we add a new language pack to our organization!

To install a new language pack, go to Settings > Administration > Languages

(Notice the "language code" corresponding to French: 1036. We'll need it later.)

When it's done, and after having imported some translations for the Student and Discipline entities, you can chosse your prefered language. For this exercice, choose French!

Now, create a new Discipline record (new_name = Computer science, new_french = Informatique). Then create a new Student, and add him the discipline you've created previously. If you look at the lookup field, you can see that the content is still "Computer science". But as a french speaking user, you have choosen a french user interface, and you would like this field to display the french property!

The noun "Computer science" doesn't correpond to the user's language.

What we want isn't complicated at all:

If language == English,
     then display new_name.
Els if language == Français,
     then display new_french.

What will be the consequences if we change the displayed value? If we take a look at the html code of the lookup field, we can see this:

<SPAN oncontextmenu=handleGridRightClick() class=ms-crm-Lookup-Item
title="Computer science" contentEditable=false
onclick=openlui() otypename="new_discipline" otype="10010"
oid="{68F3C5A6-66DA-E011-AE41-000C29CE1C0D}">
     <IMG class=ms-crm-Lookup-Item alt=""
     src="/Contoso4/_Common/icon.aspx?cache=1&amp;iconType=GridIcon&amp;objectTypeCode=10010">
     Computer science
</SPAN>

the span tag

The oid attribute contains the GUID of the referenced discipline, and otypename the type: new_discipline. Those values are required to retrieve the corresponding object, but the title attribute isn't! If you look at the URL of the window which is opened when we click on the lookup field:

The title attribute doesn't appear anywhere. It isn't necessary to open this window, and we can modify it without any consequence.

How to change this value?

At first, we need to think about the possibilities that we have to modify this value:

  •  Using javascript to make a client-side request when onload event of the form and onchange event of the lookup field are triggered.
    • Plus: doesn't overload the server even if there are lots of users at the same time.
    • Disadvantages: It needs a second request to update the field with the correct value. When opening the window, the english field will always be visible until the request is finished and the new value has replaced the original.
  • Using a plugin executed on the server to modify the result of the request and so the values that will be displayed.
    • Plus: No double request!
    • Disadvantages: more work for the server.

It's clear that the user experience will be better with the plugin solution. We will prefer this solution!

Adapt the display with a plugin

The solution that we are going to build is really simple. We will create a plugin that will be triggered with the Retrieve Message of the Student entity to modify the result of the retrieve request.

1/ Create a new solution

... of type Class Library. I simply call it Plugins, because it will contains all necessary plugins for my organization.

You can already add the following references (dlls are located in SDKbin):

  • Microsoft.Crm.Sdk.Proxy.dll
  • Microsoft.Xrm.Sdk.dll
  • System.Runtime.Serialization

I add a first class called RetrieveStudent.csimplementing IPlugin interface.

RetrieveStudent.cs
using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
namespace Plugins
{
    public class RetrieveStudent: IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            throw new NotImplementedException();
        }
    }
}

2/ Check the context.

Let's start with the context to check if we are in the expected case. The following code needs to be added in the Executre method.

RetrieveStudent.cs
// get the context
IPluginExecutionContext context = (IPluginExecutionContext)
    serviceProvider.GetService(typeof(IPluginExecutionContext));

Entity targetEntity;

// Check if the input parameters property bag contains a target
// of the retrieve operation and that target is of type Entity.
if (context.OutputParameters.Contains("BusinessEntity") &&
    context.OutputParameters["BusinessEntity"] is Entity)
{
    // Obtain the target entity from the output parmameters.
    targetEntity = (Entity)context.OutputParameters["BusinessEntity"];
    // check if the entity is type of targetEntityName!
    if (targetEntity.LogicalName != "new_student")
        return;
}
else
{
    return;
}

3/ Check the language

Now we will check which language the user has chosen! For this purpose, we will create a PluginHelper class in which we will add methods that could be used in many plugins.

PluginHelper.cs
using System;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace RetrieveTranslationPlugins.Utils
{
    static class LanguageRetriever
    {
        /// <summary>
        /// Retrieves the language code for the specified user.
        /// </summary>
        /// <param name="orgService">The org service.</param>
        /// <param name="userId">The user id.</param>
        /// <returns></returns>
        public static Int32 RetrieveLanguageForUser(
            this IOrganizationService orgService, Guid userId)
        {
            // 1036 value refers to French_Standard
            // according to http://msdn.microsoft.com/en-us/goglobal/bb895996
            // Some others value:
            // English_United_States : 1033
            // Dutch_Standard : 1043

            // Prepare a new request with this user's id
            RetrieveUserSettingsSystemUserRequest request =
                new RetrieveUserSettingsSystemUserRequest();
            request.EntityId = userId;

            // Ask for the uilanguageid attribute
            ColumnSet columns = new ColumnSet();
            columns.AddColumn("uilanguageid");
            request.ColumnSet = columns;

            RetrieveUserSettingsSystemUserResponse response =
                (RetrieveUserSettingsSystemUserResponse)orgService.Execute(request);

            Entity objUserSettings = (Entity)response.Entity;

            return (Int32)objUserSettings.Attributes["uilanguageid"];
        }
    }
}

RetrieveLanguageForUser is an extension method that will return the language code for the user with the specified GUID. If you don't really know what is an extension method, you can have a look at this post : Les méthodes d'extension en C#.

In the plugin, we will add this test at the end of the Executre method:

RetrievePlugin.cs
// the factory will allow us to create a new IOrganizationService
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)
    serviceProvider.GetService(typeof(IOrganizationServiceFactory));
// Creates a new IOrganizationService to retrieve the user language.
IOrganizationService orgService = factory.CreateOrganizationService(context.UserId);

if (orgService.RetrieveLanguageForUser(context.UserId) != 1036) // 1036 => French
    return;

In our case, there is two language pack installed. If the language choosen by the user is not french, there isn't nothing to do because the value displayed will be the english one. But in the other case, we need to do a few more operations:

4/ Changing the values.

We will add this last instruction at the end of the Execute method:

RetrievePlugin.cs
ModifyLookUpDisplay(orgService, targetEntity, "new_disciplineref", "new_french");

Of course, you'll need to create this method. So, you will be able to re-use this method easily.

RetrievePlugin.cs
        /// <summary>
        /// Modifies the name attribute of the related entity with the specified reference to display
        /// the newField attribute of this entity instead of its initial new_name attribute.
        /// </summary>
        /// <param name="orgService">The IOrganizationService service.</param>
        /// <param name="targetEntity">The target entity.</param>
        /// <param name="reference">The reference.</param>
        /// <param name="newField">The new field.</param>
        private void ModifyLookUpDisplay(
            IOrganizationService orgService,
            Entity targetEntity,
            string reference,
            string newField)
        {
            // If this Insurance object has no reference associated with, do nothing.
            if (!targetEntity.Attributes.Contains(reference))
                return;

            EntityReference entityRef = (targetEntity.Attributes[reference] as EntityReference);

            if (entityRef == null)
                return;

            ColumnSet columns = new ColumnSet();
            columns.AddColumn(newField);

            Entity resultEntity = orgService.Retrieve(entityRef.LogicalName, entityRef.Id, columns);

            entityRef.Name = resultEntity.Attributes[newField] as string;
        }

It's also very simple: we check that the entity targetEntity has an attribute corresponding to the reference parameter.  If it does, we will replace the name value of the reference by the attribute that has the name given in the newField parameter.

So, if the targetEntity has an attribute with the name of the reference parameter, we check if its type is EntityReference. An EntityReference is an object that represents a relationship and that can be displayed in a lookup field. Its main properties are :

  • LogicalName: the real type of the reference. In this case, the LogicalName will be new_disicpline, because the EntityReference is a reference to a discipline record.
  • Id: the GUID of the referenced entity, which is necessary to retrieve the entity in the organization.
  • Name: a name that represents the entity. Basically, it is the primary field of the referenced entity. In our example, the primary field is new_name. So the Name attribute of this EntityReference object will be set to the value of the new_name attribute of the referenced discipline. This is the value that will be displayed on the form!

Now, we can create a new request to retrieve the new_french attribute of the referenced discipline. This value will replace the actual Name value. To achieve this, we use the Retrieve method of the IOrganizationService that requires three parameters: the logical name of the researched record, its GUID, and the attributes that we are looking for.

The result is an Entity object. It has an Attributes property, which is a Dictionary<string, object>. It will contains the new_french attribute of our discipline record!

After the plugin has been executed, the entity contained in the outputParameters of the IPluginExecutionContext has been modified. And this is this modified object that will be send to the student form.

 3/ Register your plugin.

To finish this lab, you just need to register your plugin using the Plugin Registration Tool which can be found in the CRM 2011 SDK. Register it for the Retrieve message of the new_student entity. If you don't know how to register a plugin, you can read my previous post: [CRM 2011] Register your plugin using the Plugin Registration Tool. Now, go back to your organization, and open the form of the student you have created before. The discpline name is now in french!

Yeah, it's finished! :)

Here you can donwload the complete sources:

Visual Studio 2010 project

CRM 2011 Solution, with entities + assembly + plugin

If you want to import the solution, don't forget to install the language pack first (otherwise the translations won't be imported), and check the following checkbox to activate message processing! 

A few words...

If you have looked at the solution carefuly, you should have noticed that we have missed something:  When you have opened a student form, if you change the discipline, it will set the name back to the english value! This plugin only works with the retrieve message, when you open a student form...  But don't worry, there is also a solution for this ! We can fix this issue with another plugin :) And actually the solution can be found in this post: [CRM 2011] Change the attribute displayed in a lookup field [part 2/2]. The english version is coming soon!

[CRM 2011] Modifier l'affichage d'un champs lookup [partie 1/2]

14. September 2011 10:09 by Renaud in   //  Tags:   //   Comments (2)

Dans Microsoft Dynamics CRM 2011, les lookup fields des EntityReference affichent toujours le champs primaire de cette entité. Ce comportement est généralement suffisant, mais il arrive qu'on veuille afficher une autre propriété qui soit peut-être plus explicite, ou plus appropriée à une situation. Prenons un exemple simple:

Chaque étudiant peut s'inscrire dans une discipline. Une discipline possède deux champs requis, qui correspondent au nom du cours en anglais (new_name) et en français (new_french). Lorsque l'on se trouve sur la fiche d'un étudiant, on aimerait que le champs affichant la discipline liée à l'étudiant reprenne la valeur correspondant à la langue de l'utilisateur connecté! Mais CRM ne permet pas de réaliser simplement ce mapping... Dans cet article, nous allons voir comment obtenir le résultat suivant en développant un plugin pour CRM:

Interface en anglais

Interface en français

[caption id="attachment_465" align="aligncenter" width="322" caption="Propriété "new_name" de l'entité new_discipline"][/caption] [caption id="attachment_466" align="aligncenter" width="322" caption="Propriété "new_french" de l'entité new_discipline"][/caption]

Pour passer l'intro et aller directement à la solution, cliquez ici! :)

Mais reprenons du début:

Pour bien suivre et développer dans les mêmes conditions que moi, vous pouvez téléchargez cette solution pour CRM 2011. Elle contient simplement les entités décrites en début d'article. [caption id="attachment_126" align="aligncenter" width="79" caption="Starter kit"][/caption]

Dans cet exemple, la valeur affichée dans le champs "Discipline" n'est pas un nom propre comme pour le champs "Owner" mais un nom commun. La différence entre ces deux champs est que le champs Discipline nécessite une traduction pour s'adapter à la langue choisie par l'utilisateur.

Voyons ce qu'il se passe si nous décidons d'installer une seconde langue (au hasard, le français :) ).

[caption id="attachment_449" align="aligncenter" width="510" caption="Pour installer un nouveau language pack: Settings > Administration > Languages"][/caption]

(Notons au passage le "language code" correspondant au français: 1036)

Une fois la langue installée, et après avoir importé quelques traduction pour les entités custom (je vous passe cette étape, je n'ai fait que le minimum), l'utilisateur peut choisir d'afficher son interface en français. En retournant sur la vue d'un étudiant, on s'aperçoit que le champs Discipline, qui s'intitule désormais Courscontient toujours la valeur Computer science.

[caption id="" align="aligncenter" width="875" caption="Le terme "Computer science" ne correspond pas à la langue choisie par l'utilisateur. Problem?"][/caption]

Pourtant, notre utilisateur aimerait que son interface soit entièrement en français. Aussi étonnant que cela puisse paraître, Microsoft Dynamics CRM 2011 ne permet pas de mapper conditionnellement ce champs sur une autre propriété de l'entité new_discipline. Exemple:

Si langue = English,
     afficher la propriété new_name.
Sinon, si langue = Français,
     afficher la propriété new_french.

Et pourtant, cette valeur n'est là qu'à titre indicative, et elle n'a aucune influence sur le comportement de CRM. Si on regarde le code html où se trouve le terme "Computer Science", on peut voir ceci:

<SPAN oncontextmenu=handleGridRightClick() class=ms-crm-Lookup-Item
title="Computer science" contentEditable=false
onclick=openlui() otypename="new_discipline" otype="10010"
oid="{68F3C5A6-66DA-E011-AE41-000C29CE1C0D}">
     <IMG class=ms-crm-Lookup-Item alt=""
     src="/Contoso4/_Common/icon.aspx?cache=1&amp;iconType=GridIcon&amp;objectTypeCode=10010">
     Computer science
</SPAN>

[caption id="attachment_451" align="aligncenter" width="198" caption="La balise span correspond à l'encadré bleu"][/caption]

L'attribut oid représente le GUID de notre objet, et otypename son type: new_discipline. Ces deux valeurs suffisent pour retrouver un objet. D'ailleurs si l'on clique sur le champs pour éditer l'entité new_discipline, on peut voir dans la barre supérieur de la fenêtre l'URL générée:

L'attribut title n'apparaît évidemment nul part. Il n'est donc pas déterminant pour cette opération. Ca veut dire qu'on peut se permettre de le modifier à notre guise pour afficher ce qui nous convient le mieux :)

Quelle type de solution choisir?

Tout d'abord, quelles sont les possibilités que nous avons pour agir sur le contenu de cette fenêtre?

  •  Utiliser du javascript côté client pour lancer une requête en ajax basée sur les attributs otypename et oid trouvés dans la balise span.
    • Avantages: ne surcharge pas le serveur, même si beaucoup d'utilisateurs sont connectés.
    • Inconvénients: doit effectuer une seconde requête après avoir reçu les premières données. Double le temps d'affichage.
  •  Utiliser un plugin côté serveur pour modifier le résultat de la requête et donc les données qui vont être affichées.
    • Avantages: exécuté sur le serveur avant de renvoyer les résultats => pas de double requête web!
    • Inconvénients: augmentation de la charge de travail pour le serveur.

A priori, c'est évident que l'expérience utilisateur sera meilleure avec une solution utilisant un plugin. L'impact sur les performances se fera beaucoup moins ressentir puisqu'avec le javascript le temps d'affichage sera directement doublé. Va pour le plugin donc! :)

Adapter l'affichage à l'aide d'un plugin

La solution que l'on va développer est en fait assez simple. Nous allons créer un plugin déclenché par le message Retrieve de l'entité Student (new_student), pour modifier quelque peu le résultat de la requête.

J'espère que vous avez lu mes précédents posts si vous n'avez encore jamais créé de plugin pour CRM 2011. Ca pourrait vous éclairer un peu ;)

1/ Création de la solution.

... de type Class Library. Je l'appelle tout simplement Plugins, car elle contiendra l'ensemble des plugins dédiés à mon organisation.

On peut directement ajouter les références suivantes (SDKbin):

  • Microsoft.Crm.Sdk.Proxy.dll
  • Microsoft.Xrm.Sdk.dll
  • System.Runtime.Serialization

J'y ajoute également une première classe RetrieveStudent.cs, héritant de l'interface IPlugin.

RetrieveStudent.cs
using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
namespace Plugins
{
    public class RetrieveStudent: IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            throw new NotImplementedException();
        }
    }
}

2/ Vérification du contexte.

Commençons par récupérer le contexte et vérifier que nous sommes bien dans le cas qui nous intéresse. Ce code est à ajouter dans la méthode Execute.

RetrieveStudent.cs
// get the context
IPluginExecutionContext context = (IPluginExecutionContext)
    serviceProvider.GetService(typeof(IPluginExecutionContext));

Entity targetEntity;

// Check if the input parameters property bag contains a target
// of the retrieve operation and that target is of type Entity.
if (context.OutputParameters.Contains("BusinessEntity") &&
    context.OutputParameters["BusinessEntity"] is Entity)
{
    // Obtain the target entity from the output parmameters.
    targetEntity = (Entity)context.OutputParameters["BusinessEntity"];
    // check if the entity is type of targetEntityName!
    if (targetEntity.LogicalName != "new_student")
        return;
}
else
{
    return;
}

3/ Vérification de la langue.

Maintenant, il faut checker la langue choisie par l'utilisateur. On va créer une classe statique PluginHelper dans laquelle on mettra les méthodes qui pourront servir à différents plugins.

PluginHelper.cs
using System;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace RetrieveTranslationPlugins.Utils
{
    static class LanguageRetriever
    {
        /// <summary>
        /// Retrieves the language code for the specified user.
        /// </summary>
        /// <param name="orgService">The org service.</param>
        /// <param name="userId">The user id.</param>
        /// <returns></returns>
        public static Int32 RetrieveLanguageForUser(
            this IOrganizationService orgService, Guid userId)
        {
            // 1036 value refers to French_Standard
            // according to http://msdn.microsoft.com/en-us/goglobal/bb895996
            // Some others value:
            // English_United_States : 1033
            // Dutch_Standard : 1043

            // Prepare a new request with this user's id
            RetrieveUserSettingsSystemUserRequest request =
                new RetrieveUserSettingsSystemUserRequest();
            request.EntityId = userId;

            // Ask for the uilanguageid attribute
            ColumnSet columns = new ColumnSet();
            columns.AddColumn("uilanguageid");
            request.ColumnSet = columns;

            RetrieveUserSettingsSystemUserResponse response =
                (RetrieveUserSettingsSystemUserResponse)orgService.Execute(request);

            Entity objUserSettings = (Entity)response.Entity;

            return (Int32)objUserSettings.Attributes["uilanguageid"];
        }
    }
}

RetrieveLanguageForUser est une méthode d'extension qui va retourner le language code de l'utilsateur dont l'id est passé en paramètre. Si vous ne savez pas ce qu'est une méthode d'extension, prenez deux secondes pour lire ceci: Les méthodes d'extension en C#.

Dans le plugin, nous allons ajouter ce test à la fin de la méthode Execute:

RetrievePlugin.cs
// the factory will allow us to create a new IOrganizationService
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)
    serviceProvider.GetService(typeof(IOrganizationServiceFactory));
// Creates a new IOrganizationService to retrieve the user language.
IOrganizationService orgService = factory.CreateOrganizationService(context.UserId);

if (orgService.RetrieveLanguageForUser(context.UserId) != 1036) // 1036 => French
    return;

Dans notre cas, il n'y a que deux language packs installés. Si la langue utilisée par le user n'est pas le français, alors il n'y a rien à faire. Les infos affichées dans la Form Student seront en anglais, et c'est bien ainsi. Si par contre la langue est le français, on poursuit.

4/ Modification des données.

On va ajouter cette dernière instruction à la méthode Execute.

RetrievePlugin.cs
ModifyLookUpDisplay(orgService, targetEntity, "new_disciplineref", "new_french");

Évidemment il va falloir créer cette méthode. L'intérêt est que vous pourrez l'utiliser pour d'autres champs facilement :)

RetrievePlugin.cs
        /// <summary>
        /// Modifies the name attribute of the related entity with the specified reference to display
        /// the newField attribute of this entity instead of its initial new_name attribute.
        /// </summary>
        /// <param name="orgService">The IOrganizationService service.</param>
        /// <param name="targetEntity">The target entity.</param>
        /// <param name="reference">The reference.</param>
        /// <param name="newField">The new field.</param>
        private void ModifyLookUpDisplay(
            IOrganizationService orgService,
            Entity targetEntity,
            string reference,
            string newField)
        {
            // If this Insurance object has no reference associated with, do nothing.
            if (!targetEntity.Attributes.Contains(reference))
                return;

            EntityReference entityRef = (targetEntity.Attributes[reference] as EntityReference);

            if (entityRef == null)
                return;

            ColumnSet columns = new ColumnSet();
            columns.AddColumn(newField);

            Entity resultEntity = orgService.Retrieve(entityRef.LogicalName, entityRef.Id, columns);

            entityRef.Name = resultEntity.Attributes[newField] as string;
        }

Le fonctionnement est assez simple. On va vérifier que l'entité targetEntity possède bien un attribut correspondant à la clé reference passée en argument. Si on ne trouve rien, il n'y a rien à faire.

On récupère ensuite la valeur de cet attribut qui devrait être de type EntityReference. Si la valeur est null ou qu'elle n'est pas de type EntityReference, on s'arrête. Un objet EntityReference est tout simplement une référence à une autre entité. Ses principales propriétés sont:

  • LogicalName: le type, en fait, de l'entitié, tel qu'indiqué dans l'organisation CRM. (new_discipline dans cet exemple)
  • Id: le GUID de l'entité, qui permet de la retrouver dans notre organisation.
  • Name: un nom attribué à cette relation, et qui est en fait la valeur qui sera affichée dans le champs Lookup et que l'on aimerait changer. Par défaut, cette propriété aura donc la valeur du primary field de l'entité à laquelle cette EntityReference fait référence! :)

On crée une nouvelle requête, pour récupérer le nouveau champs (celui en français!) qui remplacera la valeur actuelle de Name. Pour cela on utilise la méthode Retrieve de IOrganizationService en lui passant en paramètres le nom de l'entité que l'on cherche, son Id, et les noms des attributs qui nous intéressent sous la forme d'un ColumnSet. Les deux premiers paramètres proviennent de l'EntityReference récupéré juste avant.

Le résultat de cette requête est un objet de type Entity qui aura un attribut avec la clé newField! Il suffit de substituer cette valeur à l'ancienne, la version anglaise.

Avec ce code, l'attribut Name de la référence à l'objet new_discipline sera à chaque fois mis à jour avec sa valeur française si la langue de l'utilisateur est le français!

 3/ Enregistrement du plugin.

Il ne reste plus qu'à signer l'assembly, à l'enregistrer à l'aide du Plugin Registration Tool, et à attacher le plugin au message Retrieve de l'entitié new_student! [CRM 2011] Enregistrer un plugin à l’aide du Plugin Registration Tool.

Ouf, enfin fini! :)

Avouez que ce n'était pas si compliqué que ça en fin de compte. On se demande juste pourquoi ce n'est pas directement proposé dans CRM. En attendant, voici les sources ;) ça peut toujours servir:

Le projet Visual Studio.

La solution CRM 2011, avec entités + assembly + plugin

Si vous importez la solution, n'oubliez pas de d'abord installer le language pack français (sans quoi les traductions ne seront pas importées), et de cocher la case d'activation des messages :

Après-propos...

Il n'aura pas échappé aux plus attentifs d'entre vous que cet article est estampillé Partie 1 sur 2! En vérité, si vous avez un peu testé le résultat, vous avez dû remarquer qu'il manque encore un petit quelque chose. Lorsque vous ouvrez la Form Student, la langue s'affiche correctement. Mais si vous cliquez sur le champs de recherche d'un cours, que vous choisissez un nouveau cours dans la fenêtre de sélection, et que vous cliquez sur Ok, vous retrouverez de nouveau le terme anglais! Aaah, mais pas de panique! Tout ça peut se régler très simplement... avec un autre plugin (le dernier, promis :) ), et un autre article!

 

[CRM 2011] Register a plugin with the Plugin Registration Tool

12. September 2011 17:05 by Renaud in   //  Tags:   //   Comments (1)

If you have already started developping your plugin, maybe you would like to add it to your CRM application to use it... This is actually quite simple. There are two main steps: first, register the dll containing your plugin and then, register a step for the plugin.

To achieve this, you just have to use the Plugin Registration Tool provided by Microsoft in the CRM 2011 SDK that you should already have downloaded!

This tool is located in the folder /tools/pluginregistration/ and consists of a Visual Studio solution. Let's open it and launch the tool:

In the Discovery URL field, type the address of your server, without forgetting the port number if any. Then, enter your username with domain.

A prompt dialog will ask you for credentials.

After what you'll see the list of organizations existing on your server. By the way, you can create new organizations with the Dynamics CRM Deployment Manager if you want to try one of your plugins in a separate environment.

Select the right organization and start registering a new assembly :

Step 1: Register an assembly

In this first step, you need to locate your .dll library containing the plugin you want to register. An assembly can contain more than one plugin at the same time. You can check the box in front of each plugin you want to register.

In CRM On-premise, you can choose to register your plugin outside the sandbox. The sandbox is a confined place that reduces functionalities available to your plugin, but it's more secure. In CRM Online, you have to register plugins into the sandbox.

Which location for my assembly?

In the database: the interest of this solution is that your assembly is available for all the servers that use the database. You only have to deplay once. It could be comfortable, but each time you have to update your assembly, you will have to use the Plugin Registration Tool again. Remind that once the assembly is loaded from the database to the server, it will be cached.

On the hard disk: this solution consists in registring the assembly from a location on your disk. If you want to debug, you'll also have to put the .pdb file into the serverbinassembly folder of your CRM 2011 installation. You'll need to do this for every installation, but in a development environment, this solution is better because you only have to register your assembly once, and then you just have to replace the .dll file to deploy your new assembly.

GAC: I've never used this option :)

Finally, you can click on the "Register Selected Plugins" button!

Step 2:  Register a new step

Now that your assembly is registered in your CRM 2011 installation, your plugins are available in the tree view on the right. You just need to register a new step to say when the plugin has to be exectued.

Select the plugin you need, ant start the registering a new step:

A new window will open and you can see on the left the main fields that we need to fill:

Message & Entities

The Message says what type of event you want to listen to. You should already have heard about messages if you have read my previous article about how to create your first plugin (création d'un premier plugin pour CRM 2011 (sorry, french only)) ! In this article you'll also find the complete list of messages available in CRM 2011. Here is an extract of the original post:

The Retrieve message is sent when you try to retrieve data about one specific record. It happens when you open the form of a entity. The RetrieveMultiple message is sent when you retrieve data about a collection. It happens each time you see a grid view!

Those two messages requests you to fill the Primary Entity field. For example, if you want to execute a plugin each time you open a Contact form, you just have to use the Retrieve message of the contact entity.

Context

The choice you make here will be an impact on the IPluginExecutionContext in your plugin. Let the Calling User option selected to have informations about the logged on user.

Eventing Pipeline Stage of Execution

This option allows you to tell when the plugin must be executed. There are two important choices: Pre-operation, and Post-operation. If you choose Pre-operation, it means that you plugin will be executed before the message is actually processed. With the Post-operation it will be executed after. To illustrate the consequences, just imagine you have an Account, and you want to modify one of its field each time the record is updated. You should use the Update message of the Account entity, but then if you choose Post-operation, the record will be updated, and only after this you will modify the fields you want to. If you don't save the changes yourselves here, nothing will happend, and you modifications will be lost. But if you choose the Pre-operation, you'll be able to set up the fields as you want, and then it will normally be saved while processing the Update operation. The execution of the plugin will happen in the database transaction. So if your plugin launches an exception, there will be a rollback :)

Execution Mode

If it's not necessary to wait until the plugin is entirely executed, you can use Asynchronous execution mode. That's all! You can now test your plugin directly on your CRM application :)

[CRM 2011] Enregistrer un plugin à l'aide du Plugin Registration Tool

12. September 2011 10:09 by Renaud in   //  Tags:   //   Comments (0)

Si vous avez commencé à développer un plugin, vous voulez certainement pouvoir l'importer dans votre solution CRM. Cela se passe en deux étapes: enregistrer la dll contenant votre plugin, et enregistrer une step pour l'un des plugins. Pour ça, rien de plus simple: utilisez le Plugin Registration Tool mis à disposition dans le SDK que vous devriez déjà avoir téléchargé!

Cet outil se trouve dans le dossier /tools/pluginregistration/ sous forme d'une solution Visual Studio. Ouvrez-la et lancez l'exécution du projet.

Dans Discovery URL, entrez l'adresse du serveur, sans oublier le port si nécessaire. Entrez ensuite l'utilisateur dans le champs User Name ainsi que le domaine.

Une boite de dialogue vous demandera votre mot de passe une première fois:

Vous verrez alors apparaître la liste des organisations découvertes. (Vous pouvez créer de nouvelles organisations pour tester vos plugins via le Deployment Manager sur votre serveur.)

Sélectionnez l'organisation qui vous intéresse et commencez l'enregistrement d'une assembly (ctrl + A):

Etape 1: Enregistrer l'assembly

Dans cette première étape, il faut aller rechercher la .dll contenant votre plugin. Comme je vous l'avais déjà signalé dans un précédent post, une assembly peut contenir plusieurs plugins. Vous pouvez alors sélectionner uniquement les plugins qui vous intéressent parmis ceux découverts.

Dans CRM On-premise, vous pouvez choisir ou non d'exécuter votre plugin dans une Sandbox (espace confiné qui permettra de garantir la sécurité de votre organisation mais réduira les fonctionnalités accessibles par le plugin).

Où stocker  l'assembly?

Dans la base de données: l'intérêt de cette solution est que l'assembly est disponnible pour tous les serveurs utilisant votre base de données, ce qui ne nécessite qu'un déploiement. Cela peut être pratique en production, mais à chaque nouvelle version de votre assembly, vous devrez passer par le Plugin Registration Tool pour mettre à jour votre base de données. Notez aussi qu'une fois l'assembly chargée dans l'application CRM, elle est mise en cache. Le fait qu'il y ait 1000 ou 10000 utilisateurs n'aura donc pas d'influence sur les performances de votre database.

Sur le disque: cette solution consiste à stocker l'assembly dans le dossier serverbinassembly de votre installation CRM. Cela nécessite de répéter la procédure pour chaque server, mais reste une solution préférable durant le développement puisque vous n'aurez qu'à faire l'enregistrement une première fois et vous pourrez ensuite remplacer votre assembly par une nouvelle version directement en écrasant la .dll directement dans le dossier.

GAC: à priori cette solution est similaire à l'option du disque, mais je ne l'ai jamais utilisée.

Vous pouvez finalement cliquer sur "Register Selected Plugins" !

Etape 2:  S'abonner à un évènement

Maintenant que l'assembly est enregistrée auprès de CRM, vos plugins sont disponnibles. Il ne reste plus qu'à préciser à quel moment le plugin doit être exécuté!

Dans le fenêtre principale, sélectionnez le plugin qui vous intéresse, et commencez l'enregistrement d'une nouvelle étape:

Une nouvelle fenêtre s'ouvre, et les champs qui nous intéressent principalement se trouvent sur la gauche: General Configuration Information.

Message & Entities

Le Message indique le type d’évènement à écouter. Vous devriez déjà en avoir entendu parler si vous avez lu mon précédent article sur la création d'un premier plugin pour CRM 2011! (Vous y trouverez également la liste complète des messages de bases disponibles dans CRM 2011. ) Si vous avez la flemme de cliquer ou que l'article ne vous intéresse pas, je vous remets ici l'extrait intéressant :)

Le message Retrieve est envoyé lorsque l’on essaie de récupérer des données sur UNE entitié. C’est ce qui se passe à chaque fois que l’on ouvre la Form d’une entité. Le RetrieveMultiple intervient lorsqu’on essaie de récupérer une collection d’entité. C’est le cas lorsque l’on affiche les lookup views qui présentent des gridview.

Ces deux messages nécessitent de préciser l'entité concernée. Par exemple, si je veux exécuter mon plugin à chaque fois que j'affiche la fenêtre de consultation/édition d'un Contact, je vais utiliser le Message Retrieve, et la Primary Entity Contact. Notez que vous ne pouvez pas faire d'associations incohérentes. Les champs Primary Entity et Secondary Entity sont des text box autocomplete qui vous proposeront uniquement les entités pour lesquelles le message sélectionné à du sens! J'ajouterai pour l'exemple les messages SetRelated et RemoveRelated qui eux tiennent comptent de deux entitiés. Par exemple, la combinaison SetRelated, LeadAccount aura pour effet d'exécuter votre plugin lorsqu'un relation sera créée entre une entité Lead et une entité Account! Plutôt simple, n'est-ce pas?

Context

Le choix que vous faites ici aura une influence sur le IPluginExecutionContext dans votre plugin. Laissez le choix sur Calling User pour obtenir des informations sur l'utilisateur connecté et faisant appel au plugin.

Eventing Pipeline Stage of Execution

Cette option permet d'indiquer au plugin de s'exécuter avant ou après l'opération du Message. Dans CRM 2011, les propriétés Pre-operation et Post-operation permettent d'exécuter le plugin dans le cadre de la transaction avec la base de données. Ceci permet un rollback automatique en cas d'exception non-catchée venant du plugin! Si par exemple vous voulez, à la création d'un Contact, créer une autre entité, et que cette opération échoue et lance une exception, vous n'aurez peut-être pas envie que la création du Contact soit tout de même validée. Dans CRM 2011, ces opérations seront annulées :)

Execution Mode

Et pour finir, si vous n'avez pas besoin d'attendre que votre plugin soit entièrement exécuté avant de traiter le message ou d'afficher le résultat à l'utilisateur, pensez à utiliser le mode asynchrone! Cela améliorera l'expérience utilisateur. Dans le cas contraire, laissez l'option sélectionnée!

C'est tout!

Vous pouvez maintenant tester vos plugins directement dans votre application CRM 2011 :)

[CRM 2011] Créer son premier plugin

10. September 2011 01:09 by Renaud in   //  Tags:   //   Comments (2)

Dans Microsoft Dynamics CRM 2011, vous pouvez facilement améliorer l'expérience utilisateur. Cela est possible de plusieurs manières: en utilisant du javascript, en ajoutant des workflows, etc...

Une autre possibilité consiste en la création d'un plugin en C#utilisant le SDK CRM 2011. J'vais vous présenter ici les bases de ce qu'il faut comprendre pour se lancer dans la création d'un plugin. Tout ça est expliqué dans le training kit mis à disposition par Microsoft, mais honnêtement cela ne m'avait pas semblé très clair jusqu'à ce que je puisse le mettre en pratique dans un vrai projet.

Concrètement, pour faire un plugin, que faut-il savoir?

Pour faire un plugin, il faut créer un projet de type Class Library. Ce projet pourra contenir l'ensemble de vos plugins. En effet il n'est pas nécessaire de faire une assembly par plugin. Cela vous permettra également de développer des classes utilitaires disponibles pour l'ensemble de vos plugins!

Créez une première classe:

namespace MyProject.Plugins{

     public class MyPlugin{

     }
}

et ajoutez-y les namespaces suivants:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

Les dlls se trouvent dans le dossier SDKbin du SDK CRM 2011 que vous avez dû télécharger :) Pour créer un plugin, il faut que votre classe implémente l'interface Microsoft.Xrm.Sdk.IPlugin. Cette interface ne contient qu'une seule méthode, et c'est ici que l'essentiel de l'action va se passer!

    public class MyPlugin: IPlugin
    {
        public void Execute(IServiceProvider serviceProvider){

        }
    }

 Commencez par récupérer les objets suivants grâce au serviceProvider passé en attribut:  

// get the required interfaces from the service provider
// the context will provide the user's id
IPluginExecutionContext context =(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// the factory will allow us to create a new IOrganizationService
IOrganizationServiceFactory factory =(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
// Creates a new IOrganizationService to interact with objects
IOrganizationService orgService = factory.CreateOrganizationService(context.UserId);

Comme indiqué dans les commentaires, le IPluginExecutionContext permet entre autre de connaître l'Id de l'utilisateur connecté. Ceci permet de savoir qui fait appel à ce service. Le context contient bien sûr d'autres informations sur l'organisation, mais aussi des infos précieuses telles que le message qui a déclenché l'appel de ce plugin, l'entité primaire et secondaire liées à ce message, etc. Je vais vous expliquer à quoi tout cela correspond dans quelques instants :)

L'objet IOrganizationServiceFactory permet de créer une nouvelle instance d'IOrganizationService. C'est cet objet, orgservice, qui va nous permettre de faire les opérations classiques (Create, Read, Update, Delete) sur les entités contenues dans notre organisation!

Il faut savoir que cet objet utilise le late-binding, ce qui signifie que vous ne travaillerez pas avec des entités fortement typées. Cela a pour inconvénient de laisser passer certaines erreurs qui auraient pu être détectées au moment de la compilation! Nous verrons un exemple par la suite, et je vous montrerai comment utiliser le early-binding dans un prochain post!

A quoi va servir le plugin?

En voilà un bonne question. On peut faire quoi exactement avec un plugin? On peut tout simplement effectuer des actions lors de certains évènements (le fameux message cité plus haut). Comme je suis super sympa j'vous met ici la liste des messages de base fournie dans le SDK :  message-entity support for plug-ins. Ne me demandez pas à quoi correspondent chacun d'entre eux, parce qu'à vrai dire je n'en ai utilisé que deux pour le moment: Retrieve et RetrieveMultiple.

Le message Retrieve est envoyé lorsque l'on essaie de récupérer des données sur UNE entitié. C'est ce qui se passe à chaque fois que l'on ouvre la Form d'une entité.

Le RetrieveMultiple intervient lorsqu'on essaie de récupérer une collection d'entité. C'est le cas lorsque l'on affiche les lookup views qui présentent des gridview.

Lorsque l'on crée un plugin et qu'on l'enregistre dans CRM, il faut l'attacher à l'un de ces messages. On va alors généralement lier ces messages à une entité, parfois deux. Par exemple, j'ai ajouté une entité Profile à mon organisation, et je souhaite pour chaque entité Profil connaître le nombre de fois qu'il a été affiché, et par quels utilisateurs. (Je sais que c'est le rêve de beaucoup d'entre vous de savoir qui regarde votre profil :P ) Pour arriver à mes fins je vais utiliser le message Retrieve avec comme entité primaire Profile.

Input and Output parameters.

Dans votre objet IPluginExecutionContext récupéré plus haut, vous trouverez également deux dictionnary: InputParameters et OutputParameters. L'InputParameters contient les paramètres envoyés par le message déclenché. L'OutputParameters va contenir les résultats de la requête liée au message, après que la requête ait été effectuée.

Pre-operation ou Post-operation?

Si vous enregistrez votre plugin en pre-operation, cela signifie que le plugin sera appelé avant que le message ne soit traîté. Le dictionnary OutputParameters sera donc encore vide. Par contre, si vous enregistré en Post-opération, il pourra contenir différentes valeur en fonction du message:

Dans le cas d'un Retrieve:

            Entity entity;
            // Check if the input parameters property bag contains a target
            // of the retrieve operation and that this target is of type Entity.
            if (context.OutputParameters.Contains("BusinessEntity") &&
              context.OutputParameters["BusinessEntity"] is Entity)
            {
                // Obtain the target business entity from the output parmameters.
                entity = (Entity)context.OutputParameters["BusinessEntity"];
                if(entity.LogicalName != "new_profile")
                    return;
            }
            else
            {
                return;
            }

On vérifie que le dictionnary contient une clé BusinessEntity et que la valeur associée est bien de type Entity. Par la suite, on teste le LogicalName de l'entity. Ainsi, dans cet exemple, la suite de la méthode ne sera exécutée que dans le cas où le LogicalName est égal à "new_profile". Cela permet de s'assurer de ne pas déclencher d'exception dans le cas où le plugin serait par erreur enregistré avec le message Retrieve d'une autre entitié.

Entity est un type utilisant le late-binding pouvant représenter n'importe quelle entité dans CRM. Pour savoir exactement de quelle entité il s'agit, on peut utiliser la propriété LogicalName qui retourne le nom d'entitié attribué dans CRM. Pour interagir avec les propriétés de votre entité, utilisez la propriété Attributes.

Vous pourriez par exemple imaginer modifier le titre du profil comme ceci:

entity.Attributes["new_title"] = "Mon profil, lol!";

Attention tout de même: si au moment de l'exécution, il s'avère que l'attribut new_title n'existe pas, une exception sera lancée et empêchera l'affichage correct de votre page.

Dans le cas du RetrieveMultiple, l'opération est quasi similaire, si ce n'est que la propriété OutputParameters contiendra un objet de type EntityCollection avec la clé BusinessEntityCollection.

Récapitulatif:

Pour créer un plugin il faut savoir quel message veut-on intercepter et pour quelle entité. Ensuite on crée une classe implémentant l'interface IPlugin de l'asssembly Microsoft.Xrm.Sdk et on écrit la logique du plugin dans l'implémentation de la méthode Execute(IServiceProvider serviceProvider).

On peut alors utiliser le serviceProvider pour récupérer les services nécessaires à l'identification de l'utilisateur (IPluginExcecutionContext) et à la manipulation des données (IOrganizationService). Et finalement on peut modifier le contenu de la propriété OutputParameters du contexte d'exécution pour par exemple changer les données qui seront affichées dans une pages (mais ceci doit faire l'objet d'un autre article ;) )

Et la suite? :)

Jusqu'à présent vous n'avez toujours pas de plugin fonctionnel dans votre application CRM! Mais ne vous inquiétez pas, ça va venir. Je vais m'arrêter là pour cet article, mais je vous raconterai la suite dans les prochains jours.

J'ai prévu de rédiger une série d'articles: comment enregistrer un plugin dans une organisation, comment débugger un plugin enregistré dans CRM avec Visual Studio 2010, comment utiliser le early-binding dans vos plugins, et comment réaliser des forms dont les champs s'adaptent à la langue de l'UI choisie par l'utilisateur (partie 1 / partie 2). (Looong weekend en perspective :) )

TextBox

About the author

I'm a developer, blog writer, and author, mainly focused on Microsoft technologies (but not only Smile). I'm Microsoft MVP Client Development since July 2013.

Microsoft Certified Professional

I'm currently working as an IT Evangelist with an awesome team at the Microsoft Innovation Center Belgique, where I spend time and energy helping people to develop their projects. I also give training to enthusiastic developers and organize afterworks with the help of the Belgian community.

MIC Belgique

Take a look at my first book (french only): Développez en HTML 5 pour Windows 8

Développez en HTML5 pour Windows 8

Membre de l'association Fier d'être développeur

TextBox

Month List