Custom Field Types / Colonnes personnelles – PART 2

Dans mon post Custom Field Types - Part 1nous avons vu comment créer une colonne personnalisée très simple. Voyons maintenant comment créer une colonne personnalisée de type recherche (Custom Lookup Field Type) capable de filtrer les éléments grâce à une requête CAML. Si vous n’avez jamais développé ce type d’élément alors consultez mon premier post.

Le contexte

Voici le problème qui m’a ammené à m’intéresser aux Custom Field Types. Imaginons une liste de tâches “Affaires” et une autre liste de tâches “Actions”. Les affaires correspondent à des demandes clients et sont susceptibles de nécessiter la création de plusieurs actions afin de répondre à la demande.
Le plus simple est donc d’ajouter dans la liste Actions une colonne Affaire de type recherche. Cependant lorsque les Affaires passent en état “Terminé” il est toujours possible de créer des actions qui y sont liées. Ainsi après une période d’utilisation assez longue le nombre d’affaires est bien trop important et la création d’une action devient très fastidieuse.

Comment créer une colonne de type recherche avec un filtre sur les éléments recherchés? La solution la plus efficace est de mettre en place un Custom Lookup Field Type.

Le projet

Voici la structure du projet créé à l’aide de STSDEV (utilisez le type Empty Solution with C# assembly):

structure_projet On retrouve tous les éléments que nous avons utilisé dans l’exemple précédent:
Une classe pour notre FieldType (FilteredLookup2Field.cs).
Une classe pour le BaseFieldControl qui servira au rendu de notre colonne (FilteredLookup2Control.cs).
Le fichier XML Fldtypes qui contient la définition de notre Field Type.
Un fichier contrôle utilisateur qui sera lié à notre BaseFieldControl (FilteredLookup2Control.ascx).

Note : dans votre projet vous ne devriez pas avoir de dossier FEATURES et IMAGES ni de classe FeatureReceiver. Il s’agit ici d’une erreur de ma part lors de la création du projet.

FLDTYPES

<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">FilteredLookup2</Field>
    <Field Name="ParentType">Lookup</Field>
    <Field Name="TypeDisplayName">Filtered Lookup 2</Field>
    <Field Name="TypeShortDescription">Filtered Lookup 2</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="AllowBaseTypeRendering">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="FieldTypeClass">FilteredLookup2.FilteredLookup2Field, FilteredLookup2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4d2fbb5068cc2292</Field>
    <PropertySchema>
      <Fields>
        <Field Name="LookupList" DisplayName="Liste de recherche" MaxLength="50" DisplaySize="15" Type="Text"></Field>
        <Field Name="LookupCAMLQuery" DislayName="Requete CAML" MaxLength="500" DisplaySize="15" Type="Text"></Field>
      </Fields>
    </PropertySchema>
  </FieldType>
</FieldTypes>

Je ne reviendrai pas précisement sur le contenu de ce fichier. Seul le contenu de l’élément ParentType diffère et prend la valeur “Lookup”.
Les éléments Field qui se trouvent dans PropertySchema définissent les propriétés de la colonne qui seront demandées lors de l’ajout / édition de la colonne. Dans notre exemple on définit une propriété permettant de configurer la colonne sur laquelle porte la recherche et une seconde pour indiquer la requête CAML à utiliser.

Voici le résultat :

proprietes

SPFieldLookup

Il vous ensuite créer une classe héritant de SPFieldLookup dont le code correspond au comportement de votre colonne. Dans notre exemple il s’agit de la classe FilteredLookup2Field.

Reprenez les différents éléments de la classe MyFirstCustomField du projet précédent. Nous allons overrider une méthode supplémentaire : OnAdded. Celle-ci sera appelée lors de l’ajout de notre colonne à une liste.

public override void OnAdded(SPAddFieldOptions op)
{
    if (this.GetCustomProperty("LookupList") != null && this.GetCustomProperty("LookupList").ToString() != string.Empty)
    {
        try
        {
            using (SPWeb web = SPContext.Current.Web)
            {
                this.LookupList = web.Lists[this.GetCustomProperty("LookupList").ToString()].ID.ToString();
                this.LookupWebId = web.ID;
                this.LookupField = "Title";

                this.Update();
                this.ParentList.Update();
            }
        }
        catch (ArgumentException)
        {
            this.Delete();
            throw new ArgumentException("La liste n'existe pas");
        }
    }
    else
    {
        this.Delete();
        throw new ArgumentException("Nom de liste incorrect.");
    }
}

Ligne 3 : GetCustomProperty(“LookupList”) permet de récupérer la valeur saisie pour la propriété LookupList lors de l’ajout de la colonne.

Ligne 9 : étant donné qu’on hérite de SPFieldLookup il nous est possible de définir la valeur des propriétés LookupList, LookupWebId et LookupField. Cette étape est indispensable pour que votre colonne soit véritablement liée à la liste recherchée. Votre colonne bénéficiera alors de toutes les fonctionnalités d’une colonne de type recherche (liens vers les éléments de la liste recherchée, mise à jour de votre colonne lors de modifications sur les éléments de la liste recherchée, etc…).

OnAdded est appelée après l’ajout de votre colonne à la liste et non pendant (comme son nom l’indique d’ailleurs). Ainsi les modifications que vous apportez doivent être suivies de this.Update() pour les appliquer. De même si les propriétés de la colonne sont incorrectes on ne peut pas annuler l’ajout de la colonne, il faut la supprimer d’où l’utilisation de this.Delete().

Web User Control

Notre contrôle utilisateur devra contenir une DropDownList pour afficher les éléments de la liste recherchée. Il serait également intéressant de pouvoir activer ou non le filtre CAML. Nous allons donc ajouter une CheckBox pour cela.

 

<%@ Control Language="C#" Debug="true" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls" %>

<SharePoint:RenderingTemplate ID="FilteredLookup2RenderingTemplate" runat="server">
    <Template>
        <asp:DropDownList ID="DropDownListLookup" runat="server" /><br />
        <asp:CheckBox ID="CheckBoxEnableFilter" runat="server" AutoPostBack="true" Checked="true" Text="Filtrer les éléments" />
    </Template>
</SharePoint:RenderingTemplate>

 

N’oubliez pas d’inclure vos contrôles dans un SharePoint:RenderingTemplate et de définir un ID qui vous permettra d’accéder à ces contrôles depuis le code behind.

BaseFieldControl

Il ne nous reste plus qu’à implémenter le code-behind de notre contrôle. Dans notre exemple c’est la classe FilteredLookup2Control qui s’en charge. Celle-ci hérite de BaseFieldControl.

Ici nous devons prendre en compte de plusieurs problèmes:

  • Que se passe-t-il lorsque la liste recherchée est vide et que notre colonne est un champ obligatoire?
  • Que se passe-t-il lorsqu’on édite un élément et que l’élément recherché a été modifié et qu’il est désormais filtré par la requête CAML?
  • Vérifier les autorisations de l’utilisateur sur la liste recherchée et ses éléments.

Dans cet exemple nous prendrons en compte les deux premiers problèmes je vous laisse gérer le problème de sécurité ;)

