SharePoint 2007 : personnaliser le comportement par défaut d'un type de colonne natif

April 23, 2010

SharePoint 2007 fournit un certain nombre de types de colonne natif. Vous pouvez également en créer de nouveau si vous avez des besoins spécifiques, en héritant de SPField ou de l’une de ses dérivées (et en avalant un cachet d’aspirine avant de lire la doc).

Toutefois cette approche pose quelques inconvénients :

  • Un peu complexe (mais bon, c’est notre pain quotidien de développeur SharePoint la complexité)
  • Impossible de convertir votre colonne depuis ou vers une colonne native
  • Les outils tiers ne reconnaîtrons pas votre colonne
  • quelques bugs qui traînent dans SharePoint (je pense à la page de propriété qui ne sait pas conserver les données et qui impose de réécrire cette page à chaque fois)

Je vous propose ici une alternative, elle aussi un peu complexe, mais qui a l’avantage de conserver les colonnes natives, en changeant simplement leur rendu.

Les ControlAdapter à votre secours

Un ControlAdapter est une classe qui va permettre de modifier le rendu d’un contrôle. Initialement prévu pour supporter le rendu pour navigateurs mobiles, les ControlAdapters ont été détournés pour personnaliser le rendu de certains contrôles du Framework sans avoir à toucher au contrôle lui même.

Les plus connus des ControlAdapters sont probablement les ASP.NET 2.0 CSS Friendly Control Adapters 1.0 qui permettent de transformer le rendu des WebControls ASP.Net afin de respecter au maximum les contraintes des standards W3C (plus de

générée, mais des).

Le principe de base consiste à créer un fichier .browser que l’on place dans le dossier App_Browsers de votre application Web. Chaque fichier va définir quand appliquer les ControlAdapter et quels sont les ControlAdapters que vous utiliserez.

Extrait de fichier browser :

<browsers>
  <browser refID="Default">
    <controlAdapters>
      <adapter
        controlType="System.Web.UI.WebControls.TextBox"
        adapterType="MyTextBoxAdapter"
        />
    </controlAdapters>
  </browser>
</browsers>

L’astuce est ici d’utiliser Default pour appliquer ce ControlAdapter systématiquement.

Les ControlAdapters appliqués à SharePoint

SharePoint, en tant qu’application Web ne déroge pas à la règle. En effet, les colonnes SharePoint utilisent des contrôles pour générer l’affichage. La classe SPField implémente une propriété virtuelle FieldRenderingControl qui permet à chaque colonne de définir quel est le contrôle utilisé pour l’édition :

public class SPField
{

  public virtual BaseFieldControl  FieldRenderingControl
  {
    get
    {
      return null;
    }
  }

}

Par exemple, si l’on observe la colonne de type url (Reflector est votre ami):

public class SPFieldUrl : SPField
{
  public override  BaseFieldControl FieldRenderingControl
  {
    get
    {
      BaseFieldControl  control = new UrlField();
      control.FieldName = base.InternalName;
      return  control;
    }
  }
}

On voit ici que le contrôle UrlField est utilisé… que nous allons personnaliser grâce à un ControlAdapter.

Implémenter votre ControlAdapter

La première étape consiste à créer votre ControlAdapter. Dans mon cas j’utilise WSPBuilder 1.4 Beta avec Visual Studio 2010, mais vous pouvez employer l’outil de dev SharePoint que vous souhaitez. J’ajoute à la solution une Feature vide à laquelle j’ajoute une classe DocumentExplorerAdapter (nous parlerons du packaging plus loin). DocumentExplorer est le nom d’une extension SharePoint en cours de développement que je publierai lorsqu’elle sera terminée.

using System.Web;
using System.Web.UI.Adapters;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace Hand.SharePoint.DocumentExplorer
{
  public class  DocumentExplorerUrlFieldAdapter : ControlAdapter
  {
    protected UrlField  UrlField { get { return (UrlField)this.Control; } }
    protected SPWeb  CurrentWeb { get { return SPControl.GetContextWeb(HttpContext.Current); } }

    protected override void  CreateChildControls()
    {
      base.CreateChildControls();

      var button = new HyperLink();
      button.ID = "test";
      button.Text = "Blog  Have a Nice Day.Net";
      button.NavigateUrl =  "http://blog.hand-net.com";
      UrlField.Controls.Add(button);
    }
  }
}

Ce code ici consiste à ajouter un lien vers ce blog. L’approche que j’ai choisie ici est de surcharger CreateChildControls, tout en appelant base.CreateChildControls. Cela me permet de ne pas modifier le fonctionnement du contrôle, si ce n’est ajouter un lien sous le contrôle. Théoriquement vous pouvez faire ce que vous voulez du contrôle en surchargeant les bonnes méthodes.

