Extension du modèle Resource-Provider ASP.NET 2.0

 

Michèle Leroux Bustamante

IDesign Inc

Octobre 2006

S’applique à :
   Microsoft ASP.NET 2.0
   Microsoft Visual Studio 2005
   Localisation

Résumé: Microsoft ASP.NET 2.0 a mis en place un certain nombre d’améliorations merveilleuses pour la localisation des applications web. Même avec tous ces avantages merveilleux, peu de temps après la localisation d’un site, vous pouvez commencer à vous poser des questions sur l’extensibilité. Cet article vous aidera à appliquer les fonctionnalités d’extensibilité de ASP.NET pour gérer les scénarios de localisation d’entreprise et améliorer votre processus de développement de localisation. (23 pages imprimées)

Téléchargez le code source de cet article.

Contenu

Introduction
Où, oh, où mes ressources doivent-elles aller ?
Modèle Resource-Provider
Création d’un fournisseur de ressources de base de données
Accès aux ressources à partir d’assemblys externes
Prise en charge des expressions de localisation personnalisées
Conclusion
Remerciements
Ressources supplémentaires

Introduction

ASP.NET 2.0 a déclenché un certain nombre d’améliorations merveilleuses pour la localisation des applications web. J’ai écrit sur ces nouvelles fonctionnalités dans l’article MSDN « fonctionnalités de localisation ASP.NET 2.0 : une nouvelle approche de la localisation des applications web ».

Après avoir joué avec ces nouvelles fonctionnalités de localisation, vous remarquerez immédiatement ce qui suit :

  • La génération de ressources pour chaque page est désormais un jeu d’enfant de Microsoft Visual Studio 2005 en appelant l’élément de menu Générer des ressources locales en mode Création de page.
  • La création et la consommation de ressources globales sont beaucoup plus simples, grâce à un meilleur éditeur de ressources et à un accès fortement typé.
  • Le mappage des entrées de ressources pour contrôler les propriétés et les zones de contenu est assez élégant, à l’aide d’expressions de localisation déclaratives.
  • Le ResourceManager ne nécessite plus d’instanciation manuelle, car ResXResourceProviderFactory coordonne la récupération des entrées de ressources à partir de ressources locales ou globales, en allouant le ResourceManager selon les besoins.
  • La détection automatique des préférences de culture du navigateur et l’affectation de cette culture au thread de requête facilitent le respect des préférences de culture utilisateur, même pour les utilisateurs anonymes.

Sans surprise, même avec tous ces avantages merveilleux, nous voulons souvent plus. Peu de temps après la localisation d’un site avec ces fonctionnalités exceptionnelles, vous pouvez commencer à vous poser des questions sur d’autres choses, telles que :

  • Comment faire extraire des ressources à partir d’un autre emplacement, tel qu’un assembly de ressources distinct ou une source de base de données ?
  • Comment faire gérer les environnements hybrides qui utilisent certaines ressources locales et globales, mais qui ont également d’autres sources de données ?
  • Comment contrôler la source des ressources et continuer à tirer parti du modèle de fournisseur de ressources ASP.NET 2.0, des expressions de localisation et d’autres fonctionnalités d’intégration du concepteur ?
  • Comment faire tirer parti des fonctionnalités de localisation existantes et des options d’extensibilité disponibles pour mieux répondre aux besoins de mon environnement de développement et de mon processus de localisation ?

C’est pourquoi l’extensibilité est si importante. Il existe de nombreuses façons d’étendre ASP.NET fonctionnalités de localisation et d’interagir avec l’environnement de développement. Cet article est le premier d’une série en trois parties qui vous aidera à appliquer les fonctionnalités d’extensibilité de ASP.NET pour gérer les scénarios de localisation d’entreprise et améliorer votre processus de développement de localisation.

Dans cet article, je vais me concentrer sur les fonctionnalités qui vous permettent de récupérer des ressources à partir d’autres emplacements de stockage et de les intégrer à l’analyse de pages, à la compilation et à l’exécution au moment de l’exécution. Je vais décrire comment y parvenir à l’aide d’une combinaison de fournisseurs de ressources personnalisés, de générateurs d’expressions personnalisées et d’autres types extensibles prenant en charge. Le deuxième article de la série vous montre comment améliorer davantage votre processus de développement en intégrant votre choix de stockage de ressources aux fonctionnalités de productivité intégrées dans Visual Studio 2005. Le troisième article traite des alternatives pour la gestion des hiérarchies de ressources complexes qui peuvent, par exemple, prendre en charge la personnalisation côté client.

Où, oh, où mes ressources doivent-elles aller ?

L’incorporation de ressources localisées dans un site Web a toujours été un effort douloureux. La génération de ressources a toujours été difficile, l’organisation des ressources pour la traduction nécessite toujours un processus managé, mais l’aspect le plus difficile des ressources d’un site Web est de savoir ce qui doit aller dans les ressources, comment allouer ces ressources et ce qui donnera les meilleures performances et facilités de maintenance.

Création et accès à des ressources avec ASP.NET 2.0

ASP.NET 2.0 nous a fourni une fonctionnalité qui générerait des ressources locales pour chaque page. À partir de là, un processus de conception et d’internationalisation de page plus efficace est né.

  1. Concevoir des pages en appliquant une combinaison de contrôles de serveur html statique et ASP.NET.
  2. Préparez les zones statiques pour la localisation en les encapsulant avec le contrôle Localize ASP.NET.
  3. Fournissez des noms de contrôle appropriés à tous les contrôles serveur afin que les gestionnaires d’événements et les clés de ressources générés puissent être facilement reconnus.
  4. Créez des ressources partagées dans le sous-répertoire App_GlobalResources. Il peut s’agir de fichiers .resx qui existent déjà ou de nouveaux fichiers .resx créés pour contenir des termes qui seront partagés sur plusieurs pages.
  5. Associez des ressources partagées à des propriétés de contrôle à l’aide d’expressions de ressource explicites, le cas échéant. Il est préférable de le faire avant de générer des ressources locales pour la page.
  6. Générez des ressources locales en mode Création de page en sélectionnant l’élément de menu Générer une ressource locale .

Après avoir généré des ressources locales, toutes les propriétés localisables pour la page et ses contrôles sont envoyées à des fichiers de ressources locaux individuels, un par page. Une expression de localisation implicite indique à l’analyseur de page de générer du code qui mappera chaque valeur de ressource d’un contrôle à sa propriété correspondante, en fonction d’un préfixe commun. Considérez l’expression implicite suivante de la page Expressions.aspx dans l’exemple de code.

  <asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

Les ressources sont stockées dans le fichier Expressions.aspx.resx sous le répertoire App_LocalResources. Les ressources de ce contrôle Label partagent le préfixe « labHelloLocalResource1 » ; par exemple, la propriété Text est stockée par la clé « labHelloLocalResource1.Text ».

Si vous prenez bien en compte votre interface utilisateur, en utilisant master pages et les contrôles utilisateur pour les régions d’interface utilisateur courantes, les ressources qui en résultent générées pour chaque page master, contrôle utilisateur et page seront également un peu bien factorées (chevauchement réduit). Cela facilite l’organisation des ressources consommées par chaque partie de page, ce qui est traditionnellement fastidieux dans les versions antérieures. Néanmoins, vous souhaitez parfois extraire des ressources à partir d’un emplacement partagé. Dans ce cas, vous devez fournir une expression de ressource explicite, telle que l’expression $Resources illustrée ici.

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

Dans ce cas, les ressources se trouvent dans CommonTerms.resx sous le répertoire App_GlobalResources. Des expressions explicites de ce type peuvent être créées à l’aide de l’éditeur d’expressions (voir l’article MSDN mentionné précédemment), pour simplifier le processus.

Les expressions implicites et explicites déclenchent la génération de code pour récupérer des valeurs de ressource à l’aide du fournisseur de ressources. Ces expressions déclaratives, combinées à la génération de code et de ressources, fournissent un outil de productivité que nous n’avions tout simplement pas auparavant, du moins pas pour les applications web.

Assemblys de ressources et ResourceManager

Il existe plusieurs façons possibles de compiler et de déployer vos applications ASP.NET 2.0 :

  • Déployez la source et JIT-compilez l’ensemble du site.
  • Précompilez le site avec des pages et des ressources pouvant être mises à jour.
  • Précompilez le site pour générer un assembly par page ou un assembly par répertoire.

Dans tous les cas, les assemblys de ressources sont finalement créés pour chaque répertoire du site, et les assemblys satellites sont générés sous leurs répertoires respectifs spécifiques à la culture. Même lorsque le site est compilé par JIT, le résultat est équivalent. La figure 1 illustre le résultat précompilé d’un site avec deux sous-répertoires et une seule traduction en espagnol.

Cliquez ici pour agrandir l’image

Figure 1. Les ressources et les assemblys satellites sont générés pour chaque répertoire d’un site Web ASP.NET. (Cliquez sur l’image pour une image plus grande.)

Ces ressources sont accessibles au moment de l’exécution via un ResourceManager. Un ResourceManager est alloué pour chaque type de ressource (par exemple, Page1.aspx et Page2.aspx) lorsque des ressources sont demandées. Les assemblys de ressources associés à chaque type de ressource sont chargés dans le domaine d’application ASP.NET lors du premier accès et y restent jusqu’à ce que le domaine d’application soit déchargé. Dans le cas de la figure 1, la première fois que \SubDir1\Page1.aspx est accessible pour la culture espagnole, l’assembly \es\App_LocalResources.subdir1.cdcab7d2.resources.dll est chargé dans le domaine d’application. Cet assembly contient des ressources en espagnol pour toutes les pages dans \SubDir1.

La figure 2 illustre la façon dont ResourceManager accède aux ressources locales pour une page particulière. Lorsque \SubDir1\Page1.aspx est chargé, le code généré à partir d’expressions implicites appelle resXResourceProviderFactory, qui retourne le LocalResXResourceProvider. Ce fournisseur crée un ResourceManager pour le type Page1.aspx dans l’assembly App_LocalResources.subdir1.cdcab7d2 . Si le thread de requête a une culture d’interface utilisateur « es », l’assembly de ressources satellite du répertoire \es est chargé dans le domaine d’application. Si la culture de l’interface utilisateur n’a pas d’assembly satellite correspondant, le ResourceManager « revient » à l’assembly de ressources main.

Cliquez ici pour agrandir l’image

Figure 2 : ResourceManager accède aux ressources à partir de l’assembly de ressources main ou à partir d’assemblys satellites localisés, une fois qu’elles ont été chargées dans le domaine d’application. (Cliquez sur l’image pour une image plus grande.)

Les assemblys de ressources et satellites restent chargés dans le domaine d’application. Chaque ResourceManager (par page ou type de ressource partagée) est également mis en cache et réutilisé pour les demandes suivantes adressées aux ressources associées.

Le comportement que je viens de décrire résume la façon dont les ressources sont accessibles à l’aide du modèle de fournisseur de ressources ASP.NET par défaut. À présent, examinons les raisons pour lesquelles vous pouvez vous écarter de cette implémentation par défaut.

Pourquoi utiliser un autre emplacement ?

Avec la nouvelle expérience ASP.NET 2.0, si vous prenez les valeurs par défaut pour la génération de ressources et l’accès au moment de l’exécution, elle offre une expérience beaucoup plus agréable que par le passé. Cela dit, il est souvent souhaitable d’explorer des alternatives pour le stockage des ressources, pour les raisons suivantes :

  • Réutilisation de ressources existantes, déjà situées dans un autre stockage
  • Aspect pratique pour stocker des blocs plus volumineux de contenu statique
  • Simplicité de gestion

Deux alternatives courantes au stockage de ressources sont les assemblys de ressources externes et la base de données.

Gestion des ressources préexistantes : vous pouvez avoir des assemblys de ressources préexistants provenant d’applications antérieures ou des assemblys de ressources partagés entre vos applications Windows et web. Normalement, si vous migrez du code de ASP.NET 1.1 vers la version 2.0, je vous recommande de prendre les fichiers .resx de l’application 1.1 et de les copier dans le répertoire App_GlobalResources de l’application 2.0. Ces .resx sont ensuite compilés avec l’application ASP.NET 2.0 et accessibles via des ressources globales fortement typées. Toutefois, pour contrôler le contrôle de version sur un assembly de ressources préexistant ou pour ne conserver qu’une seule copie des ressources pour les applications Windows et Web, il ne s’agit pas d’une option. Par conséquent, le stockage de ces ressources dans des assemblys partagés de ressources uniquement est une meilleure option. Cela signifie que vous avez besoin d’un moyen d’extraire des ressources de ces assemblys.

Stockage des ressources dans la base de données : le stockage de base de données est une option populaire pour les ressources d’application web, pour plusieurs raisons. Vous pouvez probablement deviner qu’à un moment donné, avec un site qui contient des milliers de pages et plusieurs milliers d’entrées de ressources, l’utilisation de ressources d’assembly peut ne pas être idéale. Il ajoute à l’utilisation de la mémoire au moment de l’exécution, et non à mention le nombre accru d’assemblys chargés dans le domaine d’application. Ces deux résultats peuvent avoir un impact négatif sur les performances des sites extrêmement volumineux, ce qui rend la latence d’un appel de base de données utile. Les ressources de base de données peuvent également fournir un environnement plus flexible et plus facile à gérer pour le processus de localisation, pour réduire les doublons, pour des options de mise en cache complexes et pour stocker des blocs de contenu éventuellement plus volumineux. Enfin, l’allocation de ressources à une base de données permet de prendre en charge des hiérarchies plus complexes de contenu traduit, où les clients ou les services peuvent avoir des versions personnalisées du texte qui est ensuite également localisée.

Les modèles d’extensibilité pour la localisation ASP.NET ont été spécifiquement conçus pour prendre en charge des alternatives pour le stockage des ressources, ce qui rend ces résultats facilement réalisables. En outre, vous pouvez également vous connecter à l’expérience au moment de la conception, afin que les développeurs non seulement récupèrent, mais génèrent également des ressources dans d’autres magasins, ces dernières étant abordées dans mon prochain article.

Que vous stockiez des ressources dans des assemblys de ressources externes ou dans une base de données, vous souhaitez absolument tirer parti des fonctionnalités de localisation de ASP.NET 2.0. L’objectif est de continuer à utiliser des expressions de localisation et des API de localisation, tout en accédant aux ressources depuis « où que vous soyez ». Cela est possible avec les fonctionnalités d’extensibilité que je vais aborder tout au long de cet article.

Modèle Resource-Provider

Comme je l’ai mentionné, le type ResourceManager est responsable de la récupération des ressources des assemblys au moment de l’exécution. Il encapsule la récupération du jeu de ressources correct, en fonction de la culture de l’interface utilisateur du thread de requête (Figure 2). En d’autres termes, tant que le thread de demande est défini sur la culture d’interface utilisateur appropriée pour les préférences de l’utilisateur appelant, resourceManager a toute la logique pour gérer la ressource de secours, en sélectionnant la ressource appropriée à partir de l’assembly satellite approprié. Avant ASP.NET 2.0, nous devions écrire notre propre code pour instancier un ResourceManager pour chaque type de ressource et gérer sa durée de vie. Cela nécessitait du code supplémentaire pour chaque demande de page pour créer ou accéder à l’instance ResourceManager et appeler des méthodes pour accéder à l’entrée de ressource. Pour lier des ressources à des éléments de page de manière déclarative, des instructions de liaison de données personnalisées peuvent être utilisées, mais cela nécessite également du code pour lancer la liaison de données au niveau de la page et l’allocation de variables de liaison.

Dans ASP.NET 2.0, nous pouvons utiliser des fonctions d’API de localisation à partir de n’importe quelle page ou contrôle utilisateur pour récupérer des ressources. Par exemple, le code suivant récupère une ressource de page locale et une ressource globale, respectivement.

  this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

J’ai mentionné précédemment que les expressions de localisation déclarative pouvaient également être utilisées pour définir des propriétés de page et de contrôle à partir de ressources. Expressions de localisation implicites, telles que :

  <asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

et des expressions de localisation explicites, telles que :

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

sont utilisés pour générer du code pour appeler GetLocalResourceObject() et GetGlobalResourceObject(). Ce que cela doit vous dire, c’est que la façon ASP.NET 2.0 d’accéder aux ressources se fait finalement par le biais de ces méthodes, même lorsque vous utilisez la commodité des expressions déclaratives.

C’est là qu’intervient le modèle de fournisseur de ressources. Ces appels d’API s’appuient sur un ResourceProviderFactory par défaut ou personnalisé pour rechercher l’entrée de ressource correcte et collecter sa valeur. La valeur par défaut ResourceProviderFactory est le type ResXResourceProviderFactory mentionné précédemment. Cette fabrique retourne une instance de GlobalResXResourceProvider pour les ressources globales, et une instance de LocalResXResourceProvider pour les ressources de page locales.

Au final, ces fournisseurs s’appuient sur un ResourceManager pour accéder à des ressources individuelles à partir de l’assembly satellite approprié. Le fournisseur utilise un ResourceReader pour collecter une collection de ressources de page pendant l’étape d’analyse de page. La figure 3 illustre ces composants clés associés au modèle de fournisseur de ressources par défaut.

Cliquez ici pour agrandir l’image

Figure 3. Composants qui composent le modèle de fournisseur de ressources par défaut : la fabrique de fournisseurs, les fournisseurs de ressources locaux et globaux, les gestionnaires de ressources et les lecteurs de ressources pour accéder à chaque type de ressource (cliquez sur l’image pour une image plus grande)

Ce modèle de fournisseur présente plusieurs avantages :

  1. Il gère l’activation et la durée de vie de chaque ResourceManager.
  2. Les expressions de localisation et d’autres API de ressources tirent parti du fournisseur pour trouver des ressources, augmentant ainsi la productivité grâce à des API simplifiées et abstraites.
  3. Le modèle de fournisseur est extensible, ce qui permet de changer l’emplacement où nous stockons les ressources, tout en continuant à tirer parti des fonctionnalités de productivité de ASP.NET 2.0.

Maintenant, je vais découvrir comment créer un fournisseur de ressources personnalisé.

Création d’un fournisseur de ressources de base de données

Avec un fournisseur de ressources personnalisé, vous pouvez accéder à des ressources qui ne proviennent pas de App_GlobalResources ou de App_LocalResources. Par exemple, vous pouvez utiliser un fournisseur de ressources personnalisé pour accéder aux ressources déployées dans des assemblys précompilés, ou pour accéder au contenu à partir d’une base de données. Dans cette section, j’aborderai le modèle de fournisseur de ressources de base de données, et plus loin dans cet article, je parlerai de l’accès aux assemblys de ressources externes.

Un fournisseur de ressources personnalisé comprend un ResourceProviderFactory et au moins un type de fournisseur de ressources qui implémente l’interface IResourceProvider . La fabrique est chargée d’instancier l’IResourceProvider approprié pour accéder aux ressources locales ou globales. La figure 4 illustre les composants qui composent l’implémentation du modèle de fournisseur de ressources de base de données dans l’exemple de code de cet article.

Aa905797.exaspnet20rpm04(en-us,MSDN.10).gif

Figure 4. Hiérarchie de composants pour le modèle de fournisseur de ressources de base de données personnalisé

Entrées de ressource de base de données

Il peut être utile d’abord de passer en revue la structure de la table de base de données qui stockera les entrées de ressources réelles. L’exemple inclut un script SQL pour créer une base de données nommée CustomResourceProvidersSample, avec une table nommée StringResources. Le tableau 1 comprend les champs suivants :

Tableau 1. Table de base de données avec entrées de ressource

Champ Description
resourceType Catégorie pour chaque ressource. Il peut être utilisé pour distinguer les ressources locales pour différentes pages ou types de ressources globales par un nom défini par l’utilisateur.
cultureCode Code de culture à partir des codes CultureInfo pris en charge utilisés par .NET, en fonction des normes ISO. Cela peut également être étendu pour tous les codes manquants.
Resourcekey Clé de ressource utilisée pour récupérer des ressources.
resourceValue Valeur de ressource. Ce tableau prend en charge les chaînes jusqu’à 4 Ko.

Dans cet exemple, toutes les ressources sont stockées dans une seule table, bien que, dans des environnements plus complexes ou à grande échelle, il soit possible de la répartir entre plusieurs tables pour optimiser les modèles d’utilisation classiques. La clé primaire de la table est une clé composite comprenant resourceType, cultureCode et resourceKey. Les valeurs de ressource uniques sont généralement demandées à l’aide de la clé primaire. La figure 5 montre une vue partielle du contenu de la table.

Cliquez ici pour agrandir l’image

Figure 5. Vue partielle des entrées de ressources pour cet exemple (cliquez sur l’image pour une image plus grande)

ResourceType pour les ressources de page est le nom de la page, y compris son chemin relatif dans l’application (c’est-à-dire Expressions.aspx, SubDir1/Expressions.aspx). Cette convention va lever l’ambiguïté des pages du même nom appartenant à différents sous-répertoires, de la même façon que le modèle de fournisseur de ressources par défaut attend un assembly de ressources local différent par sous-répertoire. Les clés de ressource pour les propriétés de contrôle suivent la même convention d’affectation de noms que les ressources de page standard, en utilisant un préfixe de contrôle et un nom de propriété avec la syntaxe suivante.

  [Prefix].[PropertyName]

Les ressources globales ont un resourceType défini par l’utilisateur. L’exemple de code comporte plusieurs catégories de ressources globales : Glossaire, CommonTerms et Config. Dans ce cas, les clés de ressource sont nommées intuitivement pour leur contenu.

La couche d’accès aux données, StringResourcesDALC, extrait le travail de récupération des ressources de cette table, en fonction des modèles d’utilisation du modèle de fournisseur.

Extension de ResourceProviderFactory

Le type ResourceProviderFactory est le hub pour l’accès aux ressources dans ASP.NET 2.0, chargé de retourner un fournisseur de ressources global ou local en fonction du type de ressource demandé. ResourceProviderFactory est un type de base abstrait qui nécessite une implémentation pour deux méthodes : CreateLocalResourceProvider() et CreateGlobalResourceProvider(). Pour créer une fabrique de fournisseur personnalisée, vous héritez de ce type de base en fournissant une implémentation pour ces méthodes. Les deux méthodes doivent retourner une instance d’un fournisseur de ressources qui implémente l’interface IResourceProvider.

La déclaration de type ResourceProviderFactory de base est indiquée dans la liste 1.

Liste 1. Type abstrait ResourceProviderFactory

  public abstract class ResourceProviderFactory
{
      protected ResourceProviderFactory();
      public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
      public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}

ResourceProviderFactory fournit des fournisseurs de ressources à l’étape d’analyse de page de la compilation et au moment de l’exécution pour les appels d’API de localisation.

  • Analyseur de page : la page est analysée au moment de la conception et en tant que précurseur de la compilation de pages. Les expressions explicites pour les ressources locales et globales sont validées au cours de ce processus. Pendant la compilation, le code est généré dans la page compilée pour toutes les expressions. Les fournisseurs de ressources sont utilisés par l’analyseur pendant ce processus.
  • Exécution : au moment de l’exécution, les expressions n’ont plus de signification dans les pages compilées. Le code généré pendant la compilation utilise l’API de localisation pour accéder aux ressources locales et globales. Un fournisseur de ressources est créé pour les types de ressources locaux et globaux.

Dans l’exemple de code, DBResourceProviderFactory crée un DBResourceProvider pour les deux chemins. En effet, les ressources locales et globales sont accessibles de la même façon. Le code de DBResourceProviderFactory est affiché dans la liste 2.

Liste 2. DBResourceProviderFactory est une implémentation personnalisée de ResourceProviderFactory qui prend en charge les ressources de base de données.

  using System;
using System.Web.Compilation;
using System.Web;
using System.Globalization;

namespace CustomResourceProviders
{
  public class DBResourceProviderFactory : ResourceProviderFactory
  {

    public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
    {
      return new DBResourceProvider(classKey);
    }

    public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
    {
      string classKey = virtualPath;
      if (!string.IsNullOrEmpty(virtualPath))
      {
        virtualPath = virtualPath.Remove(0, 1);
        classKey = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1);
      }
      return new DBResourceProvider(classKey);
    }
  }
}

Pour les expressions implicites ou explicites qui appellent des ressources locales, GetLocalResourceProvider() est appelé pour créer le fournisseur de la page. Voici un exemple d’expression implicite et d’expression explicite utilisant des ressources locales, comme défini dans la page Expressions.aspx de l’exemple de code.

  <asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

GetLocalResourceProvider() prend un seul paramètre, le chemin d’accès virtuel de la page, y compris le répertoire de l’application. Les deux expressions ci-dessus passent « /LocalizedWebSite/Expressions.aspx » à ce paramètre. Dans la figure 5, vous pouvez voir que les ressources locales sont stockées à l’aide d’un resourceType qui représente le chemin d’accès relatif de la page, sans le répertoire de l’application. Ainsi, GetLocalResourceProvider() supprime le répertoire de l’application du chemin d’accès avant de créer une instance de DBResourceProvider.

Pour les expressions explicites qui demandent des ressources globales, le type de ressource spécifié directement dans l’expression est passé à GetGlobalResourceProvider(). Considérez l’expression explicite suivante (également à partir de Expressions.aspx dans l’exemple de code).

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

Le type de ressource dans ce cas est CommonTerms. Par conséquent, GetGlobalResourceProvider() est appelé passage de CommonTerms comme paramètre. Une instance de DBResourceProvider est créée pour ce type.

Pour un type de ressource donné, une seule instance de DBResourceProvider est créée. Une fois créé, il est mis en cache pour une utilisation ultérieure. Par conséquent, la fabrique est appelée uniquement si le fournisseur instance n’existe pas encore dans le cache. Le processus de création et de mise en cache du fournisseur est encapsulé dans l’API de localisation utilisée pour accéder aux ressources.

ResourceProviderFactory Configuration

Le runtime utilise ResxResourceProviderFactory, sauf si vous spécifiez un autre type ResourceProviderFactory dans la configuration. La <section globalisation> du fichier de configuration web a un attribut nommé resourceProviderFactoryType. Ici, vous spécifiez le type ResourceProviderFactory à utiliser. Pour configurer DBResourceProviderFactory, vous devez ajouter le paramètre suivant.

  <system.web>

...other settings

      <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.DBResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
</system.web>

Note Dans l’exemple de code fourni, DBResourceProviderFactory appartient à l’espace de noms CustomResourceProviders , dans l’assembly CustomResourceProviders . Cet assembly porte un nom fort et peut être installé dans le Global Assembly Cache (GAC).

À présent, DBResourceProviderFactory sera utilisé pour créer des fournisseurs de ressources pendant l’analyse de page et au moment de l’exécution.

Implémentation d’IResourceProvider

Au cœur du modèle de fournisseur de ressources se trouve le type de fournisseur de ressources. Bien que ResourceProviderFactory soit une abstraction importante, les fournisseurs de ressources sont en fin de compte responsables du retour des entrées de ressources au moment de l’exécution, à partir de l’endroit où elles sont stockées. Comme indiqué dans la section précédente, les fournisseurs sont créés par l’implémentation ResourceProviderFactory , puis mis en cache pour une utilisation ultérieure. À partir du modèle de fournisseur de ressources de base de données illustré dans la figure 4, vous pouvez voir que le type DBResourceProvider prend en charge les ressources locales et globales. Ce type est chargé de récupérer les ressources de la base de données, mais il utilise les composants DBResourceReader et StringResourcesDALC pour gérer cette tâche.

Les fournisseurs de ressources implémentent l’interface IResourceProvider , illustrée dans la liste 3.

Listing 3. Interface IResourceProvider

  public interface IResourceProvider
{
      object GetObject(string resourceKey, CultureInfo culture);
      IResourceReader ResourceReader { get; }
}

Les ressources individuelles sont récupérées via GetObject(), et la propriété ResourceReader doit retourner une collection de ressources en fonction du type de ressource du fournisseur instance.

Pendant l’étape d’analyse de page, les fournisseurs sont utilisés pour récupérer toutes les ressources locales d’une page ; les expressions explicites sont validées ; et, pendant la compilation, du code est également généré pour la page. Pour les ressources locales, le lecteur de ressources est utilisé pour générer du code pour les expressions implicites. Les expressions explicites pour les ressources locales et globales sont validées individuellement avec un appel à GetObject() sur le fournisseur approprié.

Au moment de l’exécution, le code généré par l’analyseur déclenche des appels à GetObject() pour récupérer des ressources locales et globales à mesure que la page est initialisée.

Récupération de ressources de base de données individuelles

L’implémentation DBResourceProvider pour GetObject() est la suivante.

  public object GetObject(string resourceKey, CultureInfo culture)
{

  if (string.IsNullOrEmpty(resourceKey))
  {
    throw new ArgumentNullException("resourceKey");
  }  
  
  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }
            
  string resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);
}

En fait, le travail de récupération des ressources est délégué au type StringResourcesDALC (voir la figure 4) pour gérer les requêtes de base de données. Ce composant isole le fournisseur de la ressource de secours et d’autres logiques requises pour rechercher la ressource réelle.

GetResourceByCultureAndKey() initialise la connexion de base de données et exécute un SqlDataReader pour récupérer des valeurs, y compris la logique de secours de ressource requise (à aborder ultérieurement).

Récupération de ressources dans Batch

DBResourceProvider retourne une instance de DBResourceReader dans l’implémentation suivante de la propriété ResourceReader.

  public System.Resources.IResourceReader ResourceReader
{
  get
  {
    ListDictionary resourceDictionary = this.m_dalc.GetResourcesByCulture(CultureInfo.InvariantCulture);

    return new DBResourceReader(resourceDictionary);
  }
}

StringResourcesDALC est responsable de la collecte des ressources par défaut pour un type particulier (InvariantCulture). Le ListDictionary créé à partir des résultats de la requête est encapsulé par dbResourceReader pour l’énumération.

DBResourceReader implémente IResourceReader. Les éléments clés de cette implémentation sont présentés ici.

  public class DBResourceReader : DisposableBaseType, IResourceReader, IEnumerable<KeyValuePair<string, object>>
{
  private ListDictionary m_resourceDictionary;
  public DBResourceReader(ListDictionary resourceDictionary)
  {
    this.m_resourceDictionary = resourceDictionary;
  }

  public IDictionaryEnumerator GetEnumerator()
  {
    return this.m_resourceDictionary.GetEnumerator();
  }

  // other methods

}

L’analyseur de page utilise l’énumérateur de dictionnaire du lecteur pour générer du code pour les expressions implicites. Si aucun lecteur n’est fourni ou s’il contient un dictionnaire vide, le code ne peut pas être généré. Les expressions implicites ne nécessitent pas qu’une valeur soit présente pour chaque valeur de propriété, car elle n’est pas explicite. Ainsi, pour les expressions implicites, les valeurs de propriété par défaut sont affichées avec la page si aucun code n’est généré pour définir la valeur.

Ressource de secours

Le secours des ressources est une partie importante de l’implémentation du fournisseur de ressources. Les ressources sont demandées au moment de l’exécution, en fonction de la culture d’interface utilisateur actuelle pour le thread de requête.

System.Threading.Thread.Current.CurrentUICulture

Si cette culture est une culture spécifique telle que « es-EC » ou « es-ES », le fournisseur de ressources doit rechercher si une ressource existe pour cette culture spécifique. Il est possible, cependant, que les ressources aient été spécifiées uniquement pour la culture neutre, « es ». La culture neutre est le parent. Si des entrées spécifiques sont introuvables, le parent est vérifié ensuite. Au final, la culture par défaut de l’application doit être utilisée afin qu’une valeur soit trouvée. Dans cet exemple, la culture par défaut est « en ».

La ressource de secours est encapsulée par le composant d’accès aux données , StringResourcesDALC. Lorsqu’un appel est effectué pour récupérer une ressource, GetResourceByCultureAndKey() est appelé. Cette fonction est responsable de l’ouverture d’une connexion de base de données, de l’appel d’une fonction récursive qui effectue le secours des ressources, puis de la fermeture de la connexion à la base de données. L’implémentation de GetResourceByCultureAndKey() est illustrée ici.

  public string GetResourceByCultureAndKey(CultureInfo culture, string resourceKey)
{
  string resourceValue = string.Empty;

  try
  {
    if (culture == null || culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }

    this.m_connection.Open();
    resourceValue = this.GetResourceByCultureAndKeyInternal
(culture, resourceKey);
  }
  finally
  {
    this.m_connection.Close();
  }
  return resourceValue;
}

La fonction récursive , GetResourceByCultureAndKeyInternal(), tente d’abord de trouver la ressource par la culture spécifiée. Si cela est introuvable, la culture parente est recherchée et la requête est retentée. En cas d’échec, la culture par défaut est utilisée dans une dernière tentative de recherche d’une entrée de ressource. Lorsqu’une entrée de ressource est introuvable pour la culture par défaut, cela est considéré comme une exception irrécupérable dans cet exemple. La description de GetResourceByCultureAndKeyInternal() est affichée ici.

  private string GetResourceByCultureAndKeyInternal
(CultureInfo culture, string resourceKey)
{

  StringCollection resources = new StringCollection();
  string resourceValue = null;

  this.m_cmdGetResourceByCultureAndKey.Parameters["cultureCode"].Value 
= culture.Name;
                
  this.m_cmdGetResourceByCultureAndKey.Parameters["resourceKey"].Value 
= resourceKey;

  using (SqlDataReader reader = this.m_cmdGetResourceByCultureAndKey.ExecuteReader())
  {
    while (reader.Read())
    {
      resources.Add(reader.GetString(reader.GetOrdinal("resourceValue")));
    }
  }

  if (resources.Count == 0)
  {
    if (culture.Name == this.m_defaultResourceCulture)
    {
      throw new InvalidOperationException(String.Format(
Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DefaultResourceNotFound, resourceKey));
    }

    culture = culture.Parent;
    if (culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }
    resourceValue = this.GetResourceByCultureAndKeyInternal(culture, resourceKey);
  }
  else if (resources.Count == 1)
  {
    resourceValue = resources[0];
  }
  else
  {
    throw new DataException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DuplicateResourceFound, resourceKey));
  }

  return resourceValue;
}

En guise d’alternative, les ressources de secours peuvent également être encapsulées dans une procédure stockée ou un composant SQL CLR, car les règles de secours sont susceptibles d’être couplées à la conception de la base de données dans une certaine mesure, ce qui n’est pas nécessairement important pour la couche entreprise.

Mise en cache des ressources

Avec le modèle de fournisseur par défaut, lorsque des ressources sont extraites d’assemblys de ressources, les assemblys sont chargés une seule fois et mis en cache dans le domaine d’application. Pour les ressources de base de données, nous devons implémenter notre propre mécanisme de mise en cache pour éviter d’atteindre la base de données pour chaque demande d’une ressource. DBResourceProvider gère cette tâche.

Plus tôt, j’ai montré à quoi ressemblait l’implémentation GetObject() pour le fournisseur, sans mise en cache. Les ressources sont récupérées de la base de données avec un appel à la couche d’accès aux données, comme suit.

    resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

N’oubliez pas qu’un seul fournisseur instance existe par type de ressource et qu’il est mis en cache pour une utilisation répétée. À l’intérieur du fournisseur, si nous mettons en cache les entrées de ressources dans un dictionnaire pour chaque culture demandée, ces entrées de dictionnaire sont mises en cache avec le fournisseur en mémoire. Le code permettant de récupérer un objet peut d’abord rechercher la valeur dans le cache du dictionnaire et, s’il est introuvable, créer une entrée mise en cache après l’avoir récupérée à partir de la base de données. Le résultat est indiqué ici.

  string resourceValue = null;
Dictionary<string, string> resCacheByCulture = null;
if (m_resourceCache.ContainsKey(culture.Name))
{
  resCacheByCulture = m_resourceCache[culture.Name];
  if (resCacheByCulture.ContainsKey(resourceKey))
  {
    resourceValue = resCacheByCulture[resourceKey];
  }
}

if (resourceValue == null)
{
  resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

  lock(this)
  {
    if (resCacheByCulture == null)
    {
      resCacheByCulture = new Dictionary<string, string>();
      m_resourceCache.Add(culture.Name, resCacheByCulture);
    }
  resCacheByCulture.Add(resourceKey, resourceValue);
  }
}

return resourceValue;

La mise en cache est une partie nécessaire des performances lors du stockage des ressources dans la base de données. Dans cet exemple, les valeurs sont mises en cache jusqu’à ce que le domaine d’application soit libéré, ce qui signifie que les mises à jour dynamiques des ressources de la base de données ne seront pas reflétées au moment de l’exécution, sauf si vous redémarrez l’application. Pour permettre ce type de mise à jour dynamique, un travail supplémentaire est nécessaire pour mettre en cache les ressources avec une dépendance de cache de base de données.

Cohérence de thread

Une autre préoccupation que nous avons dans un environnement web est la sécurité des threads. Les composants participant au modèle de fournisseur de base de données de la figure 4 sont conçus pour être thread-safe à l’aide des techniques de synchronisation .NET.

Une instance de DBResourceProvider ou StringResourcesDALC pour un type de ressource particulier peut être appelée par plusieurs threads, simplement par deux requêtes pour la même page. Dans le composant StringResourcesDALC, les méthodes publiques qui récupèrent des données de la base de données modifient instance variables pour le type, notamment l’ouverture de la connexion, la définition de valeurs de paramètre de requête et l’exécution de SqlDataReader. Pour rendre ces fonctions thread-safe, l’attribut MethodImplAttribute a été appliqué.

  [MethodImpl(MethodImplOptions.Synchronized)]

Cet attribut verrouille l’objet StringResourcesDALC pendant la durée de l’appel de méthode, ce qui bloque les autres appelants. Si les ressources sont mises en cache, le composant d’accès aux données n’est pas appelé, ce qui améliore les performances.

Dans DBResourceProvider, les modifications apportées au cache du dictionnaire sont également verrouillées avec une instruction de verrou classique.

    lock(this)
   { ... }

La vue développée de ce code de mise en cache est présentée dans la section précédente. L’instruction lock verrouille l’objet entier et ses membres pendant la durée du bloc de code. Cela signifie qu’un seul thread peut ajouter des valeurs au cache à la fois.

Utilisation du fournisseur de ressources personnalisées

Dans le passé, nous avons utilisé pour coder manuellement l’instanciation et la gestion de la durée de vie d’un ResourceManager pour chaque type de ressource. Avec ASP.NET 2.0, le modèle de fournisseur de ressources gère cela pour nous, en créant et en mettant en cache des fournisseurs de ressources à la demande, tant que nous programmons par rapport aux API de localisation. Cela signifie que nous devons utiliser ASP.NET techniques 2.0 pour accéder aux ressources avec :

  • Méthodes page-objet.
  • Méthodes HttpContext .
  • Expressions de localisation.

Les pages maîtres, les pages web et les contrôles utilisateur partagent tous un type de base commun, TemplateControl. Ce type de base fournit les deux opérations surchargées pour l’accès aux ressources que j’ai mentionnées précédemment : GetLocalResourceObject() et GetGlobalResourceObject(). Ces opérations utilisent des fournisseurs de ressources mis en cache pour récupérer des ressources via l’implémentation GetObject() du fournisseur. Si le fournisseur n’a pas encore été mis en cache, ResourceProviderFactory est utilisé pour le créer à l’aide de CreateLocalResourceProvider() ou CreateGlobalResourceProvider(). L’avantage de cette approche est que vous pouvez écrire facilement du code de page pour récupérer des valeurs de ressource.

  this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

En fait, un code très similaire est généré pour chaque page à partir d’expressions implicites et explicites pendant la compilation.

Vous pouvez également accéder aux ressources locales et globales par le biais de méthodes statiques sur le type HttpContext , utiles pour écrire du code qui ne fait pas partie d’une page.

  this.labHelloLocal.Text = HttpContext.GetLocalResourceObject("/RuntimeCode.aspx", "labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = HttpContext.GetGlobalResourceObject("CommonTerms", "Hello") as string;

En réalité, les expressions de localisation sont un moyen beaucoup plus pratique d’accéder aux ressources. Les expressions de localisation fournissent un modèle déclaratif qui génère automatiquement du code pour accéder aux ressources via l’API de localisation. Ainsi, tous les chemins mènent à l’API de localisation et à l’objet ResourceProviderFactory configuré.

Ressources de base de données : avantages et inconvénients

Le déplacement de ressources vers la base de données offre un certain nombre d’avantages, notamment les suivants :

  • Vous pouvez introduire des exigences hiérarchiques complexes pour les ressources sans impact sur le code appelant, par exemple en autorisant la personnalisation des chaînes par défaut par le client ou le service, tout en autorisant chacune d’entre elles à traduire ces chaînes.
  • Des blocs plus importants de contenu HTML peuvent être gérés plus facilement au niveau de la base de données pour des raisons de organization de contenu et plus de flexibilité avec la mise en cache et l’utilisation de la mémoire (par défaut, n’oubliez pas que les ressources satellites sont chargées dans le domaine d’application avec tout leur contenu incorporé, tandis que les ressources de base de données peuvent être mises en cache ou libérées du cache à l’aide d’un algorithme plus affiné).
  • Le stockage d’informations dans un emplacement unique (la base de données) au lieu de nombreux fichiers .resx peut améliorer la facilité de gestion globale d’une application localisée. Il peut également simplifier votre approche de l’utilisation des traducteurs.

Le stockage de base de données présente également quelques inconvénients :

  • Cela nécessite certainement une planification et une planification supplémentaires. Comment les ressources seront-elles organisées en tables ? Doivent-ils tous être regroupés dans une seule table ? Doivent-ils être organisés par catégorie ? Doivent-ils être accessibles par le biais d’une seule procédure stockée ou d’un composant SQL CLR, pour assurer une distribution ultérieure entre les tables ?
  • Vous devez effectuer un certain travail pour intégrer les fonctionnalités de productivité de Visual Studio 2005 à vos ressources de base de données. Autrement dit, vous ne générerez pas automatiquement des ressources dans la base de données avec Générer des ressources locales, ni n’afficherez les informations de base de données dans la boîte de dialogue Expression, entre autres choses. Ce niveau d’intégration vous oblige à créer des composants personnalisés qui s’intègrent à l’expérience au moment du design pour les développeurs, ce que je vais aborder dans mon prochain article.

Malgré le travail nécessaire pour intégrer des fonctionnalités de productivité aux ressources de base de données, on pourrait faire valoir que les avantages l’emportent sur ces problèmes. De plus, être obligé de planifier la structure et la organization des ressources est quelque chose que nous « devrions » faire, même pour la structure d’allocation de ressources par défaut !

Accès aux ressources à partir d’assemblys externes

Vous pouvez également utiliser le modèle de fournisseur de ressources pour accéder aux ressources à partir d’assemblys externes précompilés. Cela permet de partager des ressources communes entre les applications Web et Windows, tout en fournissant une unité unique pour le contrôle de version et le déploiement. Dans cette section, je vais vous expliquer comment appliquer les concepts qui viennent d’être abordés pour accéder à ce type d’assembly de ressources externes.

La figure 6 montre les composants qui composent ce modèle de fournisseur de ressources externe.

Cliquez ici pour agrandir l’image

Figure 6. Hiérarchie des composants pour le modèle de fournisseur de ressources externes (cliquez sur l’image pour une image plus grande)

Vous remarquerez quelques points sur cette implémentation :

  • Seules les ressources globales sont prises en charge. Il n’est pas judicieux de remplacer le modèle de ressource de page fourni gratuitement par ASP.NET 2.0. Seules les ressources globales sont extraites d’assemblys de ressources externes.
  • Nous ne pouvons pas accéder à LocalResXResourceProvider à partir de ExternalResourceProviderFactory. Il s’agit d’un type interne qui n’est pas disponible pour la construction à partir de notre code. Si nous remplaçons le fournisseur par défaut par ExternalResourceProviderFactory, seules les ressources globales seront prises en charge (j’en parlerai dans une section ultérieure).
  • ResourceManager est utilisé pour accéder aux ressources. Le ResourceManager par défaut nous donne déjà un moyen d’accéder aux ressources à partir d’assemblys. Nous n’avons donc pas besoin de remplacer cette fonctionnalité pour accéder à des ressources externes.

Maintenant, je vais vous montrer les points forts de cette implémentation.

Mêmes expressions de localisation, cas d’usage différent

N’oubliez pas que les fournisseurs de ressources sont appelés en raison des expressions de localisation et de l’API de localisation. Pour accéder aux ressources externes, des expressions explicites sont utilisées. Ces expressions seront similaires à celles utilisées pour accéder aux ressources globales, avec quelques modifications mineures ; Plus précisément, le nom de l’assembly doit être fourni avec le type de ressource. Le fournisseur par défaut savait comment trouver l’assembly de ressources global. Ce fournisseur de ressources externes s’appuie sur le nom de l’assembly pour obtenir le même résultat.

La syntaxe d’une expression $Resources pour le modèle de fournisseur par défaut (ressources globales explicites) est la suivante.

  <%$ Resources: [resourceType], [resourceKey] %>

La même expression peut être utilisée pour accéder à des ressources externes lorsque ExternalResourceProviderFactory est configuré, avec la modification de syntaxe suivante.

  <%$ Resources: [assemblyName]|[resourceType], [resourceKey] %>

Par exemple, pour récupérer une ressource à partir de l’assembly CommonResources.dll , à partir du type de ressource global « CommonTerms », vous devez utiliser l’expression explicite suivante.

  <asp:Label ID="labGlobalResource" runat="server" Text="<%$ Resources:CommonResources|CommonTerms, Hello %>" ></asp:Label>

Cela génère le code suivant, lorsque la page est compilée.

  labGlobalResource.Text = this.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello");

Cela montre que le modèle de fournisseur de ressources externes peut tirer parti des expressions et du code existants de l’API de localisation, à condition que les informations appropriées soient fournies. Il s’agit de l’ExternalResourceProvider qui analyse finalement les informations pour séparer le nom de l’assembly du type de ressource.

ExternalResourceProviderFactory

Comme DBResourceProviderFactory, ExternalResourceProviderFactory hérite de ResourceProviderFactory et a des remplacements pour CreateGlobalResourceProvider() et CreateLocalResourceProvider(). La liste 4 montre l’implémentation complète.

Liste 4. Implémentation de ExternalResourceProviderFactory

  public class ExternalResourceProviderFactory : ResourceProviderFactory
{

  public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
  {
    return new GlobalExternalResourceProvider(classKey);
  }

  public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
  {
    throw new NotSupportedException(String.Format
(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_LocalResourcesNotSupported, "ExternalResourceProviderFactory"));
  }
}

CreateGlobalResourceProvider() est chargé d’instancier le type GlobalExternalResourceProvider avec la clé de classe fournie. N’oubliez pas que la clé de classe de ce fournisseur doit inclure le nom de l’assembly et le type de ressource. CreateLocalResourceProvider() lève une exception NotSupportedException, car nous ne stockons pas les ressources locales dans des assemblys externes. Cela entraîne en fait une exception d’analyse si vous utilisez des expressions locales sur la page. Par conséquent, si vous souhaitez continuer à prendre en charge les ressources locales, ce n’est peut-être pas le scénario idéal pour raccorder ExternalResourceProvider. Plus tard, je vous montrerai comment éviter ce problème avec les expressions de localisation personnalisées.

Pour raccorder ExternalResourceProviderFactory à des expressions existantes et à l’API de localisation, nous revenons à la <section globalisation> du Web.config.

  <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.ExternalResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />

À présent, le modèle de fournisseur par défaut est remplacé par le modèle de fournisseur de ressources externe. Voyons comment fonctionne le fournisseur.

GlobalExternalResourceProvider

GlobalExternalResourceProvider implémente IResourceProvider. Ce fournisseur est très similaire à GlobalResXResourceProvider, à une exception près : ce fournisseur récupère des ressources globales à partir d’assemblys satellites préexistants et nécessite la connaissance d’un nom d’assembly spécifique où sont stockées les ressources.

Le constructeur de GlobalExternalResourceProvider reçoit un nom d’assembly et un type de ressource séparés par le symbole de canalisation (« | »). Ces informations sont analysées, comme illustré ici.

  public GlobalExternalResourceProvider(string classKey)
{
  if (classKey.IndexOf('|') > 0)
  {
    string[] textArray = classKey.Split('|');
    this.m_assemblyName = textArray[0];
    this.m_classKey = textArray[1];
  }
  else
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_InvalidConstructor, classKey));

}