Commeçons par définir le DefaultTemplateName qui nous permettra d’accéder aux contrôles:

protected override string DefaultTemplateName
{
    get
    {
        return "FilteredLookup2RenderingTemplate";
    }
}

Passons maintenant à l’initialisation des contrôles:

protected override void CreateChildControls()
{
    if (Field == null)
        return;

    base.CreateChildControls();

    if (ControlMode == SPControlMode.Display)
        return;

    // Récupération des contrôles
    DropDownListLookup = (DropDownList)TemplateContainer.FindControl("DropDownListLookup");
    CheckBoxEnableFilter = (CheckBox)TemplateContainer.FindControl("CheckBoxEnableFilter");

   
    // Initialisation des contrôles
    CheckBoxEnableFilter.CheckedChanged += new EventHandler(CheckBoxEnableFilter_CheckedChanged);

    if (!Page.IsPostBack)
    {
        this.DropDownListLookup_Init(true);
    }
}

Vous remarquerez que nous stoppons l’exécution de la méthode lorsque la colonne est en mode d’affichage (ControlMode == SPControlMode.Display). En effet le rendu d’une colonne en mode affichage doit être défini dans le fichier XML fldtypes grâce à l’élément RenderPattern (cela n’est pas obligatoire mais c’est la méthode la plus courante). Nous n’allons donc ici traiter que de l’affichage de la colonne en mode d’ajout / édition.

Ici nous accédons aux contrôles à l’aide du TemplateContainer. Cela ne fonctionne que si vous avez correctement overrider la propriété DefaultTemplateName.

On initialise ensuite notre DropDownList à l’aide de la méthode DropDownListLookup_Init en précisant qu’il faut filtrer les éléments.

Voici la méthode DropDownListLookup_Init en question:

private void DropDownListLookup_Init(bool filter)
{
    if (DropDownListLookup == null || this.Field.GetCustomProperty("LookupList") == null || this.Field.GetCustomProperty("LookupCAMLQuery") == null)
        return;

    SPList lookupList = Web.Lists[(string)this.Field.GetCustomProperty("LookupList")];
    SPQuery query = new SPQuery() { Query = (string)this.Field.GetCustomProperty("LookupCAMLQuery") };

    SPListItemCollection items;

    if(filter)
        items = lookupList.GetItems(query);
    else
        items = lookupList.Items;

    if (items.Count == 0)
    {
        DropDownListLookup.Items.Add(new ListItem("Aucun élément dans la liste cible", string.Empty));
        DropDownListLookup.Enabled = false;
    }
    else
    {
        DropDownListLookup.Enabled = true;

        foreach (SPListItem item in items)
        {
            DropDownListLookup.Items.Add(new ListItem(item.Title, item.ID.ToString() + ";#" + item.Title));
        }
    }
}

Ici on récupère la liste recherchée et la requête CAML à l’aide des Custom Properties.
On recherche alors les éléments de notre liste en appliquant ou non le filtre CAML en fonction du paramètre filter.
Si aucun élément n’est retourné (pas d’élément dans la liste recherchée ou bien tous filtrés) on ajoute un élément vide dans la DropDownList. Dans le cas contraire on charge les éléments dans notre liste déroulante. On pense à respecter le format ID;#Title pour la valeur des éléments. En effet c’est sous ce format que sont stockées les valeurs d’un champ de type recherche.

Nous allons maintenant ajouter une méthode permettant de sélectionner le bon élément de notre DropDownList en fonction de la valeur de notre colonne. Nous la nommerons DropDownListLookup_SelectIndex.

private void DropDownListLookup_SelectIndex()
{
    if (ItemFieldValue != null)
    {
        SPFieldLookupValue val = (SPFieldLookupValue)ItemFieldValue;
        string currentValue = val.LookupId.ToString() + ";#" + val.LookupValue.ToString();

        if (DropDownListLookup.Items.FindByValue(currentValue) != null)
        {
            DropDownListLookup.SelectedValue = currentValue;
            DropDownListLookup.Enabled = true;
        }
        else
        {
            DropDownListLookup.Items.Add(new ListItem(val.LookupValue.ToString(), currentValue));
            DropDownListLookup.SelectedValue = currentValue;
            DropDownListLookup.Enabled = false;
        }
    }
}

On commence par caster la valeur du champ en SPFieldLookupValue ce qui facilite ensuite la manipulation de cette valeur.
Si la DropDownList contient la valeur c’est qu’elle n’a pas été filtrée, dans ce cas on sélectionne tout simplement l’élément correspondant. En revanche si la valeur est introuvable c’est qu’elle a été filtrée. On ajoute donc cette valeur à notre DropDownList mais on désactive cette dernière.

Voyons comment obtenir et définir la valeur de notre contrôle à l’aide de la propriété Value:

public override object Value
{
    get
    {
        EnsureChildControls();

        if (DropDownListLookup.SelectedValue != null && DropDownListLookup.SelectedValue != string.Empty)
        {
            return new SPFieldLookupValue(DropDownListLookup.SelectedValue);
        }
        else return null;
    }
    set
    {
        EnsureChildControls();
        DropDownListLookup_SelectIndex();
    }
}

Pour ce qui est de récupérer la valeur rien de plus simple. On récupère la SelectedValue de notre DropDownList que l’on cast en SPFieldLookupValue. Etant donné que le format des valeurs de la liste déroulante correspond à celui d’une valeur de type recherche cela ne pose aucun problème.

La mise à jour de la valeur de notre contrôle est tout aussi simple puisqu’en réalité nous l’avons déjà traitée dans la méthode DropDownListLookup_SelectIndex().

Gérons maintenant l’activation / désactivation du filtre à l’aide de la CheckBox. Vous aurez remarqué que nous avons abonné la méthode CheckBoxEnableFilter_CheckedChanged à l’event CheckedChanged de notre CheckBox.

void CheckBoxEnableFilter_CheckedChanged(object sender, EventArgs e)
{
    if (CheckBoxEnableFilter.Checked)
    {
        DropDownListLookup.Items.Clear();
        DropDownListLookup_Init(true);
        DropDownListLookup_SelectIndex();

    }
    else
    {
        DropDownListLookup.Items.Clear();
        DropDownListLookup_Init(false);
        DropDownListLookup_SelectIndex();
    }
}

On réinitialise la liste déroulante en appliquant le filtre ou non en fonction de l’état coché ou non de la checkbox.

Dernière étape : la validation. En effet si la colonne est un champ requis et que la liste recherchée est vide ou bien que tous les éléments sont filtrés, il faut empêcher la validation de notre contrôle. Dans ce cas ou notre colonne n’est pas un champ requis on laisse passer :)
Nous devons donc overrider la méthode Validate:

public override void Validate()
{
    if (this.Field.Required && DropDownListLookup.Items.Count == 1 && DropDownListLookup.Items[0].Value == string.Empty)
    {
        this.IsValid = false;
        this.ErrorMessage = "Aucun élément dans la liste " + Field.GetCustomProperty("LookupList");
    }
    else
    {
        this.IsValid = true;
    }
}

On vérifie si notre élément vide a été ajouté à la liste déroulante. Si c’est le cas et que la colonne est requise alors on indique que la validation a échoué et on précise le message d’erreur. Celui-ci sera automatiquement affiché à la suite de nos contrôles définis dans FilteredLookup2Control.ascx

Il ne vous reste alors plus qu’à déployer votre projet :)

Pourquoi je ne peux pas déboguer mon projet?

Vous aurez probablement besoin de debuger votre projet. Pour cela clic-droit sur le nom de votre projet dans l’explorateur de solutions de Visual Studio –> Onglet générer. Sélectionnez une configuration autre que DebugBuild et cochez la définition des constantes DEBUG et TRACE. Répétez l’opération pour les autres configurations (DebugRedeploy, DebugQuickCopy, etc…). Vous pouvez ensuite attacher Visual Studio au processus w3wp.

Démonstration

Voici une petite démonstration du projet une fois déployé:

 
Filtered lookup field

Comments (1) -

charline
charline
4/7/2009 1:47:43 AM #

clair, limpide. je n'aurais pas dit mieux moi même.
merci pour cette leçon de vie

Pingbacks and trackbacks (1)+

Add comment

biuquote
  • Comment
  • Preview
Loading

About me

After sudying at SUPINFO Indian Ocean and SUPINFO Montreal I am now Analyst Programmer at Ivanhoe Cambridge. 

Month List