Compilez ce code, déployez le (copie dans le GAC ou déploiement du WSP généré si vous préférez) puis aller dans le dossier C:\inetpub\wwwroot\wss\VirtualDirectories\80\App_Browsers. Il s’agit du site Web par défaut de ma machine, mais vous devrez peut être adapter ce chemin à votre configuration.

Dans ce dossier, créez le fichier DocumentExplorerAdapter.browser (libre à vous de le nommer comme vous le souhaitez) :

<browsers>
  <browser refID="Default">
    <controlAdapters>
      <adapter
      controlType="Microsoft.SharePoint.WebControls.UrlField,Microsoft.SharePoint,  Version=12.0.0.0, Culture=neutral,  PublicKeyToken=71e9bce111e9429c"
      adapterType="Hand.SharePoint.DocumentExplorer.DocumentExplorerUrlFieldAdapter,  Hand.SharePoint.DocumentExplorer, Version=1.0.0.0, Culture=neutral,  PublicKeyToken=127327bffe934acc"
      />
    </controlAdapters>
  </browser>
</browsers>

N’oubliez pas de mettre à jour le nom de l’assembly dans l’attribut adapterType.

Recycler votre pool d’application, et, ô miracle, un lien vers ce blog vient d’apparaître !

Note : il arrive dans certains cas (non identifiés malheureusement) où le contrôle n’était pas correctement modifié. Pour contourner le problème, ouvrez le fichier compat.browser avec le bloc note, et sauvegardez le. Cette manipulation provoque le parsing des fichiers browser et devrait résoudre le problème.

La problématique du déploiement

Comme tout bon développeur SharePoint qui se respecte, vous devriez avoir eu les cheveux qui se sont dressés sur votre tête à l’évocation de déployer des fichiers à la main. Rassurez vous, je vais vous montrer comment déployer ce fichier browser au travers d’une feature.

La première chose à savoir, c’est qu’il n’existe pas de type de feature permettant d’ajouter des fichiers dans le dossier physique de l’application web. Aussi il va falloir ruser.

Pour déposer ce ficher, je vais créer une feature dont la portée est WebApplication et qui possède un FeatureReceiver :

<Feature Id="b07b020c-bc1c-4425-8560-ae936e60a21d"
         Title="DocumentExplorerAdapterFeature"
         Description="Description  for  DocumentExplorerAdapterFeature"
         Version="1.0.0.0"
         Hidden="FALSE"
         Scope="WebApplication"
         DefaultResourceFile="core"
         ReceiverAssembly="Hand.SharePoint.DocumentExplorer,  Version=1.0.0.0, Culture=neutral,  PublicKeyToken=127327bffe934acc"
         ReceiverClass="Hand.SharePoint.DocumentExplorer.EventHandlers.Features.DocumentExplorerAdapterFeatureReceiver"
         xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
    </ElementManifests>
</Feature>

Le nœud ElementManifests reste vide puisque nous ne pouvons pas régler le problème au travers d’un type de feature. Par contre, depuis le FeatureReceiver, nous pouvons créer un job qui pourra faire le travail.

Il est possible de créer les fichiers directement depuis le FeatureReceiver, mais seulement si il n’y a qu’un serveur frontal. En effet, les évènements du FeatureReceiver sont levé depuis le serveur où l’activation a été demandée. Dans le cas d’une ferme de serveur, il est nécessaire de créer un Job personnalisé, avec dans le constructeur SPJobLockType.None. Dans ce cas, le travail sera exécuté sur chaque serveur, et surtout sur chaque nouveau serveur ajouté à la ferme.

Ainsi nous avons le code suivant pour le FeatureReceiver :

public class DocumentExplorerAdapterFeatureReceiver :  SPFeatureReceiver
{
  public override void  FeatureActivated(SPFeatureReceiverProperties properties)
  {
    var webApp =  (SPWebApplication)properties.Feature.Parent;
    var job =  DocumentExplorerSyncBrowserFilesJob.Find(webApp);
    if (job != null)  job.Delete();

    job = new DocumentExplorerSyncBrowserFilesJob(webApp);
    job.Schedule = new  SPOneTimeSchedule(DateTime.Now);
    job.Update();
  }

  public override void FeatureDeactivating(SPFeatureReceiverProperties  properties)
  {
    var webApp =  (SPWebApplication)properties.Feature.Parent;
    var job =  DocumentExplorerSyncBrowserFilesJob.Find(webApp);
    if (job != null)  job.Delete();

    job = new DocumentExplorerSyncBrowserFilesJob(webApp);
    job.Schedule = new  SPOneTimeSchedule(DateTime.Now);
    job.Update();
  }