Si le format du paramètre classKey passé au constructeur n’est pas valide, une exception ArgumentException est levée. Ainsi, l’analyseur de page signale une erreur pour les expressions explicites. Le code écrit directement sur l’API de localisation échoue au moment de l’exécution.

Un fournisseur instance est créé et mis en cache pour chaque combinaison unique d’assembly et de type de ressource. Lorsqu’une ressource est demandée pendant l’analyse de page (pour validation) ou au moment de l’exécution, GetObject() est appelé, comme illustré ici.

  public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
  this.EnsureResourceManager();
  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }
  return this.m_resourceManager.GetObject(resourceKey, culture);
}

En interne, le GlobalExternalResourceProvider s’appuie sur les fonctionnalités existantes du type ResourceManager pour récupérer des ressources et gérer le secours des ressources. L’astuce consiste à créer un ResourceManager pour l’assembly approprié. La première fois que EnsureResourceManager() est appelé, il charge l’assembly de ressource et crée une instance du ResourceManager pour le type spécifié dans cet assembly. Une exception se produit si vous spécifiez un assembly qui ne contient pas le type de ressource. Le code permettant de charger l’assembly et de créer le ResourceManager s’affiche ici.

  Assembly asm = Assembly.Load(this.m_assemblyName);
ResourceManager rm = new ResourceManager(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", this.m_assemblyName, this.m_classKey), asm);
this.m_resourceManager = rm;

À l’aide de ExternalResourceProvider, vous pouvez récupérer des ressources à partir de n’importe quel assembly déployé dans le répertoire \bin de l’application web ou du Global Assembly Cache (GAC).

Le fournisseur retourne une exception NotSupportedException pour la propriété ResourceReader , car les ressources locales ne sont pas prises en charge ; par conséquent, les expressions de localisation implicite ne sont pas analysées.

Prise en charge des expressions de localisation personnalisées

La configuration d’un fournisseur personnalisé est idéale pour les situations dans lesquelles toutes les ressources sont stockées dans un autre emplacement et où vous ne prévoyez pas de tirer parti des ressources situées dans App_LocalResources et App_GlobalResources, respectivement. Que se passe-t-il si vous souhaitez prendre en charge l’implémentation standard pour les ressources locales et globales (fournisseur par défaut), tout en ayant la possibilité d’extraire certaines ressources d’une autre source (fournisseur personnalisé) ? Vous pouvez y parvenir en implémentant des expressions personnalisées qui ciblent le fournisseur de ressources personnalisé.

Fonctionnement de ResourceExpressionBuilder

Les expressions sont traitées par les générateurs d’expressions qui interagissent avec l’étape d’analyse de page, avant la compilation. Les expressions incluent tout ce qui est délimité avec <%$ %>, y compris les paramètres d’application, les chaînes de connexion et les expressions de localisation. La syntaxe de ces expressions est illustrée ici.

  <%$ [prefix]: [declaration] %>

Comme vous le savez, les expressions de localisation utilisent le préfixe « Resources ». L’analyseur de page utilise le type ResourceExpressionBuilder pour traiter ces expressions. Cela est dû au fait que ResourceExpressionBuilder a été mappé au préfixe « Resources » pour la valeur par défaut d’exécution de la <configuration expressionBuilders> .

  <expressionBuilders>
<add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
</expressionBuilders>

Pour les pages compilées, voici comment cela fonctionne :

  • Lorsqu’une page est analysée, l’opération ParseExpression du générateur d’expressions est appelée pour valider la syntaxe de l’expression. Si l’expression n’est pas valide (par exemple, la ressource spécifiée est introuvable), une erreur d’analyse est générée.
  • Si l’analyse réussit, l’opération GetCodeExpression du générateur d’expressions est appelée pour demander le code à générer pour l’expression. C’est là que le générateur d’expressions émet du code pour l’initialisation de page. Le code est injecté dans l’il compilé pour la page.

Lorsque la compilation de pages est désactivée, les expressions sont évaluées d’une manière légèrement différente. Vous pouvez désactiver la compilation de pages pour des pages individuelles.

  <%@ Page Language="C#" CompilationMode="Never" %>

Vous pouvez également désactiver la compilation pour toutes les pages du fichier Web.config.

  <pages compilationMode="Never" />

Dans ce cas, lorsque la page est demandée, elle est analysée ; Pendant l’étape d’analyse, la propriété SupportsEvaluate du générateur d’expressions est vérifiée pour voir si la page peut être traitée sans compilation. Le code n’est pas généré pour la page.

Au moment de l’exécution, SupportsEvaluate est à nouveau vérifié, suivi d’un appel à EvaluateExpression pour récupérer une valeur pour chaque expression de localisation.

ResourceExpressionBuilder est dérivé d’ExpressionBuilder. ExpressionBuilder est le type de base courant qui expose les méthodes abstraites et virtuelles, implémentées par ResourceExpressionBuilder pour prendre en charge l’analyse de page, la génération de code et l’évaluation des expressions. Ainsi, pour prendre en charge les expressions de localisation personnalisées, vous pouvez étendre ExpressionBuilder et fournir votre propre implémentation.

Extension d’ExpressionBuilder

Pour prendre en charge les expressions de localisation personnalisées, vous avez besoin d’une implémentation de générateur d’expressions personnalisée. Comme ResourceExpressionBuilder, vous pouvez étendre ExpressionBuilder et fournir une implémentation personnalisée pour l’analyse de pages, la génération de code et l’évaluation des expressions pour les pages non compilées.

Tout d’abord, examinons l’objectif du générateur d’expressions personnalisées dans cet exemple et la syntaxe attendue pour son implémentation. L’objectif est de laisser l’implémentation par défaut pour <%$ Resources %> intacte, tout en prenant en charge les ressources qui sont extraites d’un assembly externe. Pour ce faire, au lieu de remplacer complètement le fournisseur de ressources, nous allons créer une nouvelle expression pour la gérer. Cela signifie que nous avons besoin d’un nouveau préfixe d’expression, d’un ExpressionBuilder personnalisé et d’un moyen d’associer ce nouveau préfixe à l’ExpressionBuilder personnalisé.

Pour cet exemple, le nouveau préfixe est « ExternalResource ». La syntaxe requise pour cette nouvelle expression est illustrée ici.

  <%$ ExternalResource: [assemblyName]|[resourceType], [resourceKey] %>

This expression will draw resources from a specified assembly using the same GlobalExternalResourceProviderdiscussed earlier. To support this new expression, we'll create a custom type, ExternalResourceExpressionBuilder.Le tableau 2 récapitule les fonctionnalités à fournir par chacune des méthodes ExpressionBuilder remplacées.

Tableau 2. Résumé des fonctionnalités fournies par chaque méthode remplacée

Méthode Description
EvaluateExpression Retourne la valeur de ressource d’une expression ExternalResource dans des pages non compilées.
GetCodeExpression Retourne le code à émettre pour une expression ExternalResource . Ce code appelle le fournisseur de ressources personnalisé , GlobalExternalResourceProvider.
ParseExpression Valide une expression ExternalResource en essayant d’accéder aux ressources de l’expression. L’analyse de page échoue si la ressource est introuvable.
SupportsEvaluate, propriété Indique si l’évaluation de page non compilée est prise en charge. Dans cette implémentation, retourne true.

À l’aide de ExternalResourceExpressionBuilder, des expressions de localisation personnalisées telles que les suivantes peuvent être déclarées.

  <asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

N’oubliez pas que les expressions sont analysées au moment du design et avant la compilation. ParseExpression est appelé pendant l’analyse de page pour vérifier que l’expression de ressource est exacte et que la ressource demandée existe réellement. Le code suivant illustre cette implémentation.

  public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
{
  if (string.IsNullOrEmpty(expression))
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture,Properties.Resources.Expression_TooFewParameters, expression));
  }

  ExternalResourceExpressionFields fields = null;
  string classKey = null;
  string resourceKey = null;
            
  string[] expParams = expression.Split(new char[] { ',' });
  if (expParams.Length > 2)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooManyParameters, expression));
  }
  if (expParams.Length == 1)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooFewParameters, expression));
  }
  else
  {
    classKey = expParams[0].Trim();
    resourceKey = expParams[1].Trim();
  }

  fields = new ExternalResourceExpressionFields(classKey, resourceKey);

              
  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider rp = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);
            
  object res = rp.GetObject(fields.ResourceKey, CultureInfo.InvariantCulture);
  if (res == null)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_ResourceNotFound, fields.ResourceKey));
  }
  return fields;
}

