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 :) )

[CRM 2011] Plugin registration tool - Assembly "Microsoft.IdentityModel" missing

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

En copiant le SDK de MS CRM depuis ma machine virtuelle vers mon bureau, j'ai rencontré un problème en voulant enregistrer un nouveau plugin sur le serveur CRM à l'aide du Plugin Registration Tool fourni avec le SDK (SDK/tools/pluginregistrationtool). Lors de la connexion au serveur, une exception a été lancée disant qu'il manquait une référence à l'assembly Microsoft.IdentityModel.

“Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly ‘Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, [...]″

La solution à mon problème a été trouvée sur ce post écrit par Jonathan Nachman. Le problème vient du fait qu'il faut installer Windows Identity Foundation, que vous pouvez trouver ici, et qui est utilisé dans CRM 2011 pour l'authentification. Pour Windows 7 et Windows Server 2008 R2, prenez la version 6.1. Pour Vista et Windows Server 2008, il faut la version 6.0.

Après ça mon problème n'était toujours pas résolu. J'ai certainement dû supprimer la référence à Microsoft.IdentityModel en chipotant avant de tomber sur le post précédent :)

Assembly "Microsoft.IdentityModel" manquante!

Il faut donc de nouveau ajouter la référence au projet! Mais après avoir installé Windows Identity Foundation, il se peut que l'assembly n'apparaisse pas dans la fenêtre d'ajout de référence: (System.IdentityModel != Microsoft.IdentityModel)

 

L'assembly "Microsoft.IdentityModel" n'es pas présente dans la liste.

 

Alors juste à titre informatif, vous trouverez la dll ici:

C:/Program Files/Reference Assemblies/Microsoft/Windows Identity Foundation/v3.5/Microsoft.IdentityModel.dll

[C#] Le piège des paramètres nommés et optionnels

8. August 2011 07:08 by Renaud in   //  Tags:   //   Comments (2)

Dans cet article, je vais vous montrer un petit bout de code utilisant les paramètres nommés et optionnels. Ce code est très simple mais il risque fort de vous embrouiller. Et c'est normal, tout y est fait pour!

Si vous êtes encore étudiant, c'est bien le genre de truc vicieux qu'on pourrait vous demander lors d'un test :)

using System;
class Base {
    public virtual void Foo(int x = 4, int y = 5) {
        Console.WriteLine("x:{0}, y:{1}", x, y);
    }
}

class Derived : Base {
    public override void Foo(int y = 4, int x = 5) {
        Console.WriteLine("x:{0}, y:{1}", x, y);
    }
}

class Program {
    static void Main(string[] args) {
        Base b = new Derived();
        b.Foo(y: 5, x: 4);
    }
}
Quel sera le résultat affiché? x:4, y:4 x:4, y:5 x:5, y:4 x:5, y:5

Pendant que vous réfléchissez, je vous explique ce code plus en détail...

Il y a plusieurs choses à remarquer ici:

Les paramètres optionnels: depuis C# 4.0, il est possible de préciser une valeur par défaut pour les paramètres d'une méthode ou d'un constructeur. Si parmi les paramètres, certains n'ont pas de valeur par défaut, il doivent être placés avant les paramètres optionnels de sorte que le compilateur puissé déterminer quelle méthode est appelée sans ambiguïté! Cela permet d'écrire un peu moins de code, comme dans cet exemple:

        public Personne(string nom = "Sans nom", int age = "18"){
            Nom = nom;
            Age = age;
        }

Sans les paramètres optionnels, ce constructeur aurait été écrit avec trois constructeurs:

        public Personne() : this("Sans nom", 18) { }

        public Personne(string nom) : this(nom, 18) { }

        public Personne(string nom, int age){
            Nom = nom;
            Age = age;
        }

Les paramètres nommés: également depuis C#4.0, il est permis de placer des paramètres dans n'importe quel ordre en les nommant. Dans le premier morceau de code, on passe d'abord la valeur de y, et ensuite la valeur de x!

b.Foo(y: 5, x: 4);

La surcharge de méthode: la méthode Foo est virtual dans la classe Base, ce qui permet de la surcharger dans les sous-classe grâce au mot-clé override.

Mais revenons à notre problème:

        Base b = new Derived();
        b.Foo(y: 5, x: 4);

Puisqu'on a surchargé la méthode Foo dans la classe Derived, le polymorphisme va faire que c'est celle-là qui sera exécutée. Mais aussi étrange que cela puisse paraître, le résultat affiché est : x:5, y:4 On vient pourtant de préciser que x = 4 et y = 5! Qu'est-ce qu'il s'est passé alors?

L'explication est en fait très simple, et il suffit d'écrire ce code dans Visual Studio pour s'en rendre compte: si vous tapez b.Foo(, l'IntelliSense vous affiche la signature de la méthode de la classe Base. Cela est bien normal puisqu'on l'appel sur un objet b de type Base.

Lorsqu'on indique de passer  x = 4 et y = 5 à la méthode Foo, il faut donc comprendre qu'on ne parle pas des x et y de la méthode  void Foo(int y, int x) de la classe Derived mais bien de la classe Base! Evidemment, pour rendre la chose plus confuse, les noms ont été inversés dans la surcharge, mais nous aurions pu utiliser d'autres noms: Derived.Foo(int premier, int second).

La correspondance entre arguments d'une méthode virtual et une méthode surchargée se fait sur base de l'ordre ( comme le montre cet incroyable schéma :) ) et non pas du nom des paramètres!

Dans l'exercice, on passe donc  x = 4 et y = 5, en se basant sur les paramètres de la méthode de la classe Base. Ce qui donne y=4 et x=5 dans la méthode surchargée! Voilà pourquoi le résultat de l'impression est x:5, y:4!

C'est donc là qu'est le danger, qui n'en est pas vraiment un puisque Visual Studio clarifie cette situation grâce à l'IntelliSense!

Quoi qu'il en soit, vous voilà prévenus ! :)

[Solution] WP-PageNavi ne fonctionne pas avec Platform Free Edition

7. August 2011 04:08 by Renaud in   //  Tags:   //   Comments (4)
Edit du 10 septembre 2011: fonctionne pour Platform 1.3.5
Pour ce blog, j'utilise Platform dans sa version gratuite, un thème sous licence GNU. Il me semblait à priori normal de pouvoir utiliser un plug-in du style WP-PageNavi, qui permet de faciliter la navigation en remplaçant le peu élégant "← Previous entries / Next entries →" par ceci: Mais, évidemment, ça n'aurait pas été rigolo si tout avait fonctionné du premier coup. Je ne suis pas vraiment un spécialiste de WordPress...  J'ai commencé par installer quelques plugins similaires à celui que cité plus haut avant d'admettre que je n'y arriverais pas comme ça. Quel que soit le plugin testé, rien ne se passait. Finalement j'me suis dit que le thème y était peut-être pour quelque chose. Sorry but I haven't translated this post in english yet! Anyway, you can find the solution to this problem in my comment on the following post: [resolved] Does Platform theme(free edition) support wp-pagenavi plugin?  
Edit du 10 septembre 2011: fonctionne pour Platform 1.3.5
Pour ce blog, j'utilise Platform dans sa version gratuite, un thème sous licence GNU. Il me semblait à priori normal de pouvoir utiliser un plug-in du style WP-PageNavi, qui permet de faciliter la navigation en remplaçant le peu élégant "← Previous entries / Next entries →" par ceci: Mais, évidemment, ça n'aurait pas été rigolo si tout avait fonctionné du premier coup. Je ne suis pas vraiment un spécialiste de WordPress...  J'ai commencé par installer quelques plugins similaires à celui que cité plus haut avant d'admettre que je n'y arriverais pas comme ça. Quel que soit le plugin testé, rien ne se passait. Finalement j'me suis dit que le thème y était peut-être pour quelque chose. J'ai cherché dans les réglages, et ait trouvé ceci dans la catégorie Apparence > PageLines Settings > Template Setup : Voilà ce que je cherchais, le contrôle Pagination du template Posts Page Content! C'est lui qui est responsable de l'horrible "Previous / Next" en bas de page. Je remarque également qu'en cliquant sur Show Section Descriptions, on nous assure que WP-PageNavi est bien supporté. Quelque chose ne fonctionne donc pas correctement. En lisant le readme du WP-PageNavi, j'ai appris qu'on pouvait l'installer manuellement:
In your theme, replace code like this:
<div class="navigation">
	<div class="nav-previous"><?php next_posts_link( __( '<span class="meta-nav">&larr;</span> Older posts', 'twentyten' ) ); ?></div>
	<div class="nav-next"><?php previous_posts_link( __( 'Newer posts <span class="meta-nav">&rarr;</span>', 'twentyten' ) ); ?></div>