  public override void FeatureInstalled(SPFeatureReceiverProperties properties)  {}

  public override void FeatureUninstalling(SPFeatureReceiverProperties  properties) { }
}

J'ai choisi ici de créer un Job personnalisé DocumentExplorerSyncBrowserFilesJob identique pour le déploiement et la suppression du fichier browser. Je créer ce travail (et supprimer une ancienne instance si elle existe) pour s'exécuter immédiatement. En réalité il faut `attendre` quelque secondes du fait du fonctionnement interne du minuteur SharePoint.

Le code de ce job est :


```` csharp

public class DocumentExplorerSyncBrowserFilesJob :  SPJobDefinition
{
  public const string JobName =  "DocumentExplorerSyncBrowserFilesJob";
  public const string JobTitle = "Sync  document explorer browser files";
  public  DocumentExplorerSyncBrowserFilesJob() : base() { }

  public DocumentExplorerSyncBrowserFilesJob(SPWebApplication webApp)
  :  base(JobName, webApp, null, SPJobLockType.None)
  {
    this.Title =  JobTitle;
  }

  /// <summary>
  /// Executes the job definition.
  ///  </summary>
  ///
  <param />name="targetInstanceId">Not use with this  SPJobLockType
  public override void Execute(Guid  targetInstanceId)
  {
    var webApp = this.WebApplication;

    var feature = webApp.Features[new Guid("b07b020c-bc1c-4425-8560-ae936e60a21d")];

    foreach (KeyValuePair item in  webApp.IisSettings)
    {
      var browserFilePath =  Path.Combine(
        item.Value.Path.FullName,
        @"App_Browsers\DocumentExplorerAdapters.browser"
        );
      var browserFileFeaturePath = Path.Combine(
        feature.Definition.RootDirectory,
        "DocumentExplorerAdapters.browser"
        );

      if (feature != null)
      {
        // the feature is activated, copy the file
        //  if the feature was previously activated, the file is already there, otherwise  copy it
        if  (!File.Exists(browserFilePath))
        {
          File.Copy(
            browserFileFeaturePath,
            browserFilePath,
            true
            );
        }
      }
      else
      {
        //  the feature is not activated, delete the file
        // if the file does not exists,  we assume that the feature was not activated
        if (File.Exists(browserFilePath))
        {
          File.Delete(browserFilePath);
        }
      }
    }
  }

  /// <summary>
  /// Find the DocumentExplorerSyncBrowserFilesJob in a  .
  /// </summary>
  ///
  <param name="webApp">SPWebApplication to look in</param>
  ///  The DocumentExplorerSyncBrowserFilesJob if found, otherwise  null.
  public static  DocumentExplorerSyncBrowserFilesJob Find(SPWebApplication  webApp)
  {
    foreach (var job in webApp.WebService.JobDefinitions)
    {
      if  (job.Name == JobName) return  (DocumentExplorerSyncBrowserFilesJob)job;
    }
    return null;
  }
}

Le code de ce job va explorer toutes les applications Web IIS liées à l’application web SharePoint (dans le cas où vous auriez étendu l’application web). Il vérifie ensuite si la feature est activée ou non (si elle est trouvée ou non dans la collection de features de l’application Web SharePoint).

  • Si elle trouvée, le fichier est créé si n’existait pas déjà
  • Si elle n’est pas trouvée, on supprime éventuellement le fichier.

Le résultat : une colonne personnalisée visuellement

Pour vérifier que tout fonctionne, déployons la solution (vive WSPBuilder).

Dans l’administration centrale, vous avez accès à une nouvelle feature au niveau de vos applications web :

desc

Après activation (et quelques secondes d’attentes), le fichier apparaît :


C:\>dir C:\inetpub\wwwroot\wss\VirtualDirectories\80\App_Browsers /b

compat.browser
DocumentExplorerAdapters.browser

Je crée ensuite une colonne de type Lien hypertexte (colonne native) :

desc

Et lorsque je saisi un nouvel élément :

desc

Comme prévu, le lien vers le blog apparaît !

Conclusion

Je vous ai ici montré comment personnaliser le rendu des colonnes natives de SharePoint, ainsi que la procédure propre pour les packager. Gardez à l’esprit cette possibilité car elle vous permettra de :

  • ajouter des fonctionnalités à votre environnement SharePoint, tout en conservant la compatibilité des données
  • personnaliser si nécessaire le rendu visuel pour s’adapter aux customisations graphiques