La majeure partie du code est axée sur la validation de l’expression, mais le cœur de celle-ci est la création de GlobalExternalResourceProvider, ainsi qu’un appel à GetObject() pour récupérer la ressource.

Lorsque les pages sont compilées, l’analyse des pages est suivie de la génération de code. À ce stade, l’implémentation GetCodeExpression du générateur d’expressions est appelée. Cette opération retourne le code nécessaire pour récupérer la valeur de ressource au moment de l’exécution, comme illustré ici.

  public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

 CodeMethodInvokeExpression exp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(ExternalResourceExpressionBuilder)), "GetGlobalResourceObject", new CodePrimitiveExpression(fields.ClassKey), new CodePrimitiveExpression(fields.ResourceKey));

 return exp;
}

La sortie de GetCodeExpression génère du code similaire au code en gras, illustré ici.

  labExternalResource.Text = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

Vous remarquerez que le code généré s’appuie sur une méthode statique implémentée par ExternalResourceExpressionBuilder. GetGlobalResourceObject est une méthode d’assistance qui instancie globalExternalResourceProvider et récupère l’entrée de ressource. Pour les pages compilées, ce code récupère des valeurs à partir de ressources externes au moment de l’exécution.

Pour les pages non compilées, les expressions sont évaluées au moment de l’exécution avec un appel à EvaluateExpression. ExternalResourceExpressionBuilder implémente un remplacement pour EvaluateExpression qui utilise à nouveau globalExternalResourceProvider pour récupérer la ressource appropriée.

  public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider provider = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);

  return provider.GetObject(fields.ResourceKey, null);
}

Après avoir configuré le générateur d’expressions personnalisées, vous pouvez inclure librement des instructions déclaratives pour récupérer des ressources à partir d’assemblys externes, tandis que les expressions de localisation par défaut sont toujours utilisées pour récupérer des valeurs de App_LocalResources ou de App_GlobalResources.

Configuration d’ExpressionBuilder

Pour configurer un générateur d’expressions personnalisées, vous l’ajoutez à la <section expressionBuilders> dans le Web.config. Dans cet exemple, nous associons ExternalResourceExpressionBuilder au préfixe « ExternalResources » à cette configuration.

  <expressionBuilders>
  <add expressionPrefix="ExternalResources" type="CustomResourceProviders.ExternalResourceExpressionBuilder, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1"/>
</expressionBuilders>

À présent, toutes les expressions de ressource qui utilisent le préfixe « ExternalResources » sont analysées ou évaluées en fonction de l’implémentation ExternalResourceExpressionBuilder décrite dans la section précédente.

Accès aux ressources locales, globales et externes

La liste 5 illustre l’application des trois expressions de localisation (implicite, explicite et explicite personnalisée) pour extraire des ressources de sources par défaut et personnalisées.

Liste 5. Expressions de localisation implicites, explicites et personnalisées dans une seule page

  <asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

L’utilisation d’une expression de localisation personnalisée pour les ressources externes au lieu de configurer un fournisseur de ressources de remplacement vous permet d’utiliser des ressources locales pour chaque page et des ressources globales compilées avec le site Web, tout en ayant la possibilité d’accepter des ressources à partir d’assemblys externes (ou ailleurs). À l’aide de ExternalResourceExpressionBuilder, vous pouvez également accéder aux ressources externes directement à partir du code, à l’aide de la méthode d’assistance statique mentionnée précédemment, GetGlobalResourceObject().

  string s = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

À l’aide de cette technique, vous n’avez pas besoin de remplacer le fournisseur de ressources par défaut par votre propre fournisseur. Au lieu de cela, vous allez vous appuyer sur la génération de code à partir d’expressions de localisation personnalisées pour extraire des ressources à partir d’assemblys externes à la demande.

Conclusion

Dans cet article, vous avez appris à créer un modèle de fournisseur de ressources personnalisé pour accéder aux ressources à partir de la base de données ou à partir d’assemblys de ressources externes. Vous avez également appris à créer des expressions de localisation personnalisées pour incorporer un autre stockage de ressources avec le modèle de fournisseur par défaut. En utilisant les fonctionnalités d’extensibilité de ASP.NET 2.0, vous disposez d’alternatives très accessibles pour l’allocation et la récupération des ressources. Et la meilleure partie à ce sujet est la facilité avec laquelle vous pouvez vous connecter au modèle de programmation de ASP.NET naturel 2.0 à l’aide d’expressions de localisation déclaratives.

L’article suivant de cette série explore l’autre moitié de cette image : comment se connecter à l’expérience au moment du design pour créer des expressions de ressources et générer des ressources dans l’emplacement de stockage approprié.

Remerciements

Un grand merci à Simon Calvert et Eilon Lipton de Microsoft Corporation, qui ont fourni un soutien et des commentaires précieux sur cet article pour s’assurer qu’il a développé les nombreux cas d’usage par le biais du modèle de fournisseur.

Ressources supplémentaires

Blog de Michèle : www.dasblonde.net (RSS sur la mondialisation)

IDesign Inc

 

À propos de l’auteur

Michèle Leroux Bustamante est architecte en chef d’IDesign Inc., directeur régional Microsoft pour San Diego, MVP Microsoft pour les services web XML et directeur technique de BEA. Chez IDesign, Michèle fournit des services de formation, de mentorat et de conseil en architecture haut de gamme, axés sur les services Web, la conception d’architecture évolutive et sécurisée pour .NET, l’interopérabilité et l’architecture de globalisation. Elle est membre de l’International .NET Speakers Association (INETA), une conférencière de conférences fréquentes, et présidente de conférence de la piste des services web de SD, et est fréquemment publiée dans plusieurs grands journaux technologiques.

Michèle est également membre du conseil d’administration de l’International Association of Software Architects (IASA) et conseillère de programme pour UCSD Extension. Son livre sur microsoft Windows Communication Foundation (WCF), publié par O’Reilly, devrait être publié fin 2006 (blog de livre : www.thatindigogirl.com). Contactez-la à l’adresse mlb@idesign.net, ou visitez www.idesign.net et www.dasblonde.net.

© Microsoft Corporation. Tous droits réservés.