L’objet de ce post est de proposer une méthode permettant de stocker les resources multilingues uniquement dans le répertoire 14\Resources, afin d’éliminer les problématiques liées aux ressources stockées dans webapp\App_GlobalResources.
Lorsque l’on doit gérer des libellés applicatifs traduisibles dans un projet SharePoint, on les définit dans des fichiers resx que l’on inclue dans les sources de notre projet. Toutefois, selon le contexte et la manière que l’on utilise pour faire référence à ce libellé, on devra déployer le fichier resx dans le répertoire 14\Resources (pour les libellés SharePoint) ou 14\CONFIG\Resources (pour les libellés ASP.NET utilisés dans les aspx et ascx exclusivement, le fichier étant copié par SharePoint dans le dossier App_GlobalResources de l’application web).
Bien que le principe soit facile à comprendre (cet article résume tout de manière très claire) et à mettre en oeuvre, je trouve dommage de devoir gérer 2 fois plus de fichiers resx que nécessaire, et de toujours avoir à se demander dans quel fichier mettre un libellé. Certains développeurs ont trouvé des moyens de déployer le même fichier resx aux 2 endroits via des tâches planifiées ou en bidouillant des fichiers cachés du projet Visual Studio, mais cela ne résout pas le problème fichiers stockés dans App_GlobalResources : ces derniers doivent être traités spécialement via ligne de commande ou timer job afin d’être copiés du répertoire 14\CONFIG\Resources vers le répertoire App_GlobalResources de l’application web qui l’exploite. Cela ralentie notamment le développeur au quotidien lorsqu’il doit ajouter de nouveaux libellés, car il doit soit lancer des lignes de commandes soit déclencher des timer jobs pour permettre la propagation des fichiers.
J’ai mis au point une solution qui permet de n’utiliser que les fichiers resx du répertoire 14\CONFIG. Ceci est rendu possible par le développement d’un ExpressionBuilder custom qui ira chercher les ressources avec l’API SharePoint. Restera à enregistrer la classe correspondante dans le web.config, puis à utiliser l’expression builder (que je nomme ici Resources14) dans nos pages aspx et controles ascx avec <% $Resources14:monfichier:maresource; %> plutot que le traditionnel <% $Resources:monfichier:maresource; %>
Code de l’ExpressionBuilder
[ExpressionPrefix("Resources14")]
public class Resources14ExpressionBuilder : ExpressionBuilder
{
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, Object parsedData, ExpressionBuilderContext context)
{
CodeMethodInvokeExpression codeMethodInvokeExpression = new CodeMethodInvokeExpression();
codeMethodInvokeExpression.Method.TargetObject = new CodeTypeReferenceExpression(typeof(Resources14ExpressionBuilder)); ;
codeMethodInvokeExpression.Method.MethodName = "GetLocalizedResource";
codeMethodInvokeExpression.Parameters.Add(new CodePrimitiveExpression(entry.Expression));
return codeMethodInvokeExpression;
}
public override bool SupportsEvaluate
{
get { return true; }
}
public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
return GetLocalizedResource(entry.Expression);
}
public static String GetLocalizedResource(String key)
{
return SPUtility.GetLocalizedString("$Resources:" + key, null, SPContext.Current.Web.Language);
}
}
La méthode GetCodeExpression est appelée lorsque ASP.NET parse l’expression builder. Une chose importante à savoir est que cette méthode renvoie une expression (au sens CodeDom) qui sera récupérée et mise en cache pour chaque entrée (ce qui est après le caractère ‘:’) différente. L’expression mise en cache sera par contre évaluée à chaque fois que l’expressionbuilder sera parsé sur une page.. Il est donc important que GetCodeExpression soit “language agnostic”, et retourne une référence à une méthode qui elle, tiendra compte de la langue. Ici, GetCodeExpression renvoie une sorte de pointeur vers la méthode GetLocalizedResource.
Modification du web.config
Afin de pouvoir consommer l’expression builder dans nos développements, il est nécessaire qu’il soit enregistré dans le web.Config.
Pour ce faire, on doit ajouter un noeud dans la section expressionBuilders comme ci-dessous :

Pour bien faire, il faudra faire cet enregistrement par code en utilisant la classe SPWebConfigModification afin d’être certain que la modification soit effectuée de la même manière sur tous les frontaux de la ferme.
Ci dessous un exemple d’implémentation du feature receiver d’une feature de scope web app :
public class WebApp_RegisterExpressionBuilderEventReceiver : SPFeatureReceiver
{
private const String OWNER = "ANO.Resources14_WebApp_RegisterExpressionBuilderEventReceiver";
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWebApplication webapp = properties.Feature.Parent as SPWebApplication;
//Cleanup
RemoveWebConfigEntries(webapp, OWNER);
//Adds the expressionBuilder
SPWebConfigModification mod = new SPWebConfigModification();
mod.Path = "configuration/system.web/compilation/expressionBuilders";
mod.Name = "add[@ expressionPrefix='Resources14']";
mod.Sequence = 1;
mod.Owner = OWNER;
mod.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
mod.Value = String.Format(@"<add expressionPrefix=""Resources14"" type=""{0}"" />", typeof(Resources14ExpressionBuilder).AssemblyQualifiedName);
//registers the web.config modification to the web app
webapp.WebConfigModifications.Add(mod);
webapp.Update();
//updates the web.config on each front end
SPWebService.ContentService.ApplyWebConfigModifications();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPWebApplication webapp = properties.Feature.Parent as SPWebApplication;
//Removes the expression builder registration from the web.config
if (RemoveWebConfigEntries(webapp, OWNER))
SPWebService.ContentService.ApplyWebConfigModifications();
}
private static Boolean RemoveWebConfigEntries(SPWebApplication webapp, String owner)
{
Boolean updated = false;
//Modifications to remove
List<SPWebConfigModification> modsToRemove = new List<SPWebConfigModification>();
//Identifies modifications to remove
foreach (SPWebConfigModification mod in webapp.WebConfigModifications)
if (mod.Owner == owner)
modsToRemove.Add(mod);
//Removes modifications one by one
foreach (SPWebConfigModification mod in modsToRemove)
{ webapp.WebConfigModifications.Remove(mod);
updated = true;
}
//Updates if necesssary
if (updated)
webapp.Update();
return updated;
}
}
Une fois cette feature déployée et activée sur une web app, on peut consommer notre expression builder.
Utilisation de l’expression builder
On peut maintenant utiliser notre expression builder dans les pages aspx et les controles ascx de notre projet.
Exemple :

Résultat :

Conclusion
L’utilisation de cette méthode nécessite un développement spécifique qu’il faudra embarquer dans les sources du projet, mais il me parait intéressant de le faire pour les raisons suivantes :
Un seul jeu de resx à maintenir, et à faire traduire
Plus besoin de se demander dans quel resx doit aller un libellé
Plus de problématique de déploiement spécifique aux resx qui doivent aller dans le répertoire App_GlobalResources de la web app
Gain de temps notable durant la phase de développement, vis à vis de la problématique de déploiement
Si vous souhaitez récupérer les sources du projet, elles devraient téléchargeables être en pièce jointe de ce post : ANO.Resources14.zip
Arnault Nouvel
MVP SharePoint Server
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :