Основы. Часть 9 – Строковые ресурсы

Теперь давайте обратим внимание на текстовые строки, заданные непосредственно в коде. Даже в небольших проектах они способны доставить много неудобств. Кроме того, такие веб-приложения практически невозможно локализовать. Поэтому давайте разделим текст и программный код.

Выбираем место для размещения ресурсов

Разработчики, у которых уже есть опыт работы с ASP.NET, могут сказать, что для размещения ресурсов предназначена специальная папка App_GlobalResources. Однако, в ASP.NET MVC 3 создавать ее не нужно и более того – противопоказанно.

Давайте разберемся почему. Причина такой ситуации скрыта в разногласии между идеологией шаблона MVC и особенностях работы ASP.NET с указанной папкой. Если попробовать разместить ресурсы в ней, то само веб-приложение будет запускаться и работать без каких-либо проблем. Но давайте вспомним, что одним из главных преимуществ MVC является возможность использования модульных тестов. Вот тут и ждет сюрприз.

Дело в том, что ресурсы, расположенные в App_GlobalResources, не включаются в dll-файл проекта. Они размещаются в отдельной сборке, создаваемой при запуске веб-приложения на сервере. ASP.NET маскирует данный факт с помощью специальных прокси-классов, что позволяет обращаться к ресурсам как к свойствам объектов. Чтобы убедиться в этом, достаточно заглянуть в любой из файлов <имя ресурса>.Designer.cs, которые автоматически создаются при добавлении resx-файла в проект.

Важным моментом является то, что сборка c ресурсами отсутствует как в момент загрузки, так и при компиляции проекта в Visual Studio. Это приведет к тому, что любая попытка получить содержащиеся в ней текстовые значения завершится выбросом исключения. А значит, модульные тесты завершатся неудачей, т.к. они выполняются вне сервера.

Решение рассматриваемой проблемы очень простое. Достаточно использовать любую другую папку для размещения файлов ресурсов. Поэтому добавим папку Resources в корневой каталог сайта.

Создаем хранилище ресурсов

Папки с ресурсамиИтак, место хранения ресурсов выбрано и создано. Но переносить все строки в один единственный файл не очень хорошая идея. Ведь если текста будет достаточно много, то в дальнейшем можно просто запутаться. Поэтому давайте внутри уже созданной Resources предусмотрим следующую структуру папок:

  • Views – для строк из файлов Представлений, включая общие для всех областей в папке Shared;
  • Controllers – для ресурсов, используемых в Контроллерах;
  • Models – для текстовых имен свойств классов Модели;
  • Shared – для любых общих ресурсов, которых нельзя явно отнести к одной из выше перечисленных папок .
  • Кроме того, создадим подпапку для области Admin со структурой по описанной выше схеме.

Теперь определим принципы разделения ресурсов по отдельным файлам. Для этого введем несколько соглашений, согласно которым:

  • Специфичные для каждого класса Модели или Контроллера ресурсы будут располагаться в отдельных файлах. Их имена будет равны названию класса с окончанием Res. При этом существующие уточнения, например Controller, отбрасываются. В итоге такой подход позволит избежать одинаковых имен и связанной с этим путаницы в дальнейшем;
  • В общие ресурсы (Shared) будем выносить часто используемые строки, которые при этом явно не связаны с какими-либо классами или Представлениями. Например, сообщения об ошибках или тексты для стандартных кнопок пользовательского интерфейса.

Еще один вопрос, который необходимо решить: стоит ли размещать ресурсы отдельных Представлений в разных файлах? Здесь все зависит от конкретного проекта. В частности, т.к. в данном случае текста не много, то создадим по одному файлу для каждой группы Представлений.

Модификатор доступа к ресурсуВажный момент: для всех создаваемых ресурсов обязательно необходимо установить значение модификатора доступа равное public. В противном случае, классы веб-приложения не смогут получить из них необходимые данные.

Выносим строки в ресурс

В текущей версии веб-приложения все строки находятся в Представлениях. Однако, если посмотреть внимательно, то некоторые из них используют имена свойств Модели. Поэтому ресурсы, описывающие свойства Модели, разместим в папке Resources\Models.

Имена и содержимое, отсортированное в алфавитном порядке, получившихся файлов приведено ниже:

Файл Resources\Models\BookDetailsRes.resx – названия свойств класса BookDetails
Author Author Автор(ы)
Description Description Краткое описание
IsFree Available for free Книга доступна бесплатно
IsVisible Visible in catalog Книга отображается в каталоге
Language Language Язык
PublishedAt Published date Дата публикации
Publisher Publisher Издатель
Rating Current rating Текущий рейтинг
Tags Tags Ключевые слова
Title Title Название книги
Url Website address Ссылка на сайт книги
Файл Resources\Models\LanguageRes.resx – названия свойств класса Language
Books Books available Перечень книг на указанном языке
Name Name Название языка
Файл Resources\Models\PublisherRes.resx – названия свойств класса Publisher
Books Books published Список выпущенных книг
Homepage Website address Ссылка на сайт издателя
Title Title Наименование издателя
Файл Resources\Models\TagRes.resx – названия свойств класса Tag
Books Books tagged Перечень книг c данным ключевым словом
Text Tag Ключевое слово

Теперь перейдем к Представлениям. Вынесем текст кнопок и схожие заголовки страниц в общие ресурсы, а все остальное – в отдельные файлы (по одному на группу Представлений).

Файл Resources\Views\Shared\ButtonsRes.resx – кнопки и ссылки
Administrator Administrator Администратор
Back Back Вернуться
Catalog Catalog Каталог
Create Create Создать
Delete Delete Удалить
Edit Edit Редактировать
Save Save Сохранить
Файл Resources\Views\Shared\CommonRes.resx – разные элементы интерфейса
ChooseItem Choose... Указание выбрать элемент из списка
Error Error Заголовок страницы ошибки
None None Сообщение об отсутствии значения
Файл Resources\Views\Shared\MessagesRes.resx – строки сообщений
ConfirmDeleteQuestion Are you sure you want to delete this? Вопрос на подтверждения удаления
Файл Resources\Views\CatalogRes.resx – Представления Catalog
IndexPageTitle Book catalog Заголовок страницы Index
Файл Resources\Admin\Views\CatalogRes.resx – Представления Catalog
CreatePageTitle Add new book Заголовок страницы Create
DeletePageTitle Delete selected book Заголовок страницы Delete
EditPageTitle Edit book details Заголовок страницы Edit
FormTitle Book details Заголовок формы ввода значений
IndexPageTitle Catalog Заголовок страницы Index
Файл Resources\Admin\Views\LanguagesRes.resx – Представления Languages
CreatePageTitle Add new language Заголовок страницы Create
DeletePageTitle Delete selected language Заголовок страницы Delete
EditPageTitle Edit language Заголовок страницы Edit
FormTitle Language details Заголовок формы ввода значений
IndexPageTitle Languages Заголовок страницы Index
Файл Resources\Admin\Views\PublishersRes.resx – Представления Publishers
CreatePageTitle Add new publisher Заголовок страницы Create
DeletePageTitle Delete selected publisher Заголовок страницы Delete
EditPageTitle Edit publisher details Заголовок страницы Edit
FormTitle Publisher details Заголовок формы ввода значений
IndexPageTitle Publishers Заголовок страницы Index
Файл Resources\Admin\Views\TagsRes.resx – Представления Tags
CreatePageTitle Add new tag Заголовок страницы Create
DeletePageTitle Delete selected tag Заголовок страницы Delete
EditPageTitle Edit tag Заголовок страницы Edit
FormTitle Tag details Заголовок формы ввода значений
IndexPageTitle Tags Заголовок страницы Index

После создания ресурсов пересоберём проект, чтобы информация о них стала доступна для IntelliSense.

Заменяем строки на ресурсы

Правила обращения к ресурсам

Начнем сразу с примера. Для получения строки под именем IndexPageTitle из файла ресурсов CatalogRes.resx необходимо написать следующее:

BookCatalog.Resources.Views.CatalogRes.IndexPageTitle

Здесь можно выделить следующие части:

  • BookCatalog.Resources.Views – пространство имен, определяемое именами проекта и папок, в которых находится ресурс.
  • CatalogRes – прокси-объект для работы с ресурсом, имя которого определяется именем resx файла.
  • IndexPageTitle – статическое свойство, доступное только для чтения. Предназначено для получения из ресурса значения с таким же именем.

Изменение пространства имен ресурсаНекоторым разработчикам не нравятся длинные пространства имен. В этом случае можно определить сокращенный вариант в свойствах файла ресурса. Для этого используется поле "Custom Tool Namespace". Если, например, установить его в значение, например, "Views", то указанная выше строка сократиться до BookCatalog.Views.CatalogRes.IndexPageTitle.

В данном проекте оставим оригинальные пространства имен как более наглядный вариант. Кроме того, удобнее использовать ключевое слово using, а окончание Res хорошо выделяет классы ресурсов.

Заменяем текстовые значения

Замена текста в Представлениях – простая, хоть и трудоемкая задача. Разберем этот процесс на примере Catalog/Index:

@model IEnumerable<BookCatalog.Models.BookDetails>
@using BookCatalog.Resources.Models;
@using BookCatalog.Resources.Views;
@using BookCatalog.Resources.Views.Shared;
@{ ViewBag.Title = CatalogRes.IndexPageTitle; }

<h2>@ViewBag.Title</h2>

<table>
    <tr>
        <th>@BookDetailsRes.Title</th>
        <th>@BookDetailsRes.Author</th>
        <th>@BookDetailsRes.PublishedAt</th>
        <th>@BookDetailsRes.Url</th>
        <th>@BookDetailsRes.Description</th>
        <th>@BookDetailsRes.Tags</th>
        <th>@BookDetailsRes.Rating</th>
        <th>@BookDetailsRes.IsFree</th>
        <th>@BookDetailsRes.Language</th>
        <th>@BookDetailsRes.Publisher</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>@item.Title</td>
        <td>@item.Author</td>
        <td>@String.Format("{0:D}", item.PublishedAt)</td>
        <td>@item.Url</td>
        <td>@item.Description</td>
        <td>@string.Join(", ", item.Tags.Select(tag => tag.Text).ToArray())</td>
        <td>@item.Rating</td>
        <td>@item.IsFree</td>
        <td>@(item.Language == null ? CommonRes.None : item.Language.Name)</td>
        <td>@(item.Publisher == null ? CommonRes.None : item.Publisher.Title)</td>
    </tr>
}

</table>

В данном случае Представление строго-типизированное, а Моделью выступает класс BookDetails. Чтобы постоянно не указывать пространства имен, можно задать их с помощью ключевого слова @using. В остальной части кода используются свойства прокси-объектов для получения текстовых значений.

Подобные изменения необходимо сделать во всех Представлениях. Данные действия однотипные, поэтому нет смысла приводить здесь исходный код всех изменённых файлов. Отметим только те, которые расположены в папке Shared.

Введем еще одно соглашение: имена общих Представлений будем начинать с символа подчеркивания. Поэтому переименуем Shared/Error в Shared/_Error. Кроме того, заменим в нем заголовок и предоставим возможность Контроллеру изменять текст сообщения об ошибке. Для этого будем использовать динамическое свойство ViewBag.ErrorMessage.

@using BookCatalog.Resources.Views.Shared;
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
    <title>@CommonRes.Error</title>
</head>
<body>
    <h2>@CommonRes.Error</h2>
    <hr />
    <p>@ViewBag.ErrorMessage</p>
</body>
</html>

В шаблоне разметки _Layout.cshtml  подставим значения из ресурсов вместо строк меню:

@using BookCatalog.Resources.Admin.Views;
@using BookCatalog.Resources.Views.Shared;
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
</head>
<body>
    <div class="menu">
        @Html.ActionLink(CatalogRes.IndexPageTitle, "Index", "Catalog")&nbsp;|&nbsp;
        @Html.ActionLink(PublishersRes.IndexPageTitle, "Index", "Publishers")&nbsp;|&nbsp;
        @Html.ActionLink(LanguagesRes.IndexPageTitle, "Index", "Languages")&nbsp;|&nbsp;
        @Html.ActionLink(TagsRes.IndexPageTitle, "Index", "Tags")&nbsp;|&nbsp;
        @Html.ActionLink(ButtonsRes.Back, string.Empty, string.Empty, new { area = "" }, null)
    </div>
    @RenderBody()
</body>
</html>

И в завершении отметим еще одно Представление – Admin/Catalog/_CreateOrEdit. Стоит отметить, что оно частичное и используется внутри других (как видно из названия, в Create и Edit). Подробно это будет рассмотрено в дальнейшем, а сейчас просто посмотрим его исходный код:

@model BookCatalog.Models.BookDetails

@* This partial view defines form fields that will appear when creating and editing entities *@

<div class="editor-label">
    @Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.Title)
    @Html.ValidationMessageFor(model => model.Title)
</div>

.........

Здесь для вывода подписей полей используется метод LabelFor(). Сейчас он просто отображает имена соответствующих свойств Модели. Разумеется, это далеко не всегда приемлемо. Поэтому в следующей части рассмотрим, каким образом можно исправить данную ситуацию.

А пока запустим проект и убедимся что визуально страницы не изменились. Если часть текста отсутствует, то необходимо проверить модификаторы доступа к ресурсам, как это было показано выше.


Исходный код проекта (C#, Visual Studio 2010): mvc3-in-depth-basics-09.zip

Комментарии (8) -

Виталий 26.06.2011 23:46:32

"Начнем сразу с примера. Для получения строки под именем IndexPageTitle из файла ресурсов TagsRes.resx необходимо написать следующее:"
В даной строке вместо "TagsRes.resx" должно быть "CatalogRes.resx"

@ Виталий: Спасибо

По-моему, правильнее так
В шаблоне разметки _Layout.cshtml ...


@Html.ActionLink(PublishersRes.IndexPageTitle, "Index", "Publisher")&nbsp;|&nbsp;
@Html.ActionLink(LanguagesRes.IndexPageTitle, "Index", "Language")&nbsp;|&nbsp;

Согласен - опечатка. Спасибо что заметили.

Спасибо за статьи, в рунете ваш блог один из самых полезных источников информации по ASP.Net MVC!
Не могли бы вы немного осветить процесс локализации, с использованием ресурсов

@ Alex: В планах есть. Но вот когда из-за нехватки времени руки дойдут - обещать не могу.

Александр 19.07.2012 15:11:41

Андрей, не подскажите как поступить лучше?
Есть, грубо говоря, 2 проекта: Web и Service, который используется в Web. Надо:
1. Локализовать сообщения в проекте-сервисе.
2. Файл-ресурса можно было бы вручную редактировать. Т.е. тексты с ресурсами хранятся как текстовые файлы, по примеру как в Resources.resx
3. Использовать теже ресуры в другом проекте, например в консольной утильте, которая также использует проект-сервис для своей работы.

Как вариант ресурсы разбить на несколько файлов, но у нас такой потребности пока нет, т.к. в ресурсах имеем очень мало строк.

Спасибо.

@ Александр: Не видя как построен проект сказать сложно. Я бы сохранял ресурсы по месту их использования. Например, у меня есть сборка с моделям для бизнес-логики. На классы навешаны атрибуты проверки данных. Их сообщения об ошибках для них - в этой же сборке. Сообщения для Bussines Logic - в соответствующей сборке и т.д.

Да, будет небольшое дублирование одинаковых строк (часто используемых) в разных сборках. Но, во первых, удобнее редактировать (всегда ясно где искать строку). Во вторых, сталкивался с ситуацией, когда например "Login" это и действие и "имя входа". Если все строки "Login" привязаны к одному ресурсу, то проект ждет переделка.

Добавить комментарий