Автор:
- Мишель Леруа Бустаманте (Michele Leroux Bustamante), IDesign Inc
Опубликовано: Октябрь 2006 года
Продукты и технологии: Microsoft ASP.NET 2.0 Microsoft Visual Studio 2005 Локализация
В Microsoft ASP.NET 2.0 реализовано много новых возможностей для локализации веб-приложений. Однако даже при наличии всех этих возможностей вскоре после локализации веб-узла встает вопрос его расширения. В этой статье рассказывается об использовании расширяемости (extensibility) ASP.NET для решения задач по локализации на предприятии и для оптимизации процесса разработки локализованных решений.
На этой странице…
Введение
Что делать с ресурсами?
Модель поставщиков ресурсов
Создание поставщика ресурсов базы данных
Доступ к ресурсам во внешних сборках
Поддержка собственных выражений локализации
Заключение
Благодарность
Дополнительные ресурсы Введение
В ASP.NET 2.0 реализовано множество новых возможностей локализации веб-приложений. Я написала о них статье в «ASP.NET 2.0 Localization Features: A Fresh Approach to Localizing Web Applications» (“Возможности локализации в ASP.NET 2.0: Новый подход к локализации веб-приложений»), опубликованной в библиотеке MSDN.
Посмотрев на эти новые возможности локализации, можно сразу же заметить следующее:
- теперь в Microsoft Visual Studio 2005 очень легко можно генерировать ресурсы для каждой страницы, используя пункт меню Генерировать локальные ресурсы в режиме конструктора страниц;
- создание и использование глобальных ресурсов стало намного проще благодаря качественному редактору ресурсов и строго типизированному доступу;
- сопоставление записей ресурсов со свойствами элементов управления и областями наполнения элегантно осуществляется с помощью декларативных выражений локализации;
- больше не требуется создавать экземпляр диспетчера ресурсов вручную, поскольку фабрика ResXResourceProviderFactory координирует получение записей ресурсов от глобальных и локальных ресурсов, выделяя диспетчеры ресурсов по мере необходимости;
- автоматическое обнаружение параметров культуры в обозревателе и использование этих параметров в потоке команд запроса упрощает соответствие культурным предпочтениям пользователя, даже если пользователь анонимен.
Естественно, даже со всеми этими преимуществами, нам всегда нужно что-то еще. После локализации веб-узла с использованием всех этих новых возможностей вы начинаете беспокоиться о других вещах:
- Как извлекать ресурсы из альтернативного источника, например, из другой сборки или из базы данных?
- Как работать с гибридными средами, где используются локальные и глобальные ресурсы, а также альтернативные источники данных?
- Как контролировать источник ресурсов и при этом использовать модель поставщиков ресурсов в ASP.NET 2.0, выражения локализации и возможности конструктора?
- Как использовать имеющиеся возможности локализации и расширяемости, чтобы они оптимально соответствовали потребностям локализации и конфигурации среды разработки?
Именно поэтому расширяемость очень важна. Существует много способов расширить возможности локализации в ASP.NET при взаимодействии со средой разработки. Это первая статья в серии из трех статей, посвящённых возможностям расширяемости ASP.NET в приложении к потребностям локализации в масштабах предприятия и улучшения процесса локализации-разработки.
В этой статье я расскажу о возможностях, позволяющих извлекать ресурсы из альтернативных мест хранения и интегрировать процесс извлечения с проверкой синтаксиса страниц, компиляцией и выполнением кода. Я расскажу, как этого можно добиться, используя комбинацию собственных поставщиков ресурсов, конструкторов сообщений и других extensibility-типов. Во второй статье серии рассказывается, как улучшить процесс разработки, интегрируя те или иные ресурсы хранения данных со встроенными средствами повышения продуктивности Visual Studio 2005. В третьей статье рассказывается об альтернативных способах использования сложных иерархий ресурсов, например, для поддержки индивидуальной настройки клиентами.
Что делать с ресурсами?
Локализация ресурсов веб-узлов всегда составляла сложную задачу. Генерирование ресурсов всегда было сложной задачей, а организация ресурсов для целей перевода всегда должна быть управляемым процессом. Однако сложнее всего в работе с ресурсами веб-узла – знать, из чего должны состоять ресурсы, как следует распределять эти ресурсы и что обеспечит максимальную производительность и удобство в обслуживании.
Создание ресурсов и доступ к ресурсам в ASP.NET 2.0
ASP.NET 2.0 позволяет генерировать локальные ресурсы для каждой страницы. Эта возможность повышает эффективность дизайна страниц и интернационализации.
- Создание страниц осуществляется посредством сочетания статического кода HTML и серверных элементов управления ASP.NET.
- Для подготовки статических областей к локализации их нужно поместить в область действия элемента управления ASP.NET Localize.
- Для всех серверных элементов управления нужно указывать правильные имена, что позволяет легко распознавать сгенерированные обработчики событий и ключи ресурсов.
- Общие ресурсы нужно создавать в подкаталоге App_GlobalResources. В качестве ресурсов могут использоваться уже существующие файлы .resx или новые файлы .resx, созданные для хранения терминов, которые будут общими для нескольких страниц.
- Общие ресурсы нужно связывать со свойствами элементов управления, используя прямые выражения ресурсов, там, где это можно сделать. Лучше всего сделать это до генерирования локальных ресурсов страницы.
- Локальные ресурсы можно генерировать в режиме конструктора страниц с помощью команды меню Генерировать локальный ресурс.
После генерирования локальных ресурсов все локализуемые свойства страницы и элементов управления переносятся в отдельные файлы локальных ресурсов, по одному файлу на страницу. Прямое выражение локализации требует, чтобы синтаксический анализатор страницы генерировал код, который связывал бы значения ресурсов для элемента управления с соответствующими свойствами элемента управления, основываясь на общем префиксе. Рассмотрите возможность использования следующего прямого выражения со страницы Expressions.aspx в образце кода.
<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
Ресурсы хранятся в файле Expressions.aspx.resx в каталоге App_LocalResources. Ресурсы этого элемента управления Label используют префикс "labHelloLocalResource1"; например, свойство Text хранится в ключе "labHelloLocalResource1.Text".
При хорошей организации пользовательского интерфейса с использованием мастер-страниц и пользовательских элементов управления для общих участков пользовательского интерфейса, ресурсы для каждой мастер-страницы, каждого пользовательского элемента управления и каждой страницы также будут хорошо организованы (меньше пересечений). Это упростит организацию ресурсов, используемых каждым элементом страницы, хотя в прошлых версиях это было сопряжено с проблемами. Однако в некоторых случаях возникает желание получать ресурсы из общих источников. В этом случае можно использовать прямое выражение ресурсов, например, $Resources, как показано здесь.
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>
В этом случае ресурсы располагаются в файле CommonTerms.resx в каталоге App_GlobalResources. Прямые выражения этого типа можно создавать с помощью редактора выражений Expression Editor (см. статью в MSDN, упомянутую выше), упрощающего данную процедуру.
Как прямые, так и косвенные выражения активируют генерирование кода для получения значений ресурсов у поставщика ресурсов. Эти декларативные выражения в сочетании с кодом и генерацией ресурсов дают нам средство повышения продуктивности, которого у нас просто не было раньше, по крайней мере, для веб-приложений.
Сборки ресурсов и диспетчер ресурсов
Существует несколько способов компиляции и внедрения приложений ASP.NET 2.0:
- развертывание исходного кода и JIT-компиляция всего веб-узла;
- предварительная компиляция веб-узла, с возможностью дальнейшего обновления страниц и ресурсов;
- предварительная компиляция веб-узла с генерацией сборок для каждой страницы и каждого каталога.
Во всех случаях ресурсные сборки создаются для каждого каталога узла, а в параллельных каталогах для различных языков генерируются сопутствующие сборки на других языках. Даже при использовании JIT-компиляции узла результаты аналогичны. На рисунке 1 показан готовый результат для узла с двумя подкаталогами и одним переводом на испанский язык.
Рис. 1. Ресурсы и сопутствующие сборки генерируются для каждого каталога веб-узла ASP.NET.
Доступ к этим ресурсам осуществляется при запуске через Диспетчер ресурсов. Диспетчер ресурсов выделяется для каждого типа ресурсов (например, Page1.aspx и Page2.aspx) при запросе ресурсов. Ресурсные сборки, связанные с каждым типом ресурсов, загружаются в область приложений ASP.NET при первом доступе и остаются там до разгрузки области приложений. В ситуации на рисунке 1 при первом доступе к файлу \SubDir1\Page1.aspx для испанского языка, в область приложений загружается сборка \es\App_LocalResources.subdir1.cdcab7d2.resources.dll. Эта сборка содержит ресурсы на испанском языке для всех страниц в \SubDir1.
На рисунке 2 показано, как Диспетчер ресурсов осуществляет доступ к локальным ресурсам для определенных страниц. При загрузке страницы \SubDir1\Page1.aspx код, сгенерированный косвенными выражениями запускает фабрику ResXResourceProviderFactory, которая выводит поставщик LocalResXResourceProvider. Этот поставщик создает Диспетчер ресурсов для типа Page1.aspx в сборке App_LocalResources.subdir1.cdcab7d2. Если в потоке запроса указан испанский язык пользовательского интерфейса, в область приложения загружается сопутствующая ресурсная сборка из каталога \es. Если для указанного языка нет сопутствующей сборки, Диспетчер ресурсов возвращается к основной ресурсной сборке.
Рис. 2. Диспетчер ресурсов получает доступ к ресурсам из главной ресурсной сборки или из сопутствующих локализованных сборок после их загрузки в домен приложения.
Ресурсные и сопутствующие сборки загружаются в домен приложения. Каждый Диспетчер ресурсов (для каждой страницы или для каждого общего типа ресурсов) кэшируется и может многократно использоваться при последующих запросах к соответствующим ресурсам.
Описанное мной поведение описывает доступ к ресурсам с помощью стандартной модели поставщиков ресурсов в ASP.NET. Теперь поговорим о причинах, по которым вам может понадобиться отойти от этой схемы.
Зачем использовать альтернативные источники?
В ASP.NET 2.0 стандартная схема генерирования ресурсов и доступа к ресурсам во время запуска обеспечивает намного более высокое удобство для пользователей, чем раньше. Однако, даже с учетом этого, желательно изучить альтернативные варианты хранения ресурсов, что может быть связано со следующими причинами:
- Повторное использование имеющихся ресурсов, уже хранящихся в альтернативном источнике
- Практичность хранения больших блоков статических данных
- Управляемость
Существует две популярные альтернативы хранения ресурсов – внешние ресурсные сборки и базы данных.
Использование готовых ресурсов – У вас могут иметься готовые ресурсные сборки в старых приложениях, или сборки, которые могут использоваться как Windows-приложениями, так и веб-приложениями. Обычно при преобразовании кода ASP.NET 1.1 в ASP.NET 2.0 я рекомендую использовать файлы .resx из приложения 1.1 и скопировать их в каталог App_GlobalResources приложения 2.0. Эти файлы .resx можно скомпилировать с помощью приложения ASP.NET 2.0 и использовать через строго типизированные глобальные ресурсы. Однако при необходимости контроля версий готовых ресурсных сборок или использования одного экземпляра ресурсов для Windows-приложений и веб-приложений, этот вариант невозможен. Поэтому лучшим вариантом хранения является использование общих ресурсных сборок. Это означает, что необходим способ для извлечения ресурсов из этих сборок.
Сохранение ресурсов в базе данных – Хранение ресурсов в базе данных часто используется для размещения ресурсов веб-приложений по нескольким причинам. Как вы можете догадаться, использование ресурсных сборок – не самое идеальное решение для веб-узла с тысячами страниц и многими тысячами ресурсов. Это значительно увеличивает нагрузку на память при исполнении, не говоря уже о необходимости загрузки большого числа сборок в домен приложения. В обоих указанных случаях возможно значительное снижение производительности больших веб-узлов, и задержка времени при вызовах базы данных становится оправданной. Размещение ресурсов базы данных может обеспечить гибкую и управляемую среду для процесса локализации, позволяя сократить число дублирующихся ресурсов, а также использовать более сложные варианты кэширования и хранить большие блоки данных. Наконец, размещение ресурсов в базе данных поддерживает более сложные иерархии переведенных материалов, позволяя клиентам и отделам создавать собственные версии текста, которые также локализуются.
Схемы расширяемости для локализации в ASP.NET были специально разработаны для того, чтобы поддерживать альтернативные варианты хранения ресурсов, благодаря чему желаемый результат получить очень легко. Кроме того, разработчики могут не только получать ресурсы из альтернативных источников, но и генерировать их в альтернативных источниках, о чем я расскажу в следующей статье.
Какой бы способ вы не использовали для хранения ресурсов – во внешних ресурсных сборках или в базе данных – обычно ставится задача использоваться функций локализации ASP.NET 2.0. Задача заключается в том, чтобы использовать выражения локализации и API при доступе к ресурсам в любом месте. Это возможно благодаря механизму расширяемости (extensibility), о котором я расскажу ниже.
Модель поставщиков ресурсов
Как уже говорилось выше, Диспетчер ресурсов отвечает за извлечение ресурсов из сборок в процессе исполнения приложения. Извлечение соответствующего набора ресурсов осуществляется на основании языка пользовательского интерфейса запроса (Рисунок 2). Другими словами, если язык запроса соответствует языку в настройках пользователя, Диспетчер ресурсов содержит всю необходимую логику для выполнения операции возврата ресурсов и для выбора необходимого ресурса из соответствующей ресурсной сборки. До появления ASP.NET 2.0 требовалось писать собственный код для создания экземпляра Диспетчера ресурсов для каждого типа ресурсов, а также управлять циклом существования этого экземпляра При этом для каждого запроса страницы требовался дополнительный код для создания экземпляра Диспетчера ресурсов или доступа к нему и вызовам методов для доступа к записи ресурса. Для декларативной привязки ресурсов к элементам страницы можно использовать созданные утверждения связи данных. Однако для инициации связи данных на уровне страницы и распределения переменных связи данных также требуется дополнительный код.
В ASP.NET 2.0 можно использовать для извлечения ресурсов предназначенные для локализации функции API с любой страницы или из любого пользовательского элемента управления. Например, следующий код извлекает локальный ресурс страницы и глобальный ресурс.
this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;
this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string; Выше я говорила, что декларативные выражения локализации также можно использовать для установки свойств страниц и элементов управления на базе ресурсов. Косвенные выражения локализации, например:
<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
и прямые выражения локализации, например:
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>
генерируют коды вызова GetLocalResourceObject() и GetGlobalResourceObject(). Из этого очевидно, что в ASP.NET 2.0 доступ к ресурсам осуществляется с помощью этих методов, даже в случае использования удобных декларативных выражений.
Именно здесь и требуется модель поставщиков ресурсов. Эти вызовы API используют стандартную или созданную фабрику ResourceProviderFactory для поиска нужной записи ресурсов и получения ее значения. Стандартная фабрика ResourceProviderFactory представляет собой тип ResXResourceProviderFactory, как говорилось выше. Фабрика выводит экземпляр поставщика GlobalResXResourceProvider для глобальных ресурсов и экземпляр поставщика LocalResXResourceProvider для локальных ресурсов страницы.
Эти поставщики используют Диспетчер ресурсов для доступа к отдельным ресурсом из соответствующей сборки. Поставщик использует Диспетчер ресурсов для сбора ресурсов страницы на этапе синтаксического анализа. На рисунке 3 показаны основные компоненты, связанные со стандартной моделью поставщика ресурсов.
Рис. 3. Компоненты, составляющие стандартную модель поставщика ресурсов: фабрика поставщиков, локальные и глобальные поставщики ресурсов, диспетчеры ресурсов и средства чтения ресурсов для доступа к каждому типу ресурсов.
Эта модель поставщиков имеет несколько преимуществ:
- Она отвечает за активацию и цикл существования каждого Диспетчера ресурсов.
- Выражения локализации и другие API ресурсов используют поставщиков для поиска ресурсов, повышая продуктивность за счет применения более простых и абстрагированных API.
- Модель поставщиков является расширяемой, что позволяет менять место хранения ресурсов, продолжая использовать средства повышения продуктивности ASP.NET 2.0.
Теперь я расскажу, как создать собственный поставщик ресурсов.
Создание поставщика ресурсов базы данных
Использование собственного поставщика ресурсов позволяет осуществлять доступ к ресурсам, происходящих из других источников, нежели App_GlobalResources или App_LocalResources. Например, вы можете использовать собственный поставщик ресурсов для доступа к ресурсам в скомпилированных сборках или из базы данных. В этом разделе я расскажу о модели поставщика ресурсов базы данных, а ниже я расскажу о доступе к внешним ресурсным сборкам.
Собственный поставщик ресурсов включает фабрику ResourceProviderFactory и как минимум один тип поставщика ресурсов, где реализован интерфейс IResourceProvider. Фабрика отвечает за создание соответствующего экземпляра IResourceProvider для доступа к локальным или глобальным ресурсам. На рисунке 4 показаны основные компоненты, составляющие модель поставщика ресурсов базы данных в образце кода для данной статьи.
Рис. 4. Иерархия компонентов для модели собственных поставщиков ресурсов базы данных.
Запись ресурсов в базу данных
На первых порах может оказаться полезным изучение структуры таблицы базы данных, где сохраняются реальные записи ресурсов. В этот образец включен сценарий SQL для создания базы данных под названием CustomResourceProvidersSample с таблицей StringResources. В таблицу 1 входят следующие поля:
Табл. 1. Таблица базы данных с записями ресурсов | Поле | Описание |
| resourceType | Тип ресурсов. Категория для каждого ресурса. Может использоваться для обозначения локальных ресурсов для разных страниц или глобальных ресурсов по указанному пользователем имени. |
| cultureCode | Код языка (культуры) из поддерживаемых кодов CultureInfo, используемых в .NET. Код основан на стандартах ISO. Возможно расширение с добавлением любых отсутствующих кодов. |
| resourceKey | Ключ ресурса, используемый для получения ресурсов. |
| resourceValue | Значение ресурса. В этой таблице поддерживаются строки размером до 4 КБ. |
В этом образце все ресурсы сохранены в одной таблице, хотя в более сложных и крупномасштабных средах возможно распределение ресурсов между следующими таблицами для оптимизации для стандартных моделей использования. Основной ключ таблицы представляет собой комбинированный ключ, включающий параметры resourceType, cultureCode и resourceKey. Запрос отдельных значений ресурсов обычно осуществляется с помощью основного ключа. На рисунке 5 показано частичное изображение содержания таблицы.
Рис. 5. Частичное изображение записей ресурсов для этого образца
Параметр resourceType для ресурсов страниц представляет собой имя страницы с относительным путем к странице в приложении (например, Expressions.aspx, SubDir1/Expressions.aspx). Такая схема обозначения позволяет различать страницы с одним и тем же именем, принадлежащие к разным подкаталогам, точно так же, как стандартная модель поставщиков ресурсов ожидает использования разных локальных ресурсных сборок для каждого подкаталога. Ключи ресурсов для свойств элементов управления следуют той же схеме имен, что и стандартные ресурсы страницы, используя контрольный префикс и имя свойства со следующим синтаксисом:
[Prefix].[PropertyName]
Глобальные ресурсы имеют определяемый пользователем тип resourceType. В образце кода имеется несколько категорий глобальных ресурсов: Glossary, CommonTerms и Config. В данном случае выбраны описательные имена ключей ресурсов.
Уровень доступа к данным StringResourcesDALC абстрагирует работу для извлечения ресурсов из этой таблицы, основываясь на схемах использования модели поставщиков.
Расширение фабрики ResourceProviderFactory
Фабрика ResourceProviderFactory представляет собой центральный узел доступа к ресурсам в ASP.NET 2.0, отвечающий за вывод поставщиков глобальных или локальных ресурсов, в зависимости от типа запроса. Фабрика ResourceProviderFactory представляет собой абстрактный базовый тип, требующий реализацию двух методов: CreateLocalResourceProvider() и CreateGlobalResourceProvider(). Для создания собственной фабрики поставщиков нужно использовать этот базовый тип, реализующий данные методы. Оба метода должны выводить экземпляр поставщика ресурсов, реализующий интерфейс IResourceProvider.
Декларация базового типа ResourceProviderFactory показана в листинге 1.
Листинг 1. Абстрактный тип ResourceProviderFactory
public abstract class ResourceProviderFactory
{
protected ResourceProviderFactory();
public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}
Фабрика ResourceProviderFactory обеспечивает поставщики ресурсов для синтаксического анализа при компиляции и для выполнения вызовов API локализации.
- Синтаксический анализатор страницы – Синтаксический анализ страницы осуществляется при разработке перед компиляцией страницы. В рамках синтаксического анализа выполняется проверка прямых выражений для локальных и глобальных ресурсов. При компиляции генерируется код скомпилированной страницы для всех выражений. При этом синтаксический анализ использует поставщики ресурсов.
- Исполнение – Во время исполнения выражения в компилированных страницах уже не имеют значения. Сгенерированный при компиляции код использует API локализации для доступа к локальным и глобальным ресурсам. Поставщик ресурсов создается для локальных и глобальных типов ресурсов.
В образце кода фабрика DBResourceProviderFactory создает поставщик ресурсов DBResourceProvider для обоих путей. Это связано с тем, что доступ к локальным и глобальным ресурсам осуществляется одинаково. Код фабрики DBResourceProviderFactory показан в листинге 2.
Листинг 2. Фабрика DBResourceProviderFactory – собственная реализация фабрики ResourceProviderFactory, поддерживающая ресурсы в базах данных.
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);
}
}
}
Для создания поставщика ресурсов для страниц при использовании прямых и косвенных выражений, применяющих локальные ресурсы, вызывается метод GetLocalResourceProvider(). Ниже приведен пример косвенного и прямого выражений, использующих локальные ресурсы, в соответствии с определением страницы Expressions.aspx в образце кода.
<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() имеет один параметр, виртуальный путь к странице, включающий каталог приложения. Оба вышеприведенных выражения передают этому параметру значение "/LocalizedWebSite/Expressions.aspx". Из рисунка 5 видно, что локальные ресурсы хранятся с помощью типа resourceType, представляющего относительный путь к странице без каталога приложений. Таким образом, метод GetLocalResourceProvider() использует каталог пути перед созданием экземпляра DBResourceProvider.
Для прямых выражений, требующих использования глобальных ресурсов тип ресурсов, указанный в выражении, передается методу GetGlobalResourceProvider(). Изучите следующее прямое выражение (также см. Expressions.aspx в образце кода).
<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms,
Hello %>"></asp:Label>
В данном случае используется тип ресурса CommonTerms, и метод GetGlobalResourceProvider() вызывается с параметром CommonTerms. Для этого типа создается экземпляр поставщика ресурсов DBResourceProvider.
Для любого типа ресурсов создается только один экземпляр поставщика ресурсов DBResourceProvider. После создания он кэшируется для использования в будущем. Вызов фабрики осуществляется только в случае, если в кэше нет экземпляра провайдера. Процесс создания и кэширования поставщика включен в API локализации, используемый для доступа к ресурсам.
Настройка конфигурации фабрики ResourceProviderFactory
При исполнении будет использоваться фабрика ResxResourceProviderFactory, если вы не укажете в конфигурации другой тип фабрики ResourceProviderFactory. В разделе <globalization> файла веб-конфигурации имеется атрибут resourceProviderFactoryType. Здесь можно указать тип фабрики ResourceProviderFactory, который следует использовать. Чтобы настроить фабрику DBResourceProviderFactory, нужно добавить следующий параметр.
<system.web>
...other settings
<globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.
DBResourceProviderFactory,
CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
</system.web> Примечание В представленном образце кода фабрика DBResourceProviderFactory принадлежит к пространству имен CustomResourceProviders в сборке CustomResourceProviders. Эта сборка имеет строгое название и может устанавливаться в глобальном кэше сборок (GAC).
|
Фабрика DBResourceProviderFactory используется для создания поставщиков ресурсов при синтаксическом анализе страницы и во время исполнения.
Реализация IResourceProvider
В основе модели поставщиков ресурсов лежит тип поставщика ресурсов. Хотя фабрика ResourceProviderFactory представляет собой важную абстракцию, в конечном счете за вывод записей ресурсов при исполнении отвечают поставщики ресурсов. Как уже обсуждалось в предыдущем разделе, поставщики создаются фабрикой ResourceProviderFactory и кэшируются для использования в будущем. Из модели поставщика ресурсов базы данных на рисунке 4 можно увидеть, что тип DBResourceProvider обслуживает как локальные, так и глобальные ресурсы. Этот тип отвечает за получение ресурсов базы данных, однако для выполнения этой задачи он использует компоненты DBResourceReader и StringResourcesDALC.
Поставщики ресурсов реализуют интерфейс IResourceProvider, как показано в листинге 3.
Листинг 3. Интерфейс IResourceProvider
public interface IResourceProvider
{
object GetObject(string resourceKey, CultureInfo culture);
IResourceReader ResourceReader { get; }
}
Отдельные ресурсы получаются с помощью метода GetObject(), и свойство ResourceReader должно выводить коллекцию ресурсов на основании типа ресурсов экземпляра поставщика.
На этапе синтаксического анализа страницы поставщики используются для получения всех локальных ресурсов страницы. Осуществляется проверка прямых выражений, и при компиляции также генерируется код страницы. Для локальных ресурсов система чтения ресурсов используется для генерирования кода для прямых выражений. Прямые выражения для локальных и глобальных ресурсов проверяются по отдельности с помощью вызова метода GetObject() для соответствующего поставщика.
Во время исполнения код, сгенерированный синтаксическим анализатором, осуществляет вызовы GetObject() для извлечения локальных и глобальных ресурсов после инициализации страницы.
Извлечение отдельных ресурсов базы данных
Реализация DBResourceProvider для GetObject() выполнена следующим образом.
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);
} Фактически работа по получению ресурсов делегирована для типа StringResourcesDALC (см. Рисунок 4) для работ с запросами баз данных. Этот компонент изолирует поставщиков от возврата ресурсов и другой логики, требуемой для поиска фактического ресурса.
GetResourceByCultureAndKey() инициализирует подключение к базе данных и выполняет SqlDataReader для получения значений, в том числе логику возврата ресурсов (о которой будет рассказано ниже).
Извлечение ресурсов в пакете
Поставщик DBResourceProvider выводит экземпляр DBResourceReader в следующей реализации свойства ResourceReader.
public System.Resources.IResourceReader ResourceReader
{
get
{
ListDictionary resourceDictionary = this.m_dalc.GetResourcesByCulture(CultureInfo.InvariantCulture);
return new DBResourceReader(resourceDictionary);
}
} StringResourcesDALC отвечает за сбор ресурсов по умолчанию для определенного типа (InvariantCulture). Словарь ListDictionary, созданный на основе результатов запроса, включается в DBResourceReader для целей нумерации.
DBResourceReader реализует IResourceReader. Основные элементы реализации показаны ниже.
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
} Синтаксический анализатор странице использует нумератор словаря системы чтения для генерирования код косвенных выражений. При отсутствии системы чтения или пустом словаре сгенерировать код невозможно. Косвенные выражения не требуют присутствия значений для каждого значения свойств, потому что они не являются прямыми. Поэтому, если для установки значений в косвенных выражениях не генерируется код, значения по умолчанию создаются вместе со страницей.
Возврат ресурсов
Возврат ресурсов представляет собой важную часть реализации поставщиков ресурсов. Запрос ресурсов осуществляется во время запуска на основании текущего языка пользовательского интерфейса для потока запроса.
System.Threading.Thread.Current.CurrentUICulture
Если указаны конкретный язык и конкретная культура, например "es-EC" или "es-ES", поставщик ресурсов должен проверять наличие ресурса для этой культуры. Однако ресурсы могут быть указаны только для языка, например, "es". Язык представляет собой родительский объект. При отсутствии конкретных записей проверяется родительская запись. При отсутствии всех записей используется язык по умолчанию. В данном образце по умолчанию используется язык "en".
Возврат ресурсов включен в компонент доступа к данным StringResourcesDALC. При вызове ресурса запускается метод GetResourceByCultureAndKey(). Этот метод открывает подключение к базе данных и вызывает рекурсивную функцию, выполняющую откат ресурса и закрывающую подключение к базе данных. Реализация метода GetResourceByCultureAndKey() показана здесь.
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;
} Рекурсивная функция GetResourceByCultureAndKeyInternal() сначала ищет ресурс для указанной культуры. При отсутствии выполняется поиск по языку и запрос повторяется. Если язык не также не удается найти, осуществляется последняя попытка найти запись для ресурса, использующая установленный по умолчанию язык. Отсутствие записи ресурса для языка по умолчанию считается критическим исключением для данного образца. Список GetResourceByCultureAndKeyInternal() показан здесь.
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;
} Также возврат ресурсов можно осуществить с помощью хранимой процедуры или компонента SQL CLR. Это связано с тем, что правила возврата обычно в некоторой степени связаны с конструкцией базы данных, и не обязательно играют важную роль для уровня предприятия.
Кэширование ресурсов
В модели стандартного поставщика, где ресурсы берутся из ресурсных сборок, сборки загружаются один раз, а затем кэшируются в домене приложения. Для ресурсов в базах данных требуется реализовать собственный механизм кэширования во избежание обращения к базе данных при каждом запросе. Эта задача выполняется поставщиком ресурсов DBResourceProvider.
Ранее я показала, как выглядит реализация GetObject() для поставщика без кэширования. Ресурсы извлекаются из базы данных посредством вызова уровня доступа к данным, который выполняется следующим образом:
resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);
Помните, что для каждого типа ресурсов существует один экземпляр поставщика, который кэшируется для повторного использования. Если внутри поставщика мы кэшируем записи ресурсов в словаре для каждого требуемого языка, эти записи в словаре кэшируются в памяти вместе с поставщиком. Код извлечения объекта может сначала осуществлять поиск значения в кэше словаря, а при его отсутствии создавать кэшированную запись после ее извлечения из базы данных. Результат показан здесь.
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; Кэширование играет очень важную роль для производительности при сохранении ресурсов в базе данных. В этом примере кэширование значений будет осуществляться до тех пор, пока область приложения не будет освобождена. Это означает, что динамическое обновление ресурсов в базе данных не будет производиться при исполнении, если не перезапустить приложение. Для использования такого типа динамического обновления требуется выполнить дополнительную работу для кэширования ресурсов с зависимостью от кэша базы данных.
Защита потока команд
Также в веб-разработке очень важна защита потока команд. Компоненты, используемые в модели поставщиков базы данных, показанной на рисунке 4, обеспечивают защиту потока команд с помощью методик синхронизации .NET.
Экземпляр поставщика DBResourceProvider или StringResourcesDALC для определенного типа ресурсов может вызываться несколькими потоками – т.е. двумя запросами для одной и той же страницы. В компоненте StringResourcesDALC открытые методы, извлекающие данные из базы данных, изменяют переменные экземпляров для типа, в том открывают подключение, настраивают значения параметров запроса и запускают систему чтения SqlDataReader. Для защиты потоков при использовании этих функций служит атрибут MethodImplAttribute.
[MethodImpl(MethodImplOptions.Synchronized)]
Этот атрибут блокирует объект StringResourcesDALC на время вызова метода, блокируя прочие вызовы. Если ресурсы кэшируются, компонент доступа к данным не вызывается, что обеспечивает повышение производительности.
В поставщике DBResourceProvider изменения кэша словаря также блокируются с помощью традиционного выражения блокировки.
lock(this)
{ ... } Подробный код кэширования показан в предыдущем разделе. Выражение блокировки блокирует весь объект и все его компоненты на время выполнения блока кода. Это означает, что значения в кэш могут добавляться только одним потоком в каждый момент времени.
Использование собственных поставщиков ресурсов
В прошлом нам приходилось вручную писать код для создания экземпляров и управления циклом существования экземпляров Диспетчера ресурсов для каждого типа ресурсов. В ASP.NET 2.0 эта задача выполняется моделью поставщиков ресурсов, которая по требованию создает и кэширует поставщики ресурсов, если мы используем при программировании прикладные интерфейсы локализации. Это означает, что мы должны использовать возможности ASP.NET 2.0 для доступа к ресурсам:
- методы объектов страниц;
- методы HttpContext;
- выражения локализации.
Мастер-страницы, веб-страницы и пользовательские элементы управления используют общий базовый тип TemplateControl. Этот базовый тип поддерживает две перегруженные операции для доступа к ресурсам, о которых я упомянула ранее: GetLocalResourceObject() и GetGlobalResourceObject(). Эти операции используют кэшированные поставщики ресурсов для получения ресурсов через реализацию метода GetObject() поставщика. Если поставщик еще не кэширован, он создается фабрикой ResourceProviderFactory с помощью метода CreateLocalResourceProvider() или CreateGlobalResourceProvider(). Преимущество этого подхода заключается в возможности удобного написания кода страниц для получения значений ресурсов.
this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;
this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string; Фактически очень похожий код генерируется при компиляции для каждой страницы на базе прямых и косвенных выражений.
Вы также можете осуществлять доступ к локальным и глобальным ресурсам с помощью статических методов, используя тип HttpContext. Это очень полезно для написания кода, который не составляет часть страницы.
this.labHelloLocal.Text = HttpContext.GetLocalResourceObject("/RuntimeCode.aspx", "labHelloLocalResource1.Text") as string;
this.labHelloGlobal.Text = HttpContext.GetGlobalResourceObject("CommonTerms", "Hello") as string; Фактически выражения локализации представляют собой намного более удобный способ доступа к ресурсам. Выражения локализации обеспечивают декларативную модель, которая автоматически генерирует код для доступа к ресурсам через прикладной интерфейс локализации. Таким образом, все пути ведут к API локализации и к настроенной фабрике ResourceProviderFactory.
Использование ресурсов базы данных: За и против
Использование ресурсов из базы данных дает ряд преимуществ, в том числе:
- Вы можете вводить сложные иерархические требования к ресурсам, не влияющие на вызывающий код. Например, вы можете разрешить изменение строк по умолчанию пользователями и отделами, при этом позволяя им переводить эти строки.
- Большими блоками данных в формате HTML проще управлять на уровне баз данных для организации содержания и обеспечения дополнительной гибкости с кэшированием и использованием памяти (помните, что по умолчанию параллельные ресурсы загружаются в область приложения со всем вложенным содержанием, в то время как ресурсы базы данных могут кэшироваться или удаляться из кэша с помощью более точного алгоритма).
- Хранение информации в одном месте (базе данных) вместо множества файлов .resx повышает общий уровень управляемости локализованного приложения. Оно также позволяет упростить подход к работе с переводчиками.
Однако у подхода с использованием баз данных также имеется ряд недостатков:
- Он несомненно требует более тщательного обдумывания и планирования. Как будут ресурсы организовываться в таблицы? Следует ли помещать все ресурсы в одну таблицу? Следует ли организовывать ресурсы по категориям? Следует ли реализовывать доступ к ресурсам через одну хранимую процедуру или компонент SQL CLR с последующим распределением по таблицам?
- Для интеграции функций повышения продуктивности Visual Studio 2005 с ресурсами базы данных необходимо проделать значительную работу. В частности, это означает, что ресурсы базы данных не будут автоматически генерироваться с помощью функции Generate Local Resources, информацию базы данных нельзя будет просматривать в диалоговом окне выражений Expression Dialog, и будут действовать еще некоторые другие факторы. Этот уровень интеграции требует создания собственных компонентов, интегрируемых во время разработки, о чем я расскажу в своей следующей статье.
Несмотря на дополнительную работу, которую требуется выполнить для интеграции функций повышения продуктивности с ресурсами базы данных, можно утверждать, что получаемые преимущества перевешивают эти проблемы. Более того, планировать структуру и организацию ресурсов следует даже при использовании стандартной структуры выделения ресурсов!
Доступ к ресурсам во внешних сборках
Вы также можете использовать модель поставщиков ресурсов для доступа к ресурсам в предварительно скомпилированных внешних сборках. Это позволяет совместно использовать общие ресурсы из веб-приложений и Windows-приложений, при этом в системе контроля версий или при внедрении вы имеете дело с одной сборкой. В этом разделе я расскажу, как применять описанные концепции для доступа к внешним ресурсным сборкам этого типа.
На рисунке 6 показаны компоненты, составляющие модель внешних поставщиков ресурсов.
Рис. 6. Иерархия компонентов в модели внешних поставщиков ресурсов
Обратите внимание на несколько моментов такой реализации:
- Поддерживаются только глобальные ресурсы. Неразумно заменять модель ресурсов, предоставляемую бесплатно в ASP.NET 2.0. Из внешних ресурсных сборок можно получить только глобальные ресурсы.
- Мы не можем осуществлять доступ к поставщику ресурсов LocalResXResourceProvider через фабрику ExternalResourceProviderFactory. Это внутренний тип, недоступный для нашего кода. Если мы заменим используемый по умолчанию поставщик ресурсов на ExternalResourceProviderFactory, будут поддерживаться только глобальные ресурсы (об альтернативном варианте я расскажу ниже).
- Для доступа к ресурсам также используются диспетчеры ресурсов. Используемый по умолчанию Диспетчер ресурсов обеспечивает возможность доступа к ресурсам в сборках, и поэтому для доступа к внешним ресурсам заменять эту функцию не требуется.
Теперь я продемонстрирую основные моменты реализации этой концепции.
Те же выражения локализации, другая модель использования
Помните, что поставщики ресурсов вызываются на основании выражений локализации и API локализации. Для доступа к внешним ресурсам используются прямые выражения. Эти выражения аналогичны используемым для доступа к глобальным ресурсам, однако в них имеются некоторые изменения, в частности, необходимо указывать имя сборки вместе с типом ресурсов. Используемый по умолчанию поставщик знает, как находить глобальные ресурсные сборки. Этот поставщик внешних ресурсов использует имя сборки для получения того же самого результата.
Выражение $Resources для используемой по умолчанию модели поставщиков (прямые глобальные ресурсы) имеет следующий синтаксис.
<%$ Resources: [resourceType], [resourceKey] %>
То же самое выражение может использоваться для доступа к внешним ресурсам при использовании фабрики ExternalResourceProviderFactory. Для этого требуется изменить синтаксис следующим образом.
<%$ Resources: [assemblyName]|[resourceType], [resourceKey] %>
Например, для извлечения ресурса из сборки CommonResources.dll, из типа глобального ресурса "CommonTerms", необходимо использовать следующее прямое выражение.
<asp:Label ID="labGlobalResource" runat="server" Text="<%$ Resources:CommonResources|CommonTerms,
Hello %>" ></asp:Label>
После этого при компиляции страницы генерируется следующий код.
labGlobalResource.Text = this.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello"); Это иллюстрирует, что модель внешних поставщиков ресурсов может использовать имеющиеся выражения и код из API локализации при наличии соответствующей информации. Именно поставщик ExternalResourceProvider выполняет окончательный синтаксический анализ информации для отделения имени сборки от типа ресурса.
ExternalResourceProviderFactory
Как и фабрика DBResourceProviderFactory, фабрика ExternalResourceProviderFactory наследует ResourceProviderFactory и имеет приоритет методов CreateGlobalResourceProvider() и CreateLocalResourceProvider(). Полная реализация показана в листинге 4.
Листинг 4. Реализация 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() создает экземпляр типа GlobalExternalResourceProvider с указанием класса ключа. Помните, что класс ключа для этого поставщика должен включать указание имени сборки и типа ресурса. Метод CreateLocalResourceProvider() вызывает неподдерживаемое исключение NotSupportedException, поскольку мы не храним локальные ресурсы во внешних сборках. При этом фактически происходит исключение синтаксического анализа, если на странице используются локальные выражения. Итак, если вы хотите организовать дальнейшую поддержку локальных ресурсов, этот сценарий может являться не идеальным сценарием для ExternalResourceProvider. Далее я покажу, как избежать эту проблему, используя собственные выражения локализации.
Для привязки фабрики ExternalResourceProviderFactory к существующим выражениям и API локализации необходимо вернуться в раздел <globalization> файла конфигурации Web.config.
<globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResource
Providers.ExternalResourceProviderFactory,CustomResourceProviders, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
Теперь стандартная модель поставщиков заменяется моделью внешних поставщиков ресурсов. Давайте рассмотрим работу поставщика ресурсов.
Поставщик GlobalExternalResourceProvider
Поставщик GlobalExternalResourceProvider использует поставщик IResourceProvider. Этот поставщик аналогичен поставщику GlobalResXResourceProvider, за одним исключением: Этот поставщик извлекает глобальные ресурсы из готовых сборок и требует знания конкретных имен сборок, где хранятся ресурсы.
Конструктор GlobalExternalResourceProvider получает имя сборки и тип ресурса, разделенные символом ("|"). Эта информация подвергается синтаксическому анализу, как показано здесь.
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));
} Если параметр classKey, переданный конструктору, имеет неверный формат, возникает исключение ArgumentException. Из-за этого синтаксический анализатор страницы сообщает об ошибке прямого выражения. При исполнении кода, написанного непосредственно для API локализации, возникает ошибка.
Для каждого сочетания сборки и типа ресурсов создается и кэшируется экземпляр поставщика. При запросе ресурса во время синтаксического анализа страницы (для проверки) или при запуске вызывается метод GetObject(), как показано здесь:
public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
this.EnsureResourceManager();
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
return this.m_resourceManager.GetObject(resourceKey, culture);
} Поставщик GlobalExternalResourceProvider использует имеющиеся функции типа Диспетчера ресурсов для извлечения ресурсов и обработки возврата ресурсов. Основная задача заключается в том, чтобы создать Диспетчер ресурсов для правильной сборки. При первом вызове метода EnsureResourceManager() осуществляется загрузка ресурсной сборки и создается экземпляр Диспетчера ресурсов для указанного типа в данной сборки. При указании сборки, не содержащей тип ресурсов, возникает исключение. Код для загрузки сборки и создания Диспетчера ресурсов показан здесь:
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; Используя поставщик ресурсов ExternalResourceProvider, вы можете извлекать ресурсы из любой сборки, установленной в каталоге \bin веб-приложения или из глобального кэша сборок (GAC).
Поставщик выводит неподдерживаемое исключение NotSupportedException для свойства ResourceReader, поскольку локальные ресурсы не поддерживаются. В связи с этим синтаксический анализ косвенных выражений локализации не выполняется.
Поддержка собственных выражений локализации
Создание собственных поставщиков очень полезно в ситуациях, когда все ресурсы хранятся в альтернативном источнике, и вы не планируете использовать ресурсы App_LocalResources и App_GlobalResources. Что же делать, если вы хотите обеспечить поддержку стандартной реализации для локальных и глобальных ресурсов (поставщик по умолчанию), сохранив возможность извлекать ресурсы из другого источника (собственный поставщик)? Вы можете достичь этой цели посредством реализации собственных выражений, предназначенных для собственного поставщика ресурсов.
Как работает конструктор ResourceExpressionBuilder
Выражения обрабатываются конструкторами выражений, которые взаимодействуют с синтаксическим анализом страниц перед компиляцией. Выражения могут включать любые объекты с разделителем <%$ %>, в том числе параметры приложения, строки подключения и выражения локализации. Синтаксис этих приложений показан здесь.
<%$ [prefix]: [declaration] %>
Как вы знаете, в выражениях локализации используется префикс "Resources". Синтаксический анализатор использует тип ResourceExpressionBuilder для обработки этих выражений. Это связано с тем, что конструктору ResourceExpressionBuilder соответствует префикс "Resources" для значений по умолчанию времени исполнения конфигурации <expressionBuilders>.
<expressionBuilders>
<add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder,
System.Web, Version=2.0.0.0, Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a"/>
</expressionBuilders>
Для скомпилированных страниц это работает следующим образом:
- При синтаксической проверке страницы вызывается операция ParseExpression конструктора выражений, служащая для синтаксической проверки выражений. Если выражение является недопустимым (например, если указанный ресурс нельзя найти), генерируется ошибка синтаксической проверки.
- Если синтаксическая проверка проходит успешно, вызывается операция GetCodeExpression конструктора выражений для запроса кода генерирования выражения. На этом этапе конструктор выражений создает код инициализации страниц. Код страницы вставляется в IL.
При отключении компиляции страниц выражения оцениваются несколько другим способом. Вы можете отключить компиляцию страниц для отдельных страниц.
<%@ Page Language="C#" CompilationMode="Never" %>
Также вы можете отключить компиляцию для всех страниц в файле Web.config.
<pages compilationMode="Never" />
В этом случае при запросе страницы осуществляется синтаксическая проверка. На этапе синтаксического анализа проверяется свойство SupportsEvaluate конструктора сообщений, чтобы определить, возможна ли обработка страницы без компиляции. Код страницы не генерируется.
При запуске производится еще одна проверка свойства SupportsEvaluate и вызов функции EvaluateExpression для получения значений каждого выражения локализации.
ResourceExpressionBuilder строится на основе ExpressionBuilder. ExpressionBuilder – общий базовый тип, использующий абстрактные и виртуальные методы, реализованные в ResourceExpressionBuilder для поддержки синтаксической проверки страниц, генерирования кода и оценки выражений. Таким образом, для поддержки выражений локализации можно расширить ExpressionBuilder и использовать собственную реализацию.
Расширение конструктора выражений
Для поддержки собственных выражений локализации требуется собственная реализация конструкция выражений. Как и ResourceExpressionBuilder, вы можете расширить ExpressionBuilder и создать собственную реализацию для синтаксической проверки, генерирования кода и оценки выражений для нескомпилированных страниц.
Для начала давайте изучим назначение конструктора собственных выражений в этом образце и ожидаемый синтаксис реализации. При этом необходимо оставить без изменений стандартную реализацию <%$ Resources %>, реализовав поддержку ресурсов, которые берутся из внешних сборок. Для этой цели, вместо замены всего поставщика ресурсов мы просто создадим новое выражение для работы с ним. Это означает, что нам потребуется новый префикс выражения, собственный конструктор выражений ExpressionBuilder и способ привязки нового префикса к конструктору выражений ExpressionBuilder.
В этом примере используется новый префикс "ExternalResource". Требуемый синтаксис для этого нового выражения показан здесь.
<%$ ExternalResource: [assemblyName]|[resourceType], [resourceKey] %>
Это выражение берет ресурсы указанной сборки, используя поставщик ресурсов GlobalExternalResourceProvider, о котором мы рассказывали выше. Для поддержки этого нового выражения мы создадим новый тип, ExternalResourceExpressionBuilder. В таблице 2 описываются функции, которые обеспечиваются каждым из отменяемых методов конструктора сообщений ExpressionBuilder.
Табл. 2. Описание функций каждого отменяемого метода | Метод | Описание |
| EvaluateExpression | Выводит значение ресурса для выражения ExternalResource в нескомпилированных страницах. |
| GetCodeExpression | Выводит код, отправляемый для выражения ExternalResource. Этот код вызывает собственного поставщика ресурсов GlobalExternalResourceProvider. |
| ParseExpression | Выполняет проверку выражения ExternalResource, производя попытку доступа к ресурсам выражения. Если ресурс не найден, проверка прекращается. |
| Свойство SupportsEvaluate | Указывает, поддерживается ли оценка нескомпилированных страниц. В этой реализации имеет значение true. |
С помощью конструктора ExternalResourceExpressionBuilder можно декларировать собственные выражения локализации, как показано ниже.
<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:
CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>
Не забывайте, что синтаксическая проверка выражений производится при проектировании до компиляции. Функция ParseExpression вызывается при синтаксической проверке страницы для того, чтобы проверить точность выражения ресурсов и фактическое существование запрашиваемого ресурса. Данная реализация показана в следующем коде.
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;
} Основная часть кода сосредоточена на проверке выражения, но в основе всего лежит создание поставщика GlobalExternalResourceProvider, а также вызов GetObject() для извлечения ресурса.
При компиляции страниц после синтаксической проверки страниц генерируется код. В это время производится вызов функции GetCodeExpression из конструктора выражений. Эта операция выводит необходимый код для извлечения значения ресурсов во время исполнения, как показано здесь.
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;
} Вывод результатов GetCodeExpression приводит к генерированию кода, которая аналогична выделенному полужирным шрифтом коду, показанному здесь.
labExternalResource.Text = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|
CommonTerms", "Hello") as string; Вы увидите, что сгенерированный код полагается на статические методы, реализованные конструктором ExternalResourceExpressionBuilder. Вспомогательный метод GetGlobalResourceObject создает экземпляр GlobalExternalResourceProvider и получает запись ресурсов. Для скомпилированных страниц этот код при исполнении извлекает значения внешних ресурсов.
Для нескомпилированных страниц выражения оцениваются при исполнении посредством вызова EvaluateExpression. ExternalResourceExpressionBuilder имеет приоритет для функции EvaluateExpression, которая использует поставщик GlobalExternalResourceProvider для получения соответствующего ресурса.
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);
} После настройки собственного конструктора ресурсов вы можете свободно включать декларативные утверждения для получения ресурсов из внешних сборок, используя стандартные выражения локализации для получения значений из App_LocalResources или App_GlobalResources.
Конфигурация ExpressionBuilder
Для настройки собственного конструктора сообщений необходимо добавить его в раздел <expressionBuilders> в файле Web.config. В этом примере мы связываем ExternalResourceExpressionBuilder с префиксом "ExternalResources" с этой конфигурацией.
<expressionBuilders>
<add expressionPrefix="ExternalResources" type="CustomResourceProviders.ExternalResourceExpressionBuilder,
CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1"/>
</expressionBuilders>
Теперь для всех выражений ресурсов, использующих префикс "ExternalResources", будет выполняться синтаксическая проверка или оценка в соответствии с реализацией ExternalResourceExpressionBuilder, о которой говорилось в предыдущем разделе.
Доступ к локальным, глобальным и внешним ресурсам
Листинг 5 иллюстрирует применение всех трех типов выражений локализации (косвенные, прямые и собственные прямые) для извлечения ресурсов из стандартных и собственных источников.
Листинг 5. Применение косвенных, прямых и собственных прямых выражений локализации на одной странице
<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>
Используя собственные выражения локализации для внешних ресурсов вместо настройки нового поставщика ресурсов, вы можете использовать локальные ресурсы для каждой страницы и глобальные ресурсы, скомпилированные вместе с веб-узлом, и при этом иметь необходимую гибкость для использования ресурсов внешних сборок (и других ресурсов). Используя конструктор ExternalResourceExpressionBuilder, вы также можете осуществлять доступ к внешним ресурсам непосредственно из кода, используя статический метод, упомянутый выше, GetGlobalResourceObject().
string s = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms",
"Hello") as string; Если вы будете использовать этот метод, вам не потребуется заменять используемый по умолчанию поставщик ресурсов собственным поставщиком ресурсов. Вместо этого, можно использовать средства генерирования кода в собственных выражениях локализации для извлечения ресурсов из внешних сборок по требованию.
Заключение
Из этой статьи вы узнали, как создавать собственную модель поставщиков ресурсов для доступа к ресурсам базы данных или внешних источников. Также вы узнали, как создавать собственные выражения локализации для использования альтернативных источников ресурсов вместе со стандартной моделью поставщиков. Используя функции расширяемости ASP.NET 2.0, вы получите доступные альтернативы распределения и извлечения ресурсов. Лучше всего в этом то, что вы сможете легко использовать обычную модель программирования ASP.NET 2.0, используя декларативные выражения локализации.
В следующей статье этой серии будет рассказано о другой стороне этой картины. Из нее вы узнаете, как создавать выражения ресурсов и генерировать ресурсы в источниках при разработке.
Благодарность
Выражаю огромную благодарность Саймону Калверту (Simon Calvert) и Эйлон Липтон (Eilon Lipton) из корпорации Майкрософт, которые предоставили мне ценную поддержку и отзывы по этой статье, благодаря которым в ней так подробно рассмотрено применение модели поставщиков.
Дополнительные ресурсы
Блог Мишель Леруа Бустаманте: www.dasblonde.net (RSS по глобализации)
IDesign Inc
Об авторе
Мишель Леруа Бустаманте – главный проектировщик компании IDesign Inc., региональный директор Майкрософт в Сан-Диего, обладатель звания Microsoft MVP по веб-службам XML и технический директор BEA. В компании IDesign она занимается обучением и предоставлением консультационных услуг по системным архитектурам, специализируясь на веб-службах, проектированию масштабируемой и защищенной архитектуры для решений .NET, совместимости и архитектуре глобализации. Мишель является членом международной ассоциации International .NET Speakers Association (INETA), часто выступает с докладами на конференциях, является председателем конференции SD по направлению веб-служб. Ее статьи часто публикуются в ведущих технических журналах.
Мишель также является членом совета директоров IASA (Международная ассоциация архитекторов программного обеспечения) и консультантам по программам в UCSD. Публикация ее книги под названием Изучение коммуникационной инфраструктуры Windows (издательство O'Reilly) запланирована на конец 2006 года (блог по книге: www.thatindigogirl.com). Связаться с Мишель можно по адресу mlb@idesign.net, через веб-узел www.idesign.net или через блог www.dasblonde.net.