Теперь давайте обратим внимание на текстовые строки, заданные непосредственно в коде. Даже в небольших проектах они способны доставить много неудобств. Кроме того, такие веб-приложения практически невозможно локализовать. Поэтому давайте разделим текст и программный код.
Выбираем место для размещения ресурсов
Разработчики, у которых уже есть опыт работы с 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") |
@Html.ActionLink(PublishersRes.IndexPageTitle, "Index", "Publishers") |
@Html.ActionLink(LanguagesRes.IndexPageTitle, "Index", "Languages") |
@Html.ActionLink(TagsRes.IndexPageTitle, "Index", "Tags") |
@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