</div>
with this:
<?php wp_pagenavi(); ?>
Tout n'est pas perdu dans ce cas :) j'ai donc googlé vite fait tout ça pour trouver quel fichier du thème Platform concerne la pagination, et je découvre le coupable ! wp-content/themes/platform/sections/wp/section.pagination.php Vous pouvez éditer ce fichier en utilisant un client FTP comme FileZilla! Ce fichier contient la classe PageLinesPagination, qui correspond au contrôle Pagination en image ci-dessus! Et voici la partie qui nous intéresse:
function section_template() { ?>
		<?php if(function_exists('wp_pagenavi') && show_posts_nav() && VPRO):?>
			<?php wp_pagenavi(); ?>
		<?php elseif (show_posts_nav()) : ?>
			<div class="page-nav-default fix">
				<span class="previous-entries"><?php next_posts_link(__('&larr; Previous Entries','pagelines')) ?></span>
				<span class="next-entries"><?php previous_posts_link(__('Next Entries &rarr;','pagelines')) ?></span>
			</div><!-- page nav -->
		<?php endif;?>

	<?php }
Remarquez le "&& VPRO" dans le premier test. Cela signifie que même si WP-PageNavi est installé, on ne l'utilisera pas dans la version free de Platform, mais à la place on utilisera la navigation classique indiquée juste en-dessous! Pour que WP-PageNavi s'exécute correctement, il suffit donc d'éditer cette ligne et de supprimer la dernière condition:
<?php if(function_exists('wp_pagenavi') && show_posts_nav()):?>
Il ne reste plus qu'à bidouiller un peu le CSS du plugin pour centrer tout ça un minimum... Le seul bémol, c'est qu'à chaque mise à jour du thème, il faudra corriger cette ligne. J'ai pas vraiment cherché plus loin pour savoir comment faire une solution permanente. Si vous avez une proposition, elle sera la bienvenue dans les commentaires!
J'sais pas si ça valait vraiment la peine d'écrire un article là-dessus, j'avoue, mais ça m'a fait plaisir de trouver la solution et de vous la faire partager! :)
J'ai cherché dans les réglages, et ait trouvé ceci dans la catégorie Apparence > PageLines Settings > Template Setup : Voilà ce que je cherchais, le contrôle Pagination du template Posts Page Content! C'est lui qui est responsable de l'horrible "Previous / Next" en bas de page. Je remarque également qu'en cliquant sur Show Section Descriptions, on nous assure que WP-PageNavi est bien supporté. Quelque chose ne fonctionne donc pas correctement. En lisant le readme du WP-PageNavi, j'ai appris qu'on pouvait l'installer manuellement:
In your theme, replace code like this:
<div class="navigation">
	<div class="nav-previous"><?php next_posts_link( __( '<span class="meta-nav">&larr;</span> Older posts', 'twentyten' ) ); ?></div>
	<div class="nav-next"><?php previous_posts_link( __( 'Newer posts <span class="meta-nav">&rarr;</span>', 'twentyten' ) ); ?></div>
</div>
with this:
<?php wp_pagenavi(); ?>
Tout n'est pas perdu dans ce cas :) j'ai donc googlé vite fait tout ça pour trouver quel fichier du thème Platform concerne la pagination, et je découvre le coupable ! wp-content/themes/platform/sections/wp/section.pagination.php Vous pouvez éditer ce fichier en utilisant un client FTP comme FileZilla! Ce fichier contient la classe PageLinesPagination, qui correspond au contrôle Pagination en image ci-dessus! Et voici la partie qui nous intéresse:
function section_template() { ?>
		<?php if(function_exists('wp_pagenavi') && show_posts_nav() && VPRO):?>
			<?php wp_pagenavi(); ?>
		<?php elseif (show_posts_nav()) : ?>
			<div class="page-nav-default fix">
				<span class="previous-entries"><?php next_posts_link(__('&larr; Previous Entries','pagelines')) ?></span>
				<span class="next-entries"><?php previous_posts_link(__('Next Entries &rarr;','pagelines')) ?></span>
			</div><!-- page nav -->
		<?php endif;?>

	<?php }
Remarquez le "&& VPRO" dans le premier test. Cela signifie que même si WP-PageNavi est installé, on ne l'utilisera pas dans la version free de Platform, mais à la place on utilisera la navigation classique indiquée juste en-dessous! Pour que WP-PageNavi s'exécute correctement, il suffit donc d'éditer cette ligne et de supprimer la dernière condition:
<?php if(function_exists('wp_pagenavi') && show_posts_nav()):?>
Il ne reste plus qu'à bidouiller un peu le CSS du plugin pour centrer tout ça un minimum... Le seul bémol, c'est qu'à chaque mise à jour du thème, il faudra corriger cette ligne. J'ai pas vraiment cherché plus loin pour savoir comment faire une solution permanente. Si vous avez une proposition, elle sera la bienvenue dans les commentaires!
J'sais pas si ça valait vraiment la peine d'écrire un article là-dessus, j'avoue, mais ça m'a fait plaisir de trouver la solution et de vous la faire partager! :)

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